/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.accessibility.floatingmenu; import static android.util.TypedValue.COMPLEX_UNIT_PX; import static android.view.View.MeasureSpec.AT_MOST; import static android.view.View.MeasureSpec.UNSPECIFIED; import android.annotation.UiContext; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.CornerPathEffect; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.ShapeDrawable; import android.os.Bundle; import android.text.method.MovementMethod; import android.util.DisplayMetrics; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; import com.android.settingslib.Utils; import com.android.systemui.R; import com.android.systemui.recents.TriangleShape; /** * Base tooltip view that shows the information about the operation of the * Accessibility floating menu. In addition, the anchor view is only for {@link * AccessibilityFloatingMenuView}, it should be more suited for displaying one-off menus to avoid * the performance hit for the extra window. */ class BaseTooltipView extends FrameLayout { private int mFontSize; private int mTextViewMargin; private int mTextViewPadding; private int mTextViewCornerRadius; private int mArrowMargin; private int mArrowWidth; private int mArrowHeight; private int mArrowCornerRadius; private int mScreenWidth; private boolean mIsShowing; private TextView mTextView; private final WindowManager.LayoutParams mCurrentLayoutParams; private final WindowManager mWindowManager; private final AccessibilityFloatingMenuView mAnchorView; BaseTooltipView(@UiContext Context context, AccessibilityFloatingMenuView anchorView) { super(context); mWindowManager = context.getSystemService(WindowManager.class); mAnchorView = anchorView; mCurrentLayoutParams = createDefaultLayoutParams(); initViews(); } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mAnchorView.onConfigurationChanged(newConfig); updateTooltipView(); mWindowManager.updateViewLayout(this, mCurrentLayoutParams); } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { hide(); } return super.onTouchEvent(event); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.addAction(AccessibilityAction.ACTION_DISMISS); } @Override public boolean performAccessibilityAction(int action, Bundle arguments) { if (action == AccessibilityAction.ACTION_DISMISS.getId()) { hide(); return true; } return super.performAccessibilityAction(action, arguments); } void show() { if (isShowing()) { return; } mIsShowing = true; updateTooltipView(); mWindowManager.addView(this, mCurrentLayoutParams); } void hide() { if (!isShowing()) { return; } mIsShowing = false; mWindowManager.removeView(this); } void setDescription(CharSequence text) { mTextView.setText(text); } void setMovementMethod(MovementMethod movement) { mTextView.setMovementMethod(movement); } private boolean isShowing() { return mIsShowing; } private void initViews() { final View contentView = LayoutInflater.from(getContext()).inflate( R.layout.accessibility_floating_menu_tooltip, this, false); mTextView = contentView.findViewById(R.id.text); addView(contentView); } private static WindowManager.LayoutParams createDefaultLayoutParams() { final WindowManager.LayoutParams params = new WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, PixelFormat.TRANSLUCENT); params.windowAnimations = android.R.style.Animation_Translucent; params.gravity = Gravity.START | Gravity.TOP; return params; } private void updateDimensions() { final Resources res = getResources(); final DisplayMetrics dm = res.getDisplayMetrics(); mScreenWidth = dm.widthPixels; mArrowWidth = res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_arrow_width); mArrowHeight = res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_arrow_height); mArrowMargin = res.getDimensionPixelSize( R.dimen.accessibility_floating_tooltip_arrow_margin); mArrowCornerRadius = res.getDimensionPixelSize( R.dimen.accessibility_floating_tooltip_arrow_corner_radius); mFontSize = res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_font_size); mTextViewMargin = res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_margin); mTextViewPadding = res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_padding); mTextViewCornerRadius = res.getDimensionPixelSize( R.dimen.accessibility_floating_tooltip_text_corner_radius); } private void updateTooltipView() { updateDimensions(); updateTextView(); final Rect anchorViewLocation = mAnchorView.getWindowLocationOnScreen(); updateArrowWith(anchorViewLocation); updateWidthWith(anchorViewLocation); updateLocationWith(anchorViewLocation); } private void updateTextView() { mTextView.setTextSize(COMPLEX_UNIT_PX, mFontSize); mTextView.setPadding(mTextViewPadding, mTextViewPadding, mTextViewPadding, mTextViewPadding); final GradientDrawable gradientDrawable = (GradientDrawable) mTextView.getBackground(); gradientDrawable.setCornerRadius(mTextViewCornerRadius); gradientDrawable.setColor(Utils.getColorAttrDefaultColor(getContext(), com.android.internal.R.attr.colorAccentPrimary)); } private void updateArrowWith(Rect anchorViewLocation) { final boolean isAnchorViewOnLeft = isAnchorViewOnLeft(anchorViewLocation); final View arrowView = findViewById(isAnchorViewOnLeft ? R.id.arrow_left : R.id.arrow_right); arrowView.setVisibility(VISIBLE); drawArrow(arrowView, isAnchorViewOnLeft); final LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) arrowView.getLayoutParams(); layoutParams.width = mArrowWidth; layoutParams.height = mArrowHeight; final int leftMargin = isAnchorViewOnLeft ? 0 : mArrowMargin; final int rightMargin = isAnchorViewOnLeft ? mArrowMargin : 0; layoutParams.setMargins(leftMargin, 0, rightMargin, 0); arrowView.setLayoutParams(layoutParams); } private void updateWidthWith(Rect anchorViewLocation) { final ViewGroup.LayoutParams layoutParams = mTextView.getLayoutParams(); layoutParams.width = getTextWidthWith(anchorViewLocation); mTextView.setLayoutParams(layoutParams); } private void updateLocationWith(Rect anchorViewLocation) { mCurrentLayoutParams.x = isAnchorViewOnLeft(anchorViewLocation) ? anchorViewLocation.width() : mScreenWidth - getWindowWidthWith(anchorViewLocation) - anchorViewLocation.width(); mCurrentLayoutParams.y = anchorViewLocation.centerY() - (getTextHeightWith(anchorViewLocation) / 2); } private void drawArrow(View view, boolean isPointingLeft) { final ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); final TriangleShape triangleShape = TriangleShape.createHorizontal(layoutParams.width, layoutParams.height, isPointingLeft); final ShapeDrawable arrowDrawable = new ShapeDrawable(triangleShape); final Paint arrowPaint = arrowDrawable.getPaint(); arrowPaint.setColor(Utils.getColorAttrDefaultColor(getContext(), com.android.internal.R.attr.colorAccentPrimary)); final CornerPathEffect effect = new CornerPathEffect(mArrowCornerRadius); arrowPaint.setPathEffect(effect); view.setBackground(arrowDrawable); } private boolean isAnchorViewOnLeft(Rect anchorViewLocation) { return anchorViewLocation.left < (mScreenWidth / 2); } private int getTextWidthWith(Rect anchorViewLocation) { final int widthSpec = MeasureSpec.makeMeasureSpec(getAvailableTextWidthWith(anchorViewLocation), AT_MOST); final int heightSpec = MeasureSpec.makeMeasureSpec(0, UNSPECIFIED); mTextView.measure(widthSpec, heightSpec); return mTextView.getMeasuredWidth(); } private int getTextHeightWith(Rect anchorViewLocation) { final int widthSpec = MeasureSpec.makeMeasureSpec(getAvailableTextWidthWith(anchorViewLocation), AT_MOST); final int heightSpec = MeasureSpec.makeMeasureSpec(0, UNSPECIFIED); mTextView.measure(widthSpec, heightSpec); return mTextView.getMeasuredHeight(); } private int getAvailableTextWidthWith(Rect anchorViewLocation) { return mScreenWidth - anchorViewLocation.width() - mArrowWidth - mArrowMargin - mTextViewMargin; } private int getWindowWidthWith(Rect anchorViewLocation) { return getTextWidthWith(anchorViewLocation) + mArrowWidth + mArrowMargin; } }