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