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