869 lines
40 KiB
Java
869 lines
40 KiB
Java
/*
|
|
* 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.wm.shell.pip;
|
|
|
|
import static android.util.RotationUtils.rotateBounds;
|
|
import static android.view.Surface.ROTATION_0;
|
|
import static android.view.Surface.ROTATION_270;
|
|
import static android.view.Surface.ROTATION_90;
|
|
|
|
import static com.android.wm.shell.pip.PipTransitionController.ANIM_TYPE_ALPHA;
|
|
import static com.android.wm.shell.pip.PipTransitionController.ANIM_TYPE_BOUNDS;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.RectEvaluator;
|
|
import android.animation.ValueAnimator;
|
|
import android.annotation.IntDef;
|
|
import android.annotation.NonNull;
|
|
import android.app.AppCompatTaskInfo;
|
|
import android.app.TaskInfo;
|
|
import android.content.Context;
|
|
import android.content.pm.ActivityInfo;
|
|
import android.graphics.Point;
|
|
import android.graphics.Rect;
|
|
import android.os.SystemClock;
|
|
import android.view.Surface;
|
|
import android.view.SurfaceControl;
|
|
import android.window.TaskSnapshot;
|
|
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
import com.android.internal.protolog.ProtoLog;
|
|
import com.android.launcher3.icons.IconProvider;
|
|
import com.android.wm.shell.common.pip.PipUtils;
|
|
import com.android.wm.shell.protolog.ShellProtoLogGroup;
|
|
import com.android.wm.shell.shared.animation.Interpolators;
|
|
import com.android.wm.shell.shared.pip.PipContentOverlay;
|
|
import com.android.wm.shell.transition.Transitions;
|
|
|
|
import java.lang.annotation.Retention;
|
|
import java.lang.annotation.RetentionPolicy;
|
|
import java.util.Objects;
|
|
|
|
/**
|
|
* Controller class of PiP animations (both from and to PiP mode).
|
|
*/
|
|
public class PipAnimationController {
|
|
static final float FRACTION_START = 0f;
|
|
static final float FRACTION_END = 1f;
|
|
|
|
/**
|
|
* The alpha type is set for swiping to home. But the swiped task may not enter PiP. And if
|
|
* another task enters PiP by non-swipe ways, e.g. call API in foreground or switch to 3-button
|
|
* navigation, then the alpha type is unexpected. So use a timeout to avoid applying wrong
|
|
* animation style to an unrelated task.
|
|
*/
|
|
private static final int ONE_SHOT_ALPHA_ANIMATION_TIMEOUT_MS = 800;
|
|
|
|
public static final int TRANSITION_DIRECTION_NONE = 0;
|
|
public static final int TRANSITION_DIRECTION_SAME = 1;
|
|
public static final int TRANSITION_DIRECTION_TO_PIP = 2;
|
|
public static final int TRANSITION_DIRECTION_LEAVE_PIP = 3;
|
|
public static final int TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN = 4;
|
|
public static final int TRANSITION_DIRECTION_REMOVE_STACK = 5;
|
|
public static final int TRANSITION_DIRECTION_SNAP_AFTER_RESIZE = 6;
|
|
public static final int TRANSITION_DIRECTION_USER_RESIZE = 7;
|
|
public static final int TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND = 8;
|
|
|
|
@IntDef(prefix = { "TRANSITION_DIRECTION_" }, value = {
|
|
TRANSITION_DIRECTION_NONE,
|
|
TRANSITION_DIRECTION_SAME,
|
|
TRANSITION_DIRECTION_TO_PIP,
|
|
TRANSITION_DIRECTION_LEAVE_PIP,
|
|
TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN,
|
|
TRANSITION_DIRECTION_REMOVE_STACK,
|
|
TRANSITION_DIRECTION_SNAP_AFTER_RESIZE,
|
|
TRANSITION_DIRECTION_USER_RESIZE,
|
|
TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND
|
|
})
|
|
@Retention(RetentionPolicy.SOURCE)
|
|
public @interface TransitionDirection {}
|
|
|
|
public static boolean isInPipDirection(@TransitionDirection int direction) {
|
|
return direction == TRANSITION_DIRECTION_TO_PIP;
|
|
}
|
|
|
|
public static boolean isOutPipDirection(@TransitionDirection int direction) {
|
|
return direction == TRANSITION_DIRECTION_LEAVE_PIP
|
|
|| direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN;
|
|
}
|
|
|
|
/** Whether the given direction represents removing PIP. */
|
|
public static boolean isRemovePipDirection(@TransitionDirection int direction) {
|
|
return direction == TRANSITION_DIRECTION_REMOVE_STACK;
|
|
}
|
|
|
|
private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
|
|
|
|
private PipTransitionAnimator mCurrentAnimator;
|
|
@PipTransitionController.AnimationType
|
|
private int mOneShotAnimationType = ANIM_TYPE_BOUNDS;
|
|
private long mLastOneShotAlphaAnimationTime;
|
|
|
|
public PipAnimationController(PipSurfaceTransactionHelper helper) {
|
|
mSurfaceTransactionHelper = helper;
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
@VisibleForTesting
|
|
public PipTransitionAnimator getAnimator(TaskInfo taskInfo, SurfaceControl leash,
|
|
Rect destinationBounds, float alphaStart, float alphaEnd) {
|
|
if (mCurrentAnimator == null) {
|
|
mCurrentAnimator = setupPipTransitionAnimator(
|
|
PipTransitionAnimator.ofAlpha(taskInfo, leash, destinationBounds, alphaStart,
|
|
alphaEnd));
|
|
} else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA
|
|
&& Objects.equals(destinationBounds, mCurrentAnimator.getDestinationBounds())
|
|
&& mCurrentAnimator.isRunning()) {
|
|
mCurrentAnimator.updateEndValue(alphaEnd);
|
|
} else {
|
|
mCurrentAnimator.cancel();
|
|
mCurrentAnimator = setupPipTransitionAnimator(
|
|
PipTransitionAnimator.ofAlpha(taskInfo, leash, destinationBounds, alphaStart,
|
|
alphaEnd));
|
|
}
|
|
return mCurrentAnimator;
|
|
}
|
|
|
|
/**
|
|
* Construct and return an animator that animates from the {@param startBounds} to the
|
|
* {@param endBounds} with the given {@param direction}. If {@param direction} is type
|
|
* {@link PipTransitionController#ANIM_TYPE_BOUNDS}, then {@param sourceHintRect} will be used
|
|
* to animate in a better, more smooth manner. If the original bound was rotated and a reset
|
|
* needs to happen, pass in {@param startingAngle}.
|
|
*
|
|
* In the case where one wants to start animation during an intermediate animation (for example,
|
|
* if the user is currently doing a pinch-resize, and upon letting go now PiP needs to animate
|
|
* to the correct snap fraction region), then provide the base bounds, which is current PiP
|
|
* leash bounds before transformation/any animation. This is so when we try to construct
|
|
* the different transformation matrices for the animation, we are constructing this based off
|
|
* the PiP original bounds, rather than the {@param startBounds}, which is post-transformed.
|
|
*
|
|
* If non-zero {@param rotationDelta} is given, it means that the display will be rotated by
|
|
* leaving PiP to fullscreen, and the {@param endBounds} is the fullscreen bounds before the
|
|
* rotation change.
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
@VisibleForTesting
|
|
public PipTransitionAnimator getAnimator(TaskInfo taskInfo, SurfaceControl leash,
|
|
Rect baseBounds, Rect startBounds, Rect endBounds, Rect sourceHintRect,
|
|
@PipAnimationController.TransitionDirection int direction, float startingAngle,
|
|
@Surface.Rotation int rotationDelta, boolean alwaysAnimateTaskBounds) {
|
|
if (mCurrentAnimator == null) {
|
|
mCurrentAnimator = setupPipTransitionAnimator(
|
|
PipTransitionAnimator.ofBounds(taskInfo, leash, startBounds, startBounds,
|
|
endBounds, sourceHintRect, direction, 0 /* startingAngle */,
|
|
rotationDelta, alwaysAnimateTaskBounds));
|
|
} else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA
|
|
&& mCurrentAnimator.isRunning()) {
|
|
// If we are still animating the fade into pip, then just move the surface and ensure
|
|
// we update with the new destination bounds, but don't interrupt the existing animation
|
|
// with a new bounds
|
|
mCurrentAnimator.setDestinationBounds(endBounds);
|
|
} else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_BOUNDS
|
|
&& mCurrentAnimator.isRunning()) {
|
|
mCurrentAnimator.setDestinationBounds(endBounds);
|
|
// construct new Rect instances in case they are recycled
|
|
mCurrentAnimator.updateEndValue(new Rect(endBounds));
|
|
} else {
|
|
mCurrentAnimator.cancel();
|
|
mCurrentAnimator = setupPipTransitionAnimator(
|
|
PipTransitionAnimator.ofBounds(taskInfo, leash, baseBounds, startBounds,
|
|
endBounds, sourceHintRect, direction, startingAngle, rotationDelta,
|
|
alwaysAnimateTaskBounds));
|
|
}
|
|
return mCurrentAnimator;
|
|
}
|
|
|
|
public PipTransitionAnimator getCurrentAnimator() {
|
|
return mCurrentAnimator;
|
|
}
|
|
|
|
/** Reset animator state to prevent it from being used after its lifetime. */
|
|
public void resetAnimatorState() {
|
|
mCurrentAnimator = null;
|
|
}
|
|
|
|
private PipTransitionAnimator setupPipTransitionAnimator(PipTransitionAnimator animator) {
|
|
animator.setSurfaceTransactionHelper(mSurfaceTransactionHelper);
|
|
animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
|
|
animator.setFloatValues(FRACTION_START, FRACTION_END);
|
|
return animator;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the PiP window is currently being animated.
|
|
*/
|
|
public boolean isAnimating() {
|
|
PipAnimationController.PipTransitionAnimator animator = getCurrentAnimator();
|
|
if (animator != null && animator.isRunning()) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Quietly cancel the animator by removing the listeners first.
|
|
* TODO(b/275003573): deprecate this, cancelling without the proper callbacks is problematic.
|
|
*/
|
|
static void quietCancel(@NonNull ValueAnimator animator) {
|
|
animator.removeAllUpdateListeners();
|
|
animator.removeAllListeners();
|
|
animator.cancel();
|
|
}
|
|
|
|
/**
|
|
* Sets the preferred enter animation type for one time. This is typically used to set the
|
|
* animation type to {@link PipTransitionController#ANIM_TYPE_ALPHA}.
|
|
* <p>
|
|
* For example, gesture navigation would first fade out the PiP activity, and the transition
|
|
* should be responsible to animate in (such as fade in) the PiP.
|
|
*/
|
|
public void setOneShotEnterAnimationType(
|
|
@PipTransitionController.AnimationType int animationType) {
|
|
mOneShotAnimationType = animationType;
|
|
if (animationType == ANIM_TYPE_ALPHA) {
|
|
mLastOneShotAlphaAnimationTime = SystemClock.uptimeMillis();
|
|
}
|
|
}
|
|
|
|
/** Returns the preferred animation type and consumes the one-shot type if needed. */
|
|
@PipTransitionController.AnimationType
|
|
public int takeOneShotEnterAnimationType() {
|
|
final int type = mOneShotAnimationType;
|
|
if (type == ANIM_TYPE_ALPHA) {
|
|
// Restore to default type.
|
|
mOneShotAnimationType = ANIM_TYPE_BOUNDS;
|
|
if (SystemClock.uptimeMillis() - mLastOneShotAlphaAnimationTime
|
|
> ONE_SHOT_ALPHA_ANIMATION_TIMEOUT_MS) {
|
|
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"Alpha animation is expired. Use bounds animation.");
|
|
return ANIM_TYPE_BOUNDS;
|
|
}
|
|
}
|
|
return type;
|
|
}
|
|
|
|
/**
|
|
* Additional callback interface for PiP animation
|
|
*/
|
|
public static class PipAnimationCallback {
|
|
/**
|
|
* Called when PiP animation is started.
|
|
*/
|
|
public void onPipAnimationStart(TaskInfo taskInfo, PipTransitionAnimator animator) {}
|
|
|
|
/**
|
|
* Called when PiP animation is ended.
|
|
*/
|
|
public void onPipAnimationEnd(TaskInfo taskInfo, SurfaceControl.Transaction tx,
|
|
PipTransitionAnimator animator) {}
|
|
|
|
/**
|
|
* Called when PiP animation is cancelled.
|
|
*/
|
|
public void onPipAnimationCancel(TaskInfo taskInfo, PipTransitionAnimator animator) {}
|
|
}
|
|
|
|
/**
|
|
* A handler class that could register itself to apply the transaction instead of the
|
|
* animation controller doing it. For example, the menu controller can be one such handler.
|
|
*/
|
|
public static class PipTransactionHandler {
|
|
|
|
/**
|
|
* Called when the animation controller is about to apply a transaction. Allow a registered
|
|
* handler to apply the transaction instead.
|
|
*
|
|
* @return true if handled by the handler, false otherwise.
|
|
*/
|
|
public boolean handlePipTransaction(SurfaceControl leash, SurfaceControl.Transaction tx,
|
|
Rect destinationBounds, float alpha) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Animator for PiP transition animation which supports both alpha and bounds animation.
|
|
* @param <T> Type of property to animate, either alpha (float) or bounds (Rect)
|
|
*/
|
|
public abstract static class PipTransitionAnimator<T> extends ValueAnimator implements
|
|
ValueAnimator.AnimatorUpdateListener,
|
|
ValueAnimator.AnimatorListener {
|
|
private final TaskInfo mTaskInfo;
|
|
private final SurfaceControl mLeash;
|
|
private final @PipTransitionController.AnimationType int mAnimationType;
|
|
private final Rect mDestinationBounds = new Rect();
|
|
|
|
private final Point mLeashOffset = new Point();
|
|
|
|
private T mBaseValue;
|
|
protected T mCurrentValue;
|
|
protected T mStartValue;
|
|
private T mEndValue;
|
|
private PipAnimationCallback mPipAnimationCallback;
|
|
private PipTransactionHandler mPipTransactionHandler;
|
|
private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
|
|
mSurfaceControlTransactionFactory;
|
|
private PipSurfaceTransactionHelper mSurfaceTransactionHelper;
|
|
private @TransitionDirection int mTransitionDirection;
|
|
protected PipContentOverlay mContentOverlay;
|
|
// Flag to avoid double-end
|
|
private boolean mHasRequestedEnd;
|
|
|
|
private PipTransitionAnimator(@NonNull TaskInfo taskInfo, @NonNull SurfaceControl leash,
|
|
@PipTransitionController.AnimationType int animationType,
|
|
@NonNull Rect destinationBounds, @NonNull T baseValue, @NonNull T startValue,
|
|
@NonNull T endValue) {
|
|
this(taskInfo, leash, animationType, destinationBounds, new Point(), baseValue,
|
|
startValue, endValue);
|
|
}
|
|
|
|
private PipTransitionAnimator(@NonNull TaskInfo taskInfo, @NonNull SurfaceControl leash,
|
|
@PipTransitionController.AnimationType int animationType,
|
|
@NonNull Rect destinationBounds, @NonNull Point leashOffset,
|
|
@NonNull T baseValue, @NonNull T startValue, @NonNull T endValue) {
|
|
mTaskInfo = taskInfo;
|
|
mLeash = leash;
|
|
mAnimationType = animationType;
|
|
mDestinationBounds.set(destinationBounds);
|
|
mLeashOffset.set(leashOffset);
|
|
mBaseValue = baseValue;
|
|
mStartValue = startValue;
|
|
mEndValue = endValue;
|
|
addListener(this);
|
|
addUpdateListener(this);
|
|
mSurfaceControlTransactionFactory =
|
|
new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
|
|
mTransitionDirection = TRANSITION_DIRECTION_NONE;
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationStart(Animator animation) {
|
|
mCurrentValue = mStartValue;
|
|
onStartTransaction(mLeash, mSurfaceControlTransactionFactory.getTransaction());
|
|
if (mPipAnimationCallback != null) {
|
|
mPipAnimationCallback.onPipAnimationStart(mTaskInfo, this);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationUpdate(ValueAnimator animation) {
|
|
if (mHasRequestedEnd) return;
|
|
applySurfaceControlTransaction(mLeash,
|
|
mSurfaceControlTransactionFactory.getTransaction(),
|
|
animation.getAnimatedFraction());
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
if (mHasRequestedEnd) return;
|
|
mHasRequestedEnd = true;
|
|
mCurrentValue = mEndValue;
|
|
final SurfaceControl.Transaction tx =
|
|
mSurfaceControlTransactionFactory.getTransaction();
|
|
onEndTransaction(mLeash, tx, mTransitionDirection);
|
|
if (mPipAnimationCallback != null) {
|
|
mPipAnimationCallback.onPipAnimationEnd(mTaskInfo, tx, this);
|
|
}
|
|
mTransitionDirection = TRANSITION_DIRECTION_NONE;
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationCancel(Animator animation) {
|
|
if (mPipAnimationCallback != null) {
|
|
mPipAnimationCallback.onPipAnimationCancel(mTaskInfo, this);
|
|
}
|
|
mTransitionDirection = TRANSITION_DIRECTION_NONE;
|
|
}
|
|
|
|
@Override public void onAnimationRepeat(Animator animation) {}
|
|
|
|
@VisibleForTesting
|
|
@PipTransitionController.AnimationType
|
|
public int getAnimationType() {
|
|
return mAnimationType;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
public PipTransitionAnimator<T> setPipAnimationCallback(PipAnimationCallback callback) {
|
|
mPipAnimationCallback = callback;
|
|
return this;
|
|
}
|
|
|
|
PipTransitionAnimator<T> setPipTransactionHandler(PipTransactionHandler handler) {
|
|
mPipTransactionHandler = handler;
|
|
return this;
|
|
}
|
|
|
|
boolean handlePipTransaction(SurfaceControl leash, SurfaceControl.Transaction tx,
|
|
Rect destinationBounds, float alpha) {
|
|
if (mPipTransactionHandler != null) {
|
|
return mPipTransactionHandler.handlePipTransaction(
|
|
leash, tx, destinationBounds, alpha);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
SurfaceControl getContentOverlayLeash() {
|
|
return mContentOverlay == null ? null : mContentOverlay.getLeash();
|
|
}
|
|
|
|
void setColorContentOverlay(Context context) {
|
|
reattachContentOverlay(new PipContentOverlay.PipColorOverlay(context));
|
|
}
|
|
|
|
void setSnapshotContentOverlay(TaskSnapshot snapshot, Rect sourceRectHint) {
|
|
reattachContentOverlay(
|
|
new PipContentOverlay.PipSnapshotOverlay(snapshot, sourceRectHint));
|
|
}
|
|
|
|
void setAppIconContentOverlay(Context context, Rect appBounds, Rect destinationBounds,
|
|
ActivityInfo activityInfo, int appIconSizePx) {
|
|
reattachContentOverlay(
|
|
new PipContentOverlay.PipAppIconOverlay(context, appBounds, destinationBounds,
|
|
new IconProvider(context).getIcon(activityInfo), appIconSizePx));
|
|
}
|
|
|
|
private void reattachContentOverlay(PipContentOverlay overlay) {
|
|
final SurfaceControl.Transaction tx =
|
|
mSurfaceControlTransactionFactory.getTransaction();
|
|
if (mContentOverlay != null) {
|
|
mContentOverlay.detach(tx);
|
|
}
|
|
mContentOverlay = overlay;
|
|
mContentOverlay.attach(tx, mLeash);
|
|
}
|
|
|
|
/**
|
|
* Clears the {@link #mContentOverlay}, this should be done after the content overlay is
|
|
* faded out, such as in {@link PipTaskOrganizer#fadeOutAndRemoveOverlay}
|
|
*/
|
|
void clearContentOverlay() {
|
|
mContentOverlay = null;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
@TransitionDirection public int getTransitionDirection() {
|
|
return mTransitionDirection;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
public PipTransitionAnimator<T> setTransitionDirection(@TransitionDirection int direction) {
|
|
if (direction != TRANSITION_DIRECTION_SAME) {
|
|
mTransitionDirection = direction;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
T getStartValue() {
|
|
return mStartValue;
|
|
}
|
|
|
|
T getBaseValue() {
|
|
return mBaseValue;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
public T getEndValue() {
|
|
return mEndValue;
|
|
}
|
|
|
|
Rect getDestinationBounds() {
|
|
return mDestinationBounds;
|
|
}
|
|
|
|
void setDestinationBounds(Rect destinationBounds) {
|
|
mDestinationBounds.set(destinationBounds);
|
|
if (mAnimationType == ANIM_TYPE_ALPHA) {
|
|
onStartTransaction(mLeash, mSurfaceControlTransactionFactory.getTransaction());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the offset of the {@link #mLeash}.
|
|
*/
|
|
@NonNull
|
|
Point getLeashOffset() {
|
|
// Use copy to prevent the leash to be modified unexpectedly.
|
|
return new Point(mLeashOffset);
|
|
}
|
|
|
|
void setCurrentValue(T value) {
|
|
mCurrentValue = value;
|
|
}
|
|
|
|
boolean shouldApplyShadowRadius() {
|
|
return !isRemovePipDirection(mTransitionDirection);
|
|
}
|
|
|
|
boolean inScaleTransition() {
|
|
if (mAnimationType != ANIM_TYPE_BOUNDS) return false;
|
|
final int direction = getTransitionDirection();
|
|
return !isInPipDirection(direction) && !isOutPipDirection(direction);
|
|
}
|
|
|
|
/**
|
|
* Updates the {@link #mEndValue}.
|
|
*
|
|
* NOTE: Do not forget to call {@link #setDestinationBounds(Rect)} for bounds animation.
|
|
* This is typically used when we receive a shelf height adjustment during the bounds
|
|
* animation. In which case we can update the end bounds and keep the existing animation
|
|
* running instead of cancelling it.
|
|
*/
|
|
public void updateEndValue(T endValue) {
|
|
mEndValue = endValue;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
public void setSurfaceControlTransactionFactory(
|
|
PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) {
|
|
mSurfaceControlTransactionFactory = factory;
|
|
}
|
|
|
|
PipSurfaceTransactionHelper getSurfaceTransactionHelper() {
|
|
return mSurfaceTransactionHelper;
|
|
}
|
|
|
|
void setSurfaceTransactionHelper(PipSurfaceTransactionHelper helper) {
|
|
mSurfaceTransactionHelper = helper;
|
|
}
|
|
|
|
void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {}
|
|
|
|
void onEndTransaction(SurfaceControl leash, SurfaceControl.Transaction tx,
|
|
@TransitionDirection int transitionDirection) {}
|
|
|
|
abstract void applySurfaceControlTransaction(SurfaceControl leash,
|
|
SurfaceControl.Transaction tx, float fraction);
|
|
|
|
static PipTransitionAnimator<Float> ofAlpha(TaskInfo taskInfo, SurfaceControl leash,
|
|
Rect destinationBounds, float startValue, float endValue) {
|
|
return new PipTransitionAnimator<Float>(taskInfo, leash, ANIM_TYPE_ALPHA,
|
|
destinationBounds, startValue, startValue, endValue) {
|
|
@Override
|
|
void applySurfaceControlTransaction(SurfaceControl leash,
|
|
SurfaceControl.Transaction tx, float fraction) {
|
|
final float alpha = getStartValue() * (1 - fraction) + getEndValue() * fraction;
|
|
setCurrentValue(alpha);
|
|
getSurfaceTransactionHelper().alpha(tx, leash, alpha)
|
|
.round(tx, leash, true /* applyCornerRadius */)
|
|
.shadow(tx, leash, shouldApplyShadowRadius());
|
|
if (!handlePipTransaction(leash, tx, destinationBounds, alpha)) {
|
|
tx.apply();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {
|
|
if (getTransitionDirection() == TRANSITION_DIRECTION_REMOVE_STACK) {
|
|
// while removing the pip stack, no extra work needs to be done here.
|
|
return;
|
|
}
|
|
getSurfaceTransactionHelper()
|
|
.resetScale(tx, leash, getDestinationBounds())
|
|
.cropAndPosition(tx, leash, getDestinationBounds())
|
|
.round(tx, leash, true /* applyCornerRadius */)
|
|
.shadow(tx, leash, shouldApplyShadowRadius());
|
|
tx.show(leash);
|
|
tx.apply();
|
|
}
|
|
|
|
@Override
|
|
public void updateEndValue(Float endValue) {
|
|
super.updateEndValue(endValue);
|
|
mStartValue = mCurrentValue;
|
|
}
|
|
};
|
|
}
|
|
|
|
static PipTransitionAnimator<Rect> ofBounds(TaskInfo taskInfo, SurfaceControl leash,
|
|
Rect baseValue, Rect startValue, Rect endValue, Rect sourceRectHint,
|
|
@PipAnimationController.TransitionDirection int direction, float startingAngle,
|
|
@Surface.Rotation int rotationDelta, boolean alwaysAnimateTaskBounds) {
|
|
final boolean isOutPipDirection = isOutPipDirection(direction);
|
|
final boolean isInPipDirection = isInPipDirection(direction);
|
|
// Just for simplicity we'll interpolate between the source rect hint insets and empty
|
|
// insets to calculate the window crop
|
|
final Rect initialSourceValue;
|
|
final Rect mainWindowFrame = taskInfo.topActivityMainWindowFrame;
|
|
final AppCompatTaskInfo compatInfo = taskInfo.appCompatTaskInfo;
|
|
final boolean isSizeCompatOrLetterboxed = compatInfo.isTopActivityInSizeCompat()
|
|
|| compatInfo.isTopActivityLetterboxed();
|
|
// For the animation to swipe PIP to home or restore a PIP task from home, we don't
|
|
// override to the main window frame since we should animate the whole task.
|
|
final boolean shouldUseMainWindowFrame = mainWindowFrame != null
|
|
&& !alwaysAnimateTaskBounds && !isSizeCompatOrLetterboxed;
|
|
final boolean changeOrientation =
|
|
rotationDelta == ROTATION_90 || rotationDelta == ROTATION_270;
|
|
final Rect baseBounds = new Rect(baseValue);
|
|
final Rect startBounds = new Rect(startValue);
|
|
final Rect endBounds = new Rect(endValue);
|
|
if (isOutPipDirection) {
|
|
if (shouldUseMainWindowFrame && !changeOrientation) {
|
|
endBounds.set(mainWindowFrame);
|
|
}
|
|
initialSourceValue = new Rect(endBounds);
|
|
} else if (isInPipDirection) {
|
|
if (shouldUseMainWindowFrame) {
|
|
baseBounds.set(mainWindowFrame);
|
|
if (startValue.equals(baseValue)) {
|
|
// If the start value is at initial state as in PIP animation, also override
|
|
// the start bounds with nonMatchParentBounds.
|
|
startBounds.set(mainWindowFrame);
|
|
}
|
|
}
|
|
initialSourceValue = new Rect(baseBounds);
|
|
} else {
|
|
// Note that we assume the window bounds always match task bounds in PIP mode.
|
|
initialSourceValue = new Rect(baseBounds);
|
|
}
|
|
|
|
final Point leashOffset;
|
|
if (isInPipDirection) {
|
|
leashOffset = new Point(baseValue.left, baseValue.top);
|
|
} else if (isOutPipDirection) {
|
|
leashOffset = new Point(endValue.left, endValue.top);
|
|
} else {
|
|
leashOffset = new Point(baseValue.left, baseValue.top);
|
|
}
|
|
|
|
final Rect rotatedEndRect;
|
|
final Rect lastEndRect;
|
|
final Rect initialContainerRect;
|
|
if (changeOrientation) {
|
|
lastEndRect = new Rect(endBounds);
|
|
rotatedEndRect = new Rect(endBounds);
|
|
// TODO(b/375977163): polish the animation to restoring the PIP task back from
|
|
// swipe-pip-to-home. Ideally we should send the transitionInfo after reparenting
|
|
// the PIP activity back to the original task.
|
|
if (shouldUseMainWindowFrame && isOutPipDirection) {
|
|
// If we should animate the main window frame, set it to the rotatedRect
|
|
// instead. The end bounds reported by transitionInfo is the bounds before
|
|
// rotation, while main window frame is calculated after the rotation.
|
|
// Note that we only override main window frame for leaving pip animation as
|
|
// the pip activity should match parent.
|
|
rotatedEndRect.set(mainWindowFrame);
|
|
} else {
|
|
// Rotate the end bounds according to the rotation delta because the display
|
|
// will be rotated to the same orientation.
|
|
rotateBounds(rotatedEndRect, initialSourceValue, rotationDelta);
|
|
}
|
|
// Use the rect that has the same orientation as the hint rect.
|
|
initialContainerRect = isOutPipDirection ? rotatedEndRect : initialSourceValue;
|
|
} else {
|
|
rotatedEndRect = lastEndRect = null;
|
|
initialContainerRect = initialSourceValue;
|
|
}
|
|
|
|
final Rect adjustedSourceRectHint = new Rect();
|
|
if (sourceRectHint == null || sourceRectHint.isEmpty()) {
|
|
// Crop a Rect matches the aspect ratio and pivots at the center point.
|
|
// This is done for entering case only.
|
|
if (isInPipDirection(direction)) {
|
|
final float aspectRatio = endBounds.width() / (float) endBounds.height();
|
|
adjustedSourceRectHint.set(PipUtils.getPseudoSourceRectHint(
|
|
startBounds, aspectRatio));
|
|
}
|
|
} else {
|
|
adjustedSourceRectHint.set(sourceRectHint);
|
|
if (isInPipDirection(direction)
|
|
&& rotationDelta == ROTATION_0
|
|
&& taskInfo.displayCutoutInsets != null) {
|
|
// TODO: this is to special case the issues on Foldable device
|
|
// with display cutout. This aligns with what's in SwipePipToHomeAnimator.
|
|
adjustedSourceRectHint.offset(taskInfo.displayCutoutInsets.left,
|
|
taskInfo.displayCutoutInsets.top);
|
|
}
|
|
}
|
|
final Rect sourceHintRectInsets = new Rect();
|
|
if (!adjustedSourceRectHint.isEmpty()) {
|
|
sourceHintRectInsets.set(
|
|
adjustedSourceRectHint.left - initialContainerRect.left,
|
|
adjustedSourceRectHint.top - initialContainerRect.top,
|
|
initialContainerRect.right - adjustedSourceRectHint.right,
|
|
initialContainerRect.bottom - adjustedSourceRectHint.bottom);
|
|
}
|
|
final Rect zeroInsets = new Rect(0, 0, 0, 0);
|
|
|
|
// construct new Rect instances in case they are recycled
|
|
return new PipTransitionAnimator<Rect>(taskInfo, leash, ANIM_TYPE_BOUNDS, endBounds,
|
|
leashOffset, new Rect(baseBounds), new Rect(startBounds), new Rect(endBounds)) {
|
|
private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect());
|
|
private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect());
|
|
|
|
@Override
|
|
void applySurfaceControlTransaction(SurfaceControl leash,
|
|
SurfaceControl.Transaction tx, float fraction) {
|
|
final Rect base = getBaseValue();
|
|
final Rect start = getStartValue();
|
|
final Rect end = getEndValue();
|
|
Rect bounds = mRectEvaluator.evaluate(fraction, start, end);
|
|
if (mContentOverlay != null) {
|
|
mContentOverlay.onAnimationUpdate(tx, bounds, fraction);
|
|
}
|
|
if (rotatedEndRect != null) {
|
|
// Animate the bounds in a different orientation. It only happens when
|
|
// switching between PiP and fullscreen.
|
|
applyRotation(tx, leash, fraction, start, end);
|
|
return;
|
|
}
|
|
float angle = (1.0f - fraction) * startingAngle;
|
|
setCurrentValue(bounds);
|
|
if (inScaleTransition() || adjustedSourceRectHint.isEmpty()) {
|
|
if (isOutPipDirection) {
|
|
// Use the bounds relative to the task leash in case the leash does not
|
|
// start from (0, 0).
|
|
final Rect relativeEndBounds = new Rect(end);
|
|
final Point leashOffset = getLeashOffset();
|
|
relativeEndBounds.offset(-leashOffset.x, -leashOffset.y);
|
|
getSurfaceTransactionHelper()
|
|
.crop(tx, leash, relativeEndBounds)
|
|
.scale(tx, leash, relativeEndBounds, bounds,
|
|
false /* shouldOffset */);
|
|
} else {
|
|
// TODO(b/356277166): add support to specify sourceRectHint with
|
|
// non-match parent activity.
|
|
// If there's a PIP resize animation, we should offset the bounds to
|
|
// (0, 0) since the window bounds should match the leash bounds in PIP
|
|
// mode.
|
|
getSurfaceTransactionHelper().cropAndPosition(tx, leash, base)
|
|
.scale(tx, leash, base, bounds, angle, inScaleTransition())
|
|
.round(tx, leash, base, bounds)
|
|
.shadow(tx, leash, shouldApplyShadowRadius());
|
|
}
|
|
} else {
|
|
final Rect insets = computeInsets(fraction);
|
|
getSurfaceTransactionHelper().scaleAndCrop(tx, leash,
|
|
adjustedSourceRectHint, initialSourceValue, bounds, insets,
|
|
isInPipDirection, fraction, leashOffset);
|
|
final Rect sourceBounds = new Rect(initialContainerRect);
|
|
sourceBounds.inset(insets);
|
|
getSurfaceTransactionHelper()
|
|
.round(tx, leash, sourceBounds, bounds)
|
|
.shadow(tx, leash, shouldApplyShadowRadius());
|
|
}
|
|
if (!handlePipTransaction(leash, tx, bounds, /* alpha= */ 1f)) {
|
|
tx.apply();
|
|
}
|
|
}
|
|
|
|
private void applyRotation(SurfaceControl.Transaction tx, SurfaceControl leash,
|
|
float fraction, Rect start, Rect end) {
|
|
if (!end.equals(lastEndRect)) {
|
|
// If the end bounds are changed during animating (e.g. shelf height), the
|
|
// rotated end bounds also need to be updated.
|
|
rotatedEndRect.set(endValue);
|
|
rotateBounds(rotatedEndRect, initialSourceValue, rotationDelta);
|
|
lastEndRect.set(end);
|
|
}
|
|
final Rect bounds = mRectEvaluator.evaluate(fraction, start, rotatedEndRect);
|
|
setCurrentValue(bounds);
|
|
final Rect insets = computeInsets(fraction);
|
|
final float degree, x, y;
|
|
if (Transitions.SHELL_TRANSITIONS_ROTATION) {
|
|
if (rotationDelta == ROTATION_90) {
|
|
degree = 90 * (1 - fraction);
|
|
x = fraction * (end.left - start.left)
|
|
+ start.left + start.width() * (1 - fraction);
|
|
y = fraction * (end.top - start.top) + start.top;
|
|
} else {
|
|
degree = -90 * (1 - fraction);
|
|
x = fraction * (end.left - start.left) + start.left;
|
|
y = fraction * (end.top - start.top)
|
|
+ start.top + start.height() * (1 - fraction);
|
|
}
|
|
} else {
|
|
if (rotationDelta == ROTATION_90) {
|
|
degree = 90 * fraction;
|
|
x = fraction * (end.right - start.left) + start.left;
|
|
y = fraction * (end.top - start.top) + start.top;
|
|
} else {
|
|
degree = -90 * fraction;
|
|
x = fraction * (end.left - start.left) + start.left;
|
|
y = fraction * (end.bottom - start.top) + start.top;
|
|
}
|
|
}
|
|
final Rect sourceBounds = new Rect(initialContainerRect);
|
|
Rect relativeEndWindowFrame = null;
|
|
if (isOutPipDirection) {
|
|
relativeEndWindowFrame = rotatedEndRect;
|
|
}
|
|
if (relativeEndWindowFrame != null) {
|
|
relativeEndWindowFrame.offset(leashOffset.x, leashOffset.y);
|
|
}
|
|
sourceBounds.inset(insets);
|
|
getSurfaceTransactionHelper()
|
|
.rotateAndScaleWithCrop(tx, leash, initialContainerRect, bounds,
|
|
insets, degree, x, y, isOutPipDirection,
|
|
rotationDelta == ROTATION_270 /* clockwise */,
|
|
relativeEndWindowFrame)
|
|
.round(tx, leash, sourceBounds, bounds)
|
|
.shadow(tx, leash, shouldApplyShadowRadius());
|
|
if (!handlePipTransaction(leash, tx, bounds, 1f /* alpha */)) {
|
|
tx.apply();
|
|
}
|
|
}
|
|
|
|
private Rect computeInsets(float fraction) {
|
|
final Rect startRect = isOutPipDirection ? sourceHintRectInsets : zeroInsets;
|
|
final Rect endRect = isOutPipDirection ? zeroInsets : sourceHintRectInsets;
|
|
return mInsetsEvaluator.evaluate(fraction, startRect, endRect);
|
|
}
|
|
|
|
@Override
|
|
void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {
|
|
getSurfaceTransactionHelper()
|
|
.alpha(tx, leash, 1f)
|
|
.round(tx, leash, true /* applyCornerRadius */)
|
|
.shadow(tx, leash, shouldApplyShadowRadius());
|
|
tx.show(leash);
|
|
tx.apply();
|
|
}
|
|
|
|
@Override
|
|
void onEndTransaction(SurfaceControl leash, SurfaceControl.Transaction tx,
|
|
int transitionDirection) {
|
|
// NOTE: intentionally does not apply the transaction here.
|
|
// this end transaction should get executed synchronously with the final
|
|
// WindowContainerTransaction in task organizer
|
|
final Rect destBounds = getDestinationBounds();
|
|
getSurfaceTransactionHelper().resetScale(tx, leash, destBounds);
|
|
if (isOutPipDirection(transitionDirection)) {
|
|
// Exit pip, clear scale, position and crop.
|
|
tx.setMatrix(leash, 1, 0, 0, 1);
|
|
tx.setPosition(leash, 0, 0);
|
|
tx.setWindowCrop(leash, 0, 0);
|
|
} else {
|
|
getSurfaceTransactionHelper().cropAndPosition(tx, leash, destBounds);
|
|
}
|
|
if (mContentOverlay != null) {
|
|
clearContentOverlay();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void updateEndValue(Rect endValue) {
|
|
super.updateEndValue(endValue);
|
|
if (mStartValue != null && mCurrentValue != null) {
|
|
mStartValue.set(mCurrentValue);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|