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