839 lines
37 KiB
Java
839 lines
37 KiB
Java
/*
|
|
* Copyright (C) 2021 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.tv;
|
|
|
|
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
|
|
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
|
|
import static android.view.WindowManager.TRANSIT_CHANGE;
|
|
import static android.view.WindowManager.TRANSIT_CLOSE;
|
|
import static android.view.WindowManager.TRANSIT_OPEN;
|
|
import static android.view.WindowManager.TRANSIT_PIP;
|
|
import static android.view.WindowManager.TRANSIT_TO_BACK;
|
|
import static android.view.WindowManager.transitTypeToString;
|
|
|
|
import static com.android.wm.shell.common.pip.PipMenuController.ALPHA_NO_CHANGE;
|
|
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
|
|
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK;
|
|
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
|
|
import static com.android.wm.shell.pip.PipTransitionState.ENTERED_PIP;
|
|
import static com.android.wm.shell.pip.PipTransitionState.ENTERING_PIP;
|
|
import static com.android.wm.shell.pip.PipTransitionState.EXITING_PIP;
|
|
import static com.android.wm.shell.pip.PipTransitionState.UNDEFINED;
|
|
import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
|
|
import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
|
|
|
|
import android.animation.AnimationHandler;
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.animation.AnimatorSet;
|
|
import android.animation.ValueAnimator;
|
|
import android.annotation.SuppressLint;
|
|
import android.app.ActivityManager;
|
|
import android.app.TaskInfo;
|
|
import android.content.Context;
|
|
import android.graphics.Point;
|
|
import android.graphics.Rect;
|
|
import android.graphics.RectF;
|
|
import android.os.IBinder;
|
|
import android.os.Trace;
|
|
import android.view.SurfaceControl;
|
|
import android.view.WindowManager;
|
|
import android.window.TransitionInfo;
|
|
import android.window.TransitionRequestInfo;
|
|
import android.window.WindowContainerToken;
|
|
import android.window.WindowContainerTransaction;
|
|
|
|
import androidx.annotation.FloatRange;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
|
|
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
|
|
import com.android.internal.protolog.common.ProtoLog;
|
|
import com.android.wm.shell.R;
|
|
import com.android.wm.shell.ShellTaskOrganizer;
|
|
import com.android.wm.shell.common.pip.PipDisplayLayoutState;
|
|
import com.android.wm.shell.pip.PipAnimationController;
|
|
import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
|
|
import com.android.wm.shell.pip.PipTransitionController;
|
|
import com.android.wm.shell.pip.PipTransitionState;
|
|
import com.android.wm.shell.protolog.ShellProtoLogGroup;
|
|
import com.android.wm.shell.shared.TransitionUtil;
|
|
import com.android.wm.shell.sysui.ShellInit;
|
|
import com.android.wm.shell.transition.Transitions;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
|
|
/**
|
|
* PiP Transition for TV.
|
|
*/
|
|
public class TvPipTransition extends PipTransitionController {
|
|
private static final String TAG = "TvPipTransition";
|
|
private static final float ZOOM_ANIMATION_SCALE_FACTOR = 0.97f;
|
|
|
|
private final PipTransitionState mPipTransitionState;
|
|
private final PipAnimationController mPipAnimationController;
|
|
private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
|
|
private final TvPipMenuController mTvPipMenuController;
|
|
private final PipDisplayLayoutState mPipDisplayLayoutState;
|
|
private final PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory
|
|
mTransactionFactory;
|
|
|
|
private final ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal =
|
|
ThreadLocal.withInitial(() -> {
|
|
AnimationHandler handler = new AnimationHandler();
|
|
handler.setProvider(new SfVsyncFrameCallbackProvider());
|
|
return handler;
|
|
});
|
|
|
|
private final long mEnterFadeOutDuration;
|
|
private final long mEnterFadeInDuration;
|
|
private final long mExitFadeOutDuration;
|
|
private final long mExitFadeInDuration;
|
|
|
|
@Nullable
|
|
private Animator mCurrentAnimator;
|
|
|
|
/**
|
|
* The Task window that is currently in PIP windowing mode.
|
|
*/
|
|
@Nullable
|
|
private WindowContainerToken mCurrentPipTaskToken;
|
|
|
|
@Nullable
|
|
private IBinder mPendingExitTransition;
|
|
|
|
public TvPipTransition(Context context,
|
|
@NonNull ShellInit shellInit,
|
|
@NonNull ShellTaskOrganizer shellTaskOrganizer,
|
|
@NonNull Transitions transitions,
|
|
TvPipBoundsState tvPipBoundsState,
|
|
TvPipMenuController tvPipMenuController,
|
|
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
|
|
PipTransitionState pipTransitionState,
|
|
PipAnimationController pipAnimationController,
|
|
PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
|
|
PipDisplayLayoutState pipDisplayLayoutState) {
|
|
super(shellInit, shellTaskOrganizer, transitions, tvPipBoundsState, tvPipMenuController,
|
|
tvPipBoundsAlgorithm);
|
|
mPipTransitionState = pipTransitionState;
|
|
mPipAnimationController = pipAnimationController;
|
|
mSurfaceTransactionHelper = pipSurfaceTransactionHelper;
|
|
mTvPipMenuController = tvPipMenuController;
|
|
mPipDisplayLayoutState = pipDisplayLayoutState;
|
|
mTransactionFactory =
|
|
new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
|
|
|
|
mEnterFadeOutDuration = context.getResources().getInteger(
|
|
R.integer.config_tvPipEnterFadeOutDuration);
|
|
mEnterFadeInDuration = context.getResources().getInteger(
|
|
R.integer.config_tvPipEnterFadeInDuration);
|
|
mExitFadeOutDuration = context.getResources().getInteger(
|
|
R.integer.config_tvPipExitFadeOutDuration);
|
|
mExitFadeInDuration = context.getResources().getInteger(
|
|
R.integer.config_tvPipExitFadeInDuration);
|
|
}
|
|
|
|
@Override
|
|
public void startExitTransition(int type, WindowContainerTransaction out,
|
|
@Nullable Rect destinationBounds) {
|
|
cancelAnimations();
|
|
mPendingExitTransition = mTransitions.startTransition(type, out, this);
|
|
}
|
|
|
|
@Override
|
|
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
|
|
@NonNull SurfaceControl.Transaction startTransaction,
|
|
@NonNull SurfaceControl.Transaction finishTransaction,
|
|
@NonNull Transitions.TransitionFinishCallback finishCallback) {
|
|
|
|
if (isCloseTransition(info)) {
|
|
// PiP is closing (without reentering fullscreen activity)
|
|
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: Starting close animation", TAG);
|
|
cancelAnimations();
|
|
startCloseAnimation(info, startTransaction, finishTransaction, finishCallback);
|
|
mCurrentPipTaskToken = null;
|
|
return true;
|
|
|
|
} else if (transition.equals(mPendingExitTransition)) {
|
|
// PiP is exiting (reentering fullscreen activity)
|
|
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: Starting exit animation", TAG);
|
|
|
|
final TransitionInfo.Change currentPipTaskChange = findCurrentPipTaskChange(info);
|
|
mPendingExitTransition = null;
|
|
// PipTaskChange can be null if the PIP task has been detached, for example, when the
|
|
// task contains multiple activities, the PIP will be moved to a new PIP task when
|
|
// entering, and be moved back when exiting. In that case, the PIP task will be removed
|
|
// immediately.
|
|
final TaskInfo pipTaskInfo = currentPipTaskChange != null
|
|
? currentPipTaskChange.getTaskInfo()
|
|
: mPipOrganizer.getTaskInfo();
|
|
if (pipTaskInfo == null) {
|
|
throw new RuntimeException("Cannot find the pip task for exit-pip transition.");
|
|
}
|
|
|
|
final int type = info.getType();
|
|
switch (type) {
|
|
case TRANSIT_EXIT_PIP -> {
|
|
TransitionInfo.Change pipChange = currentPipTaskChange;
|
|
SurfaceControl activitySc = null;
|
|
if (mCurrentPipTaskToken == null) {
|
|
ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: There is no existing PiP Task for TRANSIT_EXIT_PIP", TAG);
|
|
} else if (pipChange == null) {
|
|
// The pipTaskChange is null, this can happen if we are reparenting the
|
|
// PIP activity back to its original Task. In that case, we should animate
|
|
// the activity leash instead, which should be the change whose last parent
|
|
// is the recorded PiP Task.
|
|
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
|
|
final TransitionInfo.Change change = info.getChanges().get(i);
|
|
if (mCurrentPipTaskToken.equals(change.getLastParent())) {
|
|
// Find the activity that is exiting PiP.
|
|
pipChange = change;
|
|
activitySc = change.getLeash();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (pipChange == null) {
|
|
ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: No window of exiting PIP is found. Can't play expand "
|
|
+ "animation",
|
|
TAG);
|
|
removePipImmediately(info, pipTaskInfo, startTransaction, finishTransaction,
|
|
finishCallback);
|
|
return true;
|
|
}
|
|
final TransitionInfo.Root root = TransitionUtil.getRootFor(pipChange, info);
|
|
final SurfaceControl pipLeash;
|
|
if (activitySc != null) {
|
|
// Use a local leash to animate activity in case the activity has
|
|
// letterbox which may be broken by PiP animation, e.g. always end at 0,0
|
|
// in parent and unable to include letterbox area in crop bounds.
|
|
final SurfaceControl activitySurface = pipChange.getLeash();
|
|
pipLeash = new SurfaceControl.Builder()
|
|
.setName(activitySc + "_pip-leash")
|
|
.setContainerLayer()
|
|
.setHidden(false)
|
|
.setParent(root.getLeash())
|
|
.setCallsite("TvPipTransition.startAnimation")
|
|
.build();
|
|
startTransaction.reparent(activitySurface, pipLeash);
|
|
// Put the activity at local position with offset in case it is letterboxed.
|
|
final Point activityOffset = pipChange.getEndRelOffset();
|
|
startTransaction.setPosition(activitySc, activityOffset.x,
|
|
activityOffset.y);
|
|
} else {
|
|
pipLeash = pipChange.getLeash();
|
|
startTransaction.reparent(pipLeash, root.getLeash());
|
|
}
|
|
startTransaction.setLayer(pipLeash, Integer.MAX_VALUE);
|
|
final Rect currentBounds = mPipBoundsState.getBounds();
|
|
final Rect destinationBounds = new Rect(pipChange.getEndAbsBounds());
|
|
cancelAnimations();
|
|
startExitAnimation(pipTaskInfo, pipLeash, currentBounds, destinationBounds,
|
|
startTransaction,
|
|
finishTransaction, finishCallback);
|
|
}
|
|
// pass through here is intended
|
|
case TRANSIT_TO_BACK, TRANSIT_REMOVE_PIP -> removePipImmediately(info, pipTaskInfo,
|
|
startTransaction, finishTransaction,
|
|
finishCallback
|
|
);
|
|
default -> {
|
|
return false;
|
|
}
|
|
}
|
|
mCurrentPipTaskToken = null;
|
|
return true;
|
|
|
|
} else if (isEnteringPip(info)) {
|
|
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: Starting enter animation", TAG);
|
|
|
|
// Search for an Enter PiP transition
|
|
TransitionInfo.Change enterPip = null;
|
|
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
|
|
final TransitionInfo.Change change = info.getChanges().get(i);
|
|
if (change.getTaskInfo() != null
|
|
&& change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) {
|
|
enterPip = change;
|
|
}
|
|
}
|
|
if (enterPip == null) {
|
|
throw new IllegalStateException("Trying to start PiP animation without a pip"
|
|
+ "participant");
|
|
}
|
|
|
|
// Make sure other open changes are visible as entering PIP. Some may be hidden in
|
|
// Transitions#setupStartState because the transition type is OPEN (such as auto-enter).
|
|
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
|
|
final TransitionInfo.Change change = info.getChanges().get(i);
|
|
if (change == enterPip) continue;
|
|
if (TransitionUtil.isOpeningType(change.getMode())) {
|
|
final SurfaceControl leash = change.getLeash();
|
|
startTransaction.show(leash).setAlpha(leash, 1.f);
|
|
}
|
|
}
|
|
|
|
cancelAnimations();
|
|
startEnterAnimation(enterPip, startTransaction, finishTransaction, finishCallback);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* For {@link Transitions#TRANSIT_REMOVE_PIP}, we just immediately remove the PIP Task.
|
|
*/
|
|
private void removePipImmediately(@NonNull TransitionInfo info,
|
|
@NonNull TaskInfo taskInfo, @NonNull SurfaceControl.Transaction startTransaction,
|
|
@NonNull SurfaceControl.Transaction finishTransaction,
|
|
@NonNull Transitions.TransitionFinishCallback finishCallback) {
|
|
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: removePipImmediately", TAG);
|
|
cancelAnimations();
|
|
startTransaction.apply();
|
|
finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(),
|
|
mPipDisplayLayoutState.getDisplayBounds());
|
|
mTvPipMenuController.detach();
|
|
mPipOrganizer.onExitPipFinished(taskInfo);
|
|
finishCallback.onTransitionFinished(/* wct= */ null);
|
|
|
|
mPipTransitionState.setTransitionState(UNDEFINED);
|
|
sendOnPipTransitionFinished(TRANSITION_DIRECTION_REMOVE_STACK);
|
|
}
|
|
|
|
private void startCloseAnimation(@NonNull TransitionInfo info,
|
|
@NonNull SurfaceControl.Transaction startTransaction,
|
|
@NonNull SurfaceControl.Transaction finishTransaction,
|
|
@NonNull Transitions.TransitionFinishCallback finishCallback) {
|
|
final TransitionInfo.Change pipTaskChange = findCurrentPipTaskChange(info);
|
|
final SurfaceControl pipLeash = pipTaskChange.getLeash();
|
|
|
|
final List<SurfaceControl> closeLeashes = new ArrayList<>();
|
|
for (TransitionInfo.Change change : info.getChanges()) {
|
|
if (TransitionUtil.isClosingType(change.getMode()) && change != pipTaskChange) {
|
|
closeLeashes.add(change.getLeash());
|
|
}
|
|
}
|
|
|
|
final Rect pipBounds = mPipBoundsState.getBounds();
|
|
mSurfaceTransactionHelper
|
|
.resetScale(startTransaction, pipLeash, pipBounds)
|
|
.crop(startTransaction, pipLeash, pipBounds)
|
|
.shadow(startTransaction, pipLeash, false);
|
|
|
|
final SurfaceControl.Transaction transaction = mTransactionFactory.getTransaction();
|
|
for (SurfaceControl leash : closeLeashes) {
|
|
startTransaction.setShadowRadius(leash, 0f);
|
|
}
|
|
|
|
ValueAnimator closeFadeOutAnimator = createAnimator();
|
|
closeFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT);
|
|
closeFadeOutAnimator.setDuration(mExitFadeOutDuration);
|
|
closeFadeOutAnimator.addUpdateListener(
|
|
animationUpdateListener(pipLeash).fadingOut().withMenu());
|
|
for (SurfaceControl leash : closeLeashes) {
|
|
closeFadeOutAnimator.addUpdateListener(animationUpdateListener(leash).fadingOut());
|
|
}
|
|
|
|
closeFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationStart(@NonNull Animator animation) {
|
|
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: close animation: start", TAG);
|
|
for (SurfaceControl leash : closeLeashes) {
|
|
startTransaction.setShadowRadius(leash, 0f);
|
|
}
|
|
startTransaction.apply();
|
|
|
|
mPipTransitionState.setTransitionState(EXITING_PIP);
|
|
sendOnPipTransitionStarted(TRANSITION_DIRECTION_REMOVE_STACK);
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationCancel(@NonNull Animator animation) {
|
|
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: close animation: cancel", TAG);
|
|
sendOnPipTransitionCancelled(TRANSITION_DIRECTION_REMOVE_STACK);
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationEnd(@NonNull Animator animation) {
|
|
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: close animation: end", TAG);
|
|
mTvPipMenuController.detach();
|
|
finishCallback.onTransitionFinished(null /* wct */);
|
|
transaction.close();
|
|
mPipTransitionState.setTransitionState(UNDEFINED);
|
|
sendOnPipTransitionFinished(TRANSITION_DIRECTION_REMOVE_STACK);
|
|
|
|
mCurrentAnimator = null;
|
|
}
|
|
});
|
|
|
|
closeFadeOutAnimator.start();
|
|
mCurrentAnimator = closeFadeOutAnimator;
|
|
}
|
|
|
|
@Override
|
|
public void startEnterAnimation(@NonNull TransitionInfo.Change pipChange,
|
|
@NonNull SurfaceControl.Transaction startTransaction,
|
|
@NonNull SurfaceControl.Transaction finishTransaction,
|
|
@NonNull Transitions.TransitionFinishCallback finishCallback) {
|
|
// Keep track of the PIP task
|
|
mCurrentPipTaskToken = pipChange.getContainer();
|
|
final ActivityManager.RunningTaskInfo taskInfo = pipChange.getTaskInfo();
|
|
final SurfaceControl leash = pipChange.getLeash();
|
|
|
|
mTvPipMenuController.attach(leash);
|
|
setBoundsStateForEntry(taskInfo.topActivity, taskInfo.pictureInPictureParams,
|
|
taskInfo.topActivityInfo);
|
|
|
|
final Rect pipBounds =
|
|
mPipBoundsAlgorithm.getEntryDestinationBoundsIgnoringKeepClearAreas();
|
|
mPipBoundsState.setBounds(pipBounds);
|
|
mTvPipMenuController.movePipMenu(null, pipBounds, 0f);
|
|
|
|
final WindowContainerTransaction resizePipWct = new WindowContainerTransaction();
|
|
resizePipWct.setWindowingMode(taskInfo.token, WINDOWING_MODE_PINNED);
|
|
resizePipWct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_PINNED);
|
|
resizePipWct.setBounds(taskInfo.token, pipBounds);
|
|
|
|
mSurfaceTransactionHelper
|
|
.resetScale(finishTransaction, leash, pipBounds)
|
|
.crop(finishTransaction, leash, pipBounds)
|
|
.shadow(finishTransaction, leash, false);
|
|
|
|
final Rect currentBounds = pipChange.getStartAbsBounds();
|
|
final Rect fadeOutCurrentBounds = scaledRect(currentBounds, ZOOM_ANIMATION_SCALE_FACTOR);
|
|
|
|
final ValueAnimator enterFadeOutAnimator = createAnimator();
|
|
enterFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT);
|
|
enterFadeOutAnimator.setDuration(mEnterFadeOutDuration);
|
|
enterFadeOutAnimator.addUpdateListener(
|
|
animationUpdateListener(leash)
|
|
.fadingOut()
|
|
.animateBounds(currentBounds, fadeOutCurrentBounds, currentBounds));
|
|
|
|
enterFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
|
|
@SuppressLint("MissingPermission")
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: enter fade out animation: end", TAG);
|
|
SurfaceControl.Transaction tx = mTransactionFactory.getTransaction();
|
|
mSurfaceTransactionHelper
|
|
.resetScale(tx, leash, pipBounds)
|
|
.crop(tx, leash, pipBounds)
|
|
.shadow(tx, leash, false);
|
|
mShellTaskOrganizer.applyTransaction(resizePipWct);
|
|
tx.apply();
|
|
}
|
|
});
|
|
|
|
final ValueAnimator enterFadeInAnimator = createAnimator();
|
|
enterFadeInAnimator.setInterpolator(TvPipInterpolators.ENTER);
|
|
enterFadeInAnimator.setDuration(mEnterFadeInDuration);
|
|
enterFadeInAnimator.addUpdateListener(
|
|
animationUpdateListener(leash)
|
|
.fadingIn()
|
|
.withMenu()
|
|
.atBounds(pipBounds));
|
|
|
|
final AnimatorSet animatorSet = new AnimatorSet();
|
|
animatorSet
|
|
.play(enterFadeInAnimator)
|
|
.after(500)
|
|
.after(enterFadeOutAnimator);
|
|
|
|
animatorSet.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationStart(Animator animation) {
|
|
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: enter animation: start", TAG);
|
|
startTransaction.apply();
|
|
mPipTransitionState.setTransitionState(ENTERING_PIP);
|
|
sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP);
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationCancel(Animator animation) {
|
|
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: enter animation: cancel", TAG);
|
|
enterFadeInAnimator.setCurrentFraction(1f);
|
|
sendOnPipTransitionCancelled(TRANSITION_DIRECTION_TO_PIP);
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: enter animation: end", TAG);
|
|
WindowContainerTransaction wct = new WindowContainerTransaction();
|
|
wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
|
|
wct.setBounds(taskInfo.token, pipBounds);
|
|
finishCallback.onTransitionFinished(wct);
|
|
|
|
mPipTransitionState.setTransitionState(ENTERED_PIP);
|
|
sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
|
|
mCurrentAnimator = null;
|
|
}
|
|
});
|
|
|
|
animatorSet.start();
|
|
mCurrentAnimator = animatorSet;
|
|
}
|
|
|
|
private void startExitAnimation(@NonNull TaskInfo taskInfo, SurfaceControl leash,
|
|
Rect currentBounds, Rect destinationBounds,
|
|
@NonNull SurfaceControl.Transaction startTransaction,
|
|
@NonNull SurfaceControl.Transaction finishTransaction,
|
|
@NonNull Transitions.TransitionFinishCallback finishCallback) {
|
|
final Rect fadeInStartBounds = scaledRect(destinationBounds, ZOOM_ANIMATION_SCALE_FACTOR);
|
|
|
|
final ValueAnimator exitFadeOutAnimator = createAnimator();
|
|
exitFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT);
|
|
exitFadeOutAnimator.setDuration(mExitFadeOutDuration);
|
|
exitFadeOutAnimator.addUpdateListener(
|
|
animationUpdateListener(leash)
|
|
.fadingOut()
|
|
.withMenu()
|
|
.atBounds(currentBounds));
|
|
exitFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: exit fade out animation: end", TAG);
|
|
startTransaction.apply();
|
|
mPipMenuController.detach();
|
|
}
|
|
});
|
|
|
|
final ValueAnimator exitFadeInAnimator = createAnimator();
|
|
exitFadeInAnimator.setInterpolator(TvPipInterpolators.ENTER);
|
|
exitFadeInAnimator.setDuration(mExitFadeInDuration);
|
|
exitFadeInAnimator.addUpdateListener(
|
|
animationUpdateListener(leash)
|
|
.fadingIn()
|
|
.animateBounds(fadeInStartBounds, destinationBounds, destinationBounds));
|
|
|
|
final AnimatorSet animatorSet = new AnimatorSet();
|
|
animatorSet.playSequentially(
|
|
exitFadeOutAnimator,
|
|
exitFadeInAnimator
|
|
);
|
|
|
|
animatorSet.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationStart(Animator animation) {
|
|
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: exit animation: start", TAG);
|
|
mPipTransitionState.setTransitionState(EXITING_PIP);
|
|
sendOnPipTransitionStarted(TRANSITION_DIRECTION_LEAVE_PIP);
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationCancel(Animator animation) {
|
|
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: exit animation: cancel", TAG);
|
|
sendOnPipTransitionCancelled(TRANSITION_DIRECTION_LEAVE_PIP);
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: exit animation: end", TAG);
|
|
mPipOrganizer.onExitPipFinished(taskInfo);
|
|
|
|
final WindowContainerTransaction wct = new WindowContainerTransaction();
|
|
wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
|
|
wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
|
|
wct.setBounds(taskInfo.token, destinationBounds);
|
|
finishCallback.onTransitionFinished(wct);
|
|
|
|
mPipTransitionState.setTransitionState(UNDEFINED);
|
|
sendOnPipTransitionFinished(TRANSITION_DIRECTION_LEAVE_PIP);
|
|
|
|
mCurrentAnimator = null;
|
|
}
|
|
});
|
|
|
|
animatorSet.start();
|
|
mCurrentAnimator = animatorSet;
|
|
}
|
|
|
|
@NonNull
|
|
private ValueAnimator createAnimator() {
|
|
final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
|
|
animator.setAnimationHandler(mSfAnimationHandlerThreadLocal.get());
|
|
return animator;
|
|
}
|
|
|
|
@NonNull
|
|
private TvPipTransitionAnimatorUpdateListener animationUpdateListener(
|
|
@NonNull SurfaceControl leash) {
|
|
return new TvPipTransitionAnimatorUpdateListener(leash, mTvPipMenuController,
|
|
mTransactionFactory.getTransaction(), mSurfaceTransactionHelper);
|
|
}
|
|
|
|
@NonNull
|
|
private static Rect scaledRect(@NonNull Rect rect, float scale) {
|
|
final Rect out = new Rect(rect);
|
|
out.inset((int) (rect.width() * (1 - scale) / 2), (int) (rect.height() * (1 - scale) / 2));
|
|
return out;
|
|
}
|
|
|
|
private boolean isCloseTransition(TransitionInfo info) {
|
|
final TransitionInfo.Change currentPipTaskChange = findCurrentPipTaskChange(info);
|
|
return currentPipTaskChange != null && info.getType() == TRANSIT_CLOSE;
|
|
}
|
|
|
|
@Nullable
|
|
private TransitionInfo.Change findCurrentPipTaskChange(@NonNull TransitionInfo info) {
|
|
if (mCurrentPipTaskToken == null) {
|
|
return null;
|
|
}
|
|
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
|
|
final TransitionInfo.Change change = info.getChanges().get(i);
|
|
if (mCurrentPipTaskToken.equals(change.getContainer())) {
|
|
return change;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Whether we should handle the given {@link TransitionInfo} animation as entering PIP.
|
|
*/
|
|
private boolean isEnteringPip(@NonNull TransitionInfo info) {
|
|
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
|
|
final TransitionInfo.Change change = info.getChanges().get(i);
|
|
if (isEnteringPip(change, info.getType())) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Whether a particular change is a window that is entering pip.
|
|
*/
|
|
@Override
|
|
public boolean isEnteringPip(@NonNull TransitionInfo.Change change,
|
|
@WindowManager.TransitionType int transitType) {
|
|
if (change.getTaskInfo() != null
|
|
&& change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED
|
|
&& !Objects.equals(change.getContainer(), mCurrentPipTaskToken)) {
|
|
if (transitType == TRANSIT_PIP || transitType == TRANSIT_OPEN
|
|
|| transitType == TRANSIT_CHANGE) {
|
|
return true;
|
|
}
|
|
// Please file a bug to handle the unexpected transition type.
|
|
android.util.Slog.e(TAG, "Found new PIP in transition with mis-matched type="
|
|
+ transitTypeToString(transitType), new Throwable());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
|
|
@NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
|
|
@NonNull Transitions.TransitionFinishCallback finishCallback) {
|
|
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: merge animation", TAG);
|
|
if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
|
|
mCurrentAnimator.end();
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
|
|
@NonNull TransitionRequestInfo request) {
|
|
if (requestHasPipEnter(request)) {
|
|
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
|
"%s: handle PiP enter request", TAG);
|
|
WindowContainerTransaction wct = new WindowContainerTransaction();
|
|
augmentRequest(transition, request, wct);
|
|
return wct;
|
|
} else if (request.getType() == TRANSIT_TO_BACK && request.getTriggerTask() != null
|
|
&& request.getTriggerTask().getWindowingMode() == WINDOWING_MODE_PINNED) {
|
|
// if we receive a TRANSIT_TO_BACK type of request while in PiP
|
|
mPendingExitTransition = transition;
|
|
|
|
// update the transition state to avoid {@link PipTaskOrganizer#onTaskVanished()} calls
|
|
mPipTransitionState.setTransitionState(EXITING_PIP);
|
|
|
|
// return an empty WindowContainerTransaction so that we don't check other handlers
|
|
return new WindowContainerTransaction();
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void augmentRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request,
|
|
@NonNull WindowContainerTransaction outWCT) {
|
|
if (!requestHasPipEnter(request)) {
|
|
throw new IllegalStateException("Called PiP augmentRequest when request has no PiP");
|
|
}
|
|
outWCT.setActivityWindowingMode(request.getTriggerTask().token, WINDOWING_MODE_UNDEFINED);
|
|
}
|
|
|
|
/**
|
|
* Cancel any ongoing PiP transitions/animations.
|
|
*/
|
|
public void cancelAnimations() {
|
|
if (mPipAnimationController.isAnimating()) {
|
|
mPipAnimationController.getCurrentAnimator().cancel();
|
|
mPipAnimationController.resetAnimatorState();
|
|
}
|
|
if (mCurrentAnimator != null) {
|
|
mCurrentAnimator.cancel();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void end() {
|
|
if (mCurrentAnimator != null) {
|
|
mCurrentAnimator.end();
|
|
}
|
|
}
|
|
|
|
private static class TvPipTransitionAnimatorUpdateListener implements
|
|
ValueAnimator.AnimatorUpdateListener {
|
|
private final SurfaceControl mLeash;
|
|
private final TvPipMenuController mTvPipMenuController;
|
|
private final SurfaceControl.Transaction mTransaction;
|
|
private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
|
|
private final RectF mTmpRectF = new RectF();
|
|
private final Rect mTmpRect = new Rect();
|
|
|
|
private float mStartAlpha = ALPHA_NO_CHANGE;
|
|
private float mEndAlpha = ALPHA_NO_CHANGE;
|
|
|
|
@Nullable
|
|
private Rect mStartBounds;
|
|
@Nullable
|
|
private Rect mEndBounds;
|
|
private Rect mWindowContainerBounds;
|
|
private boolean mShowMenu;
|
|
|
|
TvPipTransitionAnimatorUpdateListener(@NonNull SurfaceControl leash,
|
|
@NonNull TvPipMenuController tvPipMenuController,
|
|
@NonNull SurfaceControl.Transaction transaction,
|
|
@NonNull PipSurfaceTransactionHelper pipSurfaceTransactionHelper) {
|
|
mLeash = leash;
|
|
mTvPipMenuController = tvPipMenuController;
|
|
mTransaction = transaction;
|
|
mSurfaceTransactionHelper = pipSurfaceTransactionHelper;
|
|
}
|
|
|
|
public TvPipTransitionAnimatorUpdateListener animateAlpha(
|
|
@FloatRange(from = 0.0, to = 1.0) float startAlpha,
|
|
@FloatRange(from = 0.0, to = 1.0) float endAlpha) {
|
|
mStartAlpha = startAlpha;
|
|
mEndAlpha = endAlpha;
|
|
return this;
|
|
}
|
|
|
|
public TvPipTransitionAnimatorUpdateListener animateBounds(@NonNull Rect startBounds,
|
|
@NonNull Rect endBounds, @NonNull Rect windowContainerBounds) {
|
|
mStartBounds = startBounds;
|
|
mEndBounds = endBounds;
|
|
mWindowContainerBounds = windowContainerBounds;
|
|
return this;
|
|
}
|
|
|
|
public TvPipTransitionAnimatorUpdateListener atBounds(@NonNull Rect bounds) {
|
|
return animateBounds(bounds, bounds, bounds);
|
|
}
|
|
|
|
public TvPipTransitionAnimatorUpdateListener fadingOut() {
|
|
return animateAlpha(1f, 0f);
|
|
}
|
|
|
|
public TvPipTransitionAnimatorUpdateListener fadingIn() {
|
|
return animateAlpha(0f, 1f);
|
|
}
|
|
|
|
public TvPipTransitionAnimatorUpdateListener withMenu() {
|
|
mShowMenu = true;
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationUpdate(@NonNull ValueAnimator animation) {
|
|
final float fraction = animation.getAnimatedFraction();
|
|
final float alpha = lerp(mStartAlpha, mEndAlpha, fraction);
|
|
if (mStartBounds != null && mEndBounds != null) {
|
|
lerp(mStartBounds, mEndBounds, fraction, mTmpRectF);
|
|
applyAnimatedValue(alpha, mTmpRectF);
|
|
} else {
|
|
applyAnimatedValue(alpha, null);
|
|
}
|
|
}
|
|
|
|
private void applyAnimatedValue(float alpha, @Nullable RectF bounds) {
|
|
Trace.beginSection("applyAnimatedValue");
|
|
final SurfaceControl.Transaction tx = mTransaction;
|
|
|
|
Trace.beginSection("leash scale and alpha");
|
|
if (alpha != ALPHA_NO_CHANGE) {
|
|
mSurfaceTransactionHelper.alpha(tx, mLeash, alpha);
|
|
}
|
|
if (bounds != null) {
|
|
mSurfaceTransactionHelper.scale(tx, mLeash, mWindowContainerBounds, bounds);
|
|
}
|
|
mSurfaceTransactionHelper.shadow(tx, mLeash, false);
|
|
tx.show(mLeash);
|
|
Trace.endSection();
|
|
|
|
if (mShowMenu) {
|
|
Trace.beginSection("movePipMenu");
|
|
if (bounds != null) {
|
|
mTmpRect.set((int) bounds.left, (int) bounds.top, (int) bounds.right,
|
|
(int) bounds.bottom);
|
|
mTvPipMenuController.movePipMenu(tx, mTmpRect, alpha);
|
|
} else {
|
|
mTvPipMenuController.movePipMenu(tx, null, alpha);
|
|
}
|
|
Trace.endSection();
|
|
} else {
|
|
mTvPipMenuController.movePipMenu(tx, null, 0f);
|
|
}
|
|
|
|
tx.apply();
|
|
Trace.endSection();
|
|
}
|
|
|
|
private float lerp(float start, float end, float fraction) {
|
|
return start * (1 - fraction) + end * fraction;
|
|
}
|
|
|
|
private void lerp(@NonNull Rect start, @NonNull Rect end, float fraction,
|
|
@NonNull RectF out) {
|
|
out.set(
|
|
start.left * (1 - fraction) + end.left * fraction,
|
|
start.top * (1 - fraction) + end.top * fraction,
|
|
start.right * (1 - fraction) + end.right * fraction,
|
|
start.bottom * (1 - fraction) + end.bottom * fraction);
|
|
}
|
|
}
|
|
}
|