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