Drag and drop to split transition

* DropZoneView represents a drop zone and handles
  animating the margins around it along with the
  splashscreen or highlight when the user drags into
  or out of the zone.
* DragLayout hosts two of these views and populates
  them with the relevant app info for the splashscreen
* While dragging the status bar is hidden

Test: manual/visual on large screen device:
 - have an app open and drag and drop an app to split
 - try this in portrait and landscape
 - try dragging between the two drop zones
 - try releasing not in a drop zone
 - have two apps already in split and try dragging a
   different app to split
 - have an app open and get a notif, drag it to split
 - verify that the status bar hides while dragging and shows
   again (and is interactable) after dragging is done
 - trigger the drag and drop UI, then switch theme and
   trigger again, it should match the new theme
=> verify that the animation looks correct / matches mocks
Test: atest WMShellUnitTests

Bug: 202017826
Change-Id: I806e8ff8ba30d01b9b47d12aa0987cec7aeb7d0c
This commit is contained in:
Mady Mellor
2021-10-20 14:46:20 -07:00
parent 99ab01f49b
commit 1c0aefd45b
14 changed files with 643 additions and 63 deletions

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@android:color/system_neutral1_500" android:lStar="35" />
</selector>

View File

@@ -56,6 +56,7 @@ import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
import com.android.wm.shell.displayareahelper.DisplayAreaHelperController;
import com.android.wm.shell.draganddrop.DragAndDrop;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.freeform.FreeformTaskListener;
import com.android.wm.shell.fullscreen.FullscreenTaskListener;
@@ -156,8 +157,16 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
static DragAndDropController provideDragAndDropController(Context context,
DisplayController displayController, UiEventLogger uiEventLogger) {
return new DragAndDropController(context, displayController, uiEventLogger);
DisplayController displayController, UiEventLogger uiEventLogger,
IconProvider iconProvider, @ShellMainThread ShellExecutor mainExecutor) {
return new DragAndDropController(context, displayController, uiEventLogger, iconProvider,
mainExecutor);
}
@WMSingleton
@Provides
static DragAndDrop provideDragAndDrop(DragAndDropController dragAndDropController) {
return dragAndDropController.asDragAndDrop();
}
@WMSingleton

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.wm.shell.draganddrop;
import android.content.res.Configuration;
import com.android.wm.shell.common.annotations.ExternalThread;
/**
* Interface for telling DragAndDrop stuff.
*/
@ExternalThread
public interface DragAndDrop {
/** Called when the theme changes. */
void onThemeChanged();
/** Called when the configuration changes. */
void onConfigChanged(Configuration newConfig);
}

View File

@@ -41,7 +41,6 @@ import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.DragEvent;
import android.view.LayoutInflater;
import android.view.SurfaceControl;
@@ -53,8 +52,10 @@ import android.widget.FrameLayout;
import com.android.internal.logging.InstanceId;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.protolog.common.ProtoLog;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -71,16 +72,26 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
private final Context mContext;
private final DisplayController mDisplayController;
private final DragAndDropEventLogger mLogger;
private final IconProvider mIconProvider;
private SplitScreenController mSplitScreen;
private ShellExecutor mMainExecutor;
private DragAndDropImpl mImpl;
private final SparseArray<PerDisplay> mDisplayDropTargets = new SparseArray<>();
private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
public DragAndDropController(Context context, DisplayController displayController,
UiEventLogger uiEventLogger) {
UiEventLogger uiEventLogger, IconProvider iconProvider, ShellExecutor mainExecutor) {
mContext = context;
mDisplayController = displayController;
mLogger = new DragAndDropEventLogger(uiEventLogger);
mIconProvider = iconProvider;
mMainExecutor = mainExecutor;
mImpl = new DragAndDropImpl();
}
public DragAndDrop asDragAndDrop() {
return mImpl;
}
public void initialize(Optional<SplitScreenController> splitscreen) {
@@ -117,7 +128,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
R.layout.global_drop_target, null);
rootView.setOnDragListener(this);
rootView.setVisibility(View.INVISIBLE);
DragLayout dragLayout = new DragLayout(context, mSplitScreen);
DragLayout dragLayout = new DragLayout(context, mSplitScreen, mIconProvider);
rootView.addView(dragLayout,
new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
try {
@@ -267,6 +278,18 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
return mimeTypes;
}
private void onThemeChange() {
for (int i = 0; i < mDisplayDropTargets.size(); i++) {
mDisplayDropTargets.get(i).dragLayout.onThemeChange();
}
}
private void onConfigChanged(Configuration newConfig) {
for (int i = 0; i < mDisplayDropTargets.size(); i++) {
mDisplayDropTargets.get(i).dragLayout.onConfigChanged(newConfig);
}
}
private static class PerDisplay {
final int displayId;
final Context context;
@@ -287,4 +310,21 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
dragLayout = dl;
}
}
private class DragAndDropImpl implements DragAndDrop {
@Override
public void onThemeChanged() {
mMainExecutor.execute(() -> {
DragAndDropController.this.onThemeChange();
});
}
@Override
public void onConfigChanged(Configuration newConfig) {
mMainExecutor.execute(() -> {
DragAndDropController.this.onConfigChanged(newConfig);
});
}
}
}

