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 17 package com.android.quickstep.util; 18 19 import static com.android.systemui.shared.system.InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_PIP; 20 21 import android.animation.Animator; 22 import android.animation.RectEvaluator; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.pm.ActivityInfo; 26 import android.graphics.Matrix; 27 import android.graphics.Rect; 28 import android.graphics.RectF; 29 import android.util.Log; 30 import android.view.Surface; 31 import android.view.SurfaceControl; 32 import android.view.View; 33 import android.window.PictureInPictureSurfaceTransaction; 34 35 import androidx.annotation.NonNull; 36 import androidx.annotation.Nullable; 37 38 import com.android.launcher3.anim.AnimationSuccessListener; 39 import com.android.launcher3.icons.IconProvider; 40 import com.android.quickstep.TaskAnimationManager; 41 import com.android.systemui.shared.pip.PipSurfaceTransactionHelper; 42 import com.android.systemui.shared.system.InteractionJankMonitorWrapper; 43 import com.android.wm.shell.pip.PipContentOverlay; 44 45 /** 46 * Subclass of {@link RectFSpringAnim} that animates an Activity to PiP (picture-in-picture) window 47 * when swiping up (in gesture navigation mode). 48 */ 49 public class SwipePipToHomeAnimator extends RectFSpringAnim { 50 private static final String TAG = SwipePipToHomeAnimator.class.getSimpleName(); 51 52 private static final float END_PROGRESS = 1.0f; 53 54 private final int mTaskId; 55 private final ActivityInfo mActivityInfo; 56 private final SurfaceControl mLeash; 57 private final Rect mSourceRectHint = new Rect(); 58 private final Rect mAppBounds = new Rect(); 59 private final Matrix mHomeToWindowPositionMap = new Matrix(); 60 private final Rect mStartBounds = new Rect(); 61 private final RectF mCurrentBoundsF = new RectF(); 62 private final Rect mCurrentBounds = new Rect(); 63 private final Rect mDestinationBounds = new Rect(); 64 private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; 65 66 /** 67 * For calculating transform in 68 * {@link #onAnimationUpdate(SurfaceControl.Transaction, RectF, float)} 69 */ 70 private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect()); 71 private final Rect mSourceHintRectInsets; 72 private final Rect mSourceInsets = new Rect(); 73 74 /** for rotation calculations */ 75 private final @RecentsOrientedState.SurfaceRotation int mFromRotation; 76 private final Rect mDestinationBoundsTransformed = new Rect(); 77 78 /** 79 * Flag to avoid the double-end problem since the leash would have been released 80 * after the first end call and any further operations upon it would lead to NPE. 81 */ 82 private boolean mHasAnimationEnded; 83 84 /** 85 * Wrapper of {@link SurfaceControl} that is used when entering PiP without valid 86 * source rect hint. 87 */ 88 @Nullable 89 private PipContentOverlay mPipContentOverlay; 90 91 /** 92 * @param context {@link Context} provides Launcher resources 93 * @param taskId Task id associated with this animator, see also {@link #getTaskId()} 94 * @param activityInfo {@link ActivityInfo} associated with this animator, 95 * see also {@link #getComponentName()} 96 * @param appIconSizePx The size in pixel for the app icon in content overlay 97 * @param leash {@link SurfaceControl} this animator operates on 98 * @param sourceRectHint See the definition in {@link android.app.PictureInPictureParams} 99 * @param appBounds Bounds of the application, sourceRectHint is based on this bounds 100 * @param homeToWindowPositionMap {@link Matrix} to map a Rect from home to window space 101 * @param startBounds Bounds of the application when this animator starts. This can be 102 * different from the appBounds if user has swiped a certain distance and 103 * Launcher has performed transform on the leash. 104 * @param destinationBounds Bounds of the destination this animator ends to 105 * @param fromRotation From rotation if different from final rotation, ROTATION_0 otherwise 106 * @param destinationBoundsTransformed Destination bounds in window space 107 * @param cornerRadius Corner radius in pixel value for PiP window 108 * @param shadowRadius Shadow radius in pixel value for PiP window 109 * @param view Attached view for logging purpose 110 */ SwipePipToHomeAnimator(@onNull Context context, int taskId, @NonNull ActivityInfo activityInfo, int appIconSizePx, @NonNull SurfaceControl leash, @Nullable Rect sourceRectHint, @NonNull Rect appBounds, @NonNull Matrix homeToWindowPositionMap, @NonNull RectF startBounds, @NonNull Rect destinationBounds, @RecentsOrientedState.SurfaceRotation int fromRotation, @NonNull Rect destinationBoundsTransformed, int cornerRadius, int shadowRadius, @NonNull View view)111 private SwipePipToHomeAnimator(@NonNull Context context, 112 int taskId, 113 @NonNull ActivityInfo activityInfo, 114 int appIconSizePx, 115 @NonNull SurfaceControl leash, 116 @Nullable Rect sourceRectHint, 117 @NonNull Rect appBounds, 118 @NonNull Matrix homeToWindowPositionMap, 119 @NonNull RectF startBounds, 120 @NonNull Rect destinationBounds, 121 @RecentsOrientedState.SurfaceRotation int fromRotation, 122 @NonNull Rect destinationBoundsTransformed, 123 int cornerRadius, 124 int shadowRadius, 125 @NonNull View view) { 126 super(new DefaultSpringConfig(context, null, startBounds, 127 new RectF(destinationBoundsTransformed))); 128 mTaskId = taskId; 129 mActivityInfo = activityInfo; 130 mLeash = leash; 131 mAppBounds.set(appBounds); 132 mHomeToWindowPositionMap.set(homeToWindowPositionMap); 133 startBounds.round(mStartBounds); 134 mDestinationBounds.set(destinationBounds); 135 mFromRotation = fromRotation; 136 mDestinationBoundsTransformed.set(destinationBoundsTransformed); 137 mSurfaceTransactionHelper = new PipSurfaceTransactionHelper(cornerRadius, shadowRadius); 138 139 if (sourceRectHint != null && (sourceRectHint.width() < destinationBounds.width() 140 || sourceRectHint.height() < destinationBounds.height())) { 141 // This is a situation in which the source hint rect on at least one axis is smaller 142 // than the destination bounds, which presents a problem because we would have to scale 143 // up that axis to fit the bounds. So instead, just fallback to the non-source hint 144 // animation in this case. 145 sourceRectHint = null; 146 } 147 148 if (sourceRectHint == null) { 149 mSourceRectHint.setEmpty(); 150 mSourceHintRectInsets = null; 151 152 // Create a new overlay layer. We do not call detach on this instance, it's propagated 153 // to other classes like PipTaskOrganizer / RecentsAnimationController to complete 154 // the cleanup. 155 mPipContentOverlay = new PipContentOverlay.PipAppIconOverlay(view.getContext(), 156 mAppBounds, new IconProvider(context).getIcon(mActivityInfo), 157 appIconSizePx); 158 final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); 159 mPipContentOverlay.attach(tx, mLeash); 160 } else { 161 mSourceRectHint.set(sourceRectHint); 162 mSourceHintRectInsets = new Rect(sourceRectHint.left - appBounds.left, 163 sourceRectHint.top - appBounds.top, 164 appBounds.right - sourceRectHint.right, 165 appBounds.bottom - sourceRectHint.bottom); 166 } 167 168 addAnimatorListener(new AnimationSuccessListener() { 169 @Override 170 public void onAnimationStart(Animator animation) { 171 InteractionJankMonitorWrapper.begin(view, CUJ_APP_CLOSE_TO_PIP); 172 super.onAnimationStart(animation); 173 } 174 175 @Override 176 public void onAnimationCancel(Animator animation) { 177 super.onAnimationCancel(animation); 178 InteractionJankMonitorWrapper.cancel(CUJ_APP_CLOSE_TO_PIP); 179 } 180 181 @Override 182 public void onAnimationSuccess(Animator animator) { 183 InteractionJankMonitorWrapper.end(CUJ_APP_CLOSE_TO_PIP); 184 } 185 186 @Override 187 public void onAnimationEnd(Animator animation) { 188 if (mHasAnimationEnded) return; 189 super.onAnimationEnd(animation); 190 mHasAnimationEnded = true; 191 } 192 }); 193 addOnUpdateListener(this::onAnimationUpdate); 194 } 195 onAnimationUpdate(RectF currentRect, float progress)196 private void onAnimationUpdate(RectF currentRect, float progress) { 197 if (mHasAnimationEnded) return; 198 final SurfaceControl.Transaction tx = 199 PipSurfaceTransactionHelper.newSurfaceControlTransaction(); 200 mHomeToWindowPositionMap.mapRect(mCurrentBoundsF, currentRect); 201 onAnimationUpdate(tx, mCurrentBoundsF, progress); 202 tx.apply(); 203 } 204 onAnimationUpdate(SurfaceControl.Transaction tx, RectF currentRect, float progress)205 private PictureInPictureSurfaceTransaction onAnimationUpdate(SurfaceControl.Transaction tx, 206 RectF currentRect, float progress) { 207 currentRect.round(mCurrentBounds); 208 if (mPipContentOverlay != null) { 209 mPipContentOverlay.onAnimationUpdate(tx, mCurrentBounds, progress); 210 } 211 final PictureInPictureSurfaceTransaction op; 212 if (mSourceHintRectInsets == null) { 213 // no source rect hint been set, directly scale the window down 214 op = onAnimationScale(progress, tx, mCurrentBounds); 215 } else { 216 // scale and crop according to the source rect hint 217 op = onAnimationScaleAndCrop(progress, tx, mCurrentBounds); 218 } 219 return op; 220 } 221 222 /** scale the window directly with no source rect hint being set */ onAnimationScale( float progress, SurfaceControl.Transaction tx, Rect bounds)223 private PictureInPictureSurfaceTransaction onAnimationScale( 224 float progress, SurfaceControl.Transaction tx, Rect bounds) { 225 if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) { 226 final RotatedPosition rotatedPosition = getRotatedPosition(progress); 227 return mSurfaceTransactionHelper.scale(tx, mLeash, mAppBounds, bounds, 228 rotatedPosition.degree, rotatedPosition.positionX, rotatedPosition.positionY); 229 } else { 230 return mSurfaceTransactionHelper.scale(tx, mLeash, mAppBounds, bounds); 231 } 232 } 233 234 /** scale and crop the window with source rect hint */ onAnimationScaleAndCrop( float progress, SurfaceControl.Transaction tx, Rect bounds)235 private PictureInPictureSurfaceTransaction onAnimationScaleAndCrop( 236 float progress, SurfaceControl.Transaction tx, 237 Rect bounds) { 238 final Rect insets = mInsetsEvaluator.evaluate(progress, mSourceInsets, 239 mSourceHintRectInsets); 240 if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) { 241 final RotatedPosition rotatedPosition = getRotatedPosition(progress); 242 return mSurfaceTransactionHelper.scaleAndRotate(tx, mLeash, mAppBounds, bounds, insets, 243 rotatedPosition.degree, rotatedPosition.positionX, rotatedPosition.positionY); 244 } else { 245 return mSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mSourceRectHint, mAppBounds, 246 bounds, insets, progress); 247 } 248 } 249 getTaskId()250 public int getTaskId() { 251 return mTaskId; 252 } 253 getComponentName()254 public ComponentName getComponentName() { 255 return mActivityInfo.getComponentName(); 256 } 257 getDestinationBounds()258 public Rect getDestinationBounds() { 259 return mDestinationBounds; 260 } 261 262 @Nullable getContentOverlay()263 public SurfaceControl getContentOverlay() { 264 return mPipContentOverlay == null ? null : mPipContentOverlay.getLeash(); 265 } 266 267 /** @return {@link PictureInPictureSurfaceTransaction} for the final leash transaction. */ getFinishTransaction()268 public PictureInPictureSurfaceTransaction getFinishTransaction() { 269 // get the final leash operations but do not apply to the leash. 270 final SurfaceControl.Transaction tx = 271 PipSurfaceTransactionHelper.newSurfaceControlTransaction(); 272 final PictureInPictureSurfaceTransaction pipTx = 273 onAnimationUpdate(tx, new RectF(mDestinationBounds), END_PROGRESS); 274 pipTx.setShouldDisableCanAffectSystemUiFlags(true); 275 return pipTx; 276 } 277 getRotatedPosition(float progress)278 private RotatedPosition getRotatedPosition(float progress) { 279 final float degree, positionX, positionY; 280 if (TaskAnimationManager.SHELL_TRANSITIONS_ROTATION) { 281 if (mFromRotation == Surface.ROTATION_90) { 282 degree = -90 * (1 - progress); 283 positionX = progress * (mDestinationBoundsTransformed.left - mStartBounds.left) 284 + mStartBounds.left; 285 positionY = progress * (mDestinationBoundsTransformed.top - mStartBounds.top) 286 + mStartBounds.top + mStartBounds.bottom * (1 - progress); 287 } else { 288 degree = 90 * (1 - progress); 289 positionX = progress * (mDestinationBoundsTransformed.left - mStartBounds.left) 290 + mStartBounds.left + mStartBounds.right * (1 - progress); 291 positionY = progress * (mDestinationBoundsTransformed.top - mStartBounds.top) 292 + mStartBounds.top; 293 } 294 } else { 295 if (mFromRotation == Surface.ROTATION_90) { 296 degree = -90 * progress; 297 positionX = progress * (mDestinationBoundsTransformed.left - mStartBounds.left) 298 + mStartBounds.left; 299 positionY = progress * (mDestinationBoundsTransformed.bottom - mStartBounds.top) 300 + mStartBounds.top; 301 } else { 302 degree = 90 * progress; 303 positionX = progress * (mDestinationBoundsTransformed.right - mStartBounds.left) 304 + mStartBounds.left; 305 positionY = progress * (mDestinationBoundsTransformed.top - mStartBounds.top) 306 + mStartBounds.top; 307 } 308 } 309 310 return new RotatedPosition(degree, positionX, positionY); 311 } 312 313 /** Builder class for {@link SwipePipToHomeAnimator} */ 314 public static class Builder { 315 private Context mContext; 316 private int mTaskId; 317 private ActivityInfo mActivityInfo; 318 private int mAppIconSizePx; 319 private SurfaceControl mLeash; 320 private Rect mSourceRectHint; 321 private Rect mDisplayCutoutInsets; 322 private Rect mAppBounds; 323 private Matrix mHomeToWindowPositionMap; 324 private RectF mStartBounds; 325 private Rect mDestinationBounds; 326 private int mCornerRadius; 327 private int mShadowRadius; 328 private View mAttachedView; 329 private @RecentsOrientedState.SurfaceRotation int mFromRotation = Surface.ROTATION_0; 330 private final Rect mDestinationBoundsTransformed = new Rect(); 331 setContext(Context context)332 public Builder setContext(Context context) { 333 mContext = context; 334 return this; 335 } 336 setTaskId(int taskId)337 public Builder setTaskId(int taskId) { 338 mTaskId = taskId; 339 return this; 340 } 341 setActivityInfo(ActivityInfo activityInfo)342 public Builder setActivityInfo(ActivityInfo activityInfo) { 343 mActivityInfo = activityInfo; 344 return this; 345 } 346 setAppIconSizePx(int appIconSizePx)347 public Builder setAppIconSizePx(int appIconSizePx) { 348 mAppIconSizePx = appIconSizePx; 349 return this; 350 } 351 setLeash(SurfaceControl leash)352 public Builder setLeash(SurfaceControl leash) { 353 mLeash = leash; 354 return this; 355 } 356 setSourceRectHint(Rect sourceRectHint)357 public Builder setSourceRectHint(Rect sourceRectHint) { 358 mSourceRectHint = new Rect(sourceRectHint); 359 return this; 360 } 361 setAppBounds(Rect appBounds)362 public Builder setAppBounds(Rect appBounds) { 363 mAppBounds = new Rect(appBounds); 364 return this; 365 } 366 setHomeToWindowPositionMap(Matrix homeToWindowPositionMap)367 public Builder setHomeToWindowPositionMap(Matrix homeToWindowPositionMap) { 368 mHomeToWindowPositionMap = new Matrix(homeToWindowPositionMap); 369 return this; 370 } 371 setStartBounds(RectF startBounds)372 public Builder setStartBounds(RectF startBounds) { 373 mStartBounds = new RectF(startBounds); 374 return this; 375 } 376 setDestinationBounds(Rect destinationBounds)377 public Builder setDestinationBounds(Rect destinationBounds) { 378 mDestinationBounds = new Rect(destinationBounds); 379 return this; 380 } 381 setCornerRadius(int cornerRadius)382 public Builder setCornerRadius(int cornerRadius) { 383 mCornerRadius = cornerRadius; 384 return this; 385 } 386 setShadowRadius(int shadowRadius)387 public Builder setShadowRadius(int shadowRadius) { 388 mShadowRadius = shadowRadius; 389 return this; 390 } 391 setAttachedView(View attachedView)392 public Builder setAttachedView(View attachedView) { 393 mAttachedView = attachedView; 394 return this; 395 } 396 setFromRotation(TaskViewSimulator taskViewSimulator, @RecentsOrientedState.SurfaceRotation int fromRotation, Rect displayCutoutInsets)397 public Builder setFromRotation(TaskViewSimulator taskViewSimulator, 398 @RecentsOrientedState.SurfaceRotation int fromRotation, 399 Rect displayCutoutInsets) { 400 if (fromRotation != Surface.ROTATION_90 && fromRotation != Surface.ROTATION_270) { 401 Log.wtf(TAG, "Not a supported rotation, rotation=" + fromRotation); 402 return this; 403 } 404 final Matrix matrix = new Matrix(); 405 taskViewSimulator.applyWindowToHomeRotation(matrix); 406 407 // map the destination bounds into window space. mDestinationBounds is always calculated 408 // in the final home space and the animation runs in original window space. 409 final RectF transformed = new RectF(mDestinationBounds); 410 matrix.mapRect(transformed, new RectF(mDestinationBounds)); 411 transformed.round(mDestinationBoundsTransformed); 412 413 mFromRotation = fromRotation; 414 if (displayCutoutInsets != null) { 415 mDisplayCutoutInsets = new Rect(displayCutoutInsets); 416 } 417 return this; 418 } 419 build()420 public SwipePipToHomeAnimator build() { 421 if (mDestinationBoundsTransformed.isEmpty()) { 422 mDestinationBoundsTransformed.set(mDestinationBounds); 423 } 424 // adjust the mSourceRectHint / mAppBounds by display cutout if applicable. 425 if (mSourceRectHint != null && mDisplayCutoutInsets != null) { 426 if (mFromRotation == Surface.ROTATION_90) { 427 mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top); 428 } else if (mFromRotation == Surface.ROTATION_270) { 429 mAppBounds.inset(mDisplayCutoutInsets); 430 } 431 } 432 return new SwipePipToHomeAnimator(mContext, mTaskId, mActivityInfo, mAppIconSizePx, 433 mLeash, mSourceRectHint, mAppBounds, 434 mHomeToWindowPositionMap, mStartBounds, mDestinationBounds, 435 mFromRotation, mDestinationBoundsTransformed, 436 mCornerRadius, mShadowRadius, mAttachedView); 437 } 438 } 439 440 private static class RotatedPosition { 441 private final float degree; 442 private final float positionX; 443 private final float positionY; 444 RotatedPosition(float degree, float positionX, float positionY)445 private RotatedPosition(float degree, float positionX, float positionY) { 446 this.degree = degree; 447 this.positionX = positionX; 448 this.positionY = positionY; 449 } 450 } 451 } 452