From e4bc2af04b20d542ce631ae67d785359adae40c5 Mon Sep 17 00:00:00 2001 From: Hongwei Wang Date: Tue, 6 Oct 2020 14:55:06 -0700 Subject: [PATCH] Improve PIP enter transition w/ gesture nav (3/N) Directly transition Activity to PiP mode in launcher if the Activity claims auto-pip support. Video is taken by commenting out the requirement of setting the auto-pip flag. Note that we need app to actively push up-to-dated PictureInPictureParams to the framework, otherwise we won't be get - PictureInPictureParams on first entering - Staled PictureInPictureParams if the aspect ratio is changed Video: http://rcll/aaaaaabFQoRHlzixHdtY/abenIxLFI1pZzF2O8t4TbS Bug: 143965596 Test: see demo videos Change-Id: Iea9a6ff39a79431ac1afa14aea812c500b3ca3b2 --- .../android/quickstep/AbsSwipeUpHandler.java | 117 +++++++++++-- .../quickstep/LauncherSwipeHandlerV2.java | 6 + .../quickstep/RecentsAnimationController.java | 14 ++ .../quickstep/SwipeUpAnimationLogic.java | 8 + .../util/SwipePipToHomeAnimator.java | 162 ++++++++++++++++++ 5 files changed, 290 insertions(+), 17 deletions(-) create mode 100644 quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index ef7a8f6477..cac1a817ed 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -45,6 +45,7 @@ import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHO import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.app.ActivityManager; @@ -92,8 +93,10 @@ import com.android.quickstep.util.ActivityInitListener; import com.android.quickstep.util.AnimatorControllerWithResistance; import com.android.quickstep.util.InputConsumerProxy; import com.android.quickstep.util.MotionPauseDetector; +import com.android.quickstep.util.RecentsOrientedState; import com.android.quickstep.util.RectFSpringAnim; import com.android.quickstep.util.SurfaceTransactionApplier; +import com.android.quickstep.util.SwipePipToHomeAnimator; import com.android.quickstep.util.TransformParams; import com.android.quickstep.views.LiveTileOverlay; import com.android.quickstep.views.RecentsView; @@ -229,6 +232,10 @@ public abstract class AbsSwipeUpHandler, Q extends private final Runnable mOnDeferredActivityLaunch = this::onDeferredActivityLaunch; + private static final long SWIPE_PIP_TO_HOME_DURATION = 425; + private SwipePipToHomeAnimator mSwipePipToHomeAnimator; + protected boolean mIsSwipingPipToHome; + public AbsSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState, TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs, boolean continuingLastGesture, @@ -1049,25 +1056,44 @@ public abstract class AbsSwipeUpHandler, Q extends if (mGestureState.getEndTarget() == HOME) { mTaskViewSimulator.setDrawsBelowRecents(false); - HomeAnimationFactory homeAnimFactory = createHomeAnimationFactory(duration); - RectFSpringAnim windowAnim = createWindowAnimationToHome(start, homeAnimFactory); - windowAnim.addAnimatorListener(new AnimationSuccessListener() { - @Override - public void onAnimationSuccess(Animator animator) { - if (mRecentsAnimationController == null) { - // If the recents animation is interrupted, we still end the running - // animation (not canceled) so this is still called. In that case, we can - // skip doing any future work here for the current gesture. - return; - } - // Finalize the state and notify of the change - mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED); - } - }); getOrientationHandler().adjustFloatingIconStartVelocity(velocityPxPerMs); - windowAnim.start(mContext, velocityPxPerMs); + final RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationTargets != null + ? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId()) + : null; + HomeAnimationFactory homeAnimFactory = createHomeAnimationFactory(duration); + mIsSwipingPipToHome = homeAnimFactory.supportSwipePipToHome() + && runningTaskTarget != null + && runningTaskTarget.pictureInPictureParams != null + && runningTaskTarget.pictureInPictureParams.isAutoEnterEnabled() + && runningTaskTarget.pictureInPictureParams.getSourceRectHint() != null; + if (mIsSwipingPipToHome) { + mSwipePipToHomeAnimator = getSwipePipToHomeAnimator( + homeAnimFactory, runningTaskTarget); + mSwipePipToHomeAnimator.setDuration(SWIPE_PIP_TO_HOME_DURATION); + mSwipePipToHomeAnimator.setInterpolator(interpolator); + mSwipePipToHomeAnimator.setFloatValues(0f, 1f); + mSwipePipToHomeAnimator.start(); + mRunningWindowAnim = RunningWindowAnim.wrap(mSwipePipToHomeAnimator); + } else { + mSwipePipToHomeAnimator = null; + RectFSpringAnim windowAnim = createWindowAnimationToHome(start, homeAnimFactory); + windowAnim.addAnimatorListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + if (mRecentsAnimationController == null) { + // If the recents animation is interrupted, we still end the running + // animation (not canceled) so this is still called. In that case, + // we can skip doing any future work here for the current gesture. + return; + } + // Finalize the state and notify of the change + mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED); + } + }); + windowAnim.start(mContext, velocityPxPerMs); + mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim); + } homeAnimFactory.playAtomicAnimation(velocityPxPerMs.y); - mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim); mLauncherTransitionController = null; } else { ValueAnimator windowAnim = mCurrentShift.animateToValue(start, end); @@ -1111,6 +1137,46 @@ public abstract class AbsSwipeUpHandler, Q extends } } + private SwipePipToHomeAnimator getSwipePipToHomeAnimator(HomeAnimationFactory homeAnimFactory, + RemoteAnimationTargetCompat runningTaskTarget) { + // Directly animate the app to PiP (picture-in-picture) mode + final ActivityManager.RunningTaskInfo taskInfo = mGestureState.getRunningTask(); + final RecentsOrientedState orientationState = mTaskViewSimulator.getOrientationState(); + final Rect destinationBounds = SystemUiProxy.INSTANCE.get(mContext) + .startSwipePipToHome(taskInfo.topActivity, taskInfo.topActivityInfo, + runningTaskTarget.pictureInPictureParams, + orientationState.getRecentsActivityRotation(), + mDp.hotseatBarSizePx); + final SwipePipToHomeAnimator swipePipToHomeAnimator = new SwipePipToHomeAnimator( + runningTaskTarget.taskId, + taskInfo.topActivity, + runningTaskTarget.leash.getSurfaceControl(), + runningTaskTarget.pictureInPictureParams.getSourceRectHint(), + taskInfo.configuration.windowConfiguration.getBounds(), + destinationBounds); + swipePipToHomeAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // Ensure Launcher ends in NORMAL state, we intentionally skip other callbacks + // since they are not relevant in this swipe-pip-to-home case. + homeAnimFactory.createActivityAnimationToHome().dispatchOnStart(); + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mRecentsAnimationController == null) { + // If the recents animation is interrupted, we still end the running + // animation (not canceled) so this is still called. In that case, we can + // skip doing any future work here for the current gesture. + return; + } + // Finalize the state and notify of the change + mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED); + } + }); + return swipePipToHomeAnimator; + } + private void computeRecentsScrollIfInvisible() { if (mRecentsView != null && mRecentsView.getVisibility() != View.VISIBLE) { // Views typically don't compute scroll when invisible as an optimization, @@ -1359,6 +1425,7 @@ public abstract class AbsSwipeUpHandler, Q extends // If there are no targets or the animation not started, then there is nothing to finish mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED); } else { + maybeFinishSwipePipToHome(); finishRecentsControllerToHome( () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED)); } @@ -1366,6 +1433,22 @@ public abstract class AbsSwipeUpHandler, Q extends doLogGesture(HOME, mRecentsView == null ? null : mRecentsView.getCurrentPageTaskView()); } + /** + * Resets the {@link #mIsSwipingPipToHome} and notifies SysUI that transition is finished + * if applicable. This should happen before {@link #finishRecentsControllerToHome(Runnable)}. + */ + private void maybeFinishSwipePipToHome() { + if (mIsSwipingPipToHome && mSwipePipToHomeAnimator != null) { + SystemUiProxy.INSTANCE.get(mContext).stopSwipePipToHome( + mSwipePipToHomeAnimator.getComponentName(), + mSwipePipToHomeAnimator.getDestinationBounds()); + mRecentsAnimationController.setFinishTaskBounds( + mSwipePipToHomeAnimator.getTaskId(), + mSwipePipToHomeAnimator.getDestinationBounds()); + mIsSwipingPipToHome = false; + } + } + protected abstract void finishRecentsControllerToHome(Runnable callback); private void setupLauncherUiAfterSwipeUpToRecentsAnimation() { diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java index 44114558e5..842fb84ff9 100644 --- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java +++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java @@ -56,6 +56,7 @@ public class LauncherSwipeHandlerV2 extends final TaskView runningTaskView = mRecentsView.getRunningTaskView(); final View workspaceView; if (runningTaskView != null + && !mIsSwipingPipToHome && runningTaskView.getTask().key.getComponent() != null) { workspaceView = mActivity.getWorkspace().getFirstMatchForAppClose( runningTaskView.getTask().key.getComponent().getPackageName(), @@ -140,5 +141,10 @@ public class LauncherSwipeHandlerV2 extends new StaggeredWorkspaceAnim(mActivity, velocity, true /* animateOverviewScrim */).start(); } + + @Override + public boolean supportSwipePipToHome() { + return true; + } } } diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java index 51f5e5d785..20e251552c 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java @@ -18,6 +18,8 @@ package com.android.quickstep; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +import android.graphics.Rect; + import androidx.annotation.NonNull; import androidx.annotation.UiThread; @@ -155,6 +157,18 @@ public class RecentsAnimationController { }); } + /** + * Sets the final bounds on a Task. This is used by Launcher to notify the system that + * animating Activity to PiP has completed and the associated task surface should be updated + * accordingly. This should be called before `finish` + * @param taskId for which the leash should be updated + * @param destinationBounds bounds of the final PiP window + */ + public void setFinishTaskBounds(int taskId, Rect destinationBounds) { + UI_HELPER_EXECUTOR.execute( + () -> mController.setFinishTaskBounds(taskId, destinationBounds)); + } + /** * Enables the input consumer to start intercepting touches in the app window. */ diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java index b6eaa1c7b6..18cd29dc6f 100644 --- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java +++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java @@ -159,6 +159,14 @@ public abstract class SwipeUpAnimationLogic { public void update(RectF currentRect, float progress, float radius) { } public void onCancel() { } + + /** + * @return {@code true} if this factory supports animating an Activity to PiP window on + * swiping up to home. + */ + public boolean supportSwipePipToHome() { + return false; + } } /** diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java new file mode 100644 index 0000000000..02b5dfe146 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.RectEvaluator; +import android.animation.ValueAnimator; +import android.content.ComponentName; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.SurfaceControl; + +import androidx.annotation.NonNull; + +/** + * An {@link Animator} that animates an Activity to PiP (picture-in-picture) window when + * swiping up (in gesture navigation mode). Note that this class is derived from + * {@link com.android.wm.shell.pip.PipAnimationController.PipTransitionAnimator}. + * + * TODO: consider sharing this class including the animator and leash operations between + * Launcher and SysUI. Also, there should be one source of truth for the corner radius of the + * PiP window, which would ideally be on SysUI side as well. + */ +public class SwipePipToHomeAnimator extends ValueAnimator implements + ValueAnimator.AnimatorUpdateListener { + private final int mTaskId; + private final ComponentName mComponentName; + private final SurfaceControl mLeash; + private final Rect mStartBounds = new Rect(); + private final Rect mDestinationBounds = new Rect(); + private final SurfaceTransactionHelper mSurfaceTransactionHelper; + + /** for calculating the transform in {@link #onAnimationUpdate(ValueAnimator)} */ + private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect()); + private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect()); + private final Rect mSourceHintRectInsets = new Rect(); + private final Rect mSourceInsets = new Rect(); + + /** + * Flag to avoid the double-end problem since the leash would have been released + * after the first end call and any further operations upon it would lead to NPE. + */ + private boolean mHasAnimationEnded; + + public SwipePipToHomeAnimator(int taskId, + @NonNull ComponentName componentName, + @NonNull SurfaceControl leash, + @NonNull Rect sourceRectHint, + @NonNull Rect startBounds, + @NonNull Rect destinationBounds) { + mTaskId = taskId; + mComponentName = componentName; + mLeash = leash; + mStartBounds.set(startBounds); + mDestinationBounds.set(destinationBounds); + mSurfaceTransactionHelper = new SurfaceTransactionHelper(); + + mSourceHintRectInsets.set(sourceRectHint.left - startBounds.left, + sourceRectHint.top - startBounds.top, + startBounds.right - sourceRectHint.right, + startBounds.bottom - sourceRectHint.bottom); + + addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + SwipePipToHomeAnimator.this.onAnimationEnd(); + } + }); + addUpdateListener(this); + } + + @Override + public void onAnimationUpdate(ValueAnimator animator) { + if (mHasAnimationEnded) return; + + final float fraction = animator.getAnimatedFraction(); + final Rect bounds = mRectEvaluator.evaluate(fraction, mStartBounds, mDestinationBounds); + final Rect insets = mInsetsEvaluator.evaluate(fraction, mSourceInsets, + mSourceHintRectInsets); + final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + mSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mStartBounds, bounds, insets); + tx.setCornerRadius(mLeash, 0).apply(); + } + + public int getTaskId() { + return mTaskId; + } + + public ComponentName getComponentName() { + return mComponentName; + } + + public Rect getDestinationBounds() { + return mDestinationBounds; + } + + private void onAnimationEnd() { + if (mHasAnimationEnded) return; + + final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + mSurfaceTransactionHelper.resetScale(tx, mLeash, mDestinationBounds); + mSurfaceTransactionHelper.crop(tx, mLeash, mDestinationBounds); + tx.setCornerRadius(mLeash, 0).apply(); + mHasAnimationEnded = true; + } + + /** + * Slim version of {@link com.android.wm.shell.pip.PipSurfaceTransactionHelper} + */ + private static final class SurfaceTransactionHelper { + private final Matrix mTmpTransform = new Matrix(); + private final float[] mTmpFloat9 = new float[9]; + private final RectF mTmpSourceRectF = new RectF(); + private final Rect mTmpDestinationRect = new Rect(); + + private void scaleAndCrop(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect sourceBounds, Rect destinationBounds, Rect insets) { + mTmpSourceRectF.set(sourceBounds); + mTmpDestinationRect.set(sourceBounds); + mTmpDestinationRect.inset(insets); + // Scale by the shortest edge and offset such that the top/left of the scaled inset + // source rect aligns with the top/left of the destination bounds + final float scale = sourceBounds.width() <= sourceBounds.height() + ? (float) destinationBounds.width() / sourceBounds.width() + : (float) destinationBounds.height() / sourceBounds.height(); + final float left = destinationBounds.left - insets.left * scale; + final float top = destinationBounds.top - insets.top * scale; + mTmpTransform.setScale(scale, scale); + tx.setMatrix(leash, mTmpTransform, mTmpFloat9) + .setWindowCrop(leash, mTmpDestinationRect) + .setPosition(leash, left, top); + } + + private void resetScale(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect destinationBounds) { + tx.setMatrix(leash, Matrix.IDENTITY_MATRIX, mTmpFloat9) + .setPosition(leash, destinationBounds.left, destinationBounds.top); + } + + private void crop(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect destinationBounds) { + tx.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height()) + .setPosition(leash, destinationBounds.left, destinationBounds.top); + } + } +}