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.core.view.AccessibilityDelegateCompat; 34 import androidx.lifecycle.Observer; 35 import androidx.recyclerview.widget.DiffUtil; 36 import androidx.recyclerview.widget.LinearLayoutManager; 37 import androidx.recyclerview.widget.RecyclerView; 38 import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; 39 40 import com.android.internal.accessibility.dialog.AccessibilityTarget; 41 42 import java.util.ArrayList; 43 import java.util.Collections; 44 import java.util.List; 45 46 /** 47 * The container view displays the accessibility features. 48 */ 49 @SuppressLint("ViewConstructor") 50 class MenuView extends FrameLayout implements 51 ViewTreeObserver.OnComputeInternalInsetsListener, ComponentCallbacks { 52 private static final int INDEX_MENU_ITEM = 0; 53 private final List<AccessibilityTarget> mTargetFeatures = new ArrayList<>(); 54 private final AccessibilityTargetAdapter mAdapter; 55 private final MenuViewModel mMenuViewModel; 56 private final MenuAnimationController mMenuAnimationController; 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 OnTargetFeaturesChangeListener mFeaturesChangeListener; 73 MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance)74 MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance) { 75 super(context); 76 77 mMenuViewModel = menuViewModel; 78 mMenuViewAppearance = menuViewAppearance; 79 mMenuAnimationController = new MenuAnimationController(this); 80 mAdapter = new AccessibilityTargetAdapter(mTargetFeatures); 81 mTargetFeaturesView = new RecyclerView(context); 82 mTargetFeaturesView.setAdapter(mAdapter); 83 mTargetFeaturesView.setLayoutManager(new LinearLayoutManager(context)); 84 mTargetFeaturesView.setAccessibilityDelegateCompat( 85 new RecyclerViewAccessibilityDelegate(mTargetFeaturesView) { 86 @NonNull 87 @Override 88 public AccessibilityDelegateCompat getItemDelegate() { 89 return new MenuItemAccessibilityDelegate(/* recyclerViewDelegate= */ this, 90 mMenuAnimationController); 91 } 92 }); 93 setLayoutParams(new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); 94 // Avoid drawing out of bounds of the parent view 95 setClipToOutline(true); 96 97 loadLayoutResources(); 98 99 addView(mTargetFeaturesView); 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 addOnItemTouchListenerToList(RecyclerView.OnItemTouchListener listener)140 void addOnItemTouchListenerToList(RecyclerView.OnItemTouchListener listener) { 141 mTargetFeaturesView.addOnItemTouchListener(listener); 142 } 143 getMenuAnimationController()144 MenuAnimationController getMenuAnimationController() { 145 return mMenuAnimationController; 146 } 147 148 @SuppressLint("NotifyDataSetChanged") onItemSizeChanged()149 private void onItemSizeChanged() { 150 mAdapter.setItemPadding(mMenuViewAppearance.getMenuPadding()); 151 mAdapter.setIconWidthHeight(mMenuViewAppearance.getMenuIconSize()); 152 mAdapter.notifyDataSetChanged(); 153 } 154 onSizeChanged()155 private void onSizeChanged() { 156 mBoundsInParent.set(mBoundsInParent.left, mBoundsInParent.top, 157 mBoundsInParent.left + mMenuViewAppearance.getMenuWidth(), 158 mBoundsInParent.top + mMenuViewAppearance.getMenuHeight()); 159 160 final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams(); 161 layoutParams.height = mMenuViewAppearance.getMenuHeight(); 162 setLayoutParams(layoutParams); 163 } 164 onEdgeChangedIfNeeded()165 void onEdgeChangedIfNeeded() { 166 final Rect draggableBounds = mMenuViewAppearance.getMenuDraggableBounds(); 167 if (getTranslationX() != draggableBounds.left 168 && getTranslationX() != draggableBounds.right) { 169 return; 170 } 171 172 onEdgeChanged(); 173 } 174 onEdgeChanged()175 void onEdgeChanged() { 176 final int[] insets = mMenuViewAppearance.getMenuInsets(); 177 getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2], 178 insets[3]); 179 180 final GradientDrawable gradientDrawable = getContainerViewGradient(); 181 gradientDrawable.setCornerRadii(mMenuViewAppearance.getMenuRadii()); 182 gradientDrawable.setStroke(mMenuViewAppearance.getMenuStrokeWidth(), 183 mMenuViewAppearance.getMenuStrokeColor()); 184 } 185 onMoveToTucked(boolean isMoveToTucked)186 private void onMoveToTucked(boolean isMoveToTucked) { 187 mIsMoveToTucked = isMoveToTucked; 188 189 onPositionChanged(); 190 } 191 onPercentagePosition(Position percentagePosition)192 private void onPercentagePosition(Position percentagePosition) { 193 mMenuViewAppearance.setPercentagePosition(percentagePosition); 194 195 onPositionChanged(); 196 } 197 onPositionChanged()198 void onPositionChanged() { 199 final PointF position = mMenuViewAppearance.getMenuPosition(); 200 mMenuAnimationController.moveToPosition(position); 201 onBoundsInParentChanged((int) position.x, (int) position.y); 202 203 if (isMoveToTucked()) { 204 mMenuAnimationController.moveToEdgeAndHide(); 205 } 206 } 207 208 @SuppressLint("NotifyDataSetChanged") onSizeTypeChanged(int newSizeType)209 private void onSizeTypeChanged(int newSizeType) { 210 mMenuAnimationController.fadeInNowIfEnabled(); 211 212 mMenuViewAppearance.setSizeType(newSizeType); 213 214 mAdapter.setItemPadding(mMenuViewAppearance.getMenuPadding()); 215 mAdapter.setIconWidthHeight(mMenuViewAppearance.getMenuIconSize()); 216 mAdapter.notifyDataSetChanged(); 217 218 onSizeChanged(); 219 onEdgeChanged(); 220 onPositionChanged(); 221 222 mMenuAnimationController.fadeOutIfEnabled(); 223 } 224 onTargetFeaturesChanged(List<AccessibilityTarget> newTargetFeatures)225 private void onTargetFeaturesChanged(List<AccessibilityTarget> newTargetFeatures) { 226 mMenuAnimationController.fadeInNowIfEnabled(); 227 228 final List<AccessibilityTarget> targetFeatures = 229 Collections.unmodifiableList(mTargetFeatures.stream().toList()); 230 mTargetFeatures.clear(); 231 mTargetFeatures.addAll(newTargetFeatures); 232 mMenuViewAppearance.setTargetFeaturesSize(newTargetFeatures.size()); 233 mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode()); 234 DiffUtil.calculateDiff( 235 new MenuTargetsCallback(targetFeatures, newTargetFeatures)).dispatchUpdatesTo( 236 mAdapter); 237 238 onSizeChanged(); 239 onEdgeChanged(); 240 onPositionChanged(); 241 242 if (mFeaturesChangeListener != null) { 243 mFeaturesChangeListener.onChange(newTargetFeatures); 244 } 245 mMenuAnimationController.fadeOutIfEnabled(); 246 } 247 onMenuFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo)248 private void onMenuFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo) { 249 mMenuAnimationController.updateOpacityWith(fadeEffectInfo.isFadeEffectEnabled(), 250 fadeEffectInfo.getOpacity()); 251 } 252 getMenuDraggableBounds()253 Rect getMenuDraggableBounds() { 254 return mMenuViewAppearance.getMenuDraggableBounds(); 255 } 256 getMenuDraggableBoundsExcludeIme()257 Rect getMenuDraggableBoundsExcludeIme() { 258 return mMenuViewAppearance.getMenuDraggableBoundsExcludeIme(); 259 } 260 getMenuHeight()261 int getMenuHeight() { 262 return mMenuViewAppearance.getMenuHeight(); 263 } 264 getMenuWidth()265 int getMenuWidth() { 266 return mMenuViewAppearance.getMenuWidth(); 267 } 268 getMenuPosition()269 PointF getMenuPosition() { 270 return mMenuViewAppearance.getMenuPosition(); 271 } 272 persistPositionAndUpdateEdge(Position percentagePosition)273 void persistPositionAndUpdateEdge(Position percentagePosition) { 274 mMenuViewModel.updateMenuSavingPosition(percentagePosition); 275 mMenuViewAppearance.setPercentagePosition(percentagePosition); 276 277 onEdgeChangedIfNeeded(); 278 } 279 isMoveToTucked()280 boolean isMoveToTucked() { 281 return mIsMoveToTucked; 282 } 283 updateMenuMoveToTucked(boolean isMoveToTucked)284 void updateMenuMoveToTucked(boolean isMoveToTucked) { 285 mIsMoveToTucked = isMoveToTucked; 286 mMenuViewModel.updateMenuMoveToTucked(isMoveToTucked); 287 } 288 289 290 /** 291 * Uses the touch events from the parent view to identify if users clicked the extra 292 * space of the menu view. If yes, will use the percentage position and update the 293 * translations of the menu view to meet the effect of moving out from the edge. It’s only 294 * used when the menu view is hidden to the screen edge. 295 * 296 * @param x the current x of the touch event from the parent {@link MenuViewLayer} of the 297 * {@link MenuView}. 298 * @param y the current y of the touch event from the parent {@link MenuViewLayer} of the 299 * {@link MenuView}. 300 * @return true if consume the touch event, otherwise false. 301 */ maybeMoveOutEdgeAndShow(int x, int y)302 boolean maybeMoveOutEdgeAndShow(int x, int y) { 303 // Utilizes the touch region of the parent view to implement that users could tap extra 304 // the space region to show the menu from the edge. 305 if (!isMoveToTucked() || !mBoundsInParent.contains(x, y)) { 306 return false; 307 } 308 309 mMenuAnimationController.fadeInNowIfEnabled(); 310 311 mMenuAnimationController.moveOutEdgeAndShow(); 312 313 mMenuAnimationController.fadeOutIfEnabled(); 314 return true; 315 } 316 show()317 void show() { 318 mMenuViewModel.getPercentagePositionData().observeForever(mPercentagePositionObserver); 319 mMenuViewModel.getFadeEffectInfoData().observeForever(mFadeEffectInfoObserver); 320 mMenuViewModel.getTargetFeaturesData().observeForever(mTargetFeaturesObserver); 321 mMenuViewModel.getSizeTypeData().observeForever(mSizeTypeObserver); 322 mMenuViewModel.getMoveToTuckedData().observeForever(mMoveToTuckedObserver); 323 setVisibility(VISIBLE); 324 mMenuViewModel.registerObserversAndCallbacks(); 325 getViewTreeObserver().addOnComputeInternalInsetsListener(this); 326 getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater); 327 } 328 hide()329 void hide() { 330 setVisibility(GONE); 331 mBoundsInParent.setEmpty(); 332 mMenuViewModel.getPercentagePositionData().removeObserver(mPercentagePositionObserver); 333 mMenuViewModel.getFadeEffectInfoData().removeObserver(mFadeEffectInfoObserver); 334 mMenuViewModel.getTargetFeaturesData().removeObserver(mTargetFeaturesObserver); 335 mMenuViewModel.getSizeTypeData().removeObserver(mSizeTypeObserver); 336 mMenuViewModel.getMoveToTuckedData().removeObserver(mMoveToTuckedObserver); 337 mMenuViewModel.unregisterObserversAndCallbacks(); 338 getViewTreeObserver().removeOnComputeInternalInsetsListener(this); 339 getViewTreeObserver().removeOnDrawListener(mSystemGestureExcludeUpdater); 340 } 341 onDraggingStart()342 void onDraggingStart() { 343 final int[] insets = mMenuViewAppearance.getMenuMovingStateInsets(); 344 getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2], 345 insets[3]); 346 347 final GradientDrawable gradientDrawable = getContainerViewGradient(); 348 gradientDrawable.setCornerRadii(mMenuViewAppearance.getMenuMovingStateRadii()); 349 } 350 onBoundsInParentChanged(int newLeft, int newTop)351 void onBoundsInParentChanged(int newLeft, int newTop) { 352 mBoundsInParent.offsetTo(newLeft, newTop); 353 } 354 loadLayoutResources()355 void loadLayoutResources() { 356 mMenuViewAppearance.update(); 357 358 mTargetFeaturesView.setContentDescription(mMenuViewAppearance.getContentDescription()); 359 setBackground(mMenuViewAppearance.getMenuBackground()); 360 setElevation(mMenuViewAppearance.getMenuElevation()); 361 onItemSizeChanged(); 362 onSizeChanged(); 363 onEdgeChanged(); 364 onPositionChanged(); 365 } 366 getContainerViewInsetLayer()367 private InstantInsetLayerDrawable getContainerViewInsetLayer() { 368 return (InstantInsetLayerDrawable) getBackground(); 369 } 370 getContainerViewGradient()371 private GradientDrawable getContainerViewGradient() { 372 return (GradientDrawable) getContainerViewInsetLayer().getDrawable(INDEX_MENU_ITEM); 373 } 374 updateSystemGestureExcludeRects()375 private void updateSystemGestureExcludeRects() { 376 final ViewGroup parentView = (ViewGroup) getParent(); 377 parentView.setSystemGestureExclusionRects(Collections.singletonList(mBoundsInParent)); 378 } 379 380 /** 381 * Interface definition for the {@link AccessibilityTarget} list changes. 382 */ 383 interface OnTargetFeaturesChangeListener { 384 /** 385 * Called when the list of accessibility target features was updated. This will be 386 * invoked when the end of {@code onTargetFeaturesChanged}. 387 * 388 * @param newTargetFeatures the list related to the current accessibility features. 389 */ onChange(List<AccessibilityTarget> newTargetFeatures)390 void onChange(List<AccessibilityTarget> newTargetFeatures); 391 } 392 } 393