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