View File

@@ -16,78 +16,138 @@
package com.android.wm.shell.draganddrop;
import static com.android.wm.shell.animation.Interpolators.FAST_OUT_LINEAR_IN;
import static com.android.wm.shell.animation.Interpolators.FAST_OUT_SLOW_IN;
import static com.android.wm.shell.animation.Interpolators.LINEAR;
import static com.android.wm.shell.animation.Interpolators.LINEAR_OUT_SLOW_IN;
import static android.app.StatusBarManager.DISABLE_NONE;
import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.StatusBarManager;
import android.content.ClipData;
import android.content.Context;
import android.graphics.Canvas;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.RemoteException;
import android.view.DragEvent;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowInsets.Type;
import androidx.annotation.NonNull;
import android.widget.LinearLayout;
import com.android.internal.logging.InstanceId;
import com.android.internal.protolog.common.ProtoLog;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController;
import java.util.ArrayList;
import java.util.List;
/**
* Coordinates the visible drop targets for the current drag.
*/
public class DragLayout extends View {
public class DragLayout extends LinearLayout {
// While dragging the status bar is hidden.
private static final int HIDE_STATUS_BAR_FLAGS = StatusBarManager.DISABLE_NOTIFICATION_ICONS
| StatusBarManager.DISABLE_NOTIFICATION_ALERTS
| StatusBarManager.DISABLE_CLOCK
| StatusBarManager.DISABLE_SYSTEM_INFO;
private final DragAndDropPolicy mPolicy;
private final SplitScreenController mSplitScreenController;
private final IconProvider mIconProvider;
private final StatusBarManager mStatusBarManager;
private DragAndDropPolicy.Target mCurrentTarget = null;
private DropOutlineDrawable mDropOutline;
private DropZoneView mDropZoneView1;
private DropZoneView mDropZoneView2;
private int mDisplayMargin;
private Insets mInsets = Insets.NONE;
private boolean mIsShowing;
private boolean mHasDropped;
public DragLayout(Context context, SplitScreenController splitscreen) {
@SuppressLint("WrongConstant")
public DragLayout(Context context, SplitScreenController splitScreenController,
IconProvider iconProvider) {
super(context);
mPolicy = new DragAndDropPolicy(context, splitscreen);
mSplitScreenController = splitScreenController;
mIconProvider = iconProvider;
mPolicy = new DragAndDropPolicy(context, splitScreenController);
mStatusBarManager = context.getSystemService(StatusBarManager.class);
mDisplayMargin = context.getResources().getDimensionPixelSize(
R.dimen.drop_layout_display_margin);
mDropOutline = new DropOutlineDrawable(context);
setBackground(mDropOutline);
setWillNotDraw(false);
mDropZoneView1 = new DropZoneView(context);
mDropZoneView2 = new DropZoneView(context);
addView(mDropZoneView1, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
addView(mDropZoneView2, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
((LayoutParams) mDropZoneView1.getLayoutParams()).weight = 1;
((LayoutParams) mDropZoneView2.getLayoutParams()).weight = 1;
updateContainerMargins();
}
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
mInsets = insets.getInsets(Type.systemBars() | Type.displayCutout());
recomputeDropTargets();
final int orientation = getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
mDropZoneView1.setBottomInset(mInsets.bottom);
mDropZoneView2.setBottomInset(mInsets.bottom);
} else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
mDropZoneView1.setBottomInset(0);
mDropZoneView2.setBottomInset(mInsets.bottom);
}
return super.onApplyWindowInsets(insets);
}
@Override
protected boolean verifyDrawable(@NonNull Drawable who) {
return who == mDropOutline || super.verifyDrawable(who);
public void onThemeChange() {
mDropZoneView1.onThemeChange();
mDropZoneView2.onThemeChange();
}
@Override
protected void onDraw(Canvas canvas) {
if (mCurrentTarget != null) {
mDropOutline.draw(canvas);
public void onConfigChanged(Configuration newConfig) {
final int orientation = getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE
&& getOrientation() != HORIZONTAL) {
setOrientation(LinearLayout.HORIZONTAL);
updateContainerMargins();
} else if (orientation == Configuration.ORIENTATION_PORTRAIT
&& getOrientation() != VERTICAL) {
setOrientation(LinearLayout.VERTICAL);
updateContainerMargins();
}
}
private void updateContainerMargins() {
final int orientation = getResources().getConfiguration().orientation;
final float halfMargin = mDisplayMargin / 2f;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
mDropZoneView1.setContainerMargin(
mDisplayMargin, mDisplayMargin, halfMargin, mDisplayMargin);
mDropZoneView2.setContainerMargin(
halfMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin);
} else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
mDropZoneView1.setContainerMargin(
mDisplayMargin, mDisplayMargin, mDisplayMargin, halfMargin);
mDropZoneView2.setContainerMargin(
mDisplayMargin, halfMargin, mDisplayMargin, mDisplayMargin);
}
}
@@ -104,6 +164,43 @@ public class DragLayout extends View {
mPolicy.start(displayLayout, initialData, loggerSessionId);
mHasDropped = false;
mCurrentTarget = null;
List<ActivityManager.RunningTaskInfo> tasks = null;
// Figure out the splashscreen info for the existing task(s).
try {
tasks = ActivityTaskManager.getService().getTasks(2,
false /* filterOnlyVisibleRecents */,
false /* keepIntentExtra */);
} catch (RemoteException e) {
// don't show an icon / will just use the defaults
}
if (tasks != null && !tasks.isEmpty()) {
ActivityManager.RunningTaskInfo taskInfo1 = tasks.get(0);
Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo);
int bgColor1 = getResizingBackgroundColor(taskInfo1);
boolean alreadyInSplit = mSplitScreenController != null
&& mSplitScreenController.isSplitScreenVisible();
if (alreadyInSplit && tasks.size() > 1) {
ActivityManager.RunningTaskInfo taskInfo2 = tasks.get(1);
Drawable icon2 = mIconProvider.getIcon(taskInfo2.topActivityInfo);
int bgColor2 = getResizingBackgroundColor(taskInfo2);
// figure out which task is on which side
int splitPosition1 = mSplitScreenController.getSplitPosition(taskInfo1.taskId);
boolean isTask1TopOrLeft = splitPosition1 == SPLIT_POSITION_TOP_OR_LEFT;
if (isTask1TopOrLeft) {
mDropZoneView1.setAppInfo(bgColor1, icon1);
mDropZoneView2.setAppInfo(bgColor2, icon2);
} else {
mDropZoneView2.setAppInfo(bgColor1, icon1);
mDropZoneView1.setAppInfo(bgColor2, icon2);
}
} else {
mDropZoneView1.setAppInfo(bgColor1, icon1);
mDropZoneView2.setAppInfo(bgColor1, icon1);
}
}
}
public void show() {
@@ -139,20 +236,14 @@ public class DragLayout extends View {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Current target: %s", target);
if (target == null) {
// Animating to no target
mDropOutline.startVisibilityAnimation(false, LINEAR);
Rect finalBounds = new Rect(mCurrentTarget.drawRegion);
finalBounds.inset(mDisplayMargin, mDisplayMargin);
mDropOutline.startBoundsAnimation(finalBounds, FAST_OUT_LINEAR_IN);
animateSplitContainers(false, null /* animCompleteCallback */);
} else if (mCurrentTarget == null) {
// Animating to first target
mDropOutline.startVisibilityAnimation(true, LINEAR);
Rect initialBounds = new Rect(target.drawRegion);
initialBounds.inset(mDisplayMargin, mDisplayMargin);
mDropOutline.setRegionBounds(initialBounds);
mDropOutline.startBoundsAnimation(target.drawRegion, LINEAR_OUT_SLOW_IN);
animateSplitContainers(true, null /* animCompleteCallback */);
animateHighlight(target);
} else {
// Bounds change
mDropOutline.startBoundsAnimation(target.drawRegion, FAST_OUT_SLOW_IN);
// Switching between targets
animateHighlight(target);
}
mCurrentTarget = target;
}
@@ -163,26 +254,7 @@ public class DragLayout extends View {
*/
public void hide(DragEvent event, Runnable hideCompleteCallback) {
mIsShowing = false;
ObjectAnimator alphaAnimator = mDropOutline.startVisibilityAnimation(false, LINEAR);
ObjectAnimator boundsAnimator = null;
if (mCurrentTarget != null) {
Rect finalBounds = new Rect(mCurrentTarget.drawRegion);
finalBounds.inset(mDisplayMargin, mDisplayMargin);
boundsAnimator = mDropOutline.startBoundsAnimation(finalBounds, FAST_OUT_LINEAR_IN);
}
if (hideCompleteCallback != null) {
ObjectAnimator lastAnim = boundsAnimator != null
? boundsAnimator
: alphaAnimator;
lastAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
hideCompleteCallback.run();
}
});
}
animateSplitContainers(false, hideCompleteCallback);
mCurrentTarget = null;
}
@@ -201,4 +273,49 @@ public class DragLayout extends View {
hide(event, dropCompleteCallback);
return handledDrop;
}
private void animateSplitContainers(boolean visible, Runnable animCompleteCallback) {
mStatusBarManager.disable(visible
? HIDE_STATUS_BAR_FLAGS
: DISABLE_NONE);
mDropZoneView1.setShowingMargin(visible);
mDropZoneView2.setShowingMargin(visible);
ObjectAnimator animator = mDropZoneView1.getAnimator();
if (animCompleteCallback != null) {
if (animator != null) {
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animCompleteCallback.run();
}
});
} else {
// If there's no animator the animation is done so run immediately
animCompleteCallback.run();
}
}
}
private void animateHighlight(DragAndDropPolicy.Target target) {
if (target.type == DragAndDropPolicy.Target.TYPE_SPLIT_LEFT
|| target.type == DragAndDropPolicy.Target.TYPE_SPLIT_TOP) {
mDropZoneView1.setShowingHighlight(true);
mDropZoneView1.setShowingSplash(false);
mDropZoneView2.setShowingHighlight(false);
mDropZoneView2.setShowingSplash(true);
} else if (target.type == DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT
|| target.type == DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM) {
mDropZoneView1.setShowingHighlight(false);
mDropZoneView1.setShowingSplash(true);
mDropZoneView2.setShowingHighlight(true);
mDropZoneView2.setShowingSplash(false);
}
}
private static int getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) {
final int taskBgColor = taskInfo.taskDescription.getBackgroundColor();
return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb();
}
}

