1 /* 2 * Copyright (C) 2021 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.Utilities.dpToPx; 19 import static com.android.launcher3.anim.Interpolators.LINEAR; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.animation.ObjectAnimator; 24 import android.animation.ValueAnimator; 25 import android.content.Context; 26 import android.graphics.PointF; 27 import android.graphics.RectF; 28 import android.util.PathParser; 29 import android.util.Property; 30 import android.view.animation.Interpolator; 31 32 import androidx.core.view.animation.PathInterpolatorCompat; 33 import androidx.dynamicanimation.animation.FloatPropertyCompat; 34 import androidx.dynamicanimation.animation.SpringAnimation; 35 import androidx.dynamicanimation.animation.SpringForce; 36 37 import com.android.launcher3.R; 38 import com.android.launcher3.Utilities; 39 import com.android.launcher3.util.DynamicResource; 40 import com.android.systemui.plugins.ResourceProvider; 41 42 import java.util.ArrayList; 43 import java.util.List; 44 import java.util.Locale; 45 46 /** 47 * Applies spring forces to animate from a starting rect to a target rect, 48 * while providing update callbacks to the caller. 49 */ 50 public class RectFSpringAnim2 extends RectFSpringAnim { 51 52 private static final FloatPropertyCompat<RectFSpringAnim2> RECT_CENTER_X = 53 new FloatPropertyCompat<RectFSpringAnim2>("rectCenterXSpring") { 54 @Override 55 public float getValue(RectFSpringAnim2 anim) { 56 return anim.mCurrentCenterX; 57 } 58 59 @Override 60 public void setValue(RectFSpringAnim2 anim, float currentCenterX) { 61 anim.mCurrentCenterX = currentCenterX; 62 anim.onUpdate(); 63 } 64 }; 65 66 private static final FloatPropertyCompat<RectFSpringAnim2> RECT_Y = 67 new FloatPropertyCompat<RectFSpringAnim2>("rectYSpring") { 68 @Override 69 public float getValue(RectFSpringAnim2 anim) { 70 return anim.mCurrentCenterY; 71 } 72 73 @Override 74 public void setValue(RectFSpringAnim2 anim, float y) { 75 anim.mCurrentCenterY = y; 76 anim.onUpdate(); 77 } 78 }; 79 80 private static final Property<RectFSpringAnim2, Float> PROGRESS = 81 new Property<RectFSpringAnim2, Float>(Float.class, "rectFProgress") { 82 @Override 83 public Float get(RectFSpringAnim2 rectFSpringAnim) { 84 return rectFSpringAnim.mProgress; 85 } 86 87 @Override 88 public void set(RectFSpringAnim2 rectFSpringAnim, Float progress) { 89 rectFSpringAnim.mProgress = progress; 90 rectFSpringAnim.onUpdate(); 91 } 92 }; 93 94 private final RectF mStartRect; 95 private final RectF mTargetRect; 96 private final RectF mCurrentRect = new RectF(); 97 private final List<OnUpdateListener> mOnUpdateListeners = new ArrayList<>(); 98 private final List<Animator.AnimatorListener> mAnimatorListeners = new ArrayList<>(); 99 100 private float mCurrentCenterX; 101 private float mCurrentCenterY; 102 103 private float mTargetX; 104 private float mTargetY; 105 106 // If true, tracking the bottom of the rects, else tracking the top. 107 private float mProgress; 108 private SpringAnimation mRectXAnim; 109 private SpringAnimation mRectYAnim; 110 private ValueAnimator mRectScaleAnim; 111 private boolean mAnimsStarted; 112 private boolean mRectXAnimEnded; 113 private boolean mRectYAnimEnded; 114 private boolean mRectScaleAnimEnded; 115 116 private final float mXDamping; 117 private final float mXStiffness; 118 119 private final float mYDamping; 120 private float mYStiffness; 121 122 private long mDuration; 123 124 private final Interpolator mCloseInterpolator; 125 126 private AppCloseConfig mValues; 127 final float mStartRadius; 128 final float mEndRadius; 129 130 final float mHomeTransYEnd; 131 final float mScaleStart; 132 RectFSpringAnim2(RectF startRect, RectF targetRect, Context context, float startRadius, float endRadius)133 public RectFSpringAnim2(RectF startRect, RectF targetRect, Context context, float startRadius, 134 float endRadius) { 135 super(startRect, targetRect, context); 136 mStartRect = startRect; 137 mTargetRect = targetRect; 138 139 mCurrentCenterY = mStartRect.centerY(); 140 mCurrentCenterX = mStartRect.centerX(); 141 142 mTargetY = mTargetRect.centerY(); 143 mTargetX = mTargetRect.centerX(); 144 145 ResourceProvider rp = DynamicResource.provider(context); 146 mXDamping = rp.getFloat(R.dimen.swipe_up_rect_2_x_damping_ratio); 147 mXStiffness = rp.getFloat(R.dimen.swipe_up_rect_2_x_stiffness); 148 149 mYDamping = rp.getFloat(R.dimen.swipe_up_rect_2_y_damping_ratio); 150 mYStiffness = rp.getFloat(R.dimen.swipe_up_rect_2_y_stiffness); 151 mDuration = Math.round(rp.getFloat(R.dimen.swipe_up_duration)); 152 153 mHomeTransYEnd = dpToPx(rp.getFloat(R.dimen.swipe_up_trans_y_dp)); 154 mScaleStart = rp.getFloat(R.dimen.swipe_up_scale_start); 155 156 mCloseInterpolator = getAppCloseInterpolator(context); 157 158 // End on a "round-enough" radius so that the shape reveal doesn't have to do too much 159 // rounding at the end of the animation. 160 mStartRadius = startRadius; 161 mEndRadius = endRadius; 162 163 setCanRelease(true); 164 } 165 onTargetPositionChanged()166 public void onTargetPositionChanged() { 167 if (mRectXAnim != null && mTargetX != mTargetRect.centerX()) { 168 mTargetX = mTargetRect.centerX(); 169 mRectXAnim.animateToFinalPosition(mTargetX); 170 } 171 172 if (mRectYAnim != null) { 173 if (mTargetY != mTargetRect.centerY()) { 174 mTargetY = mTargetRect.centerY(); 175 mRectYAnim.animateToFinalPosition(mTargetY); 176 } 177 } 178 } 179 addOnUpdateListener(OnUpdateListener onUpdateListener)180 public void addOnUpdateListener(OnUpdateListener onUpdateListener) { 181 mOnUpdateListeners.add(onUpdateListener); 182 } 183 addAnimatorListener(Animator.AnimatorListener animatorListener)184 public void addAnimatorListener(Animator.AnimatorListener animatorListener) { 185 mAnimatorListeners.add(animatorListener); 186 } 187 188 /** 189 * Starts the fling/spring animation. 190 * @param context The activity context. 191 * @param velocityPxPerMs Velocity of swipe in px/ms. 192 */ start(Context context, PointF velocityPxPerMs)193 public void start(Context context, PointF velocityPxPerMs) { 194 mRectXAnim = new SpringAnimation(this, RECT_CENTER_X) 195 .setStartValue(mCurrentCenterX) 196 .setStartVelocity(velocityPxPerMs.x * 1000) 197 .setSpring(new SpringForce(mTargetX) 198 .setStiffness(mXStiffness) 199 .setDampingRatio(mXDamping)); 200 mRectXAnim.addEndListener(((animation, canceled, centerX, velocityX) -> { 201 mRectXAnimEnded = true; 202 maybeOnEnd(); 203 })); 204 205 mRectYAnim = new SpringAnimation(this, RECT_Y) 206 .setStartValue(mCurrentCenterY) 207 .setStartVelocity(velocityPxPerMs.y * 1000) 208 .setSpring(new SpringForce(mTargetY) 209 .setStiffness(mYStiffness) 210 .setDampingRatio(mYDamping)); 211 mRectYAnim.addEndListener(((animation, canceled, centerY, velocityY) -> { 212 mRectYAnimEnded = true; 213 maybeOnEnd(); 214 })); 215 216 mRectScaleAnim = ObjectAnimator.ofFloat(this, PROGRESS, 0, 1f) 217 .setDuration(mDuration); 218 mRectScaleAnim.setInterpolator(mCloseInterpolator); 219 mRectScaleAnim.addListener(new AnimatorListenerAdapter() { 220 @Override 221 public void onAnimationEnd(Animator animation) { 222 mRectScaleAnimEnded = true; 223 maybeOnEnd(); 224 } 225 }); 226 227 mValues = buildConfig(); 228 mRectScaleAnim.addUpdateListener(mValues); 229 230 setCanRelease(false); 231 mAnimsStarted = true; 232 233 mRectXAnim.start(); 234 mRectYAnim.start(); 235 mRectScaleAnim.start(); 236 for (Animator.AnimatorListener animatorListener : mAnimatorListeners) { 237 animatorListener.onAnimationStart(null); 238 } 239 } 240 buildConfig()241 private AppCloseConfig buildConfig() { 242 return new AppCloseConfig() { 243 FloatProp mHomeTransY = new FloatProp(0, mHomeTransYEnd, 0, mDuration, LINEAR); 244 FloatProp mHomeScale = new FloatProp(mScaleStart, 1f, 0, mDuration, LINEAR); 245 FloatProp mWindowFadeOut = new FloatProp(1f, 0f, 0, 116, LINEAR); 246 // There should be a slight overlap b/w window fading out and fg fading in. 247 // (fg startDelay < window fade out duration) 248 FloatProp mFgFadeIn = new FloatProp(0, 255f, 100, mDuration - 100, LINEAR); 249 FloatProp mRadius = new FloatProp(mStartRadius, mEndRadius, 0, mDuration, LINEAR); 250 FloatProp mThreePointInterpolation = new FloatProp(0, 1, 0, mDuration, LINEAR); 251 252 @Override 253 public float getWorkspaceTransY() { 254 return mHomeTransY.value; 255 } 256 257 @Override 258 public float getWorkspaceScale() { 259 return mHomeScale.value; 260 } 261 262 @Override 263 public float getWindowAlpha() { 264 return mWindowFadeOut.value; 265 } 266 267 @Override 268 public int getFgAlpha() { 269 return (int) mFgFadeIn.value; 270 } 271 272 @Override 273 public float getCornerRadius() { 274 return mRadius.value; 275 } 276 277 @Override 278 public float getInterpolatedProgress() { 279 return mThreePointInterpolation.value; 280 } 281 282 @Override 283 public void onUpdate(float percent, boolean initOnly) {} 284 }; 285 } 286 end()287 public void end() { 288 if (mAnimsStarted) { 289 if (mRectXAnim.canSkipToEnd()) { 290 mRectXAnim.skipToEnd(); 291 } 292 if (mRectYAnim.canSkipToEnd()) { 293 mRectYAnim.skipToEnd(); 294 } 295 mRectScaleAnim.end(); 296 } 297 mRectXAnimEnded = true; 298 mRectYAnimEnded = true; 299 mRectScaleAnimEnded = true; 300 maybeOnEnd(); 301 } 302 isEnded()303 private boolean isEnded() { 304 return mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded; 305 } 306 onUpdate()307 private void onUpdate() { 308 if (isEnded()) { 309 // Prevent further updates from being called. This can happen between callbacks for 310 // ending the x/y/scale animations. 311 return; 312 } 313 314 if (!mOnUpdateListeners.isEmpty()) { 315 float rectProgress = mProgress; 316 float currentWidth = Utilities.mapRange(rectProgress, mStartRect.width(), 317 mTargetRect.width()); 318 float currentHeight = Utilities.mapRange(rectProgress, mStartRect.height(), 319 mTargetRect.height()); 320 321 mCurrentRect.set(mCurrentCenterX - currentWidth / 2, 322 mCurrentCenterY - currentHeight / 2, 323 mCurrentCenterX + currentWidth / 2, 324 mCurrentCenterY + currentHeight / 2); 325 326 float currentPlayTime = mRectScaleAnimEnded ? mRectScaleAnim.getDuration() 327 : mRectScaleAnim.getCurrentPlayTime(); 328 float linearProgress = Math.min(1f, currentPlayTime / mRectScaleAnim.getDuration()); 329 for (OnUpdateListener onUpdateListener : mOnUpdateListeners) { 330 onUpdateListener.onUpdate(mValues, mCurrentRect, linearProgress); 331 } 332 } 333 } 334 maybeOnEnd()335 private void maybeOnEnd() { 336 if (mAnimsStarted && isEnded()) { 337 mAnimsStarted = false; 338 setCanRelease(true); 339 for (Animator.AnimatorListener animatorListener : mAnimatorListeners) { 340 animatorListener.onAnimationEnd(null); 341 } 342 } 343 } 344 cancel()345 public void cancel() { 346 if (mAnimsStarted) { 347 for (OnUpdateListener onUpdateListener : mOnUpdateListeners) { 348 onUpdateListener.onCancel(); 349 } 350 } 351 end(); 352 } 353 getAppCloseInterpolator(Context context)354 private Interpolator getAppCloseInterpolator(Context context) { 355 ResourceProvider rp = DynamicResource.provider(context); 356 String path = String.format(Locale.ENGLISH, 357 "M 0,0 C %f, %f, %f, %f, %f, %f C %f, %f, %f, %f, 1, 1", 358 rp.getFloat(R.dimen.c1_a), 359 rp.getFloat(R.dimen.c1_b), 360 rp.getFloat(R.dimen.c1_c), 361 rp.getFloat(R.dimen.c1_d), 362 rp.getFloat(R.dimen.mp_x), 363 rp.getFloat(R.dimen.mp_y), 364 rp.getFloat(R.dimen.c2_a), 365 rp.getFloat(R.dimen.c2_b), 366 rp.getFloat(R.dimen.c2_c), 367 rp.getFloat(R.dimen.c2_d)); 368 return PathInterpolatorCompat.create(PathParser.createPathFromPathData(path)); 369 } 370 } 371