1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.accessibility.floatingmenu; 18 19 import static android.util.TypedValue.COMPLEX_UNIT_PX; 20 import static android.view.View.MeasureSpec.AT_MOST; 21 import static android.view.View.MeasureSpec.UNSPECIFIED; 22 23 import android.annotation.UiContext; 24 import android.content.Context; 25 import android.content.res.Configuration; 26 import android.content.res.Resources; 27 import android.graphics.CornerPathEffect; 28 import android.graphics.Paint; 29 import android.graphics.PixelFormat; 30 import android.graphics.Rect; 31 import android.graphics.drawable.GradientDrawable; 32 import android.graphics.drawable.ShapeDrawable; 33 import android.os.Bundle; 34 import android.text.method.MovementMethod; 35 import android.util.DisplayMetrics; 36 import android.view.Gravity; 37 import android.view.LayoutInflater; 38 import android.view.MotionEvent; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.view.WindowManager; 42 import android.view.accessibility.AccessibilityNodeInfo; 43 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 44 import android.widget.FrameLayout; 45 import android.widget.LinearLayout; 46 import android.widget.TextView; 47 48 import com.android.settingslib.Utils; 49 import com.android.systemui.R; 50 import com.android.systemui.recents.TriangleShape; 51 52 /** 53 * Base tooltip view that shows the information about the operation of the 54 * Accessibility floating menu. In addition, the anchor view is only for {@link 55 * AccessibilityFloatingMenuView}, it should be more suited for displaying one-off menus to avoid 56 * the performance hit for the extra window. 57 */ 58 class BaseTooltipView extends FrameLayout { 59 private int mFontSize; 60 private int mTextViewMargin; 61 private int mTextViewPadding; 62 private int mTextViewCornerRadius; 63 private int mArrowMargin; 64 private int mArrowWidth; 65 private int mArrowHeight; 66 private int mArrowCornerRadius; 67 private int mScreenWidth; 68 private boolean mIsShowing; 69 private TextView mTextView; 70 private final WindowManager.LayoutParams mCurrentLayoutParams; 71 private final WindowManager mWindowManager; 72 private final AccessibilityFloatingMenuView mAnchorView; 73 BaseTooltipView(@iContext Context context, AccessibilityFloatingMenuView anchorView)74 BaseTooltipView(@UiContext Context context, AccessibilityFloatingMenuView anchorView) { 75 super(context); 76 mWindowManager = context.getSystemService(WindowManager.class); 77 mAnchorView = anchorView; 78 mCurrentLayoutParams = createDefaultLayoutParams(); 79 80 initViews(); 81 } 82 83 @Override onConfigurationChanged(Configuration newConfig)84 protected void onConfigurationChanged(Configuration newConfig) { 85 super.onConfigurationChanged(newConfig); 86 87 mAnchorView.onConfigurationChanged(newConfig); 88 updateTooltipView(); 89 90 mWindowManager.updateViewLayout(this, mCurrentLayoutParams); 91 } 92 93 @Override onTouchEvent(MotionEvent event)94 public boolean onTouchEvent(MotionEvent event) { 95 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { 96 hide(); 97 } 98 99 return super.onTouchEvent(event); 100 } 101 102 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)103 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 104 super.onInitializeAccessibilityNodeInfo(info); 105 106 info.addAction(AccessibilityAction.ACTION_DISMISS); 107 } 108 109 @Override performAccessibilityAction(int action, Bundle arguments)110 public boolean performAccessibilityAction(int action, Bundle arguments) { 111 if (action == AccessibilityAction.ACTION_DISMISS.getId()) { 112 hide(); 113 return true; 114 } 115 116 return super.performAccessibilityAction(action, arguments); 117 } 118 show()119 void show() { 120 if (isShowing()) { 121 return; 122 } 123 124 mIsShowing = true; 125 updateTooltipView(); 126 127 mWindowManager.addView(this, mCurrentLayoutParams); 128 } 129 hide()130 void hide() { 131 if (!isShowing()) { 132 return; 133 } 134 135 mIsShowing = false; 136 mWindowManager.removeView(this); 137 } 138 setDescription(CharSequence text)139 void setDescription(CharSequence text) { 140 mTextView.setText(text); 141 } 142 setMovementMethod(MovementMethod movement)143 void setMovementMethod(MovementMethod movement) { 144 mTextView.setMovementMethod(movement); 145 } 146 isShowing()147 private boolean isShowing() { 148 return mIsShowing; 149 } 150 initViews()151 private void initViews() { 152 final View contentView = 153 LayoutInflater.from(getContext()).inflate( 154 R.layout.accessibility_floating_menu_tooltip, this, false); 155 156 mTextView = contentView.findViewById(R.id.text); 157 158 addView(contentView); 159 } 160 createDefaultLayoutParams()161 private static WindowManager.LayoutParams createDefaultLayoutParams() { 162 final WindowManager.LayoutParams params = new WindowManager.LayoutParams( 163 WindowManager.LayoutParams.WRAP_CONTENT, 164 WindowManager.LayoutParams.WRAP_CONTENT, 165 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, 166 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 167 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, 168 PixelFormat.TRANSLUCENT); 169 params.windowAnimations = android.R.style.Animation_Translucent; 170 params.gravity = Gravity.START | Gravity.TOP; 171 172 return params; 173 } 174 updateDimensions()175 private void updateDimensions() { 176 final Resources res = getResources(); 177 final DisplayMetrics dm = res.getDisplayMetrics(); 178 mScreenWidth = dm.widthPixels; 179 mArrowWidth = 180 res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_arrow_width); 181 mArrowHeight = 182 res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_arrow_height); 183 mArrowMargin = 184 res.getDimensionPixelSize( 185 R.dimen.accessibility_floating_tooltip_arrow_margin); 186 mArrowCornerRadius = 187 res.getDimensionPixelSize( 188 R.dimen.accessibility_floating_tooltip_arrow_corner_radius); 189 mFontSize = 190 res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_font_size); 191 mTextViewMargin = 192 res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_margin); 193 mTextViewPadding = 194 res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_padding); 195 mTextViewCornerRadius = 196 res.getDimensionPixelSize( 197 R.dimen.accessibility_floating_tooltip_text_corner_radius); 198 } 199 updateTooltipView()200 private void updateTooltipView() { 201 updateDimensions(); 202 updateTextView(); 203 204 final Rect anchorViewLocation = mAnchorView.getWindowLocationOnScreen(); 205 updateArrowWith(anchorViewLocation); 206 updateWidthWith(anchorViewLocation); 207 updateLocationWith(anchorViewLocation); 208 } 209 updateTextView()210 private void updateTextView() { 211 mTextView.setTextSize(COMPLEX_UNIT_PX, mFontSize); 212 mTextView.setPadding(mTextViewPadding, mTextViewPadding, mTextViewPadding, 213 mTextViewPadding); 214 215 final GradientDrawable gradientDrawable = (GradientDrawable) mTextView.getBackground(); 216 gradientDrawable.setCornerRadius(mTextViewCornerRadius); 217 gradientDrawable.setColor(Utils.getColorAttrDefaultColor(getContext(), 218 com.android.internal.R.attr.colorAccentPrimary)); 219 } 220 updateArrowWith(Rect anchorViewLocation)221 private void updateArrowWith(Rect anchorViewLocation) { 222 final boolean isAnchorViewOnLeft = isAnchorViewOnLeft(anchorViewLocation); 223 final View arrowView = findViewById(isAnchorViewOnLeft 224 ? R.id.arrow_left 225 : R.id.arrow_right); 226 arrowView.setVisibility(VISIBLE); 227 drawArrow(arrowView, isAnchorViewOnLeft); 228 229 final LinearLayout.LayoutParams layoutParams = 230 (LinearLayout.LayoutParams) arrowView.getLayoutParams(); 231 layoutParams.width = mArrowWidth; 232 layoutParams.height = mArrowHeight; 233 234 final int leftMargin = isAnchorViewOnLeft ? 0 : mArrowMargin; 235 final int rightMargin = isAnchorViewOnLeft ? mArrowMargin : 0; 236 layoutParams.setMargins(leftMargin, 0, rightMargin, 0); 237 arrowView.setLayoutParams(layoutParams); 238 } 239 updateWidthWith(Rect anchorViewLocation)240 private void updateWidthWith(Rect anchorViewLocation) { 241 final ViewGroup.LayoutParams layoutParams = mTextView.getLayoutParams(); 242 layoutParams.width = getTextWidthWith(anchorViewLocation); 243 mTextView.setLayoutParams(layoutParams); 244 } 245 updateLocationWith(Rect anchorViewLocation)246 private void updateLocationWith(Rect anchorViewLocation) { 247 mCurrentLayoutParams.x = isAnchorViewOnLeft(anchorViewLocation) 248 ? anchorViewLocation.width() 249 : mScreenWidth - getWindowWidthWith(anchorViewLocation) 250 - anchorViewLocation.width(); 251 mCurrentLayoutParams.y = 252 anchorViewLocation.centerY() - (getTextHeightWith(anchorViewLocation) / 2); 253 } 254 drawArrow(View view, boolean isPointingLeft)255 private void drawArrow(View view, boolean isPointingLeft) { 256 final ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); 257 final TriangleShape triangleShape = 258 TriangleShape.createHorizontal(layoutParams.width, layoutParams.height, 259 isPointingLeft); 260 final ShapeDrawable arrowDrawable = new ShapeDrawable(triangleShape); 261 final Paint arrowPaint = arrowDrawable.getPaint(); 262 arrowPaint.setColor(Utils.getColorAttrDefaultColor(getContext(), 263 com.android.internal.R.attr.colorAccentPrimary)); 264 final CornerPathEffect effect = new CornerPathEffect(mArrowCornerRadius); 265 arrowPaint.setPathEffect(effect); 266 view.setBackground(arrowDrawable); 267 } 268 isAnchorViewOnLeft(Rect anchorViewLocation)269 private boolean isAnchorViewOnLeft(Rect anchorViewLocation) { 270 return anchorViewLocation.left < (mScreenWidth / 2); 271 } 272 getTextWidthWith(Rect anchorViewLocation)273 private int getTextWidthWith(Rect anchorViewLocation) { 274 final int widthSpec = 275 MeasureSpec.makeMeasureSpec(getAvailableTextWidthWith(anchorViewLocation), AT_MOST); 276 final int heightSpec = 277 MeasureSpec.makeMeasureSpec(0, UNSPECIFIED); 278 mTextView.measure(widthSpec, heightSpec); 279 return mTextView.getMeasuredWidth(); 280 } 281 getTextHeightWith(Rect anchorViewLocation)282 private int getTextHeightWith(Rect anchorViewLocation) { 283 final int widthSpec = 284 MeasureSpec.makeMeasureSpec(getAvailableTextWidthWith(anchorViewLocation), AT_MOST); 285 final int heightSpec = 286 MeasureSpec.makeMeasureSpec(0, UNSPECIFIED); 287 mTextView.measure(widthSpec, heightSpec); 288 return mTextView.getMeasuredHeight(); 289 } 290 getAvailableTextWidthWith(Rect anchorViewLocation)291 private int getAvailableTextWidthWith(Rect anchorViewLocation) { 292 return mScreenWidth - anchorViewLocation.width() - mArrowWidth - mArrowMargin 293 - mTextViewMargin; 294 } 295 getWindowWidthWith(Rect anchorViewLocation)296 private int getWindowWidthWith(Rect anchorViewLocation) { 297 return getTextWidthWith(anchorViewLocation) + mArrowWidth + mArrowMargin; 298 } 299 } 300