Visual indicator of transition to fullscreen
First step in providing animations for transition from freeform to fullscreen. Blue transparent indicator will appear behind a dragged task when ending the task in that position will result in a transition to fullscreen. Bug: 259278770 Bug: 266973675 Test: Manual, drag desktop mode tasks to taskbar, ensure visual indicator and animation both appear correctly. Test: atest WindowDecorationTests, DesktopModeControllerTest, DesktopModeWindowDecorViewModelTests Change-Id: Ia2308a61092262f719154e255d86c11fe5901123
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<shape android:shape="rectangle"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#bf309fb5" />
|
||||
<corners android:radius="20dp" />
|
||||
<stroke android:width="1dp" color="#A00080FF"/>
|
||||
</shape>
|
||||
@@ -659,13 +659,17 @@ public abstract class WMShellModule {
|
||||
Context context,
|
||||
ShellInit shellInit,
|
||||
ShellController shellController,
|
||||
DisplayController displayController,
|
||||
ShellTaskOrganizer shellTaskOrganizer,
|
||||
SyncTransactionQueue syncQueue,
|
||||
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
|
||||
Transitions transitions,
|
||||
@DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
|
||||
@ShellMainThread ShellExecutor mainExecutor
|
||||
) {
|
||||
return new DesktopTasksController(context, shellInit, shellController, shellTaskOrganizer,
|
||||
transitions, desktopModeTaskRepository, mainExecutor);
|
||||
return new DesktopTasksController(context, shellInit, shellController, displayController,
|
||||
shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, transitions,
|
||||
desktopModeTaskRepository, mainExecutor);
|
||||
}
|
||||
|
||||
@WMSingleton
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* 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 android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.RectEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.SurfaceControlViewHost;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowlessWindowManager;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.common.DisplayController;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
|
||||
/**
|
||||
* Animated visual indicator for Desktop Mode windowing transitions.
|
||||
*/
|
||||
public class DesktopModeVisualIndicator {
|
||||
|
||||
private final Context mContext;
|
||||
private final DisplayController mDisplayController;
|
||||
private final ShellTaskOrganizer mTaskOrganizer;
|
||||
private final RootTaskDisplayAreaOrganizer mRootTdaOrganizer;
|
||||
private final ActivityManager.RunningTaskInfo mTaskInfo;
|
||||
private final SurfaceControl mTaskSurface;
|
||||
private SurfaceControl mLeash;
|
||||
|
||||
private final SyncTransactionQueue mSyncQueue;
|
||||
private SurfaceControlViewHost mViewHost;
|
||||
|
||||
public DesktopModeVisualIndicator(SyncTransactionQueue syncQueue,
|
||||
ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController,
|
||||
Context context, SurfaceControl taskSurface, ShellTaskOrganizer taskOrganizer,
|
||||
RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer) {
|
||||
mSyncQueue = syncQueue;
|
||||
mTaskInfo = taskInfo;
|
||||
mDisplayController = displayController;
|
||||
mContext = context;
|
||||
mTaskSurface = taskSurface;
|
||||
mTaskOrganizer = taskOrganizer;
|
||||
mRootTdaOrganizer = taskDisplayAreaOrganizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and animate the indicator for the exit desktop mode transition.
|
||||
*/
|
||||
public void createFullscreenIndicator() {
|
||||
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
|
||||
final Resources resources = mContext.getResources();
|
||||
final DisplayMetrics metrics = resources.getDisplayMetrics();
|
||||
final int screenWidth = metrics.widthPixels;
|
||||
final int screenHeight = metrics.heightPixels;
|
||||
final int padding = mDisplayController
|
||||
.getDisplayLayout(mTaskInfo.displayId).stableInsets().top;
|
||||
final ImageView v = new ImageView(mContext);
|
||||
v.setImageResource(R.drawable.desktop_windowing_transition_background);
|
||||
final SurfaceControl.Builder builder = new SurfaceControl.Builder();
|
||||
mRootTdaOrganizer.attachToDisplayArea(mTaskInfo.displayId, builder);
|
||||
mLeash = builder
|
||||
.setName("Fullscreen Indicator")
|
||||
.setContainerLayer()
|
||||
.build();
|
||||
t.show(mLeash);
|
||||
final WindowManager.LayoutParams lp =
|
||||
new WindowManager.LayoutParams(screenWidth, screenHeight,
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
|
||||
lp.setTitle("Fullscreen indicator for Task=" + mTaskInfo.taskId);
|
||||
lp.setTrustedOverlay();
|
||||
final WindowlessWindowManager windowManager = new WindowlessWindowManager(
|
||||
mTaskInfo.configuration, mLeash,
|
||||
null /* hostInputToken */);
|
||||
mViewHost = new SurfaceControlViewHost(mContext,
|
||||
mDisplayController.getDisplay(mTaskInfo.displayId), windowManager,
|
||||
"FullscreenVisualIndicator");
|
||||
mViewHost.setView(v, lp);
|
||||
// We want this indicator to be behind the dragged task, but in front of all others.
|
||||
t.setRelativeLayer(mLeash, mTaskSurface, -1);
|
||||
|
||||
mSyncQueue.runInSync(transaction -> {
|
||||
transaction.merge(t);
|
||||
t.close();
|
||||
});
|
||||
final Rect startBounds = new Rect(padding, padding,
|
||||
screenWidth - padding, screenHeight - padding);
|
||||
final VisualIndicatorAnimator animator = VisualIndicatorAnimator.fullscreenIndicator(v,
|
||||
startBounds);
|
||||
animator.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the indicator and its components when it is no longer needed.
|
||||
*/
|
||||
public void releaseFullscreenIndicator() {
|
||||
if (mViewHost == null) return;
|
||||
if (mViewHost != null) {
|
||||
mViewHost.release();
|
||||
mViewHost = null;
|
||||
}
|
||||
|
||||
if (mLeash != null) {
|
||||
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
|
||||
t.remove(mLeash);
|
||||
mLeash = null;
|
||||
mSyncQueue.runInSync(transaction -> {
|
||||
transaction.merge(t);
|
||||
t.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Animator for Desktop Mode transitions which supports bounds and alpha animation.
|
||||
*/
|
||||
private static class VisualIndicatorAnimator extends ValueAnimator {
|
||||
private static final int FULLSCREEN_INDICATOR_DURATION = 200;
|
||||
private static final float SCALE_ADJUSTMENT_PERCENT = 0.015f;
|
||||
private static final float INDICATOR_FINAL_OPACITY = 0.7f;
|
||||
|
||||
private final ImageView mView;
|
||||
private final Rect mStartBounds;
|
||||
private final Rect mEndBounds;
|
||||
private final RectEvaluator mRectEvaluator;
|
||||
|
||||
private VisualIndicatorAnimator(ImageView view, Rect startBounds,
|
||||
Rect endBounds) {
|
||||
mView = view;
|
||||
mStartBounds = new Rect(startBounds);
|
||||
mEndBounds = endBounds;
|
||||
setFloatValues(0, 1);
|
||||
mRectEvaluator = new RectEvaluator(new Rect());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create animator for visual indicator of fullscreen transition
|
||||
*
|
||||
* @param view the view for this indicator
|
||||
* @param startBounds the starting bounds of the fullscreen indicator
|
||||
*/
|
||||
public static VisualIndicatorAnimator fullscreenIndicator(ImageView view,
|
||||
Rect startBounds) {
|
||||
view.getDrawable().setBounds(startBounds);
|
||||
int width = startBounds.width();
|
||||
int height = startBounds.height();
|
||||
Rect endBounds = new Rect((int) (startBounds.left - (SCALE_ADJUSTMENT_PERCENT * width)),
|
||||
(int) (startBounds.top - (SCALE_ADJUSTMENT_PERCENT * height)),
|
||||
(int) (startBounds.right + (SCALE_ADJUSTMENT_PERCENT * width)),
|
||||
(int) (startBounds.bottom + (SCALE_ADJUSTMENT_PERCENT * height)));
|
||||
VisualIndicatorAnimator animator = new VisualIndicatorAnimator(
|
||||
view, startBounds, endBounds);
|
||||
animator.setInterpolator(new DecelerateInterpolator());
|
||||
setupFullscreenIndicatorAnimation(animator);
|
||||
return animator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add necessary listener for animation of fullscreen indicator
|
||||
*/
|
||||
private static void setupFullscreenIndicatorAnimation(
|
||||
VisualIndicatorAnimator animator) {
|
||||
animator.addUpdateListener(a -> {
|
||||
if (animator.mView != null) {
|
||||
animator.updateBounds(a.getAnimatedFraction(), animator.mView);
|
||||
animator.updateIndicatorAlpha(a.getAnimatedFraction(), animator.mView);
|
||||
} else {
|
||||
animator.cancel();
|
||||
}
|
||||
});
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
animator.mView.getDrawable().setBounds(animator.mEndBounds);
|
||||
}
|
||||
});
|
||||
animator.setDuration(FULLSCREEN_INDICATOR_DURATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update bounds of view based on current animation fraction.
|
||||
* Use of delta is to animate bounds independently, in case we need to
|
||||
* run multiple animations simultaneously.
|
||||
*
|
||||
* @param fraction fraction to use, compared against previous fraction
|
||||
* @param view the view to update
|
||||
*/
|
||||
private void updateBounds(float fraction, ImageView view) {
|
||||
Rect currentBounds = mRectEvaluator.evaluate(fraction, mStartBounds, mEndBounds);
|
||||
view.getDrawable().setBounds(currentBounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fade in the fullscreen indicator
|
||||
*
|
||||
* @param fraction current animation fraction
|
||||
*/
|
||||
private void updateIndicatorAlpha(float fraction, View view) {
|
||||
view.setAlpha(fraction * INDICATOR_FINAL_OPACITY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.android.wm.shell.desktopmode
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.ActivityManager.RunningTaskInfo
|
||||
import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
|
||||
import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
|
||||
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
|
||||
@@ -37,11 +38,14 @@ import android.window.WindowContainerToken
|
||||
import android.window.WindowContainerTransaction
|
||||
import androidx.annotation.BinderThread
|
||||
import com.android.internal.protolog.common.ProtoLog
|
||||
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
|
||||
import com.android.wm.shell.ShellTaskOrganizer
|
||||
import com.android.wm.shell.common.DisplayController
|
||||
import com.android.wm.shell.common.ExecutorUtils
|
||||
import com.android.wm.shell.common.ExternalInterfaceBinder
|
||||
import com.android.wm.shell.common.RemoteCallable
|
||||
import com.android.wm.shell.common.ShellExecutor
|
||||
import com.android.wm.shell.common.SyncTransactionQueue
|
||||
import com.android.wm.shell.common.annotations.ExternalThread
|
||||
import com.android.wm.shell.common.annotations.ShellMainThread
|
||||
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener
|
||||
@@ -55,16 +59,20 @@ import java.util.function.Consumer
|
||||
|
||||
/** Handles moving tasks in and out of desktop */
|
||||
class DesktopTasksController(
|
||||
private val context: Context,
|
||||
shellInit: ShellInit,
|
||||
private val shellController: ShellController,
|
||||
private val shellTaskOrganizer: ShellTaskOrganizer,
|
||||
private val transitions: Transitions,
|
||||
private val desktopModeTaskRepository: DesktopModeTaskRepository,
|
||||
@ShellMainThread private val mainExecutor: ShellExecutor
|
||||
private val context: Context,
|
||||
shellInit: ShellInit,
|
||||
private val shellController: ShellController,
|
||||
private val displayController: DisplayController,
|
||||
private val shellTaskOrganizer: ShellTaskOrganizer,
|
||||
private val syncQueue: SyncTransactionQueue,
|
||||
private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
|
||||
private val transitions: Transitions,
|
||||
private val desktopModeTaskRepository: DesktopModeTaskRepository,
|
||||
@ShellMainThread private val mainExecutor: ShellExecutor
|
||||
) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler {
|
||||
|
||||
private val desktopMode: DesktopModeImpl
|
||||
private var visualIndicator: DesktopModeVisualIndicator? = null
|
||||
|
||||
init {
|
||||
desktopMode = DesktopModeImpl()
|
||||
@@ -297,6 +305,52 @@ class DesktopTasksController(
|
||||
return desktopMode
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform checks required on drag move. Create/release fullscreen indicator as needed.
|
||||
*
|
||||
* @param taskInfo the task being dragged.
|
||||
* @param taskSurface SurfaceControl of dragged task.
|
||||
* @param y coordinate of dragged task. Used for checks against status bar height.
|
||||
*/
|
||||
fun onDragPositioningMove(
|
||||
taskInfo: RunningTaskInfo,
|
||||
taskSurface: SurfaceControl,
|
||||
y: Float
|
||||
) {
|
||||
val statusBarHeight = displayController
|
||||
.getDisplayLayout(taskInfo.displayId)?.stableInsets()?.top ?: 0
|
||||
if (taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) {
|
||||
if (y <= statusBarHeight && visualIndicator == null) {
|
||||
visualIndicator = DesktopModeVisualIndicator(syncQueue, taskInfo,
|
||||
displayController, context, taskSurface, shellTaskOrganizer,
|
||||
rootTaskDisplayAreaOrganizer)
|
||||
visualIndicator?.createFullscreenIndicator()
|
||||
} else if (y > statusBarHeight && visualIndicator != null) {
|
||||
visualIndicator?.releaseFullscreenIndicator()
|
||||
visualIndicator = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform checks required on drag end. Move to fullscreen if drag ends in status bar area.
|
||||
*
|
||||
* @param taskInfo the task being dragged.
|
||||
* @param y height of drag, to be checked against status bar height.
|
||||
*/
|
||||
fun onDragPositioningEnd(
|
||||
taskInfo: RunningTaskInfo,
|
||||
y: Float
|
||||
) {
|
||||
val statusBarHeight = displayController
|
||||
.getDisplayLayout(taskInfo.displayId)?.stableInsets()?.top ?: 0
|
||||
if (y <= statusBarHeight && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) {
|
||||
moveToFullscreen(taskInfo.taskId)
|
||||
visualIndicator?.releaseFullscreenIndicator()
|
||||
visualIndicator = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to find out about changes in the visibility of freeform tasks.
|
||||
*
|
||||
|
||||
@@ -302,7 +302,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
|
||||
return false;
|
||||
}
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
final DesktopModeWindowDecoration decoration =
|
||||
mWindowDecorByTaskId.get(mTaskId);
|
||||
final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
|
||||
mDesktopTasksController.ifPresent(c -> c.onDragPositioningMove(taskInfo,
|
||||
decoration.mTaskSurface, e.getRawY(dragPointerIdx)));
|
||||
mDragPositioningCallback.onDragPositioningMove(
|
||||
e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
|
||||
mIsDragging = true;
|
||||
@@ -311,18 +315,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL: {
|
||||
final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
|
||||
final int statusBarHeight = mDisplayController
|
||||
.getDisplayLayout(taskInfo.displayId).stableInsets().top;
|
||||
mDragPositioningCallback.onDragPositioningEnd(
|
||||
e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
|
||||
if (e.getRawY(dragPointerIdx) <= statusBarHeight) {
|
||||
if (DesktopModeStatus.isProto2Enabled()
|
||||
&& taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
|
||||
// Switch a single task to fullscreen
|
||||
mDesktopTasksController.ifPresent(
|
||||
c -> c.moveToFullscreen(taskInfo));
|
||||
}
|
||||
}
|
||||
mDesktopTasksController.ifPresent(c -> c.onDragPositioningEnd(taskInfo,
|
||||
e.getRawY(dragPointerIdx)));
|
||||
final boolean wasDragging = mIsDragging;
|
||||
mIsDragging = false;
|
||||
return wasDragging;
|
||||
|
||||
@@ -375,7 +375,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
|
||||
- mResult.mDecorContainerOffsetX;
|
||||
final int y = mRelayoutParams.mCaptionY - mResult.mDecorContainerOffsetY;
|
||||
mHandleMenuPosition.set(x, y);
|
||||
String namePrefix = "Caption Menu";
|
||||
final String namePrefix = "Caption Menu";
|
||||
mHandleMenu = addWindow(R.layout.desktop_mode_decor_handle_menu, namePrefix, t, x, y,
|
||||
menuWidth, menuHeight, shadowRadius, cornerRadius);
|
||||
mSyncQueue.runInSync(transaction -> {
|
||||
|
||||
@@ -37,11 +37,14 @@ import androidx.test.filters.SmallTest
|
||||
import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
|
||||
import com.android.dx.mockito.inline.extended.ExtendedMockito.never
|
||||
import com.android.dx.mockito.inline.extended.StaticMockitoSession
|
||||
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
|
||||
import com.android.wm.shell.ShellTaskOrganizer
|
||||
import com.android.wm.shell.ShellTestCase
|
||||
import com.android.wm.shell.TestRunningTaskInfoBuilder
|
||||
import com.android.wm.shell.TestShellExecutor
|
||||
import com.android.wm.shell.common.DisplayController
|
||||
import com.android.wm.shell.common.ShellExecutor
|
||||
import com.android.wm.shell.common.SyncTransactionQueue
|
||||
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
|
||||
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask
|
||||
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createHomeTask
|
||||
@@ -73,7 +76,10 @@ class DesktopTasksControllerTest : ShellTestCase() {
|
||||
|
||||
@Mock lateinit var testExecutor: ShellExecutor
|
||||
@Mock lateinit var shellController: ShellController
|
||||
@Mock lateinit var displayController: DisplayController
|
||||
@Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer
|
||||
@Mock lateinit var syncQueue: SyncTransactionQueue
|
||||
@Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
|
||||
@Mock lateinit var transitions: Transitions
|
||||
|
||||
lateinit var mockitoSession: StaticMockitoSession
|
||||
@@ -105,7 +111,10 @@ class DesktopTasksControllerTest : ShellTestCase() {
|
||||
context,
|
||||
shellInit,
|
||||
shellController,
|
||||
displayController,
|
||||
shellTaskOrganizer,
|
||||
syncQueue,
|
||||
rootTaskDisplayAreaOrganizer,
|
||||
transitions,
|
||||
desktopModeTaskRepository,
|
||||
TestShellExecutor()
|
||||
|
||||
Reference in New Issue
Block a user