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 com.android.launcher3.Flags.enableScalingRevealHomeAnimation; 19 20 import static java.lang.annotation.RetentionPolicy.SOURCE; 21 22 import android.animation.Animator; 23 import android.content.Context; 24 import android.graphics.PointF; 25 import android.graphics.Rect; 26 import android.graphics.RectF; 27 28 import androidx.annotation.IntDef; 29 import androidx.annotation.Nullable; 30 import androidx.dynamicanimation.animation.DynamicAnimation.OnAnimationEndListener; 31 import androidx.dynamicanimation.animation.FloatPropertyCompat; 32 import androidx.dynamicanimation.animation.SpringAnimation; 33 import androidx.dynamicanimation.animation.SpringForce; 34 35 import com.android.launcher3.DeviceProfile; 36 import com.android.launcher3.R; 37 import com.android.launcher3.Utilities; 38 import com.android.launcher3.anim.FlingSpringAnim; 39 import com.android.launcher3.touch.OverScroll; 40 import com.android.launcher3.util.DynamicResource; 41 import com.android.quickstep.RemoteAnimationTargets.ReleaseCheck; 42 import com.android.systemui.plugins.ResourceProvider; 43 44 import java.lang.annotation.Retention; 45 import java.util.ArrayList; 46 import java.util.List; 47 48 49 /** 50 * Applies spring forces to animate from a starting rect to a target rect, 51 * while providing update callbacks to the caller. 52 */ 53 public class RectFSpringAnim extends ReleaseCheck { 54 55 private static final FloatPropertyCompat<RectFSpringAnim> RECT_CENTER_X = 56 new FloatPropertyCompat<RectFSpringAnim>("rectCenterXSpring") { 57 @Override 58 public float getValue(RectFSpringAnim anim) { 59 return anim.mCurrentCenterX; 60 } 61 62 @Override 63 public void setValue(RectFSpringAnim anim, float currentCenterX) { 64 anim.mCurrentCenterX = currentCenterX; 65 anim.onUpdate(); 66 } 67 }; 68 69 private static final FloatPropertyCompat<RectFSpringAnim> RECT_Y = 70 new FloatPropertyCompat<RectFSpringAnim>("rectYSpring") { 71 @Override 72 public float getValue(RectFSpringAnim anim) { 73 return anim.mCurrentY; 74 } 75 76 @Override 77 public void setValue(RectFSpringAnim anim, float y) { 78 anim.mCurrentY = y; 79 anim.onUpdate(); 80 } 81 }; 82 83 private static final FloatPropertyCompat<RectFSpringAnim> RECT_SCALE_PROGRESS = 84 new FloatPropertyCompat<RectFSpringAnim>("rectScaleProgress") { 85 @Override 86 public float getValue(RectFSpringAnim object) { 87 return object.mCurrentScaleProgress; 88 } 89 90 @Override 91 public void setValue(RectFSpringAnim object, float value) { 92 object.mCurrentScaleProgress = value; 93 object.onUpdate(); 94 } 95 }; 96 97 private final RectF mStartRect; 98 private final RectF mTargetRect; 99 private final RectF mCurrentRect = new RectF(); 100 private final List<OnUpdateListener> mOnUpdateListeners = new ArrayList<>(); 101 private final List<Animator.AnimatorListener> mAnimatorListeners = new ArrayList<>(); 102 103 private float mCurrentCenterX; 104 private float mCurrentY; 105 // If true, tracking the bottom of the rects, else tracking the top. 106 private float mCurrentScaleProgress; 107 private FlingSpringAnim mRectXAnim; 108 private FlingSpringAnim mRectYAnim; 109 private SpringAnimation mRectXSpring; 110 private SpringAnimation mRectYSpring; 111 private SpringAnimation mRectScaleAnim; 112 private boolean mAnimsStarted; 113 private boolean mRectXAnimEnded; 114 private boolean mRectYAnimEnded; 115 private boolean mRectScaleAnimEnded; 116 117 private float mMinVisChange; 118 private int mMaxVelocityPxPerS; 119 120 /** 121 * Indicates which part of the start & target rects we are interpolating between. 122 */ 123 public static final int TRACKING_TOP = 0; 124 public static final int TRACKING_CENTER = 1; 125 public static final int TRACKING_BOTTOM = 2; 126 127 @Retention(SOURCE) 128 @IntDef(value = {TRACKING_TOP, 129 TRACKING_CENTER, 130 TRACKING_BOTTOM}) 131 public @interface Tracking{} 132 133 @Tracking 134 public final int mTracking; 135 protected final float mStiffnessX; 136 protected final float mStiffnessY; 137 protected final float mDampingX; 138 protected final float mDampingY; 139 protected final float mRectStiffness; 140 RectFSpringAnim(SpringConfig config)141 public RectFSpringAnim(SpringConfig config) { 142 mStartRect = config.startRect; 143 mTargetRect = config.targetRect; 144 mCurrentCenterX = mStartRect.centerX(); 145 146 mMinVisChange = config.minVisChange; 147 mMaxVelocityPxPerS = config.maxVelocityPxPerS; 148 setCanRelease(true); 149 150 mTracking = config.tracking; 151 mStiffnessX = config.stiffnessX; 152 mStiffnessY = config.stiffnessY; 153 mDampingX = config.dampingX; 154 mDampingY = config.dampingY; 155 mRectStiffness = config.rectStiffness; 156 157 mCurrentY = getTrackedYFromRect(mStartRect); 158 } 159 getTargetRect()160 public RectF getTargetRect() { 161 return mTargetRect; 162 } 163 getTrackedYFromRect(RectF rect)164 private float getTrackedYFromRect(RectF rect) { 165 switch (mTracking) { 166 case TRACKING_TOP: 167 return rect.top; 168 case TRACKING_BOTTOM: 169 return rect.bottom; 170 case TRACKING_CENTER: 171 default: 172 return rect.centerY(); 173 } 174 } 175 onTargetPositionChanged()176 public void onTargetPositionChanged() { 177 if (enableScalingRevealHomeAnimation()) { 178 if (isEnded()) { 179 return; 180 } 181 182 if (mRectXSpring != null) { 183 mRectXSpring.animateToFinalPosition(mTargetRect.centerX()); 184 mRectXAnimEnded = false; 185 } 186 187 if (mRectYSpring != null) { 188 switch (mTracking) { 189 case TRACKING_TOP: 190 mRectYSpring.animateToFinalPosition(mTargetRect.top); 191 break; 192 case TRACKING_BOTTOM: 193 mRectYSpring.animateToFinalPosition(mTargetRect.bottom); 194 break; 195 case TRACKING_CENTER: 196 mRectYSpring.animateToFinalPosition(mTargetRect.centerY()); 197 break; 198 } 199 mRectYAnimEnded = false; 200 } 201 } else { 202 if (mRectXAnim != null && mRectXAnim.getTargetPosition() != mTargetRect.centerX()) { 203 mRectXAnim.updatePosition(mCurrentCenterX, mTargetRect.centerX()); 204 } 205 206 if (mRectYAnim != null) { 207 switch (mTracking) { 208 case TRACKING_TOP: 209 if (mRectYAnim.getTargetPosition() != mTargetRect.top) { 210 mRectYAnim.updatePosition(mCurrentY, mTargetRect.top); 211 } 212 break; 213 case TRACKING_BOTTOM: 214 if (mRectYAnim.getTargetPosition() != mTargetRect.bottom) { 215 mRectYAnim.updatePosition(mCurrentY, mTargetRect.bottom); 216 } 217 break; 218 case TRACKING_CENTER: 219 if (mRectYAnim.getTargetPosition() != mTargetRect.centerY()) { 220 mRectYAnim.updatePosition(mCurrentY, mTargetRect.centerY()); 221 } 222 break; 223 } 224 } 225 } 226 } 227 addOnUpdateListener(OnUpdateListener onUpdateListener)228 public void addOnUpdateListener(OnUpdateListener onUpdateListener) { 229 mOnUpdateListeners.add(onUpdateListener); 230 } 231 addAnimatorListener(Animator.AnimatorListener animatorListener)232 public void addAnimatorListener(Animator.AnimatorListener animatorListener) { 233 mAnimatorListeners.add(animatorListener); 234 } 235 236 /** 237 * Starts the fling/spring animation. 238 * @param context The activity context. 239 * @param velocityPxPerMs Velocity of swipe in px/ms. 240 */ start(Context context, @Nullable DeviceProfile profile, PointF velocityPxPerMs)241 public void start(Context context, @Nullable DeviceProfile profile, PointF velocityPxPerMs) { 242 // Only tell caller that we ended if both x and y animations have ended. 243 OnAnimationEndListener onXEndListener = ((animation, canceled, centerX, velocityX) -> { 244 mRectXAnimEnded = true; 245 maybeOnEnd(); 246 }); 247 OnAnimationEndListener onYEndListener = ((animation, canceled, centerY, velocityY) -> { 248 mRectYAnimEnded = true; 249 maybeOnEnd(); 250 }); 251 252 float xVelocityPxPerS = velocityPxPerMs.x * 1000; 253 float yVelocityPxPerS = velocityPxPerMs.y * 1000; 254 float startX = mCurrentCenterX; 255 float endX = mTargetRect.centerX(); 256 float startY = mCurrentY; 257 float endY = getTrackedYFromRect(mTargetRect); 258 float minVisibleChange = Math.abs(1f / mStartRect.height()); 259 260 if (enableScalingRevealHomeAnimation()) { 261 ResourceProvider rp = DynamicResource.provider(context); 262 long minVelocityXPxPerS = rp.getInt(R.dimen.swipe_up_min_velocity_x_px_per_s); 263 long maxVelocityXPxPerS = rp.getInt(R.dimen.swipe_up_max_velocity_x_px_per_s); 264 long minVelocityYPxPerS = rp.getInt(R.dimen.swipe_up_min_velocity_y_px_per_s); 265 long maxVelocityYPxPerS = rp.getInt(R.dimen.swipe_up_max_velocity_y_px_per_s); 266 float fallOffFactor = rp.getFloat(R.dimen.swipe_up_max_velocity_fall_off_factor); 267 268 // We want the actual initial velocity to never dip below the minimum, and to taper off 269 // once it's above the soft cap so that we can prevent the window from flying off 270 // screen, while maintaining a natural feel. 271 xVelocityPxPerS = adjustVelocity( 272 xVelocityPxPerS, minVelocityXPxPerS, maxVelocityXPxPerS, fallOffFactor); 273 yVelocityPxPerS = adjustVelocity( 274 yVelocityPxPerS, minVelocityYPxPerS, maxVelocityYPxPerS, fallOffFactor); 275 276 float stiffnessX = rp.getFloat(R.dimen.swipe_up_rect_x_stiffness); 277 float dampingX = rp.getFloat(R.dimen.swipe_up_rect_x_damping_ratio); 278 mRectXSpring = 279 new SpringAnimation(this, RECT_CENTER_X) 280 .setSpring( 281 new SpringForce(endX) 282 .setStiffness(stiffnessX) 283 .setDampingRatio(dampingX) 284 ).setStartValue(startX) 285 .setStartVelocity(xVelocityPxPerS) 286 .addEndListener(onXEndListener); 287 288 float stiffnessY = rp.getFloat(R.dimen.swipe_up_rect_y_stiffness); 289 float dampingY = rp.getFloat(R.dimen.swipe_up_rect_y_damping_ratio); 290 mRectYSpring = 291 new SpringAnimation(this, RECT_Y) 292 .setSpring( 293 new SpringForce(endY) 294 .setStiffness(stiffnessY) 295 .setDampingRatio(dampingY) 296 ) 297 .setStartValue(startY) 298 .setStartVelocity(yVelocityPxPerS) 299 .addEndListener(onYEndListener); 300 301 float stiffnessZ = rp.getFloat(R.dimen.swipe_up_rect_scale_stiffness_v2); 302 float dampingZ = rp.getFloat(R.dimen.swipe_up_rect_scale_damping_ratio_v2); 303 mRectScaleAnim = 304 new SpringAnimation(this, RECT_SCALE_PROGRESS) 305 .setSpring( 306 new SpringForce(1f) 307 .setStiffness(stiffnessZ) 308 .setDampingRatio(dampingZ)) 309 .setStartVelocity(velocityPxPerMs.y * minVisibleChange) 310 .setMaxValue(1f) 311 .setMinimumVisibleChange(minVisibleChange) 312 .addEndListener((animation, canceled, value, velocity) -> { 313 mRectScaleAnimEnded = true; 314 maybeOnEnd(); 315 }); 316 317 setCanRelease(false); 318 mAnimsStarted = true; 319 320 mRectXSpring.start(); 321 mRectYSpring.start(); 322 } else { 323 // We dampen the user velocity here to keep the natural feeling and to prevent the 324 // rect from straying too from a linear path. 325 final float dampedXVelocityPxPerS = OverScroll.dampedScroll( 326 Math.abs(xVelocityPxPerS), mMaxVelocityPxPerS) * Math.signum(xVelocityPxPerS); 327 final float dampedYVelocityPxPerS = OverScroll.dampedScroll( 328 Math.abs(yVelocityPxPerS), mMaxVelocityPxPerS) * Math.signum(yVelocityPxPerS); 329 330 float minXValue = Math.min(startX, endX); 331 float maxXValue = Math.max(startX, endX); 332 333 mRectXAnim = new FlingSpringAnim(this, context, RECT_CENTER_X, startX, endX, 334 dampedXVelocityPxPerS, mMinVisChange, minXValue, maxXValue, mDampingX, 335 mStiffnessX, onXEndListener); 336 337 float minYValue = Math.min(startY, endY); 338 float maxYValue = Math.max(startY, endY); 339 mRectYAnim = new FlingSpringAnim(this, context, RECT_Y, startY, endY, 340 dampedYVelocityPxPerS, mMinVisChange, minYValue, maxYValue, mDampingY, 341 mStiffnessY, onYEndListener); 342 343 ResourceProvider rp = DynamicResource.provider(context); 344 float damping = rp.getFloat(R.dimen.swipe_up_rect_scale_damping_ratio); 345 346 // Increase the stiffness for devices where we want the window size to transform 347 // quicker. 348 boolean shouldUseHigherStiffness = profile != null 349 && (profile.isLandscape || profile.isTablet); 350 float stiffness = shouldUseHigherStiffness 351 ? rp.getFloat(R.dimen.swipe_up_rect_scale_higher_stiffness) 352 : rp.getFloat(R.dimen.swipe_up_rect_scale_stiffness); 353 354 mRectScaleAnim = new SpringAnimation(this, RECT_SCALE_PROGRESS) 355 .setSpring(new SpringForce(1f) 356 .setDampingRatio(damping) 357 .setStiffness(stiffness)) 358 .setStartVelocity(velocityPxPerMs.y * minVisibleChange) 359 .setMaxValue(1f) 360 .setMinimumVisibleChange(minVisibleChange) 361 .addEndListener((animation, canceled, value, velocity) -> { 362 mRectScaleAnimEnded = true; 363 maybeOnEnd(); 364 }); 365 366 setCanRelease(false); 367 mAnimsStarted = true; 368 369 mRectXAnim.start(); 370 mRectYAnim.start(); 371 } 372 373 mRectScaleAnim.start(); 374 for (Animator.AnimatorListener animatorListener : mAnimatorListeners) { 375 animatorListener.onAnimationStart(null); 376 } 377 } 378 end()379 public void end() { 380 if (mAnimsStarted) { 381 if (enableScalingRevealHomeAnimation()) { 382 if (mRectXSpring.canSkipToEnd()) { 383 mRectXSpring.skipToEnd(); 384 } 385 if (mRectYSpring.canSkipToEnd()) { 386 mRectYSpring.skipToEnd(); 387 } 388 } else { 389 mRectXAnim.end(); 390 mRectYAnim.end(); 391 } 392 if (mRectScaleAnim.canSkipToEnd()) { 393 mRectScaleAnim.skipToEnd(); 394 } 395 mCurrentScaleProgress = mRectScaleAnim.getSpring().getFinalPosition(); 396 397 // Ensures that we end the animation with the final values. 398 mRectXAnimEnded = false; 399 mRectYAnimEnded = false; 400 mRectScaleAnimEnded = false; 401 onUpdate(); 402 } 403 404 mRectXAnimEnded = true; 405 mRectYAnimEnded = true; 406 mRectScaleAnimEnded = true; 407 maybeOnEnd(); 408 } 409 isEnded()410 private boolean isEnded() { 411 return mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded; 412 } 413 onUpdate()414 private void onUpdate() { 415 if (isEnded()) { 416 // Prevent further updates from being called. This can happen between callbacks for 417 // ending the x/y/scale animations. 418 return; 419 } 420 421 if (!mOnUpdateListeners.isEmpty()) { 422 float currentWidth = Utilities.mapRange(mCurrentScaleProgress, mStartRect.width(), 423 mTargetRect.width()); 424 float currentHeight = Utilities.mapRange(mCurrentScaleProgress, mStartRect.height(), 425 mTargetRect.height()); 426 switch (mTracking) { 427 case TRACKING_TOP: 428 mCurrentRect.set(mCurrentCenterX - currentWidth / 2, 429 mCurrentY, 430 mCurrentCenterX + currentWidth / 2, 431 mCurrentY + currentHeight); 432 break; 433 case TRACKING_BOTTOM: 434 mCurrentRect.set(mCurrentCenterX - currentWidth / 2, 435 mCurrentY - currentHeight, 436 mCurrentCenterX + currentWidth / 2, 437 mCurrentY); 438 break; 439 case TRACKING_CENTER: 440 mCurrentRect.set(mCurrentCenterX - currentWidth / 2, 441 mCurrentY - currentHeight / 2, 442 mCurrentCenterX + currentWidth / 2, 443 mCurrentY + currentHeight / 2); 444 break; 445 } 446 for (OnUpdateListener onUpdateListener : mOnUpdateListeners) { 447 onUpdateListener.onUpdate(mCurrentRect, mCurrentScaleProgress); 448 } 449 } 450 } 451 maybeOnEnd()452 private void maybeOnEnd() { 453 if (mAnimsStarted && isEnded()) { 454 mAnimsStarted = false; 455 setCanRelease(true); 456 for (Animator.AnimatorListener animatorListener : mAnimatorListeners) { 457 animatorListener.onAnimationEnd(null); 458 } 459 } 460 } 461 cancel()462 public void cancel() { 463 if (mAnimsStarted) { 464 for (OnUpdateListener onUpdateListener : mOnUpdateListeners) { 465 onUpdateListener.onCancel(); 466 } 467 } 468 end(); 469 } 470 471 /** 472 * Modify the given velocity so that it's never below the minimum value, and falls off by the 473 * given factor once it goes above the maximum value. 474 * In order for the max soft cap to be enforced, the fall-off factor must be >1. 475 */ adjustVelocity(float velocity, long min, long max, float factor)476 private static float adjustVelocity(float velocity, long min, long max, float factor) { 477 float sign = Math.signum(velocity); 478 float magnitude = Math.abs(velocity); 479 480 // If the absolute velocity is less than the min, bump it up. 481 if (magnitude < min) { 482 return min * sign; 483 } 484 485 // If the absolute velocity falls between min and max, or the fall-off factor is invalid, 486 // do nothing. 487 if (magnitude <= max || factor <= 1) { 488 return velocity; 489 } 490 491 // Scale the excess velocity by the fall-off factor. 492 float excess = magnitude - max; 493 float scaled = (float) Math.pow(excess, 1f / factor); 494 return (max + scaled) * sign; 495 } 496 497 public interface OnUpdateListener { 498 /** 499 * Called when an update is made to the animation. 500 * @param currentRect The rect of the window. 501 * @param progress [0, 1] The progress of the rect scale animation. 502 */ onUpdate(RectF currentRect, float progress)503 void onUpdate(RectF currentRect, float progress); 504 onCancel()505 default void onCancel() { } 506 } 507 508 private abstract static class SpringConfig { 509 protected RectF startRect; 510 protected RectF targetRect; 511 protected @Tracking int tracking; 512 protected float stiffnessX; 513 protected float stiffnessY; 514 protected float dampingX; 515 protected float dampingY; 516 protected float rectStiffness; 517 protected float minVisChange; 518 protected int maxVelocityPxPerS; 519 SpringConfig(Context context, RectF start, RectF target)520 private SpringConfig(Context context, RectF start, RectF target) { 521 startRect = start; 522 targetRect = target; 523 524 ResourceProvider rp = DynamicResource.provider(context); 525 minVisChange = rp.getDimension(R.dimen.swipe_up_fling_min_visible_change); 526 maxVelocityPxPerS = (int) rp.getDimension(R.dimen.swipe_up_max_velocity); 527 } 528 } 529 530 /** 531 * Standard spring configuration parameters. 532 */ 533 public static class DefaultSpringConfig extends SpringConfig { 534 DefaultSpringConfig(Context context, DeviceProfile deviceProfile, RectF startRect, RectF targetRect)535 public DefaultSpringConfig(Context context, DeviceProfile deviceProfile, 536 RectF startRect, RectF targetRect) { 537 super(context, startRect, targetRect); 538 539 ResourceProvider rp = DynamicResource.provider(context); 540 tracking = getDefaultTracking(deviceProfile); 541 stiffnessX = rp.getFloat(R.dimen.swipe_up_rect_xy_stiffness); 542 stiffnessY = rp.getFloat(R.dimen.swipe_up_rect_xy_stiffness); 543 dampingX = rp.getFloat(R.dimen.swipe_up_rect_xy_damping_ratio); 544 dampingY = rp.getFloat(R.dimen.swipe_up_rect_xy_damping_ratio); 545 546 this.startRect = startRect; 547 this.targetRect = targetRect; 548 549 // Increase the stiffness for devices where we want the window size to transform 550 // quicker. 551 boolean shouldUseHigherStiffness = deviceProfile != null 552 && (deviceProfile.isLandscape || deviceProfile.isTablet); 553 rectStiffness = shouldUseHigherStiffness 554 ? rp.getFloat(R.dimen.swipe_up_rect_scale_higher_stiffness) 555 : rp.getFloat(R.dimen.swipe_up_rect_scale_stiffness); 556 } 557 getDefaultTracking(@ullable DeviceProfile deviceProfile)558 private @Tracking int getDefaultTracking(@Nullable DeviceProfile deviceProfile) { 559 @Tracking int tracking; 560 if (deviceProfile == null) { 561 tracking = startRect.bottom < targetRect.bottom 562 ? TRACKING_BOTTOM 563 : TRACKING_TOP; 564 } else { 565 int heightPx = deviceProfile.heightPx; 566 Rect padding = deviceProfile.workspacePadding; 567 568 final float topThreshold = heightPx / 3f; 569 final float bottomThreshold = deviceProfile.heightPx - padding.bottom; 570 571 if (targetRect.bottom > bottomThreshold) { 572 if (enableScalingRevealHomeAnimation()) { 573 tracking = TRACKING_CENTER; 574 } else { 575 tracking = TRACKING_BOTTOM; 576 } 577 } else if (targetRect.top < topThreshold) { 578 tracking = TRACKING_TOP; 579 } else { 580 tracking = TRACKING_CENTER; 581 } 582 } 583 return tracking; 584 } 585 } 586 587 /** 588 * Spring configuration parameters for Taskbar/Hotseat items on devices that have a taskbar. 589 */ 590 public static class TaskbarHotseatSpringConfig extends SpringConfig { 591 TaskbarHotseatSpringConfig(Context context, RectF start, RectF target)592 public TaskbarHotseatSpringConfig(Context context, RectF start, RectF target) { 593 super(context, start, target); 594 595 ResourceProvider rp = DynamicResource.provider(context); 596 tracking = TRACKING_CENTER; 597 stiffnessX = rp.getFloat(R.dimen.taskbar_swipe_up_rect_x_stiffness); 598 stiffnessY = rp.getFloat(R.dimen.taskbar_swipe_up_rect_y_stiffness); 599 dampingX = rp.getFloat(R.dimen.taskbar_swipe_up_rect_x_damping); 600 dampingY = rp.getFloat(R.dimen.taskbar_swipe_up_rect_y_damping); 601 rectStiffness = rp.getFloat(R.dimen.taskbar_swipe_up_rect_scale_stiffness); 602 } 603 } 604 605 } 606