Animation For Fullscreen -> Freeform Transition
When dragged below the status bar, the task transitions to 40% of its original size. If the task is released below the status bar, it will use shell transitions to animate from 40 to 60 percent. Bug: 266973706 Test: Manual Testing Change-Id: Ia3237db751192816dded46c8898a9b383703c993
This commit is contained in:
committed by
mattsziklay
parent
165e3db6ff
commit
f694bd83db
@@ -54,6 +54,7 @@ import com.android.wm.shell.desktopmode.DesktopModeController;
|
||||
import com.android.wm.shell.desktopmode.DesktopModeStatus;
|
||||
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
|
||||
import com.android.wm.shell.desktopmode.DesktopTasksController;
|
||||
import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler;
|
||||
import com.android.wm.shell.draganddrop.DragAndDropController;
|
||||
import com.android.wm.shell.freeform.FreeformComponents;
|
||||
import com.android.wm.shell.freeform.FreeformTaskListener;
|
||||
@@ -676,12 +677,20 @@ public abstract class WMShellModule {
|
||||
SyncTransactionQueue syncQueue,
|
||||
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
|
||||
Transitions transitions,
|
||||
EnterDesktopTaskTransitionHandler transitionHandler,
|
||||
@DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
|
||||
@ShellMainThread ShellExecutor mainExecutor
|
||||
) {
|
||||
return new DesktopTasksController(context, shellInit, shellController, displayController,
|
||||
shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, transitions,
|
||||
desktopModeTaskRepository, mainExecutor);
|
||||
transitionHandler, desktopModeTaskRepository, mainExecutor);
|
||||
}
|
||||
|
||||
@WMSingleton
|
||||
@Provides
|
||||
static EnterDesktopTaskTransitionHandler provideEnterDesktopModeTaskTransitionHandler(
|
||||
Transitions transitions) {
|
||||
return new EnterDesktopTaskTransitionHandler(transitions);
|
||||
}
|
||||
|
||||
@WMSingleton
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
|
||||
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
|
||||
import android.app.WindowConfiguration.WindowingMode
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.os.IBinder
|
||||
import android.os.SystemProperties
|
||||
import android.view.SurfaceControl
|
||||
@@ -67,6 +68,7 @@ class DesktopTasksController(
|
||||
private val syncQueue: SyncTransactionQueue,
|
||||
private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
|
||||
private val transitions: Transitions,
|
||||
private val animationTransitionHandler: EnterDesktopTaskTransitionHandler,
|
||||
private val desktopModeTaskRepository: DesktopModeTaskRepository,
|
||||
@ShellMainThread private val mainExecutor: ShellExecutor
|
||||
) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler {
|
||||
@@ -133,6 +135,44 @@ class DesktopTasksController(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a single task to freeform and sets the taskBounds to the passed in bounds,
|
||||
* startBounds
|
||||
*/
|
||||
fun moveToFreeform(
|
||||
taskInfo: RunningTaskInfo,
|
||||
startBounds: Rect
|
||||
) {
|
||||
val wct = WindowContainerTransaction()
|
||||
moveHomeTaskToFront(wct)
|
||||
addMoveToDesktopChanges(wct, taskInfo.getToken())
|
||||
wct.setBounds(taskInfo.token, startBounds)
|
||||
|
||||
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
|
||||
animationTransitionHandler.startTransition(
|
||||
Transitions.TRANSIT_ENTER_FREEFORM, wct)
|
||||
} else {
|
||||
shellTaskOrganizer.applyTransaction(wct)
|
||||
}
|
||||
}
|
||||
|
||||
/** Brings apps to front and sets freeform task bounds */
|
||||
fun moveToDesktopWithAnimation(
|
||||
taskInfo: RunningTaskInfo,
|
||||
freeformBounds: Rect
|
||||
) {
|
||||
val wct = WindowContainerTransaction()
|
||||
bringDesktopAppsToFront(wct)
|
||||
addMoveToDesktopChanges(wct, taskInfo.getToken())
|
||||
wct.setBounds(taskInfo.token, freeformBounds)
|
||||
|
||||
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
|
||||
animationTransitionHandler.startTransition(Transitions.TRANSIT_ENTER_DESKTOP_MODE, wct)
|
||||
} else {
|
||||
shellTaskOrganizer.applyTransaction(wct)
|
||||
}
|
||||
}
|
||||
|
||||
/** Move a task with given `taskId` to fullscreen */
|
||||
fun moveToFullscreen(taskId: Int) {
|
||||
shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToFullscreen(task) }
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.desktopmode;
|
||||
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.app.ActivityManager;
|
||||
import android.graphics.Rect;
|
||||
import android.os.IBinder;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.WindowManager;
|
||||
import android.window.TransitionInfo;
|
||||
import android.window.TransitionRequestInfo;
|
||||
import android.window.WindowContainerTransaction;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.wm.shell.transition.Transitions;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* The {@link Transitions.TransitionHandler} that handles transitions for desktop mode tasks
|
||||
* entering and exiting freeform.
|
||||
*/
|
||||
public class EnterDesktopTaskTransitionHandler implements Transitions.TransitionHandler {
|
||||
|
||||
private final Transitions mTransitions;
|
||||
private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
|
||||
|
||||
// The size of the screen during drag relative to the fullscreen size
|
||||
public static final float DRAG_FREEFORM_SCALE = 0.4f;
|
||||
// The size of the screen after drag relative to the fullscreen size
|
||||
public static final float FINAL_FREEFORM_SCALE = 0.6f;
|
||||
public static final int FREEFORM_ANIMATION_DURATION = 336;
|
||||
|
||||
private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
|
||||
|
||||
public EnterDesktopTaskTransitionHandler(
|
||||
Transitions transitions) {
|
||||
this(transitions, SurfaceControl.Transaction::new);
|
||||
}
|
||||
|
||||
public EnterDesktopTaskTransitionHandler(
|
||||
Transitions transitions,
|
||||
Supplier<SurfaceControl.Transaction> supplier) {
|
||||
mTransitions = transitions;
|
||||
mTransactionSupplier = supplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts Transition of a given type
|
||||
* @param type Transition type
|
||||
* @param wct WindowContainerTransaction for transition
|
||||
*/
|
||||
public void startTransition(@WindowManager.TransitionType int type,
|
||||
@NonNull WindowContainerTransaction wct) {
|
||||
final IBinder token = mTransitions.startTransition(type, wct, this);
|
||||
mPendingTransitionTokens.add(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
|
||||
@NonNull SurfaceControl.Transaction startT,
|
||||
@NonNull SurfaceControl.Transaction finishT,
|
||||
@NonNull Transitions.TransitionFinishCallback finishCallback) {
|
||||
boolean transitionHandled = false;
|
||||
for (TransitionInfo.Change change : info.getChanges()) {
|
||||
if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
|
||||
if (taskInfo == null || taskInfo.taskId == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (change.getMode() == WindowManager.TRANSIT_CHANGE) {
|
||||
transitionHandled |= startChangeTransition(
|
||||
transition, info.getType(), change, startT, finishCallback);
|
||||
}
|
||||
}
|
||||
|
||||
mPendingTransitionTokens.remove(transition);
|
||||
|
||||
return transitionHandled;
|
||||
}
|
||||
|
||||
private boolean startChangeTransition(
|
||||
@NonNull IBinder transition,
|
||||
@WindowManager.TransitionType int type,
|
||||
@NonNull TransitionInfo.Change change,
|
||||
@NonNull SurfaceControl.Transaction startT,
|
||||
@NonNull Transitions.TransitionFinishCallback finishCallback) {
|
||||
if (!mPendingTransitionTokens.contains(transition)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
|
||||
if (type == Transitions.TRANSIT_ENTER_FREEFORM
|
||||
&& taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
|
||||
// Transitioning to freeform but keeping fullscreen bounds, so the crop is set
|
||||
// to null and we don't require an animation
|
||||
final SurfaceControl sc = change.getLeash();
|
||||
startT.setWindowCrop(sc, null);
|
||||
startT.apply();
|
||||
mTransitions.getMainExecutor().execute(
|
||||
() -> finishCallback.onTransitionFinished(null, null));
|
||||
return true;
|
||||
}
|
||||
|
||||
Rect endBounds = change.getEndAbsBounds();
|
||||
if (type == Transitions.TRANSIT_ENTER_DESKTOP_MODE
|
||||
&& taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
|
||||
&& !endBounds.isEmpty()) {
|
||||
// This Transition animates a task to freeform bounds after being dragged into freeform
|
||||
// mode and brings the remaining freeform tasks to front
|
||||
final SurfaceControl sc = change.getLeash();
|
||||
startT.setWindowCrop(sc, endBounds.width(),
|
||||
endBounds.height());
|
||||
startT.apply();
|
||||
|
||||
// We want to find the scale of the current bounds relative to the end bounds. The
|
||||
// task is currently scaled to DRAG_FREEFORM_SCALE and the final bounds will be
|
||||
// scaled to FINAL_FREEFORM_SCALE. So, it is scaled to
|
||||
// DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE relative to the freeform bounds
|
||||
final ValueAnimator animator =
|
||||
ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE, 1f);
|
||||
animator.setDuration(FREEFORM_ANIMATION_DURATION);
|
||||
final SurfaceControl.Transaction t = mTransactionSupplier.get();
|
||||
animator.addUpdateListener(animation -> {
|
||||
final float animationValue = (float) animation.getAnimatedValue();
|
||||
t.setScale(sc, animationValue, animationValue);
|
||||
|
||||
final float animationWidth = endBounds.width() * animationValue;
|
||||
final float animationHeight = endBounds.height() * animationValue;
|
||||
final int animationX = endBounds.centerX() - (int) (animationWidth / 2);
|
||||
final int animationY = endBounds.centerY() - (int) (animationHeight / 2);
|
||||
|
||||
t.setPosition(sc, animationX, animationY);
|
||||
t.apply();
|
||||
});
|
||||
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
mTransitions.getMainExecutor().execute(
|
||||
() -> finishCallback.onTransitionFinished(null, null));
|
||||
}
|
||||
});
|
||||
|
||||
animator.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
|
||||
@NonNull TransitionRequestInfo request) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -112,6 +112,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs
|
||||
onChangeTransitionReady(change, startT, finishT);
|
||||
break;
|
||||
}
|
||||
mWindowDecorViewModel.onTransitionReady(transition, info, change);
|
||||
}
|
||||
mTransitionToTaskInfo.put(transition, taskInfoList);
|
||||
}
|
||||
@@ -168,6 +169,8 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs
|
||||
} else {
|
||||
mTransitionToTaskInfo.put(playing, infoOfMerged);
|
||||
}
|
||||
|
||||
mWindowDecorViewModel.onTransitionMerged(merged, playing);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -175,7 +178,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs
|
||||
final List<ActivityManager.RunningTaskInfo> taskInfo =
|
||||
mTransitionToTaskInfo.getOrDefault(transition, Collections.emptyList());
|
||||
mTransitionToTaskInfo.remove(transition);
|
||||
|
||||
mWindowDecorViewModel.onTransitionFinished(transition);
|
||||
for (int i = 0; i < taskInfo.size(); ++i) {
|
||||
mWindowDecorViewModel.destroyWindowDecoration(taskInfo.get(i));
|
||||
}
|
||||
|
||||
@@ -133,6 +133,12 @@ public class Transitions implements RemoteCallable<Transitions> {
|
||||
/** Transition type for maximize to freeform transition. */
|
||||
public static final int TRANSIT_RESTORE_FROM_MAXIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 9;
|
||||
|
||||
/** Transition type to freeform in desktop mode. */
|
||||
public static final int TRANSIT_ENTER_FREEFORM = WindowManager.TRANSIT_FIRST_CUSTOM + 10;
|
||||
|
||||
/** Transition type to freeform in desktop mode. */
|
||||
public static final int TRANSIT_ENTER_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 11;
|
||||
|
||||
private final WindowOrganizer mOrganizer;
|
||||
private final Context mContext;
|
||||
private final ShellExecutor mMainExecutor;
|
||||
|
||||
@@ -23,11 +23,13 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
|
||||
import android.app.ActivityManager.RunningTaskInfo;
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.util.SparseArray;
|
||||
import android.view.Choreographer;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.View;
|
||||
import android.window.TransitionInfo;
|
||||
import android.window.WindowContainerToken;
|
||||
import android.window.WindowContainerTransaction;
|
||||
|
||||
@@ -71,6 +73,16 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransitionReady(IBinder transition, TransitionInfo info,
|
||||
TransitionInfo.Change change) {}
|
||||
|
||||
@Override
|
||||
public void onTransitionMerged(IBinder merged, IBinder playing) {}
|
||||
|
||||
@Override
|
||||
public void onTransitionFinished(IBinder transition) {}
|
||||
|
||||
@Override
|
||||
public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) {
|
||||
mTaskOperations = new TaskOperations(transitionStarter, mContext, mSyncQueue);
|
||||
|
||||
@@ -22,7 +22,11 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
|
||||
|
||||
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
|
||||
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
|
||||
import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.DRAG_FREEFORM_SCALE;
|
||||
import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FINAL_FREEFORM_SCALE;
|
||||
import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FREEFORM_ANIMATION_DURATION;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityManager.RunningTaskInfo;
|
||||
import android.app.ActivityTaskManager;
|
||||
@@ -30,6 +34,7 @@ import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.util.SparseArray;
|
||||
import android.view.Choreographer;
|
||||
@@ -39,9 +44,13 @@ import android.view.InputEventReceiver;
|
||||
import android.view.InputMonitor;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.SurfaceControl.Transaction;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.window.TransitionInfo;
|
||||
import android.window.WindowContainerToken;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
@@ -54,8 +63,10 @@ import com.android.wm.shell.desktopmode.DesktopModeStatus;
|
||||
import com.android.wm.shell.desktopmode.DesktopTasksController;
|
||||
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
|
||||
import com.android.wm.shell.splitscreen.SplitScreenController;
|
||||
import com.android.wm.shell.transition.Transitions;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* View model for the window decoration with a caption and shadows. Works with
|
||||
@@ -83,9 +94,20 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
|
||||
private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl();
|
||||
private final InputMonitorFactory mInputMonitorFactory;
|
||||
private TaskOperations mTaskOperations;
|
||||
private final Supplier<SurfaceControl.Transaction> mTransactionFactory;
|
||||
|
||||
private Optional<SplitScreenController> mSplitScreenController;
|
||||
|
||||
private ValueAnimator mDragToDesktopValueAnimator;
|
||||
private final Rect mDragToDesktopAnimationStartBounds = new Rect();
|
||||
private boolean mDragToDesktopAnimationStarted;
|
||||
private float mCaptionDragStartX;
|
||||
|
||||
// These values keep track of any transitions to freeform to stop relayout from running on
|
||||
// changing task so that shellTransitions has a chance to animate the transition
|
||||
private int mPauseRelayoutForTask = -1;
|
||||
private IBinder mTransitionPausingRelayout;
|
||||
|
||||
public DesktopModeWindowDecorViewModel(
|
||||
Context context,
|
||||
Handler mainHandler,
|
||||
@@ -107,7 +129,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
|
||||
desktopTasksController,
|
||||
splitScreenController,
|
||||
new DesktopModeWindowDecoration.Factory(),
|
||||
new InputMonitorFactory());
|
||||
new InputMonitorFactory(),
|
||||
SurfaceControl.Transaction::new);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -122,7 +145,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
|
||||
Optional<DesktopTasksController> desktopTasksController,
|
||||
Optional<SplitScreenController> splitScreenController,
|
||||
DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory,
|
||||
InputMonitorFactory inputMonitorFactory) {
|
||||
InputMonitorFactory inputMonitorFactory,
|
||||
Supplier<SurfaceControl.Transaction> transactionFactory) {
|
||||
mContext = context;
|
||||
mMainHandler = mainHandler;
|
||||
mMainChoreographer = mainChoreographer;
|
||||
@@ -136,6 +160,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
|
||||
|
||||
mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory;
|
||||
mInputMonitorFactory = inputMonitorFactory;
|
||||
mTransactionFactory = transactionFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -154,6 +179,31 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransitionReady(
|
||||
@NonNull IBinder transition,
|
||||
@NonNull TransitionInfo info,
|
||||
@NonNull TransitionInfo.Change change) {
|
||||
if (change.getMode() == WindowManager.TRANSIT_CHANGE
|
||||
&& info.getType() == Transitions.TRANSIT_ENTER_DESKTOP_MODE) {
|
||||
mTransitionPausingRelayout = transition;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {
|
||||
if (mTransitionPausingRelayout.equals(merged)) {
|
||||
mTransitionPausingRelayout = playing;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransitionFinished(@NonNull IBinder transition) {
|
||||
if (transition.equals(mTransitionPausingRelayout)) {
|
||||
mPauseRelayoutForTask = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
|
||||
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
|
||||
@@ -165,7 +215,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
|
||||
incrementEventReceiverTasks(taskInfo.displayId);
|
||||
}
|
||||
|
||||
decoration.relayout(taskInfo);
|
||||
// TaskListener callbacks and shell transitions aren't synchronized, so starting a shell
|
||||
// transition can trigger an onTaskInfoChanged call that updates the task's SurfaceControl
|
||||
// and interferes with the transition animation that is playing at the same time.
|
||||
if (taskInfo.taskId != mPauseRelayoutForTask) {
|
||||
decoration.relayout(taskInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -295,7 +350,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
|
||||
case MotionEvent.ACTION_DOWN: {
|
||||
mDragPointerId = e.getPointerId(0);
|
||||
mDragPositioningCallback.onDragPositioningStart(
|
||||
0 /* ctrlType */, e.getRawX(0), e.getRawY(0));
|
||||
0 /* ctrlType */, e.getRawX(0),
|
||||
e.getRawY(0));
|
||||
mIsDragging = false;
|
||||
return false;
|
||||
}
|
||||
@@ -403,7 +459,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
|
||||
final DesktopModeWindowDecoration relevantDecor = getRelevantWindowDecor(ev);
|
||||
if (DesktopModeStatus.isProto2Enabled()) {
|
||||
if (relevantDecor == null
|
||||
|| relevantDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM) {
|
||||
|| relevantDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM
|
||||
|| mTransitionDragActive) {
|
||||
handleCaptionThroughStatusBar(ev, relevantDecor);
|
||||
}
|
||||
}
|
||||
@@ -444,8 +501,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
|
||||
DesktopModeWindowDecoration relevantDecor) {
|
||||
switch (ev.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN: {
|
||||
mCaptionDragStartX = ev.getX();
|
||||
// Begin drag through status bar if applicable.
|
||||
if (relevantDecor != null) {
|
||||
mDragToDesktopAnimationStartBounds.set(
|
||||
relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds());
|
||||
boolean dragFromStatusBarAllowed = false;
|
||||
if (DesktopModeStatus.isProto2Enabled()) {
|
||||
// In proto2 any full screen task can be dragged to freeform
|
||||
@@ -461,33 +521,105 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
|
||||
}
|
||||
case MotionEvent.ACTION_UP: {
|
||||
if (relevantDecor == null) {
|
||||
mDragToDesktopAnimationStarted = false;
|
||||
mTransitionDragActive = false;
|
||||
return;
|
||||
}
|
||||
if (mTransitionDragActive) {
|
||||
mTransitionDragActive = false;
|
||||
final int statusBarHeight = mDisplayController
|
||||
.getDisplayLayout(relevantDecor.mTaskInfo.displayId).stableInsets().top;
|
||||
final int statusBarHeight = getStatusBarHeight(
|
||||
relevantDecor.mTaskInfo.displayId);
|
||||
if (ev.getY() > statusBarHeight) {
|
||||
if (DesktopModeStatus.isProto2Enabled()) {
|
||||
mPauseRelayoutForTask = relevantDecor.mTaskInfo.taskId;
|
||||
mDesktopTasksController.ifPresent(
|
||||
c -> c.moveToDesktop(relevantDecor.mTaskInfo));
|
||||
c -> c.moveToDesktopWithAnimation(relevantDecor.mTaskInfo,
|
||||
getFreeformBounds(ev)));
|
||||
} else if (DesktopModeStatus.isProto1Enabled()) {
|
||||
mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true));
|
||||
}
|
||||
|
||||
mDragToDesktopAnimationStarted = false;
|
||||
return;
|
||||
} else if (mDragToDesktopAnimationStarted) {
|
||||
mDesktopTasksController.ifPresent(c ->
|
||||
c.moveToFullscreen(relevantDecor.mTaskInfo));
|
||||
mDragToDesktopAnimationStarted = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
relevantDecor.checkClickEvent(ev);
|
||||
break;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
if (relevantDecor == null) {
|
||||
return;
|
||||
}
|
||||
if (mTransitionDragActive) {
|
||||
final int statusBarHeight = mDisplayController
|
||||
.getDisplayLayout(
|
||||
relevantDecor.mTaskInfo.displayId).stableInsets().top;
|
||||
if (ev.getY() > statusBarHeight) {
|
||||
if (!mDragToDesktopAnimationStarted) {
|
||||
mDragToDesktopAnimationStarted = true;
|
||||
mDesktopTasksController.ifPresent(
|
||||
c -> c.moveToFreeform(relevantDecor.mTaskInfo,
|
||||
mDragToDesktopAnimationStartBounds));
|
||||
startAnimation(relevantDecor);
|
||||
}
|
||||
}
|
||||
if (mDragToDesktopAnimationStarted) {
|
||||
Transaction t = mTransactionFactory.get();
|
||||
float width = (float) mDragToDesktopValueAnimator.getAnimatedValue()
|
||||
* mDragToDesktopAnimationStartBounds.width();
|
||||
float x = ev.getX() - (width / 2);
|
||||
t.setPosition(relevantDecor.mTaskSurface, x, ev.getY());
|
||||
t.apply();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_CANCEL: {
|
||||
mTransitionDragActive = false;
|
||||
mDragToDesktopAnimationStarted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Rect getFreeformBounds(@NonNull MotionEvent ev) {
|
||||
final Rect endBounds = new Rect();
|
||||
final int finalWidth = (int) (FINAL_FREEFORM_SCALE
|
||||
* mDragToDesktopAnimationStartBounds.width());
|
||||
final int finalHeight = (int) (FINAL_FREEFORM_SCALE
|
||||
* mDragToDesktopAnimationStartBounds.height());
|
||||
|
||||
endBounds.left = mDragToDesktopAnimationStartBounds.centerX() - finalWidth / 2
|
||||
+ (int) (ev.getX() - mCaptionDragStartX);
|
||||
endBounds.right = endBounds.left + (int) (FINAL_FREEFORM_SCALE
|
||||
* mDragToDesktopAnimationStartBounds.width());
|
||||
endBounds.top = (int) (ev.getY()
|
||||
- ((FINAL_FREEFORM_SCALE - DRAG_FREEFORM_SCALE)
|
||||
* mDragToDesktopAnimationStartBounds.height() / 2));
|
||||
endBounds.bottom = endBounds.top + finalHeight;
|
||||
|
||||
return endBounds;
|
||||
}
|
||||
|
||||
private void startAnimation(@NonNull DesktopModeWindowDecoration focusedDecor) {
|
||||
mDragToDesktopValueAnimator = ValueAnimator.ofFloat(1f, DRAG_FREEFORM_SCALE);
|
||||
mDragToDesktopValueAnimator.setDuration(FREEFORM_ANIMATION_DURATION);
|
||||
final Transaction t = mTransactionFactory.get();
|
||||
mDragToDesktopValueAnimator.addUpdateListener(animation -> {
|
||||
final float animatorValue = (float) animation.getAnimatedValue();
|
||||
SurfaceControl sc = focusedDecor.mTaskSurface;
|
||||
t.setScale(sc, animatorValue, animatorValue);
|
||||
t.apply();
|
||||
});
|
||||
|
||||
mDragToDesktopValueAnimator.start();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private DesktopModeWindowDecoration getRelevantWindowDecor(MotionEvent ev) {
|
||||
if (mSplitScreenController.isPresent()
|
||||
@@ -534,6 +666,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
|
||||
return focusedDecor;
|
||||
}
|
||||
|
||||
private int getStatusBarHeight(int displayId) {
|
||||
return mDisplayController.getDisplayLayout(displayId).stableInsets().top;
|
||||
}
|
||||
|
||||
private void createInputChannel(int displayId) {
|
||||
final InputManager inputManager = mContext.getSystemService(InputManager.class);
|
||||
final InputMonitor inputMonitor =
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
package com.android.wm.shell.windowdecor;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.os.IBinder;
|
||||
import android.view.SurfaceControl;
|
||||
import android.window.TransitionInfo;
|
||||
|
||||
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
|
||||
|
||||
@@ -95,4 +97,34 @@ public interface WindowDecorViewModel {
|
||||
* @param taskInfo the info of the task
|
||||
*/
|
||||
void destroyWindowDecoration(ActivityManager.RunningTaskInfo taskInfo);
|
||||
|
||||
/**
|
||||
* Notifies that a shell transition is about to start. If the transition is of type
|
||||
* TRANSIT_ENTER_DESKTOP, it will save that transition to unpause relayout for the transitioning
|
||||
* task after the transition has ended.
|
||||
*
|
||||
* @param transition the ready transaction
|
||||
* @param info of Transition to check if relayout needs to be paused for a task
|
||||
* @param change a change in the given transition
|
||||
*/
|
||||
default void onTransitionReady(IBinder transition, TransitionInfo info,
|
||||
TransitionInfo.Change change) {}
|
||||
|
||||
/**
|
||||
* Notifies that a shell transition is about to merge with another to give the window
|
||||
* decoration a chance to prepare for this merge.
|
||||
*
|
||||
* @param merged the transaction being merged
|
||||
* @param playing the transaction being merged into
|
||||
*/
|
||||
default void onTransitionMerged(IBinder merged, IBinder playing) {}
|
||||
|
||||
/**
|
||||
* Notifies that a shell transition is about to finish to give the window decoration a chance
|
||||
* to clean up.
|
||||
*
|
||||
* @param transaction
|
||||
*/
|
||||
default void onTransitionFinished(IBinder transaction) {}
|
||||
|
||||
}
|
||||
@@ -81,6 +81,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
|
||||
@Mock lateinit var syncQueue: SyncTransactionQueue
|
||||
@Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
|
||||
@Mock lateinit var transitions: Transitions
|
||||
@Mock lateinit var transitionHandler: EnterDesktopTaskTransitionHandler
|
||||
|
||||
lateinit var mockitoSession: StaticMockitoSession
|
||||
lateinit var controller: DesktopTasksController
|
||||
@@ -116,6 +117,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
|
||||
syncQueue,
|
||||
rootTaskDisplayAreaOrganizer,
|
||||
transitions,
|
||||
transitionHandler,
|
||||
desktopModeTaskRepository,
|
||||
TestShellExecutor()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.desktopmode;
|
||||
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
|
||||
|
||||
import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.WindowConfiguration;
|
||||
import android.graphics.Rect;
|
||||
import android.os.IBinder;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.WindowManager;
|
||||
import android.window.IWindowContainerToken;
|
||||
import android.window.TransitionInfo;
|
||||
import android.window.WindowContainerToken;
|
||||
import android.window.WindowContainerTransaction;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.common.ShellExecutor;
|
||||
import com.android.wm.shell.transition.Transitions;
|
||||
|
||||
import junit.framework.AssertionFailedError;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** Tests of {@link com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler} */
|
||||
@SmallTest
|
||||
public class EnterDesktopTaskTransitionHandlerTest {
|
||||
|
||||
@Mock
|
||||
private Transitions mTransitions;
|
||||
@Mock
|
||||
IBinder mToken;
|
||||
@Mock
|
||||
Supplier<SurfaceControl.Transaction> mTransactionFactory;
|
||||
@Mock
|
||||
SurfaceControl.Transaction mStartT;
|
||||
@Mock
|
||||
SurfaceControl.Transaction mFinishT;
|
||||
@Mock
|
||||
SurfaceControl.Transaction mAnimationT;
|
||||
@Mock
|
||||
Transitions.TransitionFinishCallback mTransitionFinishCallback;
|
||||
@Mock
|
||||
ShellExecutor mExecutor;
|
||||
@Mock
|
||||
SurfaceControl mSurfaceControl;
|
||||
|
||||
private EnterDesktopTaskTransitionHandler mEnterDesktopTaskTransitionHandler;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
doReturn(mExecutor).when(mTransitions).getMainExecutor();
|
||||
doReturn(mAnimationT).when(mTransactionFactory).get();
|
||||
|
||||
mEnterDesktopTaskTransitionHandler = new EnterDesktopTaskTransitionHandler(mTransitions,
|
||||
mTransactionFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEnterFreeformAnimation() {
|
||||
final int transitionType = Transitions.TRANSIT_ENTER_FREEFORM;
|
||||
final int taskId = 1;
|
||||
WindowContainerTransaction wct = new WindowContainerTransaction();
|
||||
doReturn(mToken).when(mTransitions)
|
||||
.startTransition(transitionType, wct, mEnterDesktopTaskTransitionHandler);
|
||||
mEnterDesktopTaskTransitionHandler.startTransition(transitionType, wct);
|
||||
|
||||
TransitionInfo.Change change =
|
||||
createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FREEFORM);
|
||||
TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_ENTER_FREEFORM, change);
|
||||
|
||||
|
||||
assertTrue(mEnterDesktopTaskTransitionHandler
|
||||
.startAnimation(mToken, info, mStartT, mFinishT, mTransitionFinishCallback));
|
||||
|
||||
verify(mStartT).setWindowCrop(mSurfaceControl, null);
|
||||
verify(mStartT).apply();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTransitEnterDesktopModeAnimation() throws Throwable {
|
||||
final int transitionType = Transitions.TRANSIT_ENTER_DESKTOP_MODE;
|
||||
final int taskId = 1;
|
||||
WindowContainerTransaction wct = new WindowContainerTransaction();
|
||||
doReturn(mToken).when(mTransitions)
|
||||
.startTransition(transitionType, wct, mEnterDesktopTaskTransitionHandler);
|
||||
mEnterDesktopTaskTransitionHandler.startTransition(transitionType, wct);
|
||||
|
||||
TransitionInfo.Change change =
|
||||
createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FREEFORM);
|
||||
change.setEndAbsBounds(new Rect(0, 0, 1, 1));
|
||||
TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_ENTER_DESKTOP_MODE, change);
|
||||
|
||||
runOnUiThread(() -> {
|
||||
try {
|
||||
assertTrue(mEnterDesktopTaskTransitionHandler
|
||||
.startAnimation(mToken, info, mStartT, mFinishT,
|
||||
mTransitionFinishCallback));
|
||||
} catch (Exception e) {
|
||||
throw new AssertionFailedError(e.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
verify(mStartT).setWindowCrop(mSurfaceControl, change.getEndAbsBounds().width(),
|
||||
change.getEndAbsBounds().height());
|
||||
verify(mStartT).apply();
|
||||
}
|
||||
|
||||
private TransitionInfo.Change createChange(@WindowManager.TransitionType int type, int taskId,
|
||||
@WindowConfiguration.WindowingMode int windowingMode) {
|
||||
final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
|
||||
taskInfo.taskId = taskId;
|
||||
taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
|
||||
final TransitionInfo.Change change = new TransitionInfo.Change(
|
||||
new WindowContainerToken(mock(IWindowContainerToken.class)), mSurfaceControl);
|
||||
change.setMode(type);
|
||||
change.setTaskInfo(taskInfo);
|
||||
return change;
|
||||
}
|
||||
|
||||
private static TransitionInfo createTransitionInfo(
|
||||
@WindowManager.TransitionType int type, @NonNull TransitionInfo.Change change) {
|
||||
TransitionInfo info = new TransitionInfo(type, 0);
|
||||
info.addChange(change);
|
||||
return info;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.WindowConfiguration;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.hardware.display.VirtualDisplay;
|
||||
import android.hardware.input.InputManager;
|
||||
@@ -60,6 +61,7 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** Tests of {@link DesktopModeWindowDecorViewModel} */
|
||||
@SmallTest
|
||||
@@ -80,8 +82,9 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase {
|
||||
@Mock private DesktopTasksController mDesktopTasksController;
|
||||
@Mock private InputMonitor mInputMonitor;
|
||||
@Mock private InputManager mInputManager;
|
||||
|
||||
@Mock private DesktopModeWindowDecorViewModel.InputMonitorFactory mMockInputMonitorFactory;
|
||||
@Mock private Supplier<SurfaceControl.Transaction> mTransactionFactory;
|
||||
@Mock private SurfaceControl.Transaction mTransaction;
|
||||
private final List<InputManager> mMockInputManagers = new ArrayList<>();
|
||||
|
||||
private DesktopModeWindowDecorViewModel mDesktopModeWindowDecorViewModel;
|
||||
@@ -102,12 +105,14 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase {
|
||||
Optional.of(mDesktopTasksController),
|
||||
Optional.of(mSplitScreenController),
|
||||
mDesktopModeWindowDecorFactory,
|
||||
mMockInputMonitorFactory
|
||||
mMockInputMonitorFactory,
|
||||
mTransactionFactory
|
||||
);
|
||||
|
||||
doReturn(mDesktopModeWindowDecoration)
|
||||
.when(mDesktopModeWindowDecorFactory)
|
||||
.create(any(), any(), any(), any(), any(), any(), any(), any());
|
||||
doReturn(mTransaction).when(mTransactionFactory).get();
|
||||
|
||||
when(mMockInputMonitorFactory.create(any(), any())).thenReturn(mInputMonitor);
|
||||
// InputChannel cannot be mocked because it passes to InputEventReceiver.
|
||||
@@ -250,7 +255,7 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase {
|
||||
}
|
||||
|
||||
private static ActivityManager.RunningTaskInfo createTaskInfo(int taskId,
|
||||
int displayId, int windowingMode) {
|
||||
int displayId, @WindowConfiguration.WindowingMode int windowingMode) {
|
||||
ActivityManager.RunningTaskInfo taskInfo =
|
||||
new TestRunningTaskInfoBuilder()
|
||||
.setDisplayId(displayId)
|
||||
|
||||
Reference in New Issue
Block a user