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 android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ValueAnimator;
22 import android.view.MotionEvent;
23 
24 import androidx.annotation.NonNull;
25 import androidx.dynamicanimation.animation.DynamicAnimation;
26 
27 import com.android.systemui.R;
28 import com.android.wm.shell.common.bubbles.DismissView;
29 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
30 
31 /**
32  * Controls the interaction between {@link MagnetizedObject} and
33  * {@link MagnetizedObject.MagneticTarget}.
34  */
35 class DismissAnimationController {
36     private static final boolean ENABLE_FLING_TO_DISMISS_MENU = false;
37     private static final float COMPLETELY_OPAQUE = 1.0f;
38     private static final float COMPLETELY_TRANSPARENT = 0.0f;
39     private static final float CIRCLE_VIEW_DEFAULT_SCALE = 1.0f;
40     private static final float ANIMATING_MAX_ALPHA = 0.7f;
41 
42     private final DismissView mDismissView;
43     private final MenuView mMenuView;
44     private final ValueAnimator mDismissAnimator;
45     private final MagnetizedObject<?> mMagnetizedObject;
46     private float mMinDismissSize;
47     private float mSizePercent;
48 
DismissAnimationController(DismissView dismissView, MenuView menuView)49     DismissAnimationController(DismissView dismissView, MenuView menuView) {
50         mDismissView = dismissView;
51         mDismissView.setPivotX(dismissView.getWidth() / 2.0f);
52         mDismissView.setPivotY(dismissView.getHeight() / 2.0f);
53         mMenuView = menuView;
54 
55         updateResources();
56 
57         mDismissAnimator = ValueAnimator.ofFloat(COMPLETELY_OPAQUE, COMPLETELY_TRANSPARENT);
58         mDismissAnimator.addUpdateListener(dismissAnimation -> {
59             final float animatedValue = (float) dismissAnimation.getAnimatedValue();
60             final float scaleValue = Math.max(animatedValue, mSizePercent);
61             dismissView.getCircle().setScaleX(scaleValue);
62             dismissView.getCircle().setScaleY(scaleValue);
63 
64             menuView.setAlpha(Math.max(animatedValue, ANIMATING_MAX_ALPHA));
65         });
66 
67         mDismissAnimator.addListener(new AnimatorListenerAdapter() {
68             @Override
69             public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) {
70                 super.onAnimationEnd(animation, isReverse);
71 
72                 if (isReverse) {
73                     mDismissView.getCircle().setScaleX(CIRCLE_VIEW_DEFAULT_SCALE);
74                     mDismissView.getCircle().setScaleY(CIRCLE_VIEW_DEFAULT_SCALE);
75                     mMenuView.setAlpha(COMPLETELY_OPAQUE);
76                 }
77             }
78         });
79 
80         mMagnetizedObject =
81                 new MagnetizedObject<MenuView>(mMenuView.getContext(), mMenuView,
82                         new MenuAnimationController.MenuPositionProperty(
83                                 DynamicAnimation.TRANSLATION_X),
84                         new MenuAnimationController.MenuPositionProperty(
85                                 DynamicAnimation.TRANSLATION_Y)) {
86                     @Override
87                     public void getLocationOnScreen(MenuView underlyingObject, int[] loc) {
88                         underlyingObject.getLocationOnScreen(loc);
89                     }
90 
91                     @Override
92                     public float getHeight(MenuView underlyingObject) {
93                         return underlyingObject.getHeight();
94                     }
95 
96                     @Override
97                     public float getWidth(MenuView underlyingObject) {
98                         return underlyingObject.getWidth();
99                     }
100                 };
101 
102         final MagnetizedObject.MagneticTarget magneticTarget = new MagnetizedObject.MagneticTarget(
103                 dismissView.getCircle(), (int) mMinDismissSize);
104         mMagnetizedObject.addTarget(magneticTarget);
105         mMagnetizedObject.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_MENU);
106     }
107 
showDismissView(boolean show)108     void showDismissView(boolean show) {
109         if (show) {
110             mDismissView.show();
111         } else {
112             mDismissView.hide();
113         }
114     }
115 
setMagnetListener(MagnetizedObject.MagnetListener magnetListener)116     void setMagnetListener(MagnetizedObject.MagnetListener magnetListener) {
117         mMagnetizedObject.setMagnetListener(magnetListener);
118     }
119 
maybeConsumeDownMotionEvent(MotionEvent event)120     void maybeConsumeDownMotionEvent(MotionEvent event) {
121         mMagnetizedObject.maybeConsumeMotionEvent(event);
122     }
123 
124     /**
125      * This used to pass {@link MotionEvent#ACTION_DOWN} to the magnetized object to check if it was
126      * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}.
127      *
128      * @param event that move the magnetized object which is also the menu list view.
129      * @return true if the location of the motion events moves within the magnetic field of a
130      * target, but false if didn't set
131      * {@link DismissAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}.
132      */
maybeConsumeMoveMotionEvent(MotionEvent event)133     boolean maybeConsumeMoveMotionEvent(MotionEvent event) {
134         return mMagnetizedObject.maybeConsumeMotionEvent(event);
135     }
136 
137     /**
138      * This used to pass {@link MotionEvent#ACTION_UP} to the magnetized object to check if it was
139      * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}.
140      *
141      * @param event that move the magnetized object which is also the menu list view.
142      * @return true if the location of the motion events moves within the magnetic field of a
143      * target, but false if didn't set
144      * {@link DismissAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}.
145      */
maybeConsumeUpMotionEvent(MotionEvent event)146     boolean maybeConsumeUpMotionEvent(MotionEvent event) {
147         return mMagnetizedObject.maybeConsumeMotionEvent(event);
148     }
149 
animateDismissMenu(boolean scaleUp)150     void animateDismissMenu(boolean scaleUp) {
151         if (scaleUp) {
152             mDismissAnimator.start();
153         } else {
154             mDismissAnimator.reverse();
155         }
156     }
157 
updateResources()158     void updateResources() {
159         final float maxDismissSize = mDismissView.getResources().getDimensionPixelSize(
160                 R.dimen.dismiss_circle_size);
161         mMinDismissSize = mDismissView.getResources().getDimensionPixelSize(
162                 R.dimen.dismiss_circle_small);
163         mSizePercent = mMinDismissSize / maxDismissSize;
164     }
165 
166     interface DismissCallback {
onDismiss()167         void onDismiss();
168     }
169 }
170