View File

@@ -0,0 +1,313 @@
/*
* 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.draganddrop;
import static com.android.wm.shell.animation.Interpolators.FAST_OUT_SLOW_IN;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Path;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.IntProperty;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.annotation.Nullable;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.wm.shell.R;
/**
* Renders a drop zone area for items being dragged.
*/
public class DropZoneView extends FrameLayout {
private static final int SPLASHSCREEN_ALPHA_INT = (int) (255 * 0.90f);
private static final int HIGHLIGHT_ALPHA_INT = 255;
private static final int MARGIN_ANIMATION_ENTER_DURATION = 400;
private static final int MARGIN_ANIMATION_EXIT_DURATION = 250;
private static final FloatProperty<DropZoneView> INSETS =
new FloatProperty<DropZoneView>("insets") {
@Override
public void setValue(DropZoneView v, float percent) {
v.setMarginPercent(percent);
}
@Override
public Float get(DropZoneView v) {
return v.getMarginPercent();
}
};
private static final IntProperty<ColorDrawable> SPLASHSCREEN_ALPHA =
new IntProperty<ColorDrawable>("splashscreen") {
@Override
public void setValue(ColorDrawable d, int alpha) {
d.setAlpha(alpha);
}
@Override
public Integer get(ColorDrawable d) {
return d.getAlpha();
}
};
private static final IntProperty<ColorDrawable> HIGHLIGHT_ALPHA =
new IntProperty<ColorDrawable>("highlight") {
@Override
public void setValue(ColorDrawable d, int alpha) {
d.setAlpha(alpha);
}
@Override
public Integer get(ColorDrawable d) {
return d.getAlpha();
}
};
private final Path mPath = new Path();
private final float[] mContainerMargin = new float[4];
private float mCornerRadius;
private float mBottomInset;
private int mMarginColor; // i.e. color used for negative space like the container insets
private int mHighlightColor;
private boolean mShowingHighlight;
private boolean mShowingSplash;
private boolean mShowingMargin;
// TODO: might be more seamless to animate between splash/highlight color instead of 2 separate
private ObjectAnimator mSplashAnimator;
private ObjectAnimator mHighlightAnimator;
private ObjectAnimator mMarginAnimator;
private float mMarginPercent;
// Renders a highlight or neutral transparent color
private ColorDrawable mDropZoneDrawable;
// Renders the translucent splashscreen with the app icon in the middle
private ImageView mSplashScreenView;
private ColorDrawable mSplashBackgroundDrawable;
// Renders the margin / insets around the dropzone container
private MarginView mMarginView;
public DropZoneView(Context context) {
this(context, null);
}
public DropZoneView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DropZoneView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public DropZoneView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setContainerMargin(0, 0, 0, 0); // make sure it's populated
mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
mMarginColor = getResources().getColor(R.color.taskbar_background);
mHighlightColor = getResources().getColor(android.R.color.system_accent1_500);
mDropZoneDrawable = new ColorDrawable();
mDropZoneDrawable.setColor(mHighlightColor);
mDropZoneDrawable.setAlpha(0);
setBackgroundDrawable(mDropZoneDrawable);
mSplashScreenView = new ImageView(context);
mSplashScreenView.setScaleType(ImageView.ScaleType.CENTER);
mSplashBackgroundDrawable = new ColorDrawable();
mSplashBackgroundDrawable.setColor(Color.WHITE);
mSplashBackgroundDrawable.setAlpha(SPLASHSCREEN_ALPHA_INT);
mSplashScreenView.setBackgroundDrawable(mSplashBackgroundDrawable);
addView(mSplashScreenView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
mSplashScreenView.setAlpha(0f);
mMarginView = new MarginView(context);
addView(mMarginView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
}
public void onThemeChange() {
mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(getContext());
mMarginColor = getResources().getColor(R.color.taskbar_background);
mHighlightColor = getResources().getColor(android.R.color.system_accent1_500);
final int alpha = mDropZoneDrawable.getAlpha();
mDropZoneDrawable.setColor(mHighlightColor);
mDropZoneDrawable.setAlpha(alpha);
if (mMarginPercent > 0) {
mMarginView.invalidate();
}
}
/** Sets the desired margins around the drop zone container when fully showing. */
public void setContainerMargin(float left, float top, float right, float bottom) {
mContainerMargin[0] = left;
mContainerMargin[1] = top;
mContainerMargin[2] = right;
mContainerMargin[3] = bottom;
if (mMarginPercent > 0) {
mMarginView.invalidate();
}
}
/** Sets the bottom inset so the drop zones are above bottom navigation. */
public void setBottomInset(float bottom) {
mBottomInset = bottom;
((LayoutParams) mSplashScreenView.getLayoutParams()).bottomMargin = (int) bottom;
if (mMarginPercent > 0) {
mMarginView.invalidate();
}
}
/** Sets the color and icon to use for the splashscreen when shown. */
public void setAppInfo(int splashScreenColor, Drawable appIcon) {
mSplashBackgroundDrawable.setColor(splashScreenColor);
mSplashScreenView.setImageDrawable(appIcon);
}
/** @return an active animator for this view if one exists. */
@Nullable
public ObjectAnimator getAnimator() {
if (mMarginAnimator != null && mMarginAnimator.isRunning()) {
return mMarginAnimator;
} else if (mHighlightAnimator != null && mHighlightAnimator.isRunning()) {
return mHighlightAnimator;
} else if (mSplashAnimator != null && mSplashAnimator.isRunning()) {
return mSplashAnimator;
}
return null;
}
/** Animates the splashscreen to show or hide. */
public void setShowingSplash(boolean showingSplash) {
if (mShowingSplash != showingSplash) {
mShowingSplash = showingSplash;
animateSplashToState();
}
}
/** Animates the highlight indicating the zone is hovered on or not. */
public void setShowingHighlight(boolean showingHighlight) {
if (mShowingHighlight != showingHighlight) {
mShowingHighlight = showingHighlight;
animateHighlightToState();
}
}
/** Animates the margins around the drop zone to show or hide. */
public void setShowingMargin(boolean visible) {
if (mShowingMargin != visible) {
mShowingMargin = visible;
animateMarginToState();
}
if (!mShowingMargin) {
setShowingHighlight(false);
setShowingSplash(false);
}
}
private void animateSplashToState() {
if (mSplashAnimator != null) {
mSplashAnimator.cancel();
}
mSplashAnimator = ObjectAnimator.ofInt(mSplashBackgroundDrawable,
SPLASHSCREEN_ALPHA,
mSplashBackgroundDrawable.getAlpha(),
mShowingSplash ? SPLASHSCREEN_ALPHA_INT : 0);
if (!mShowingSplash) {
mSplashAnimator.setInterpolator(FAST_OUT_SLOW_IN);
}
mSplashAnimator.start();
mSplashScreenView.animate().alpha(mShowingSplash ? 1f : 0f).start();
}
private void animateHighlightToState() {
if (mHighlightAnimator != null) {
mHighlightAnimator.cancel();
}
mHighlightAnimator = ObjectAnimator.ofInt(mDropZoneDrawable,
HIGHLIGHT_ALPHA,
mDropZoneDrawable.getAlpha(),
mShowingHighlight ? HIGHLIGHT_ALPHA_INT : 0);
if (!mShowingHighlight) {
mHighlightAnimator.setInterpolator(FAST_OUT_SLOW_IN);
}
mHighlightAnimator.start();
}
private void animateMarginToState() {
if (mMarginAnimator != null) {
mMarginAnimator.cancel();
}
mMarginAnimator = ObjectAnimator.ofFloat(this, INSETS,
mMarginPercent,
mShowingMargin ? 1f : 0f);
mMarginAnimator.setInterpolator(FAST_OUT_SLOW_IN);
mMarginAnimator.setDuration(mShowingMargin
? MARGIN_ANIMATION_ENTER_DURATION
: MARGIN_ANIMATION_EXIT_DURATION);
mMarginAnimator.start();
}
private void setMarginPercent(float percent) {
if (percent != mMarginPercent) {
mMarginPercent = percent;
mMarginView.invalidate();
}
}
private float getMarginPercent() {
return mMarginPercent;
}
/** Simple view that draws a rounded rect margin around its contents. **/
private class MarginView extends View {
MarginView(Context context) {
super(context);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
mPath.addRoundRect(mContainerMargin[0] * mMarginPercent,
mContainerMargin[1] * mMarginPercent,
getWidth() - (mContainerMargin[2] * mMarginPercent),
getHeight() - (mContainerMargin[3] * mMarginPercent) - mBottomInset,
mCornerRadius * mMarginPercent,
mCornerRadius * mMarginPercent,
Path.Direction.CW);
mPath.setFillType(Path.FillType.INVERSE_EVEN_ODD);
canvas.clipPath(mPath);
canvas.drawColor(mMarginColor);
}
}
}

View File

@@ -188,6 +188,10 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
&& mStageCoordinator.getStageOfTask(taskId) != STAGE_TYPE_UNDEFINED;
}
public @SplitPosition int getSplitPosition(int taskId) {
return mStageCoordinator.getSplitPosition(taskId);
}
public boolean moveToSideStage(int taskId, @SplitPosition int sideStagePosition) {
return moveToStage(taskId, STAGE_TYPE_SIDE, sideStagePosition,
new WindowContainerTransaction());

View File

@@ -678,6 +678,16 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
outBottomOrRightBounds.set(mSplitLayout.getBounds2());
}
@SplitPosition
int getSplitPosition(int taskId) {
if (mSideStage.getTopVisibleChildTaskId() == taskId) {
return getSideStagePosition();
} else if (mMainStage.getTopVisibleChildTaskId() == taskId) {
return getMainStagePosition();
}
return SPLIT_POSITION_UNDEFINED;
}
private void addActivityOptions(Bundle opts, StageTaskListener stage) {
opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token);
}

