1 /*
2  * Copyright (C) 2023 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.launcher3.taskbar.bubbles;
18 
19 import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY;
20 import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW;
21 
22 import android.content.res.Resources;
23 import android.graphics.PointF;
24 import android.view.View;
25 
26 import androidx.annotation.NonNull;
27 import androidx.annotation.Nullable;
28 import androidx.dynamicanimation.animation.DynamicAnimation;
29 import androidx.dynamicanimation.animation.FloatPropertyCompat;
30 
31 import com.android.launcher3.R;
32 import com.android.wm.shell.common.bubbles.DismissCircleView;
33 import com.android.wm.shell.common.bubbles.DismissView;
34 import com.android.wm.shell.shared.animation.PhysicsAnimator;
35 
36 /**
37  * The animator performs the bubble animations while dragging and coordinates bubble and dismiss
38  * view animations when it gets magnetized, released or dismissed.
39  */
40 public class BubbleDragAnimator {
41     private static final float SCALE_BUBBLE_FOCUSED = 1.2f;
42     private static final float SCALE_BUBBLE_CAPTURED = 0.9f;
43     private static final float SCALE_BUBBLE_BAR_FOCUSED = 1.1f;
44     // 400f matches to MEDIUM_LOW spring stiffness
45     private static final float TRANSLATION_SPRING_STIFFNESS = 400f;
46 
47     private final PhysicsAnimator.SpringConfig mDefaultConfig =
48             new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY);
49     private final PhysicsAnimator.SpringConfig mTranslationConfig =
50             new PhysicsAnimator.SpringConfig(TRANSLATION_SPRING_STIFFNESS,
51                     DAMPING_RATIO_LOW_BOUNCY);
52     @NonNull
53     private final View mView;
54     @NonNull
55     private final PhysicsAnimator<View> mBubbleAnimator;
56     @Nullable
57     private DismissView mDismissView;
58     @Nullable
59     private PhysicsAnimator<DismissCircleView> mDismissAnimator;
60     private final float mBubbleFocusedScale;
61     private final float mBubbleCapturedScale;
62     private final float mDismissCapturedScale;
63 
64     /**
65      * Should be initialised for each dragged view
66      *
67      * @param view the dragged view to animate
68      */
BubbleDragAnimator(@onNull View view)69     public BubbleDragAnimator(@NonNull View view) {
70         mView = view;
71         mBubbleAnimator = PhysicsAnimator.getInstance(view);
72         mBubbleAnimator.setDefaultSpringConfig(mDefaultConfig);
73 
74         Resources resources = view.getResources();
75         final int collapsedSize = resources.getDimensionPixelSize(
76                 R.dimen.bubblebar_dismiss_target_small_size);
77         final int expandedSize = resources.getDimensionPixelSize(
78                 R.dimen.bubblebar_dismiss_target_size);
79         mDismissCapturedScale = (float) collapsedSize / expandedSize;
80 
81         if (view instanceof BubbleBarView) {
82             mBubbleFocusedScale = SCALE_BUBBLE_BAR_FOCUSED;
83             mBubbleCapturedScale = mDismissCapturedScale;
84         } else {
85             mBubbleFocusedScale = SCALE_BUBBLE_FOCUSED;
86             mBubbleCapturedScale = SCALE_BUBBLE_CAPTURED;
87         }
88     }
89 
90     /**
91      * Sets dismiss view to be animated alongside the dragged bubble
92      */
setDismissView(@onNull DismissView dismissView)93     public void setDismissView(@NonNull DismissView dismissView) {
94         mDismissView = dismissView;
95         mDismissAnimator = PhysicsAnimator.getInstance(dismissView.getCircle());
96         mDismissAnimator.setDefaultSpringConfig(mDefaultConfig);
97     }
98 
99     /**
100      * Animates the focused state of the bubble when the dragging starts
101      */
animateFocused()102     public void animateFocused() {
103         mBubbleAnimator.cancel();
104         mBubbleAnimator
105                 .spring(DynamicAnimation.SCALE_X, mBubbleFocusedScale)
106                 .spring(DynamicAnimation.SCALE_Y, mBubbleFocusedScale)
107                 .start();
108     }
109 
110     /**
111      * Animates the dragged bubble movement back to the initial position.
112      *
113      * @param restingPosition the position to animate to
114      * @param velocity        the initial velocity to use for the spring animation
115      * @param endActions      gets called when the animation completes or gets cancelled
116      */
animateToRestingState(@onNull PointF restingPosition, @NonNull PointF velocity, @Nullable Runnable endActions)117     public void animateToRestingState(@NonNull PointF restingPosition, @NonNull PointF velocity,
118             @Nullable Runnable endActions) {
119         mBubbleAnimator.cancel();
120         mBubbleAnimator
121                 .spring(DynamicAnimation.SCALE_X, 1f)
122                 .spring(DynamicAnimation.SCALE_Y, 1f)
123                 .spring(BubbleDragController.DRAG_TRANSLATION_X, restingPosition.x, velocity.x,
124                         mTranslationConfig)
125                 .spring(DynamicAnimation.TRANSLATION_Y, restingPosition.y, velocity.y,
126                         mTranslationConfig)
127                 .addEndListener((View target, @NonNull FloatPropertyCompat<? super View> property,
128                         boolean wasFling, boolean canceled, float finalValue, float finalVelocity,
129                         boolean allRelevantPropertyAnimationsEnded) -> {
130                     if (canceled || allRelevantPropertyAnimationsEnded) {
131                         resetAnimatedViews(restingPosition);
132                         if (endActions != null) {
133                             endActions.run();
134                         }
135                     }
136                 })
137                 .start();
138     }
139 
140     /**
141      * Animates the dragged view alongside the dismiss view when it gets captured in the dismiss
142      * target area.
143      */
animateDismissCaptured()144     public void animateDismissCaptured() {
145         mBubbleAnimator.cancel();
146         mBubbleAnimator
147                 .spring(DynamicAnimation.SCALE_X, mBubbleCapturedScale)
148                 .spring(DynamicAnimation.SCALE_Y, mBubbleCapturedScale)
149                 .spring(DynamicAnimation.ALPHA, mDismissCapturedScale)
150                 .start();
151 
152         if (mDismissAnimator != null) {
153             mDismissAnimator.cancel();
154             mDismissAnimator
155                     .spring(DynamicAnimation.SCALE_X, mDismissCapturedScale)
156                     .spring(DynamicAnimation.SCALE_Y, mDismissCapturedScale)
157                     .start();
158         }
159     }
160 
161     /**
162      * Animates the dragged view alongside the dismiss view when it gets released from the dismiss
163      * target area.
164      */
animateDismissReleased()165     public void animateDismissReleased() {
166         mBubbleAnimator.cancel();
167         mBubbleAnimator
168                 .spring(DynamicAnimation.SCALE_X, mBubbleFocusedScale)
169                 .spring(DynamicAnimation.SCALE_Y, mBubbleFocusedScale)
170                 .spring(DynamicAnimation.ALPHA, 1f)
171                 .start();
172 
173         if (mDismissAnimator != null) {
174             mDismissAnimator.cancel();
175             mDismissAnimator
176                     .spring(DynamicAnimation.SCALE_X, 1f)
177                     .spring(DynamicAnimation.SCALE_Y, 1f)
178                     .start();
179         }
180     }
181 
182     /**
183      * Animates the dragged bubble dismiss when it's released in the dismiss target area.
184      *
185      * @param initialPosition the initial position to move the bubble too after animation finishes
186      * @param endActions      gets called when the animation completes or gets cancelled
187      */
animateDismiss(@onNull PointF initialPosition, @Nullable Runnable endActions)188     public void animateDismiss(@NonNull PointF initialPosition, @Nullable Runnable endActions) {
189         float dismissHeight = mDismissView != null ? mDismissView.getHeight() : 0f;
190         float translationY = mView.getTranslationY() + dismissHeight;
191         mBubbleAnimator
192                 .spring(DynamicAnimation.TRANSLATION_Y, translationY)
193                 .spring(DynamicAnimation.SCALE_X, 0f)
194                 .spring(DynamicAnimation.SCALE_Y, 0f)
195                 .spring(DynamicAnimation.ALPHA, 0f)
196                 .addEndListener((View target, @NonNull FloatPropertyCompat<? super View> property,
197                         boolean wasFling, boolean canceled, float finalValue, float finalVelocity,
198                         boolean allRelevantPropertyAnimationsEnded) -> {
199                     if (canceled || allRelevantPropertyAnimationsEnded) {
200                         resetAnimatedViews(initialPosition);
201                         if (endActions != null) endActions.run();
202                     }
203                 })
204                 .start();
205     }
206 
207     /**
208      * Reset the animated views to the initial state
209      *
210      * @param initialPosition position of the bubble
211      */
resetAnimatedViews(@onNull PointF initialPosition)212     private void resetAnimatedViews(@NonNull PointF initialPosition) {
213         mView.setScaleX(1f);
214         mView.setScaleY(1f);
215         mView.setAlpha(1f);
216         mView.setTranslationX(initialPosition.x);
217         mView.setTranslationY(initialPosition.y);
218 
219         if (mDismissView != null) {
220             mDismissView.getCircle().setScaleX(1f);
221             mDismissView.getCircle().setScaleY(1f);
222         }
223     }
224 }
225