1 /* 2 * Copyright (C) 2022 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.view.ViewGroup.LayoutParams.WRAP_CONTENT; 20 21 import android.annotation.SuppressLint; 22 import android.content.ComponentCallbacks; 23 import android.content.Context; 24 import android.content.res.Configuration; 25 import android.graphics.PointF; 26 import android.graphics.Rect; 27 import android.graphics.drawable.GradientDrawable; 28 import android.view.ViewGroup; 29 import android.view.ViewTreeObserver; 30 import android.widget.FrameLayout; 31 32 import androidx.annotation.NonNull; 33 import androidx.lifecycle.Observer; 34 import androidx.recyclerview.widget.DiffUtil; 35 import androidx.recyclerview.widget.LinearLayoutManager; 36 import androidx.recyclerview.widget.RecyclerView; 37 38 import com.android.internal.accessibility.dialog.AccessibilityTarget; 39 import com.android.modules.expresslog.Counter; 40 import com.android.systemui.Flags; 41 import com.android.systemui.util.settings.SecureSettings; 42 43 import java.util.ArrayList; 44 import java.util.Collections; 45 import java.util.List; 46 47 /** 48 * The container view displays the accessibility features. 49 */ 50 @SuppressLint("ViewConstructor") 51 class MenuView extends FrameLayout implements 52 ViewTreeObserver.OnComputeInternalInsetsListener, ComponentCallbacks { 53 private static final int INDEX_MENU_ITEM = 0; 54 private final List<AccessibilityTarget> mTargetFeatures = new ArrayList<>(); 55 private final AccessibilityTargetAdapter mAdapter; 56 private final MenuViewModel mMenuViewModel; 57 private final Rect mBoundsInParent = new Rect(); 58 private final RecyclerView mTargetFeaturesView; 59 private final ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater = 60 this::updateSystemGestureExcludeRects; 61 private final Observer<MenuFadeEffectInfo> mFadeEffectInfoObserver = 62 this::onMenuFadeEffectInfoChanged; 63 private final Observer<Boolean> mMoveToTuckedObserver = this::onMoveToTucked; 64 private final Observer<Position> mPercentagePositionObserver = this::onPercentagePosition; 65 private final Observer<Integer> mSizeTypeObserver = this::onSizeTypeChanged; 66 private final Observer<List<AccessibilityTarget>> mTargetFeaturesObserver = 67 this::onTargetFeaturesChanged; 68 private final MenuViewAppearance mMenuViewAppearance; 69 70 private boolean mIsMoveToTucked; 71 72 private final MenuAnimationController mMenuAnimationController; 73 private OnTargetFeaturesChangeListener mFeaturesChangeListener; 74 private OnMoveToTuckedListener mMoveToTuckedListener; 75 private SecureSettings mSecureSettings; 76 MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance, SecureSettings secureSettings)77 MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance, 78 SecureSettings secureSettings) { 79 super(context); 80 81 mMenuViewModel = menuViewModel; 82 mMenuViewAppearance = menuViewAppearance; 83 mSecureSettings = secureSettings; 84 mMenuAnimationController = new MenuAnimationController(this, menuViewAppearance); 85 mAdapter = new AccessibilityTargetAdapter(mTargetFeatures); 86 mTargetFeaturesView = new RecyclerView(context); 87 mTargetFeaturesView.setAdapter(mAdapter); 88 mTargetFeaturesView.setLayoutManager(new LinearLayoutManager(context)); 89 setLayoutParams(new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); 90 // Avoid drawing out of bounds of the parent view 91 setClipToOutline(true); 92 93 loadLayoutResources(); 94 95 addView(mTargetFeaturesView); 96 97 setClickable(false); 98 setFocusable(false); 99 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 100 } 101 102 @Override onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)103 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { 104 inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 105 if (getVisibility() == VISIBLE) { 106 inoutInfo.touchableRegion.union(mBoundsInParent); 107 } 108 } 109 110 @Override onConfigurationChanged(@onNull Configuration newConfig)111 public void onConfigurationChanged(@NonNull Configuration newConfig) { 112 loadLayoutResources(); 113 114 mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode()); 115 } 116 117 @Override onLowMemory()118 public void onLowMemory() { 119 // Do nothing. 120 } 121 122 @Override onAttachedToWindow()123 protected void onAttachedToWindow() { 124 super.onAttachedToWindow(); 125 126 getContext().registerComponentCallbacks(this); 127 } 128 129 @Override onDetachedFromWindow()130 protected void onDetachedFromWindow() { 131 super.onDetachedFromWindow(); 132 133 getContext().unregisterComponentCallbacks(this); 134 } 135 setOnTargetFeaturesChangeListener(OnTargetFeaturesChangeListener listener)136 void setOnTargetFeaturesChangeListener(OnTargetFeaturesChangeListener listener) { 137 mFeaturesChangeListener = listener; 138 } 139 setMoveToTuckedListener(OnMoveToTuckedListener listener)140 void setMoveToTuckedListener(OnMoveToTuckedListener listener) { 141 mMoveToTuckedListener = listener; 142 } 143 addOnItemTouchListenerToList(RecyclerView.OnItemTouchListener listener)144 void addOnItemTouchListenerToList(RecyclerView.OnItemTouchListener listener) { 145 mTargetFeaturesView.addOnItemTouchListener(listener); 146 } 147 getMenuAnimationController()148 MenuAnimationController getMenuAnimationController() { 149 return mMenuAnimationController; 150 } 151 152 @SuppressLint("NotifyDataSetChanged") onItemSizeChanged()153 private void onItemSizeChanged() { 154 mAdapter.setItemPadding(mMenuViewAppearance.getMenuPadding()); 155 mAdapter.setIconWidthHeight(mMenuViewAppearance.getMenuIconSize()); 156 mAdapter.notifyDataSetChanged(); 157 } 158 onSizeChanged()159 private void onSizeChanged() { 160 mBoundsInParent.set(mBoundsInParent.left, mBoundsInParent.top, 161 mBoundsInParent.left + mMenuViewAppearance.getMenuWidth(), 162 mBoundsInParent.top + mMenuViewAppearance.getMenuHeight()); 163 164 final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams(); 165 layoutParams.height = mMenuViewAppearance.getMenuHeight(); 166 setLayoutParams(layoutParams); 167 } 168 onEdgeChangedIfNeeded()169 void onEdgeChangedIfNeeded() { 170 final Rect draggableBounds = mMenuViewAppearance.getMenuDraggableBounds(); 171 if (getTranslationX() != draggableBounds.left 172 && getTranslationX() != draggableBounds.right) { 173 return; 174 } 175 176 onEdgeChanged(); 177 } 178 onEdgeChanged()179 void onEdgeChanged() { 180 final int[] insets = mMenuViewAppearance.getMenuInsets(); 181 getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2], 182 insets[3]); 183 184 final GradientDrawable gradientDrawable = getContainerViewGradient(); 185 gradientDrawable.setStroke(mMenuViewAppearance.getMenuStrokeWidth(), 186 mMenuViewAppearance.getMenuStrokeColor()); 187 mMenuAnimationController.startRadiiAnimation(mMenuViewAppearance.getMenuRadii()); 188 } 189 setRadii(float[] radii)190 void setRadii(float[] radii) { 191 getContainerViewGradient().setCornerRadii(radii); 192 } 193 onMoveToTucked(boolean isMoveToTucked)194 private void onMoveToTucked(boolean isMoveToTucked) { 195 mIsMoveToTucked = isMoveToTucked; 196 197 onPositionChanged(); 198 } 199 onPercentagePosition(Position percentagePosition)200 private void onPercentagePosition(Position percentagePosition) { 201 mMenuViewAppearance.setPercentagePosition(percentagePosition); 202 203 onPositionChanged(); 204 } 205 onPositionChanged()206 void onPositionChanged() { 207 onPositionChanged(/* animateMovement = */ false); 208 } 209 onPositionChanged(boolean animateMovement)210 void onPositionChanged(boolean animateMovement) { 211 final PointF position; 212 if (isMoveToTucked()) { 213 position = mMenuAnimationController.getTuckedMenuPosition(); 214 } else { 215 position = getMenuPosition(); 216 } 217 218 // We can skip animating if FAB is not visible 219 if (animateMovement && getVisibility() == VISIBLE) { 220 mMenuAnimationController.moveToPosition(position, /* animateMovement = */ true); 221 // onArrivalAtPosition() is called at the end of the animation. 222 } else { 223 mMenuAnimationController.moveToPosition(position); 224 onArrivalAtPosition(true); // no animation, so we call this immediately. 225 } 226 } 227 onArrivalAtPosition(boolean moveToEdgeIfTucked)228 void onArrivalAtPosition(boolean moveToEdgeIfTucked) { 229 final PointF position = getMenuPosition(); 230 onBoundsInParentChanged((int) position.x, (int) position.y); 231 232 if (isMoveToTucked() && moveToEdgeIfTucked) { 233 mMenuAnimationController.moveToEdgeAndHide(); 234 } 235 } 236 237 @SuppressLint("NotifyDataSetChanged") onSizeTypeChanged(int newSizeType)238 private void onSizeTypeChanged(int newSizeType) { 239 mMenuAnimationController.fadeInNowIfEnabled(); 240 241 mMenuViewAppearance.setSizeType(newSizeType); 242 243 mAdapter.setItemPadding(mMenuViewAppearance.getMenuPadding()); 244 mAdapter.setIconWidthHeight(mMenuViewAppearance.getMenuIconSize()); 245 mAdapter.notifyDataSetChanged(); 246 247 onSizeChanged(); 248 onEdgeChanged(); 249 onPositionChanged(); 250 251 mMenuAnimationController.fadeOutIfEnabled(); 252 } 253 onTargetFeaturesChanged(List<AccessibilityTarget> newTargetFeatures)254 private void onTargetFeaturesChanged(List<AccessibilityTarget> newTargetFeatures) { 255 mMenuAnimationController.fadeInNowIfEnabled(); 256 257 final List<AccessibilityTarget> targetFeatures = 258 Collections.unmodifiableList(mTargetFeatures.stream().toList()); 259 mTargetFeatures.clear(); 260 mTargetFeatures.addAll(newTargetFeatures); 261 mMenuViewAppearance.setTargetFeaturesSize(newTargetFeatures.size()); 262 mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode()); 263 DiffUtil.calculateDiff( 264 new MenuTargetsCallback(targetFeatures, newTargetFeatures)).dispatchUpdatesTo( 265 mAdapter); 266 267 onSizeChanged(); 268 onEdgeChanged(); 269 onPositionChanged(); 270 271 if (mFeaturesChangeListener != null) { 272 mFeaturesChangeListener.onChange(newTargetFeatures); 273 } 274 275 mMenuAnimationController.fadeOutIfEnabled(); 276 } 277 onMenuFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo)278 private void onMenuFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo) { 279 mMenuAnimationController.updateOpacityWith(fadeEffectInfo.isFadeEffectEnabled(), 280 fadeEffectInfo.getOpacity()); 281 } 282 getMenuDraggableBounds()283 Rect getMenuDraggableBounds() { 284 return mMenuViewAppearance.getMenuDraggableBounds(); 285 } 286 getMenuDraggableBoundsExcludeIme()287 Rect getMenuDraggableBoundsExcludeIme() { 288 return mMenuViewAppearance.getMenuDraggableBoundsExcludeIme(); 289 } 290 getMenuHeight()291 int getMenuHeight() { 292 return mMenuViewAppearance.getMenuHeight(); 293 } 294 getMenuWidth()295 int getMenuWidth() { 296 return mMenuViewAppearance.getMenuWidth(); 297 } 298 getMenuPosition()299 PointF getMenuPosition() { 300 return mMenuViewAppearance.getMenuPosition(); 301 } 302 getTargetFeaturesView()303 RecyclerView getTargetFeaturesView() { 304 return mTargetFeaturesView; 305 } 306 persistPositionAndUpdateEdge(Position percentagePosition)307 void persistPositionAndUpdateEdge(Position percentagePosition) { 308 mMenuViewModel.updateMenuSavingPosition(percentagePosition); 309 mMenuViewAppearance.setPercentagePosition(percentagePosition); 310 311 onEdgeChangedIfNeeded(); 312 } 313 isMoveToTucked()314 boolean isMoveToTucked() { 315 return mIsMoveToTucked; 316 } 317 updateMenuMoveToTucked(boolean isMoveToTucked)318 void updateMenuMoveToTucked(boolean isMoveToTucked) { 319 mIsMoveToTucked = isMoveToTucked; 320 mMenuViewModel.updateMenuMoveToTucked(isMoveToTucked); 321 if (mMoveToTuckedListener != null) { 322 mMoveToTuckedListener.onMoveToTuckedChanged(isMoveToTucked); 323 } 324 } 325 326 327 /** 328 * Uses the touch events from the parent view to identify if users clicked the extra 329 * space of the menu view. If yes, will use the percentage position and update the 330 * translations of the menu view to meet the effect of moving out from the edge. It’s only 331 * used when the menu view is hidden to the screen edge. 332 * 333 * @param x the current x of the touch event from the parent {@link MenuViewLayer} of the 334 * {@link MenuView}. 335 * @param y the current y of the touch event from the parent {@link MenuViewLayer} of the 336 * {@link MenuView}. 337 * @return true if consume the touch event, otherwise false. 338 */ maybeMoveOutEdgeAndShow(int x, int y)339 boolean maybeMoveOutEdgeAndShow(int x, int y) { 340 // Utilizes the touch region of the parent view to implement that users could tap extra 341 // the space region to show the menu from the edge. 342 if (!isMoveToTucked() || !mBoundsInParent.contains(x, y)) { 343 return false; 344 } 345 346 mMenuAnimationController.fadeInNowIfEnabled(); 347 348 mMenuAnimationController.moveOutEdgeAndShow(); 349 350 mMenuAnimationController.fadeOutIfEnabled(); 351 return true; 352 } 353 show()354 void show() { 355 mMenuViewModel.getPercentagePositionData().observeForever(mPercentagePositionObserver); 356 mMenuViewModel.getFadeEffectInfoData().observeForever(mFadeEffectInfoObserver); 357 mMenuViewModel.getTargetFeaturesData().observeForever(mTargetFeaturesObserver); 358 mMenuViewModel.getSizeTypeData().observeForever(mSizeTypeObserver); 359 mMenuViewModel.getMoveToTuckedData().observeForever(mMoveToTuckedObserver); 360 setVisibility(VISIBLE); 361 mMenuViewModel.registerObserversAndCallbacks(); 362 getViewTreeObserver().addOnComputeInternalInsetsListener(this); 363 getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater); 364 } 365 hide()366 void hide() { 367 setVisibility(GONE); 368 mBoundsInParent.setEmpty(); 369 mMenuViewModel.getPercentagePositionData().removeObserver(mPercentagePositionObserver); 370 mMenuViewModel.getFadeEffectInfoData().removeObserver(mFadeEffectInfoObserver); 371 mMenuViewModel.getTargetFeaturesData().removeObserver(mTargetFeaturesObserver); 372 mMenuViewModel.getSizeTypeData().removeObserver(mSizeTypeObserver); 373 mMenuViewModel.getMoveToTuckedData().removeObserver(mMoveToTuckedObserver); 374 mMenuViewModel.unregisterObserversAndCallbacks(); 375 getViewTreeObserver().removeOnComputeInternalInsetsListener(this); 376 getViewTreeObserver().removeOnDrawListener(mSystemGestureExcludeUpdater); 377 } 378 onDraggingStart()379 void onDraggingStart() { 380 final int[] insets = mMenuViewAppearance.getMenuMovingStateInsets(); 381 getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2], 382 insets[3]); 383 384 mMenuAnimationController.startRadiiAnimation( 385 mMenuViewAppearance.getMenuMovingStateRadii()); 386 } 387 onBoundsInParentChanged(int newLeft, int newTop)388 void onBoundsInParentChanged(int newLeft, int newTop) { 389 mBoundsInParent.offsetTo(newLeft, newTop); 390 } 391 loadLayoutResources()392 void loadLayoutResources() { 393 mMenuViewAppearance.update(); 394 395 mTargetFeaturesView.setContentDescription(mMenuViewAppearance.getContentDescription()); 396 setBackground(mMenuViewAppearance.getMenuBackground()); 397 setElevation(mMenuViewAppearance.getMenuElevation()); 398 onItemSizeChanged(); 399 onSizeChanged(); 400 onEdgeChanged(); 401 onPositionChanged(); 402 } 403 incrementTexMetric(String metric)404 void incrementTexMetric(String metric) { 405 if (!Flags.floatingMenuDragToEdit()) { 406 return; 407 } 408 Counter.logIncrement(metric); 409 } 410 getContainerViewInsetLayer()411 private InstantInsetLayerDrawable getContainerViewInsetLayer() { 412 return (InstantInsetLayerDrawable) getBackground(); 413 } 414 getContainerViewGradient()415 private GradientDrawable getContainerViewGradient() { 416 return (GradientDrawable) getContainerViewInsetLayer().getDrawable(INDEX_MENU_ITEM); 417 } 418 updateSystemGestureExcludeRects()419 private void updateSystemGestureExcludeRects() { 420 final ViewGroup parentView = (ViewGroup) getParent(); 421 parentView.setSystemGestureExclusionRects(Collections.singletonList(mBoundsInParent)); 422 } 423 424 /** 425 * Interface definition for the {@link AccessibilityTarget} list changes. 426 */ 427 interface OnTargetFeaturesChangeListener { 428 /** 429 * Called when the list of accessibility target features was updated. This will be 430 * invoked when the end of {@code onTargetFeaturesChanged}. 431 * 432 * @param newTargetFeatures the list related to the current accessibility features. 433 */ onChange(List<AccessibilityTarget> newTargetFeatures)434 void onChange(List<AccessibilityTarget> newTargetFeatures); 435 } 436 437 /** 438 * Interface containing a callback for when MoveToTucked changes. 439 */ 440 interface OnMoveToTuckedListener { onMoveToTuckedChanged(boolean moveToTucked)441 void onMoveToTuckedChanged(boolean moveToTucked); 442 } 443 } 444