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
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index ef7a8f6..cac1a81 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.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.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 @@
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 @@
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 @@
}
}
+ 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 @@
// 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 @@
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 4411455..842fb84 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -56,6 +56,7 @@
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 @@
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 51f5e5d..20e2515 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -18,6 +18,8 @@
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;
@@ -156,6 +158,18 @@
}
/**
+ * 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.
*/
public void enableInputConsumer() {
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index b6eaa1c..18cd29d 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -159,6 +159,14 @@
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 0000000..02b5dfe
--- /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);
+ }
+ }
+}