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.R.id.empty;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.animation.ValueAnimator;
24 import android.util.ArrayMap;
25 import android.util.Pair;
26 import android.view.MotionEvent;
27 
28 import androidx.annotation.NonNull;
29 import androidx.dynamicanimation.animation.DynamicAnimation;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.systemui.Flags;
33 import com.android.wm.shell.common.bubbles.DismissCircleView;
34 import com.android.wm.shell.common.bubbles.DismissView;
35 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
36 
37 import java.util.Map;
38 import java.util.Objects;
39 
40 /**
41  * Controls the interaction between {@link MagnetizedObject} and
42  * {@link MagnetizedObject.MagneticTarget}.
43  */
44 class DragToInteractAnimationController {
45     private static final float COMPLETELY_OPAQUE = 1.0f;
46     private static final float COMPLETELY_TRANSPARENT = 0.0f;
47     private static final float CIRCLE_VIEW_DEFAULT_SCALE = 1.0f;
48     private static final float ANIMATING_MAX_ALPHA = 0.7f;
49 
50     private final DragToInteractView mInteractView;
51     private final DismissView mDismissView;
52     private final MenuView mMenuView;
53 
54     /**
55      * MagnetizedObject cannot differentiate between its MagnetizedTargets,
56      * so we need an object & an animator for every interactable.
57      */
58     private final ArrayMap<Integer, Pair<MagnetizedObject<MenuView>, ValueAnimator>> mInteractMap;
59 
60     private float mMinInteractSize;
61     private float mSizePercent;
62 
DragToInteractAnimationController(DragToInteractView interactView, MenuView menuView)63     DragToInteractAnimationController(DragToInteractView interactView, MenuView menuView) {
64         mDismissView = null;
65         mInteractView = interactView;
66         mInteractView.setPivotX(interactView.getWidth() / 2.0f);
67         mInteractView.setPivotY(interactView.getHeight() / 2.0f);
68         mMenuView = menuView;
69 
70         updateResources();
71 
72         mInteractMap = new ArrayMap<>();
73         interactView.getInteractMap().forEach((viewId, pair) -> {
74             DismissCircleView circleView = pair.getFirst();
75             createMagnetizedObjectAndAnimator(circleView);
76         });
77     }
78 
DragToInteractAnimationController(DismissView dismissView, MenuView menuView)79     DragToInteractAnimationController(DismissView dismissView, MenuView menuView) {
80         mDismissView = dismissView;
81         mInteractView = null;
82         mDismissView.setPivotX(dismissView.getWidth() / 2.0f);
83         mDismissView.setPivotY(dismissView.getHeight() / 2.0f);
84         mMenuView = menuView;
85 
86         updateResources();
87 
88         mInteractMap = new ArrayMap<>();
89         createMagnetizedObjectAndAnimator(dismissView.getCircle());
90     }
91 
showInteractView(boolean show)92     void showInteractView(boolean show) {
93         if (Flags.floatingMenuDragToEdit() && mInteractView != null) {
94             if (show) {
95                 mInteractView.show();
96             } else {
97                 mInteractView.hide();
98             }
99         } else if (mDismissView != null) {
100             if (show) {
101                 mDismissView.show();
102             } else {
103                 mDismissView.hide();
104             }
105         }
106     }
107 
setMagnetListener(MagnetizedObject.MagnetListener magnetListener)108     void setMagnetListener(MagnetizedObject.MagnetListener magnetListener) {
109         mInteractMap.forEach((viewId, pair) -> {
110             MagnetizedObject<?> magnetizedObject = pair.first;
111             magnetizedObject.setMagnetListener(magnetListener);
112         });
113     }
114 
115     @VisibleForTesting
getMagnetListener(int id)116     MagnetizedObject.MagnetListener getMagnetListener(int id) {
117         return Objects.requireNonNull(mInteractMap.get(id)).first.getMagnetListener();
118     }
119 
maybeConsumeDownMotionEvent(MotionEvent event)120     void maybeConsumeDownMotionEvent(MotionEvent event) {
121         mInteractMap.forEach((viewId, pair) -> {
122             MagnetizedObject<?> magnetizedObject = pair.first;
123             magnetizedObject.maybeConsumeMotionEvent(event);
124         });
125     }
126 
maybeConsumeMotionEvent(MotionEvent event)127     private int maybeConsumeMotionEvent(MotionEvent event) {
128         for (Map.Entry<Integer, Pair<MagnetizedObject<MenuView>, ValueAnimator>> set:
129                 mInteractMap.entrySet()) {
130             MagnetizedObject<MenuView> magnetizedObject = set.getValue().first;
131             if (magnetizedObject.maybeConsumeMotionEvent(event)) {
132                 return set.getKey();
133             }
134         }
135         return empty;
136     }
137 
138     /**
139      * This used to pass {@link MotionEvent#ACTION_DOWN} to the magnetized objects
140      * to check if it was within a magnetic field.
141      * It should be used in the {@link MenuListViewTouchHandler}.
142      *
143      * @param event that move the magnetized object which is also the menu list view.
144      * @return id of a target if the location of the motion events moves
145      * within the field of the target, otherwise it returns{@link android.R.id#empty}.
146      * <p>
147      * {@link DragToInteractAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}.
148      */
maybeConsumeMoveMotionEvent(MotionEvent event)149     int maybeConsumeMoveMotionEvent(MotionEvent event) {
150         return maybeConsumeMotionEvent(event);
151     }
152 
153     /**
154      * This used to pass {@link MotionEvent#ACTION_UP} to the magnetized object to check if it was
155      * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}.
156      *
157      * @param event that move the magnetized object which is also the menu list view.
158      * @return id of a target if the location of the motion events moves
159      * within the field of the target, otherwise it returns{@link android.R.id#empty}.
160      * {@link DragToInteractAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}.
161      */
maybeConsumeUpMotionEvent(MotionEvent event)162     int maybeConsumeUpMotionEvent(MotionEvent event) {
163         return maybeConsumeMotionEvent(event);
164     }
165 
animateInteractMenu(int targetViewId, boolean scaleUp)166     void animateInteractMenu(int targetViewId, boolean scaleUp) {
167         Pair<MagnetizedObject<MenuView>, ValueAnimator> value = mInteractMap.get(targetViewId);
168         if (value == null) {
169             return;
170         }
171         ValueAnimator animator = value.second;
172         if (scaleUp) {
173             animator.start();
174         } else {
175             animator.reverse();
176         }
177     }
178 
updateResources()179     void updateResources() {
180         final float maxInteractSize = mMenuView.getResources().getDimensionPixelSize(
181                 com.android.wm.shell.R.dimen.dismiss_circle_size);
182         mMinInteractSize = mMenuView.getResources().getDimensionPixelSize(
183                 com.android.wm.shell.R.dimen.dismiss_circle_small);
184         mSizePercent = mMinInteractSize / maxInteractSize;
185     }
186 
187     /**
188      * Creates a magnetizedObject & valueAnimator pair for the provided circleView,
189      * and adds them to the interactMap.
190      *
191      * @param circleView circleView to create objects for.
192      */
createMagnetizedObjectAndAnimator(DismissCircleView circleView)193     private void createMagnetizedObjectAndAnimator(DismissCircleView circleView) {
194         MagnetizedObject<MenuView> magnetizedObject = new MagnetizedObject<MenuView>(
195                 mMenuView.getContext(), mMenuView,
196                 new MenuAnimationController.MenuPositionProperty(
197                         DynamicAnimation.TRANSLATION_X),
198                 new MenuAnimationController.MenuPositionProperty(
199                         DynamicAnimation.TRANSLATION_Y)) {
200             @Override
201             public void getLocationOnScreen(MenuView underlyingObject, @NonNull int[] loc) {
202                 underlyingObject.getLocationOnScreen(loc);
203             }
204 
205             @Override
206             public float getHeight(MenuView underlyingObject) {
207                 return underlyingObject.getHeight();
208             }
209 
210             @Override
211             public float getWidth(MenuView underlyingObject) {
212                 return underlyingObject.getWidth();
213             }
214         };
215         // Avoid unintended selection of an object / option
216         magnetizedObject.setFlingToTargetEnabled(false);
217         magnetizedObject.addTarget(new MagnetizedObject.MagneticTarget(
218                 circleView, (int) mMinInteractSize));
219 
220         final ValueAnimator animator =
221                 ValueAnimator.ofFloat(COMPLETELY_OPAQUE, COMPLETELY_TRANSPARENT);
222 
223         animator.addUpdateListener(dismissAnimation -> {
224             final float animatedValue = (float) dismissAnimation.getAnimatedValue();
225             final float scaleValue = Math.max(animatedValue, mSizePercent);
226             circleView.setScaleX(scaleValue);
227             circleView.setScaleY(scaleValue);
228 
229             mMenuView.setAlpha(Math.max(animatedValue, ANIMATING_MAX_ALPHA));
230         });
231 
232         animator.addListener(new AnimatorListenerAdapter() {
233             @Override
234             public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) {
235                 super.onAnimationEnd(animation, isReverse);
236 
237                 if (isReverse) {
238                     circleView.setScaleX(CIRCLE_VIEW_DEFAULT_SCALE);
239                     circleView.setScaleY(CIRCLE_VIEW_DEFAULT_SCALE);
240                     mMenuView.setAlpha(COMPLETELY_OPAQUE);
241                 }
242             }
243         });
244 
245         mInteractMap.put(circleView.getId(), new Pair<>(magnetizedObject, animator));
246     }
247 }
248