1 /*
2  * Copyright (C) 2019 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 package com.android.quickstep.util;
17 
18 import static java.lang.annotation.RetentionPolicy.SOURCE;
19 
20 import android.animation.Animator;
21 import android.content.Context;
22 import android.graphics.PointF;
23 import android.graphics.Rect;
24 import android.graphics.RectF;
25 
26 import androidx.annotation.IntDef;
27 import androidx.annotation.Nullable;
28 import androidx.dynamicanimation.animation.DynamicAnimation.OnAnimationEndListener;
29 import androidx.dynamicanimation.animation.FloatPropertyCompat;
30 import androidx.dynamicanimation.animation.SpringAnimation;
31 import androidx.dynamicanimation.animation.SpringForce;
32 
33 import com.android.launcher3.DeviceProfile;
34 import com.android.launcher3.R;
35 import com.android.launcher3.Utilities;
36 import com.android.launcher3.anim.FlingSpringAnim;
37 import com.android.launcher3.touch.OverScroll;
38 import com.android.launcher3.util.DynamicResource;
39 import com.android.quickstep.RemoteAnimationTargets.ReleaseCheck;
40 import com.android.systemui.plugins.ResourceProvider;
41 
42 import java.lang.annotation.Retention;
43 import java.util.ArrayList;
44 import java.util.List;
45 
46 
47 /**
48  * Applies spring forces to animate from a starting rect to a target rect,
49  * while providing update callbacks to the caller.
50  */
51 public class RectFSpringAnim extends ReleaseCheck {
52 
53     private static final FloatPropertyCompat<RectFSpringAnim> RECT_CENTER_X =
54             new FloatPropertyCompat<RectFSpringAnim>("rectCenterXSpring") {
55                 @Override
56                 public float getValue(RectFSpringAnim anim) {
57                     return anim.mCurrentCenterX;
58                 }
59 
60                 @Override
61                 public void setValue(RectFSpringAnim anim, float currentCenterX) {
62                     anim.mCurrentCenterX = currentCenterX;
63                     anim.onUpdate();
64                 }
65             };
66 
67     private static final FloatPropertyCompat<RectFSpringAnim> RECT_Y =
68             new FloatPropertyCompat<RectFSpringAnim>("rectYSpring") {
69                 @Override
70                 public float getValue(RectFSpringAnim anim) {
71                     return anim.mCurrentY;
72                 }
73 
74                 @Override
75                 public void setValue(RectFSpringAnim anim, float y) {
76                     anim.mCurrentY = y;
77                     anim.onUpdate();
78                 }
79             };
80 
81     private static final FloatPropertyCompat<RectFSpringAnim> RECT_SCALE_PROGRESS =
82             new FloatPropertyCompat<RectFSpringAnim>("rectScaleProgress") {
83                 @Override
84                 public float getValue(RectFSpringAnim object) {
85                     return object.mCurrentScaleProgress;
86                 }
87 
88                 @Override
89                 public void setValue(RectFSpringAnim object, float value) {
90                     object.mCurrentScaleProgress = value;
91                     object.onUpdate();
92                 }
93             };
94 
95     private final RectF mStartRect;
96     private final RectF mTargetRect;
97     private final RectF mCurrentRect = new RectF();
98     private final List<OnUpdateListener> mOnUpdateListeners = new ArrayList<>();
99     private final List<Animator.AnimatorListener> mAnimatorListeners = new ArrayList<>();
100 
101     private float mCurrentCenterX;
102     private float mCurrentY;
103     // If true, tracking the bottom of the rects, else tracking the top.
104     private float mCurrentScaleProgress;
105     private FlingSpringAnim mRectXAnim;
106     private FlingSpringAnim mRectYAnim;
107     private SpringAnimation mRectScaleAnim;
108     private boolean mAnimsStarted;
109     private boolean mRectXAnimEnded;
110     private boolean mRectYAnimEnded;
111     private boolean mRectScaleAnimEnded;
112 
113     private float mMinVisChange;
114     private int mMaxVelocityPxPerS;
115 
116     /**
117      * Indicates which part of the start & target rects we are interpolating between.
118      */
119     public static final int TRACKING_TOP = 0;
120     public static final int TRACKING_CENTER = 1;
121     public static final int TRACKING_BOTTOM = 2;
122 
123     @Retention(SOURCE)
124     @IntDef(value = {TRACKING_TOP,
125                     TRACKING_CENTER,
126                     TRACKING_BOTTOM})
127     public @interface Tracking{}
128 
129     @Tracking
130     public final int mTracking;
131 
RectFSpringAnim(RectF startRect, RectF targetRect, Context context, @Nullable DeviceProfile deviceProfile)132     public RectFSpringAnim(RectF startRect, RectF targetRect, Context context,
133             @Nullable DeviceProfile deviceProfile) {
134         mStartRect = startRect;
135         mTargetRect = targetRect;
136         mCurrentCenterX = mStartRect.centerX();
137 
138         ResourceProvider rp = DynamicResource.provider(context);
139         mMinVisChange = rp.getDimension(R.dimen.swipe_up_fling_min_visible_change);
140         mMaxVelocityPxPerS = (int) rp.getDimension(R.dimen.swipe_up_max_velocity);
141         setCanRelease(true);
142 
143         if (deviceProfile == null) {
144             mTracking = startRect.bottom < targetRect.bottom
145                     ? TRACKING_BOTTOM
146                     : TRACKING_TOP;
147         } else {
148             int heightPx = deviceProfile.heightPx;
149             Rect padding = deviceProfile.workspacePadding;
150 
151             final float topThreshold = heightPx / 3f;
152             final float bottomThreshold = deviceProfile.heightPx - padding.bottom;
153 
154             if (targetRect.bottom > bottomThreshold) {
155                 mTracking = TRACKING_BOTTOM;
156             } else if (targetRect.top < topThreshold) {
157                 mTracking = TRACKING_TOP;
158             } else {
159                 mTracking = TRACKING_CENTER;
160             }
161         }
162 
163         mCurrentY = getTrackedYFromRect(mStartRect);
164     }
165 
getTrackedYFromRect(RectF rect)166     private float getTrackedYFromRect(RectF rect) {
167         switch (mTracking) {
168             case TRACKING_TOP:
169                 return rect.top;
170             case TRACKING_BOTTOM:
171                 return rect.bottom;
172             case TRACKING_CENTER:
173             default:
174                 return rect.centerY();
175         }
176     }
177 
onTargetPositionChanged()178     public void onTargetPositionChanged() {
179         if (mRectXAnim != null && mRectXAnim.getTargetPosition() != mTargetRect.centerX()) {
180             mRectXAnim.updatePosition(mCurrentCenterX, mTargetRect.centerX());
181         }
182 
183         if (mRectYAnim != null) {
184             switch (mTracking) {
185                 case TRACKING_TOP:
186                     if (mRectYAnim.getTargetPosition() != mTargetRect.top) {
187                         mRectYAnim.updatePosition(mCurrentY, mTargetRect.top);
188                     }
189                     break;
190                 case TRACKING_BOTTOM:
191                     if (mRectYAnim.getTargetPosition() != mTargetRect.bottom) {
192                         mRectYAnim.updatePosition(mCurrentY, mTargetRect.bottom);
193                     }
194                     break;
195                 case TRACKING_CENTER:
196                     if (mRectYAnim.getTargetPosition() != mTargetRect.centerY()) {
197                         mRectYAnim.updatePosition(mCurrentY, mTargetRect.centerY());
198                     }
199                     break;
200             }
201         }
202     }
203 
addOnUpdateListener(OnUpdateListener onUpdateListener)204     public void addOnUpdateListener(OnUpdateListener onUpdateListener) {
205         mOnUpdateListeners.add(onUpdateListener);
206     }
207 
addAnimatorListener(Animator.AnimatorListener animatorListener)208     public void addAnimatorListener(Animator.AnimatorListener animatorListener) {
209         mAnimatorListeners.add(animatorListener);
210     }
211 
212     /**
213      * Starts the fling/spring animation.
214      * @param context The activity context.
215      * @param velocityPxPerMs Velocity of swipe in px/ms.
216      */
start(Context context, PointF velocityPxPerMs)217     public void start(Context context, PointF velocityPxPerMs) {
218         // Only tell caller that we ended if both x and y animations have ended.
219         OnAnimationEndListener onXEndListener = ((animation, canceled, centerX, velocityX) -> {
220             mRectXAnimEnded = true;
221             maybeOnEnd();
222         });
223         OnAnimationEndListener onYEndListener = ((animation, canceled, centerY, velocityY) -> {
224             mRectYAnimEnded = true;
225             maybeOnEnd();
226         });
227 
228         // We dampen the user velocity here to keep the natural feeling and to prevent the
229         // rect from straying too from a linear path.
230         final float xVelocityPxPerS = velocityPxPerMs.x * 1000;
231         final float yVelocityPxPerS = velocityPxPerMs.y * 1000;
232         final float dampedXVelocityPxPerS = OverScroll.dampedScroll(
233                 Math.abs(xVelocityPxPerS), mMaxVelocityPxPerS) * Math.signum(xVelocityPxPerS);
234         final float dampedYVelocityPxPerS = OverScroll.dampedScroll(
235                 Math.abs(yVelocityPxPerS), mMaxVelocityPxPerS) * Math.signum(yVelocityPxPerS);
236 
237         float startX = mCurrentCenterX;
238         float endX = mTargetRect.centerX();
239         float minXValue = Math.min(startX, endX);
240         float maxXValue = Math.max(startX, endX);
241 
242         mRectXAnim = new FlingSpringAnim(this, context, RECT_CENTER_X, startX, endX,
243                 dampedXVelocityPxPerS, mMinVisChange, minXValue, maxXValue, onXEndListener);
244 
245         float startY = mCurrentY;
246         float endY = getTrackedYFromRect(mTargetRect);
247         float minYValue = Math.min(startY, endY);
248         float maxYValue = Math.max(startY, endY);
249         mRectYAnim = new FlingSpringAnim(this, context, RECT_Y, startY, endY, dampedYVelocityPxPerS,
250                 mMinVisChange, minYValue, maxYValue, onYEndListener);
251 
252         float minVisibleChange = Math.abs(1f / mStartRect.height());
253         ResourceProvider rp = DynamicResource.provider(context);
254         float damping = rp.getFloat(R.dimen.swipe_up_rect_scale_damping_ratio);
255         float stiffness = rp.getFloat(R.dimen.swipe_up_rect_scale_stiffness);
256 
257         mRectScaleAnim = new SpringAnimation(this, RECT_SCALE_PROGRESS)
258                 .setSpring(new SpringForce(1f)
259                 .setDampingRatio(damping)
260                 .setStiffness(stiffness))
261                 .setStartVelocity(velocityPxPerMs.y * minVisibleChange)
262                 .setMaxValue(1f)
263                 .setMinimumVisibleChange(minVisibleChange)
264                 .addEndListener((animation, canceled, value, velocity) -> {
265                     mRectScaleAnimEnded = true;
266                     maybeOnEnd();
267                 });
268 
269         setCanRelease(false);
270         mAnimsStarted = true;
271 
272         mRectXAnim.start();
273         mRectYAnim.start();
274         mRectScaleAnim.start();
275         for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
276             animatorListener.onAnimationStart(null);
277         }
278     }
279 
end()280     public void end() {
281         if (mAnimsStarted) {
282             mRectXAnim.end();
283             mRectYAnim.end();
284             if (mRectScaleAnim.canSkipToEnd()) {
285                 mRectScaleAnim.skipToEnd();
286             }
287         }
288         mRectXAnimEnded = true;
289         mRectYAnimEnded = true;
290         mRectScaleAnimEnded = true;
291         maybeOnEnd();
292     }
293 
isEnded()294     private boolean isEnded() {
295         return mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded;
296     }
297 
onUpdate()298     private void onUpdate() {
299         if (isEnded()) {
300             // Prevent further updates from being called. This can happen between callbacks for
301             // ending the x/y/scale animations.
302             return;
303         }
304 
305         if (!mOnUpdateListeners.isEmpty()) {
306             float currentWidth = Utilities.mapRange(mCurrentScaleProgress, mStartRect.width(),
307                     mTargetRect.width());
308             float currentHeight = Utilities.mapRange(mCurrentScaleProgress, mStartRect.height(),
309                     mTargetRect.height());
310             switch (mTracking) {
311                 case TRACKING_TOP:
312                     mCurrentRect.set(mCurrentCenterX - currentWidth / 2,
313                             mCurrentY,
314                             mCurrentCenterX + currentWidth / 2,
315                             mCurrentY + currentHeight);
316                     break;
317                 case TRACKING_BOTTOM:
318                     mCurrentRect.set(mCurrentCenterX - currentWidth / 2,
319                             mCurrentY - currentHeight,
320                             mCurrentCenterX + currentWidth / 2,
321                             mCurrentY);
322                     break;
323                 case TRACKING_CENTER:
324                     mCurrentRect.set(mCurrentCenterX - currentWidth / 2,
325                             mCurrentY - currentHeight / 2,
326                             mCurrentCenterX + currentWidth / 2,
327                             mCurrentY + currentHeight / 2);
328                     break;
329             }
330             for (OnUpdateListener onUpdateListener : mOnUpdateListeners) {
331                 onUpdateListener.onUpdate(mCurrentRect, mCurrentScaleProgress);
332             }
333         }
334     }
335 
maybeOnEnd()336     private void maybeOnEnd() {
337         if (mAnimsStarted && isEnded()) {
338             mAnimsStarted = false;
339             setCanRelease(true);
340             for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
341                 animatorListener.onAnimationEnd(null);
342             }
343         }
344     }
345 
cancel()346     public void cancel() {
347         if (mAnimsStarted) {
348             for (OnUpdateListener onUpdateListener : mOnUpdateListeners) {
349                 onUpdateListener.onCancel();
350             }
351         }
352         end();
353     }
354 
355     public interface OnUpdateListener {
356         /**
357          * Called when an update is made to the animation.
358          * @param currentRect The rect of the window.
359          * @param progress [0, 1] The progress of the rect scale animation.
360          */
361         void onUpdate(RectF currentRect, float progress);
362 
onCancel()363         default void onCancel() { }
364     }
365 }
366