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