Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1c0aefd4 authored by Mady Mellor's avatar Mady Mellor
Browse files

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
parent 99ab01f4
Loading
Loading
Loading
Loading
+19 −0
Original line number Diff line number Diff line
<?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>
 No newline at end of file
+11 −2
Original line number Diff line number Diff line
@@ -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
+34 −0
Original line number Diff line number Diff line
/*
 * 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);
}
+43 −3
Original line number Diff line number Diff line
@@ -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);
            });
        }
    }
}
+171 −54
Original line number Diff line number Diff line
@@ -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();
    }
}
Loading