• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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     protected final float mStiffnessX;
132     protected final float mStiffnessY;
133     protected final float mDampingX;
134     protected final float mDampingY;
135     protected final float mRectStiffness;
136 
RectFSpringAnim(SpringConfig config)137     public RectFSpringAnim(SpringConfig config) {
138         mStartRect = config.startRect;
139         mTargetRect = config.targetRect;
140         mCurrentCenterX = mStartRect.centerX();
141 
142         mMinVisChange = config.minVisChange;
143         mMaxVelocityPxPerS = config.maxVelocityPxPerS;
144         setCanRelease(true);
145 
146         mTracking = config.tracking;
147         mStiffnessX = config.stiffnessX;
148         mStiffnessY = config.stiffnessY;
149         mDampingX = config.dampingX;
150         mDampingY = config.dampingY;
151         mRectStiffness = config.rectStiffness;
152 
153         mCurrentY = getTrackedYFromRect(mStartRect);
154     }
155 
getTrackedYFromRect(RectF rect)156     private float getTrackedYFromRect(RectF rect) {
157         switch (mTracking) {
158             case TRACKING_TOP:
159                 return rect.top;
160             case TRACKING_BOTTOM:
161                 return rect.bottom;
162             case TRACKING_CENTER:
163             default:
164                 return rect.centerY();
165         }
166     }
167 
onTargetPositionChanged()168     public void onTargetPositionChanged() {
169         if (mRectXAnim != null && mRectXAnim.getTargetPosition() != mTargetRect.centerX()) {
170             mRectXAnim.updatePosition(mCurrentCenterX, mTargetRect.centerX());
171         }
172 
173         if (mRectYAnim != null) {
174             switch (mTracking) {
175                 case TRACKING_TOP:
176                     if (mRectYAnim.getTargetPosition() != mTargetRect.top) {
177                         mRectYAnim.updatePosition(mCurrentY, mTargetRect.top);
178                     }
179                     break;
180                 case TRACKING_BOTTOM:
181                     if (mRectYAnim.getTargetPosition() != mTargetRect.bottom) {
182                         mRectYAnim.updatePosition(mCurrentY, mTargetRect.bottom);
183                     }
184                     break;
185                 case TRACKING_CENTER:
186                     if (mRectYAnim.getTargetPosition() != mTargetRect.centerY()) {
187                         mRectYAnim.updatePosition(mCurrentY, mTargetRect.centerY());
188                     }
189                     break;
190             }
191         }
192     }
193 
addOnUpdateListener(OnUpdateListener onUpdateListener)194     public void addOnUpdateListener(OnUpdateListener onUpdateListener) {
195         mOnUpdateListeners.add(onUpdateListener);
196     }
197 
addAnimatorListener(Animator.AnimatorListener animatorListener)198     public void addAnimatorListener(Animator.AnimatorListener animatorListener) {
199         mAnimatorListeners.add(animatorListener);
200     }
201 
202     /**
203      * Starts the fling/spring animation.
204      * @param context The activity context.
205      * @param velocityPxPerMs Velocity of swipe in px/ms.
206      */
start(Context context, @Nullable DeviceProfile profile, PointF velocityPxPerMs)207     public void start(Context context, @Nullable DeviceProfile profile, PointF velocityPxPerMs) {
208         // Only tell caller that we ended if both x and y animations have ended.
209         OnAnimationEndListener onXEndListener = ((animation, canceled, centerX, velocityX) -> {
210             mRectXAnimEnded = true;
211             maybeOnEnd();
212         });
213         OnAnimationEndListener onYEndListener = ((animation, canceled, centerY, velocityY) -> {
214             mRectYAnimEnded = true;
215             maybeOnEnd();
216         });
217 
218         // We dampen the user velocity here to keep the natural feeling and to prevent the
219         // rect from straying too from a linear path.
220         final float xVelocityPxPerS = velocityPxPerMs.x * 1000;
221         final float yVelocityPxPerS = velocityPxPerMs.y * 1000;
222         final float dampedXVelocityPxPerS = OverScroll.dampedScroll(
223                 Math.abs(xVelocityPxPerS), mMaxVelocityPxPerS) * Math.signum(xVelocityPxPerS);
224         final float dampedYVelocityPxPerS = OverScroll.dampedScroll(
225                 Math.abs(yVelocityPxPerS), mMaxVelocityPxPerS) * Math.signum(yVelocityPxPerS);
226 
227         float startX = mCurrentCenterX;
228         float endX = mTargetRect.centerX();
229         float minXValue = Math.min(startX, endX);
230         float maxXValue = Math.max(startX, endX);
231 
232         mRectXAnim = new FlingSpringAnim(this, context, RECT_CENTER_X, startX, endX,
233                 dampedXVelocityPxPerS, mMinVisChange, minXValue, maxXValue, mDampingX, mStiffnessX,
234                 onXEndListener);
235 
236         float startY = mCurrentY;
237         float endY = getTrackedYFromRect(mTargetRect);
238         float minYValue = Math.min(startY, endY);
239         float maxYValue = Math.max(startY, endY);
240         mRectYAnim = new FlingSpringAnim(this, context, RECT_Y, startY, endY, dampedYVelocityPxPerS,
241                 mMinVisChange, minYValue, maxYValue, mDampingY, mStiffnessY, onYEndListener);
242 
243         float minVisibleChange = Math.abs(1f / mStartRect.height());
244         ResourceProvider rp = DynamicResource.provider(context);
245         float damping = rp.getFloat(R.dimen.swipe_up_rect_scale_damping_ratio);
246 
247         // Increase the stiffness for devices where we want the window size to transform quicker.
248         boolean shouldUseHigherStiffness = profile != null
249                 && (profile.isLandscape || profile.isTablet);
250         float stiffness = shouldUseHigherStiffness
251                 ? rp.getFloat(R.dimen.swipe_up_rect_scale_higher_stiffness)
252                 : rp.getFloat(R.dimen.swipe_up_rect_scale_stiffness);
253 
254         mRectScaleAnim = new SpringAnimation(this, RECT_SCALE_PROGRESS)
255                 .setSpring(new SpringForce(1f)
256                 .setDampingRatio(damping)
257                 .setStiffness(stiffness))
258                 .setStartVelocity(velocityPxPerMs.y * minVisibleChange)
259                 .setMaxValue(1f)
260                 .setMinimumVisibleChange(minVisibleChange)
261                 .addEndListener((animation, canceled, value, velocity) -> {
262                     mRectScaleAnimEnded = true;
263                     maybeOnEnd();
264                 });
265 
266         setCanRelease(false);
267         mAnimsStarted = true;
268 
269         mRectXAnim.start();
270         mRectYAnim.start();
271         mRectScaleAnim.start();
272         for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
273             animatorListener.onAnimationStart(null);
274         }
275     }
276 
end()277     public void end() {
278         if (mAnimsStarted) {
279             mRectXAnim.end();
280             mRectYAnim.end();
281             if (mRectScaleAnim.canSkipToEnd()) {
282                 mRectScaleAnim.skipToEnd();
283             }
284         }
285         mRectXAnimEnded = true;
286         mRectYAnimEnded = true;
287         mRectScaleAnimEnded = true;
288         maybeOnEnd();
289     }
290 
isEnded()291     private boolean isEnded() {
292         return mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded;
293     }
294 
onUpdate()295     private void onUpdate() {
296         if (isEnded()) {
297             // Prevent further updates from being called. This can happen between callbacks for
298             // ending the x/y/scale animations.
299             return;
300         }
301 
302         if (!mOnUpdateListeners.isEmpty()) {
303             float currentWidth = Utilities.mapRange(mCurrentScaleProgress, mStartRect.width(),
304                     mTargetRect.width());
305             float currentHeight = Utilities.mapRange(mCurrentScaleProgress, mStartRect.height(),
306                     mTargetRect.height());
307             switch (mTracking) {
308                 case TRACKING_TOP:
309                     mCurrentRect.set(mCurrentCenterX - currentWidth / 2,
310                             mCurrentY,
311                             mCurrentCenterX + currentWidth / 2,
312                             mCurrentY + currentHeight);
313                     break;
314                 case TRACKING_BOTTOM:
315                     mCurrentRect.set(mCurrentCenterX - currentWidth / 2,
316                             mCurrentY - currentHeight,
317                             mCurrentCenterX + currentWidth / 2,
318                             mCurrentY);
319                     break;
320                 case TRACKING_CENTER:
321                     mCurrentRect.set(mCurrentCenterX - currentWidth / 2,
322                             mCurrentY - currentHeight / 2,
323                             mCurrentCenterX + currentWidth / 2,
324                             mCurrentY + currentHeight / 2);
325                     break;
326             }
327             for (OnUpdateListener onUpdateListener : mOnUpdateListeners) {
328                 onUpdateListener.onUpdate(mCurrentRect, mCurrentScaleProgress);
329             }
330         }
331     }
332 
maybeOnEnd()333     private void maybeOnEnd() {
334         if (mAnimsStarted && isEnded()) {
335             mAnimsStarted = false;
336             setCanRelease(true);
337             for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
338                 animatorListener.onAnimationEnd(null);
339             }
340         }
341     }
342 
cancel()343     public void cancel() {
344         if (mAnimsStarted) {
345             for (OnUpdateListener onUpdateListener : mOnUpdateListeners) {
346                 onUpdateListener.onCancel();
347             }
348         }
349         end();
350     }
351 
352     public interface OnUpdateListener {
353         /**
354          * Called when an update is made to the animation.
355          * @param currentRect The rect of the window.
356          * @param progress [0, 1] The progress of the rect scale animation.
357          */
onUpdate(RectF currentRect, float progress)358         void onUpdate(RectF currentRect, float progress);
359 
onCancel()360         default void onCancel() { }
361     }
362 
363     private abstract static class SpringConfig {
364         protected RectF startRect;
365         protected RectF targetRect;
366         protected @Tracking int tracking;
367         protected float stiffnessX;
368         protected float stiffnessY;
369         protected float dampingX;
370         protected float dampingY;
371         protected float rectStiffness;
372         protected float minVisChange;
373         protected int maxVelocityPxPerS;
374 
SpringConfig(Context context, RectF start, RectF target)375         private SpringConfig(Context context, RectF start, RectF target) {
376             startRect = start;
377             targetRect = target;
378 
379             ResourceProvider rp = DynamicResource.provider(context);
380             minVisChange = rp.getDimension(R.dimen.swipe_up_fling_min_visible_change);
381             maxVelocityPxPerS = (int) rp.getDimension(R.dimen.swipe_up_max_velocity);
382         }
383     }
384 
385     /**
386      * Standard spring configuration parameters.
387      */
388     public static class DefaultSpringConfig extends SpringConfig {
389 
DefaultSpringConfig(Context context, DeviceProfile deviceProfile, RectF startRect, RectF targetRect)390         public DefaultSpringConfig(Context context, DeviceProfile deviceProfile,
391                 RectF startRect, RectF targetRect) {
392             super(context, startRect, targetRect);
393 
394             ResourceProvider rp = DynamicResource.provider(context);
395             tracking = getDefaultTracking(deviceProfile);
396             stiffnessX = rp.getFloat(R.dimen.swipe_up_rect_xy_stiffness);
397             stiffnessY = rp.getFloat(R.dimen.swipe_up_rect_xy_stiffness);
398             dampingX = rp.getFloat(R.dimen.swipe_up_rect_xy_damping_ratio);
399             dampingY = rp.getFloat(R.dimen.swipe_up_rect_xy_damping_ratio);
400 
401             this.startRect = startRect;
402             this.targetRect = targetRect;
403 
404             // Increase the stiffness for devices where we want the window size to transform
405             // quicker.
406             boolean shouldUseHigherStiffness = deviceProfile != null
407                     && (deviceProfile.isLandscape || deviceProfile.isTablet);
408             rectStiffness = shouldUseHigherStiffness
409                     ? rp.getFloat(R.dimen.swipe_up_rect_scale_higher_stiffness)
410                     : rp.getFloat(R.dimen.swipe_up_rect_scale_stiffness);
411         }
412 
getDefaultTracking(@ullable DeviceProfile deviceProfile)413         private @Tracking int getDefaultTracking(@Nullable DeviceProfile deviceProfile) {
414             @Tracking int tracking;
415             if (deviceProfile == null) {
416                 tracking = startRect.bottom < targetRect.bottom
417                         ? TRACKING_BOTTOM
418                         : TRACKING_TOP;
419             } else {
420                 int heightPx = deviceProfile.heightPx;
421                 Rect padding = deviceProfile.workspacePadding;
422 
423                 final float topThreshold = heightPx / 3f;
424                 final float bottomThreshold = deviceProfile.heightPx - padding.bottom;
425 
426                 if (targetRect.bottom > bottomThreshold) {
427                     tracking = TRACKING_BOTTOM;
428                 } else if (targetRect.top < topThreshold) {
429                     tracking = TRACKING_TOP;
430                 } else {
431                     tracking = TRACKING_CENTER;
432                 }
433             }
434             return tracking;
435         }
436     }
437 
438     /**
439      * Spring configuration parameters for Taskbar/Hotseat items on devices that have a taskbar.
440      */
441     public static class TaskbarHotseatSpringConfig extends SpringConfig {
442 
TaskbarHotseatSpringConfig(Context context, RectF start, RectF target)443         public TaskbarHotseatSpringConfig(Context context, RectF start, RectF target) {
444             super(context, start, target);
445 
446             ResourceProvider rp = DynamicResource.provider(context);
447             tracking = TRACKING_CENTER;
448             stiffnessX = rp.getFloat(R.dimen.taskbar_swipe_up_rect_x_stiffness);
449             stiffnessY = rp.getFloat(R.dimen.taskbar_swipe_up_rect_y_stiffness);
450             dampingX = rp.getFloat(R.dimen.taskbar_swipe_up_rect_x_damping);
451             dampingY = rp.getFloat(R.dimen.taskbar_swipe_up_rect_y_damping);
452             rectStiffness = rp.getFloat(R.dimen.taskbar_swipe_up_rect_scale_stiffness);
453         }
454     }
455 
456 }
457