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:
mattsziklay
2023-03-02 14:57:03 -08:00
parent 15efb18c6b
commit 2c2cf3ae18
7 changed files with 332 additions and 20 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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.
*

View File

@@ -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;

View File

@@ -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 -> {

View File

@@ -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()