1 /* 2 * Copyright (C) 2020 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; 17 18 import static com.android.launcher3.anim.Interpolators.ACCEL_1_5; 19 import static com.android.launcher3.anim.Interpolators.LINEAR; 20 21 import android.animation.Animator; 22 import android.content.Context; 23 import android.graphics.Matrix; 24 import android.graphics.Matrix.ScaleToFit; 25 import android.graphics.Rect; 26 import android.graphics.RectF; 27 import android.view.RemoteAnimationTarget; 28 29 import androidx.annotation.NonNull; 30 import androidx.annotation.UiThread; 31 32 import com.android.launcher3.DeviceProfile; 33 import com.android.launcher3.Utilities; 34 import com.android.launcher3.anim.AnimatedFloat; 35 import com.android.launcher3.anim.AnimationSuccessListener; 36 import com.android.launcher3.anim.AnimatorPlaybackController; 37 import com.android.launcher3.anim.PendingAnimation; 38 import com.android.launcher3.touch.PagedOrientationHandler; 39 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle; 40 import com.android.quickstep.util.AnimatorControllerWithResistance; 41 import com.android.quickstep.util.RectFSpringAnim; 42 import com.android.quickstep.util.RectFSpringAnim.DefaultSpringConfig; 43 import com.android.quickstep.util.RectFSpringAnim.TaskbarHotseatSpringConfig; 44 import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties; 45 import com.android.quickstep.util.TaskViewSimulator; 46 import com.android.quickstep.util.TransformParams; 47 import com.android.quickstep.util.TransformParams.BuilderProxy; 48 49 import java.util.Arrays; 50 import java.util.function.Consumer; 51 52 public abstract class SwipeUpAnimationLogic implements 53 RecentsAnimationCallbacks.RecentsAnimationListener{ 54 55 protected static final Rect TEMP_RECT = new Rect(); 56 protected final RemoteTargetGluer mTargetGluer; 57 58 protected DeviceProfile mDp; 59 60 protected final Context mContext; 61 protected final RecentsAnimationDeviceState mDeviceState; 62 protected final GestureState mGestureState; 63 64 protected RemoteTargetHandle[] mRemoteTargetHandles; 65 66 // Shift in the range of [0, 1]. 67 // 0 => preview snapShot is completely visible, and hotseat is completely translated down 68 // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely 69 // visible. 70 protected final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift); 71 protected float mCurrentDisplacement; 72 73 // The distance needed to drag to reach the task size in recents. 74 protected int mTransitionDragLength; 75 // How much further we can drag past recents, as a factor of mTransitionDragLength. 76 protected float mDragLengthFactor = 1; 77 78 protected boolean mIsSwipeForSplit; 79 SwipeUpAnimationLogic(Context context, RecentsAnimationDeviceState deviceState, GestureState gestureState)80 public SwipeUpAnimationLogic(Context context, RecentsAnimationDeviceState deviceState, 81 GestureState gestureState) { 82 mContext = context; 83 mDeviceState = deviceState; 84 mGestureState = gestureState; 85 mIsSwipeForSplit = TopTaskTracker.INSTANCE.get(context).getRunningSplitTaskIds().length > 1; 86 87 mTargetGluer = new RemoteTargetGluer(mContext, mGestureState.getActivityInterface()); 88 mRemoteTargetHandles = mTargetGluer.getRemoteTargetHandles(); 89 runActionOnRemoteHandles(remoteTargetHandle -> 90 remoteTargetHandle.getTaskViewSimulator().getOrientationState().update( 91 mDeviceState.getRotationTouchHelper().getCurrentActiveRotation(), 92 mDeviceState.getRotationTouchHelper().getDisplayRotation() 93 )); 94 } 95 initTransitionEndpoints(DeviceProfile dp)96 protected void initTransitionEndpoints(DeviceProfile dp) { 97 mDp = dp; 98 mTransitionDragLength = mGestureState.getActivityInterface().getSwipeUpDestinationAndLength( 99 dp, mContext, TEMP_RECT, mRemoteTargetHandles[0].getTaskViewSimulator() 100 .getOrientationState().getOrientationHandler()); 101 mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength; 102 103 for (RemoteTargetHandle remoteHandle : mRemoteTargetHandles) { 104 PendingAnimation pendingAnimation = new PendingAnimation(mTransitionDragLength * 2); 105 TaskViewSimulator taskViewSimulator = remoteHandle.getTaskViewSimulator(); 106 taskViewSimulator.setDp(dp); 107 taskViewSimulator.addAppToOverviewAnim(pendingAnimation, LINEAR); 108 AnimatorPlaybackController playbackController = 109 pendingAnimation.createPlaybackController(); 110 111 remoteHandle.setPlaybackController(AnimatorControllerWithResistance.createForRecents( 112 playbackController, mContext, taskViewSimulator.getOrientationState(), 113 mDp, taskViewSimulator.recentsViewScale, AnimatedFloat.VALUE, 114 taskViewSimulator.recentsViewSecondaryTranslation, AnimatedFloat.VALUE 115 )); 116 } 117 } 118 119 @UiThread updateDisplacement(float displacement)120 public void updateDisplacement(float displacement) { 121 // We are moving in the negative x/y direction 122 displacement = overrideDisplacementForTransientTaskbar(-displacement); 123 mCurrentDisplacement = displacement; 124 125 float shift; 126 if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) { 127 shift = mDragLengthFactor; 128 } else { 129 float translation = Math.max(displacement, 0); 130 shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength; 131 } 132 133 mCurrentShift.updateValue(shift); 134 } 135 136 /** 137 * When Transient Taskbar is enabled, subclasses can override the displacement to keep the app 138 * window at the bottom of the screen while taskbar is being swiped in. 139 * @param displacement The distance the user has swiped up from the bottom of the screen. This 140 * value will be positive unless the user swipe downwards. 141 * @return the overridden displacement. 142 */ overrideDisplacementForTransientTaskbar(float displacement)143 protected float overrideDisplacementForTransientTaskbar(float displacement) { 144 return displacement; 145 } 146 147 /** 148 * Called when the value of {@link #mCurrentShift} changes 149 */ 150 @UiThread updateFinalShift()151 public abstract void updateFinalShift(); 152 getOrientationHandler()153 protected PagedOrientationHandler getOrientationHandler() { 154 // OrientationHandler should be independent of remote target, can directly take one 155 return mRemoteTargetHandles[0].getTaskViewSimulator() 156 .getOrientationState().getOrientationHandler(); 157 } 158 159 protected abstract class HomeAnimationFactory { 160 protected float mSwipeVelocity; 161 162 /** 163 * Returns true if we know the home animation involves an item in the hotseat. 164 */ isInHotseat()165 public boolean isInHotseat() { 166 return false; 167 } 168 getWindowTargetRect()169 public @NonNull RectF getWindowTargetRect() { 170 PagedOrientationHandler orientationHandler = getOrientationHandler(); 171 DeviceProfile dp = mDp; 172 final int halfIconSize = dp.iconSizePx / 2; 173 float primaryDimension = orientationHandler 174 .getPrimaryValue(dp.availableWidthPx, dp.availableHeightPx); 175 float secondaryDimension = orientationHandler 176 .getSecondaryValue(dp.availableWidthPx, dp.availableHeightPx); 177 final float targetX = primaryDimension / 2f; 178 final float targetY = secondaryDimension - dp.hotseatBarSizePx; 179 // Fallback to animate to center of screen. 180 return new RectF(targetX - halfIconSize, targetY - halfIconSize, 181 targetX + halfIconSize, targetY + halfIconSize); 182 } 183 184 /** Returns the corner radius of the window at the end of the animation. */ getEndRadius(RectF cropRectF)185 public float getEndRadius(RectF cropRectF) { 186 return cropRectF.width() / 2f; 187 } 188 createActivityAnimationToHome()189 public abstract @NonNull AnimatorPlaybackController createActivityAnimationToHome(); 190 setSwipeVelocity(float velocity)191 public void setSwipeVelocity(float velocity) { 192 mSwipeVelocity = velocity; 193 } 194 playAtomicAnimation(float velocity)195 public void playAtomicAnimation(float velocity) { 196 // No-op 197 } 198 setAnimation(RectFSpringAnim anim)199 public void setAnimation(RectFSpringAnim anim) { } 200 update(RectF currentRect, float progress, float radius)201 public void update(RectF currentRect, float progress, float radius) { } 202 onCancel()203 public void onCancel() { } 204 205 /** 206 * @param progress The progress of the animation to the home screen. 207 * @return The current alpha to set on the animating app window. 208 */ getWindowAlpha(float progress)209 protected float getWindowAlpha(float progress) { 210 // Alpha interpolates between [1, 0] between progress values [start, end] 211 final float start = 0f; 212 final float end = 0.85f; 213 214 if (progress <= start) { 215 return 1f; 216 } 217 if (progress >= end) { 218 return 0f; 219 } 220 return Utilities.mapToRange(progress, start, end, 1, 0, ACCEL_1_5); 221 } 222 } 223 224 /** 225 * Update with start progress for window animation to home. 226 * @param outMatrix {@link Matrix} to map a rect in Launcher space to window space. 227 * @param startProgress The progress of {@link #mCurrentShift} to start thw window from. 228 * @return {@link RectF} represents the bounds as starting point in window space. 229 */ updateProgressForStartRect(Matrix[] outMatrix, float startProgress)230 protected RectF[] updateProgressForStartRect(Matrix[] outMatrix, float startProgress) { 231 mCurrentShift.updateValue(startProgress); 232 RectF[] startRects = new RectF[mRemoteTargetHandles.length]; 233 for (int i = 0, mRemoteTargetHandlesLength = mRemoteTargetHandles.length; 234 i < mRemoteTargetHandlesLength; i++) { 235 RemoteTargetHandle remoteHandle = mRemoteTargetHandles[i]; 236 TaskViewSimulator tvs = remoteHandle.getTaskViewSimulator(); 237 tvs.apply(remoteHandle.getTransformParams().setProgress(startProgress)); 238 239 startRects[i] = new RectF(tvs.getCurrentCropRect()); 240 outMatrix[i] = new Matrix(); 241 tvs.applyWindowToHomeRotation(outMatrix[i]); 242 tvs.getCurrentMatrix().mapRect(startRects[i]); 243 } 244 return startRects; 245 } 246 247 /** Helper to avoid writing some for-loops to iterate over {@link #mRemoteTargetHandles} */ runActionOnRemoteHandles(Consumer<RemoteTargetHandle> consumer)248 protected void runActionOnRemoteHandles(Consumer<RemoteTargetHandle> consumer) { 249 for (RemoteTargetHandle handle : mRemoteTargetHandles) { 250 consumer.accept(handle); 251 } 252 } 253 254 /** @return only the TaskViewSimulators from {@link #mRemoteTargetHandles} */ getRemoteTaskViewSimulators()255 protected TaskViewSimulator[] getRemoteTaskViewSimulators() { 256 return Arrays.stream(mRemoteTargetHandles) 257 .map(remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()) 258 .toArray(TaskViewSimulator[]::new); 259 } 260 261 /** 262 * Creates an animation that transforms the current app window into the home app. 263 * @param startProgress The progress of {@link #mCurrentShift} to start the window from. 264 * @param homeAnimationFactory The home animation factory. 265 */ createWindowAnimationToHome(float startProgress, HomeAnimationFactory homeAnimationFactory)266 protected RectFSpringAnim[] createWindowAnimationToHome(float startProgress, 267 HomeAnimationFactory homeAnimationFactory) { 268 // TODO(b/195473584) compute separate end targets for different staged split 269 final RectF targetRect = homeAnimationFactory.getWindowTargetRect(); 270 RectFSpringAnim[] out = new RectFSpringAnim[mRemoteTargetHandles.length]; 271 Matrix[] homeToWindowPositionMap = new Matrix[mRemoteTargetHandles.length]; 272 RectF[] startRects = updateProgressForStartRect(homeToWindowPositionMap, startProgress); 273 for (int i = 0, mRemoteTargetHandlesLength = mRemoteTargetHandles.length; 274 i < mRemoteTargetHandlesLength; i++) { 275 RemoteTargetHandle remoteHandle = mRemoteTargetHandles[i]; 276 out[i] = getWindowAnimationToHomeInternal(homeAnimationFactory, 277 targetRect, remoteHandle.getTransformParams(), 278 remoteHandle.getTaskViewSimulator(), startRects[i], homeToWindowPositionMap[i]); 279 } 280 return out; 281 } 282 getWindowAnimationToHomeInternal( HomeAnimationFactory homeAnimationFactory, RectF targetRect, TransformParams transformParams, TaskViewSimulator taskViewSimulator, RectF startRect, Matrix homeToWindowPositionMap)283 private RectFSpringAnim getWindowAnimationToHomeInternal( 284 HomeAnimationFactory homeAnimationFactory, RectF targetRect, 285 TransformParams transformParams, TaskViewSimulator taskViewSimulator, 286 RectF startRect, Matrix homeToWindowPositionMap) { 287 RectF cropRectF = new RectF(taskViewSimulator.getCurrentCropRect()); 288 // Move the startRect to Launcher space as floatingIconView runs in Launcher 289 Matrix windowToHomePositionMap = new Matrix(); 290 291 // If the start rect ends up overshooting too much to the left/right offscreen, bring it 292 // back to fullscreen. This can happen when the recentsScroll value isn't aligned with 293 // the pageScroll value for a given taskView, see b/228829958#comment12 294 mRemoteTargetHandles[0].getTaskViewSimulator().getOrientationState().getOrientationHandler() 295 .fixBoundsForHomeAnimStartRect(startRect, mDp); 296 297 homeToWindowPositionMap.invert(windowToHomePositionMap); 298 windowToHomePositionMap.mapRect(startRect); 299 300 boolean useTaskbarHotseatParams = mDp.isTaskbarPresent 301 && homeAnimationFactory.isInHotseat(); 302 RectFSpringAnim anim = new RectFSpringAnim(useTaskbarHotseatParams 303 ? new TaskbarHotseatSpringConfig(mContext, startRect, targetRect) 304 : new DefaultSpringConfig(mContext, mDp, startRect, targetRect)); 305 homeAnimationFactory.setAnimation(anim); 306 307 SpringAnimationRunner runner = new SpringAnimationRunner( 308 homeAnimationFactory, cropRectF, homeToWindowPositionMap, 309 transformParams, taskViewSimulator); 310 anim.addAnimatorListener(runner); 311 anim.addOnUpdateListener(runner); 312 return anim; 313 } 314 315 protected class SpringAnimationRunner extends AnimationSuccessListener 316 implements RectFSpringAnim.OnUpdateListener, BuilderProxy { 317 318 final Rect mCropRect = new Rect(); 319 final Matrix mMatrix = new Matrix(); 320 321 final RectF mWindowCurrentRect = new RectF(); 322 final Matrix mHomeToWindowPositionMap; 323 private final TransformParams mLocalTransformParams; 324 final HomeAnimationFactory mAnimationFactory; 325 326 final AnimatorPlaybackController mHomeAnim; 327 final RectF mCropRectF; 328 329 final float mStartRadius; 330 final float mEndRadius; 331 SpringAnimationRunner(HomeAnimationFactory factory, RectF cropRectF, Matrix homeToWindowPositionMap, TransformParams transformParams, TaskViewSimulator taskViewSimulator)332 SpringAnimationRunner(HomeAnimationFactory factory, RectF cropRectF, 333 Matrix homeToWindowPositionMap, TransformParams transformParams, 334 TaskViewSimulator taskViewSimulator) { 335 mAnimationFactory = factory; 336 mHomeAnim = factory.createActivityAnimationToHome(); 337 mCropRectF = cropRectF; 338 mHomeToWindowPositionMap = homeToWindowPositionMap; 339 mLocalTransformParams = transformParams; 340 341 cropRectF.roundOut(mCropRect); 342 343 // End on a "round-enough" radius so that the shape reveal doesn't have to do too much 344 // rounding at the end of the animation. 345 mStartRadius = taskViewSimulator.getCurrentCornerRadius(); 346 mEndRadius = factory.getEndRadius(cropRectF); 347 } 348 349 @Override onUpdate(RectF currentRect, float progress)350 public void onUpdate(RectF currentRect, float progress) { 351 mHomeAnim.setPlayFraction(progress); 352 mHomeToWindowPositionMap.mapRect(mWindowCurrentRect, currentRect); 353 354 mMatrix.setRectToRect(mCropRectF, mWindowCurrentRect, ScaleToFit.FILL); 355 float cornerRadius = Utilities.mapRange(progress, mStartRadius, mEndRadius); 356 float alpha = mAnimationFactory.getWindowAlpha(progress); 357 mLocalTransformParams 358 .setTargetAlpha(alpha) 359 .setCornerRadius(cornerRadius); 360 mLocalTransformParams.applySurfaceParams(mLocalTransformParams 361 .createSurfaceParams(this)); 362 mAnimationFactory.update(currentRect, progress, mMatrix.mapRadius(cornerRadius)); 363 } 364 365 @Override onBuildTargetParams(SurfaceProperties builder, RemoteAnimationTarget app, TransformParams params)366 public void onBuildTargetParams(SurfaceProperties builder, RemoteAnimationTarget app, 367 TransformParams params) { 368 builder.setMatrix(mMatrix) 369 .setWindowCrop(mCropRect) 370 .setCornerRadius(params.getCornerRadius()); 371 } 372 373 @Override onCancel()374 public void onCancel() { 375 mAnimationFactory.onCancel(); 376 } 377 378 @Override onAnimationStart(Animator animation)379 public void onAnimationStart(Animator animation) { 380 mHomeAnim.dispatchOnStart(); 381 } 382 383 @Override onAnimationSuccess(Animator animator)384 public void onAnimationSuccess(Animator animator) { 385 mHomeAnim.getAnimationPlayer().end(); 386 } 387 } 388 389 public interface RunningWindowAnim { end()390 void end(); 391 cancel()392 void cancel(); 393 wrap(Animator animator)394 static RunningWindowAnim wrap(Animator animator) { 395 return new RunningWindowAnim() { 396 @Override 397 public void end() { 398 animator.end(); 399 } 400 401 @Override 402 public void cancel() { 403 animator.cancel(); 404 } 405 }; 406 } 407 wrap(RectFSpringAnim rectFSpringAnim)408 static RunningWindowAnim wrap(RectFSpringAnim rectFSpringAnim) { 409 return new RunningWindowAnim() { 410 @Override 411 public void end() { 412 rectFSpringAnim.end(); 413 } 414 415 @Override 416 public void cancel() { 417 rectFSpringAnim.cancel(); 418 } 419 }; 420 } 421 } 422 } 423