View File

@@ -30,7 +30,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.internal.logging.UiEventLogger;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
import org.junit.Before;
import org.junit.Test;
@@ -59,8 +61,8 @@ public class DragAndDropControllerTest {
@Before
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
mController = new DragAndDropController(mContext, mDisplayController, mUiEventLogger);
mController = new DragAndDropController(mContext, mDisplayController, mUiEventLogger,
mock(IconProvider.class), mock(ShellExecutor.class));
}
@Test

View File

@@ -121,7 +121,8 @@ public class SystemUIFactory {
.setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper())
.setTaskSurfaceHelper(mWMComponent.getTaskSurfaceHelper())
.setRecentTasks(mWMComponent.getRecentTasks())
.setSizeCompatUI(Optional.of(mWMComponent.getSizeCompatUI()));
.setSizeCompatUI(Optional.of(mWMComponent.getSizeCompatUI()))
.setDragAndDrop(Optional.of(mWMComponent.getDragAndDrop()));
} else {
// TODO: Call on prepareSysUIComponentBuilder but not with real components. Other option
// is separating this logic into newly creating SystemUITestsFactory.
@@ -140,7 +141,8 @@ public class SystemUIFactory {
.setStartingSurface(Optional.ofNullable(null))
.setTaskSurfaceHelper(Optional.ofNullable(null))
.setRecentTasks(Optional.ofNullable(null))
.setSizeCompatUI(Optional.ofNullable(null));
.setSizeCompatUI(Optional.ofNullable(null))
.setDragAndDrop(Optional.ofNullable(null));
}
mSysUIComponent = builder.build();
if (mInitializeComponents) {

View File

@@ -32,6 +32,7 @@ import com.android.wm.shell.TaskViewFactory;
import com.android.wm.shell.apppairs.AppPairs;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
import com.android.wm.shell.draganddrop.DragAndDrop;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
import com.android.wm.shell.onehanded.OneHanded;
@@ -111,6 +112,9 @@ public interface SysUIComponent {
@BindsInstance
Builder setSizeCompatUI(Optional<SizeCompatUI> s);
@BindsInstance
Builder setDragAndDrop(Optional<DragAndDrop> d);
SysUIComponent build();
}

View File

@@ -29,6 +29,7 @@ import com.android.wm.shell.dagger.TvWMShellModule;
import com.android.wm.shell.dagger.WMShellModule;
import com.android.wm.shell.dagger.WMSingleton;
import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
import com.android.wm.shell.draganddrop.DragAndDrop;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
import com.android.wm.shell.onehanded.OneHanded;
@@ -119,4 +120,7 @@ public interface WMComponent {
@WMSingleton
SizeCompatUI getSizeCompatUI();
@WMSingleton
DragAndDrop getDragAndDrop();
}

View File

@@ -56,6 +56,7 @@ import com.android.systemui.statusbar.policy.UserInfoController;
import com.android.systemui.tracing.ProtoTracer;
import com.android.systemui.tracing.nano.SystemUiTraceProto;
import com.android.wm.shell.ShellCommandHandler;
import com.android.wm.shell.draganddrop.DragAndDrop;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
import com.android.wm.shell.nano.WmShellTraceProto;
@@ -114,6 +115,7 @@ public final class WMShell extends SystemUI
private final Optional<HideDisplayCutout> mHideDisplayCutoutOptional;
private final Optional<ShellCommandHandler> mShellCommandHandler;
private final Optional<SizeCompatUI> mSizeCompatUIOptional;
private final Optional<DragAndDrop> mDragAndDropOptional;
private final CommandQueue mCommandQueue;
private final ConfigurationController mConfigurationController;
@@ -142,6 +144,7 @@ public final class WMShell extends SystemUI
Optional<HideDisplayCutout> hideDisplayCutoutOptional,
Optional<ShellCommandHandler> shellCommandHandler,
Optional<SizeCompatUI> sizeCompatUIOptional,
Optional<DragAndDrop> dragAndDropOptional,
CommandQueue commandQueue,
ConfigurationController configurationController,
KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -167,6 +170,7 @@ public final class WMShell extends SystemUI
mProtoTracer = protoTracer;
mShellCommandHandler = shellCommandHandler;
mSizeCompatUIOptional = sizeCompatUIOptional;
mDragAndDropOptional = dragAndDropOptional;
mSysUiMainExecutor = sysUiMainExecutor;
}
@@ -182,6 +186,7 @@ public final class WMShell extends SystemUI
mOneHandedOptional.ifPresent(this::initOneHanded);
mHideDisplayCutoutOptional.ifPresent(this::initHideDisplayCutout);
mSizeCompatUIOptional.ifPresent(this::initSizeCompatUi);
mDragAndDropOptional.ifPresent(this::initDragAndDrop);
}
@VisibleForTesting
@@ -396,6 +401,20 @@ public final class WMShell extends SystemUI
mKeyguardUpdateMonitor.registerCallback(mSizeCompatUIKeyguardCallback);
}
void initDragAndDrop(DragAndDrop dragAndDrop) {
mConfigurationController.addCallback(new ConfigurationController.ConfigurationListener() {
@Override
public void onConfigChanged(Configuration newConfig) {
dragAndDrop.onConfigChanged(newConfig);
}
@Override
public void onThemeChanged() {
dragAndDrop.onThemeChanged();
}
});
}
@Override
public void writeToProto(SystemUiTraceProto proto) {
if (proto.wmShell == null) {

View File

@@ -35,6 +35,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.tracing.ProtoTracer;
import com.android.wm.shell.ShellCommandHandler;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.draganddrop.DragAndDrop;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
import com.android.wm.shell.onehanded.OneHanded;
@@ -79,6 +80,7 @@ public class WMShellTest extends SysuiTestCase {
@Mock ShellCommandHandler mShellCommandHandler;
@Mock SizeCompatUI mSizeCompatUI;
@Mock ShellExecutor mSysUiMainExecutor;
@Mock DragAndDrop mDragAndDrop;
@Before
public void setUp() {
@@ -87,6 +89,7 @@ public class WMShellTest extends SysuiTestCase {
mWMShell = new WMShell(mContext, Optional.of(mPip), Optional.of(mLegacySplitScreen),
Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mHideDisplayCutout),
Optional.of(mShellCommandHandler), Optional.of(mSizeCompatUI),
Optional.of(mDragAndDrop),
mCommandQueue, mConfigurationController, mKeyguardUpdateMonitor,
mNavigationModeController, mScreenLifecycle, mSysUiState, mProtoTracer,
mWakefulnessLifecycle, mSysUiMainExecutor);