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

Commit 1fa042b6 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Support dragging divider bar of app-pair to resize the splits"

parents 089d0f68 0c9c2cfa
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -14,7 +14,7 @@
  ~ limitations under the License.
  -->

<com.android.wm.shell.apppairs.DividerView
<com.android.wm.shell.common.split.DividerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent">
@@ -24,4 +24,4 @@
        android:id="@+id/docked_divider_background"
        android:background="@color/docked_divider_background"/>

</com.android.wm.shell.apppairs.DividerView>
</com.android.wm.shell.common.split.DividerView>
+66 −25
Original line number Diff line number Diff line
@@ -29,11 +29,13 @@ import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.split.SplitLayout;

import java.io.PrintWriter;

@@ -42,7 +44,7 @@ import java.io.PrintWriter;
 * {@link #mTaskInfo1} and {@link #mTaskInfo2} in the pair.
 * Also includes all UI for managing the pair like the divider.
 */
class AppPair implements ShellTaskOrganizer.TaskListener {
class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.LayoutChangeListener {
    private static final String TAG = AppPair.class.getSimpleName();

    private ActivityManager.RunningTaskInfo mRootTaskInfo;
@@ -55,7 +57,7 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
    private final AppPairsController mController;
    private final SyncTransactionQueue mSyncQueue;
    private final DisplayController mDisplayController;
    private AppPairLayout mAppPairLayout;
    private SplitLayout mSplitLayout;

    AppPair(AppPairsController controller) {
        mController = controller;
@@ -92,11 +94,9 @@ class AppPair implements ShellTaskOrganizer.TaskListener {

        mTaskInfo1 = task1;
        mTaskInfo2 = task2;
        mAppPairLayout = new AppPairLayout(
        mSplitLayout = new SplitLayout(
                mDisplayController.getDisplayContext(mRootTaskInfo.displayId),
                mDisplayController.getDisplay(mRootTaskInfo.displayId),
                mRootTaskInfo.configuration,
                mRootTaskLeash);
                mRootTaskInfo.configuration, this, mRootTaskLeash);

        final WindowContainerToken token1 = task1.token;
        final WindowContainerToken token2 = task2.token;
@@ -107,8 +107,8 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
                .reparent(token2, mRootTaskInfo.token, true /* onTop */)
                .setWindowingMode(token1, WINDOWING_MODE_MULTI_WINDOW)
                .setWindowingMode(token2, WINDOWING_MODE_MULTI_WINDOW)
                .setBounds(token1, mAppPairLayout.getBounds1())
                .setBounds(token2, mAppPairLayout.getBounds2())
                .setBounds(token1, mSplitLayout.getBounds1())
                .setBounds(token2, mSplitLayout.getBounds2())
                // Moving the root task to top after the child tasks were repareted , or the root
                // task cannot be visible and focused.
                .reorder(mRootTaskInfo.token, true);
@@ -117,6 +117,10 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
    }

    void unpair() {
        unpair(null /* toTopToken */);
    }

    private void unpair(@Nullable WindowContainerToken toTopToken) {
        final WindowContainerToken token1 = mTaskInfo1.token;
        final WindowContainerToken token2 = mTaskInfo2.token;
        final WindowContainerTransaction wct = new WindowContainerTransaction();
@@ -124,16 +128,16 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
        // Reparent out of this container and reset windowing mode.
        wct.setHidden(mRootTaskInfo.token, true)
                .reorder(mRootTaskInfo.token, false)
                .reparent(token1, null, false /* onTop */)
                .reparent(token2, null, false /* onTop */)
                .reparent(token1, null, token1 == toTopToken /* onTop */)
                .reparent(token2, null, token2 == toTopToken /* onTop */)
                .setWindowingMode(token1, WINDOWING_MODE_UNDEFINED)
                .setWindowingMode(token2, WINDOWING_MODE_UNDEFINED);
        mController.getTaskOrganizer().applyTransaction(wct);

        mTaskInfo1 = null;
        mTaskInfo2 = null;
        mAppPairLayout.release();
        mAppPairLayout = null;
        mSplitLayout.release();
        mSplitLayout = null;
    }

    @Override
@@ -153,17 +157,17 @@ class AppPair implements ShellTaskOrganizer.TaskListener {

        if (mTaskLeash1 == null || mTaskLeash2 == null) return;

        mAppPairLayout.init();
        final SurfaceControl dividerLeash = mAppPairLayout.getDividerLeash();
        final Rect dividerBounds = mAppPairLayout.getDividerBounds();
        mSplitLayout.init();
        final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
        final Rect dividerBounds = mSplitLayout.getDividerBounds();

        // TODO: Is there more we need to do here?
        mSyncQueue.runInSync(t -> {
            t.setPosition(mTaskLeash1, mTaskInfo1.positionInParent.x,
            t.setLayer(dividerLeash, Integer.MAX_VALUE)
                    .setPosition(mTaskLeash1, mTaskInfo1.positionInParent.x,
                            mTaskInfo1.positionInParent.y)
                    .setPosition(mTaskLeash2, mTaskInfo2.positionInParent.x,
                            mTaskInfo2.positionInParent.y)
                    .setLayer(dividerLeash, Integer.MAX_VALUE)
                    .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)
                    .show(mRootTaskLeash)
                    .show(mTaskLeash1)
@@ -185,14 +189,14 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
            }
            mRootTaskInfo = taskInfo;

            if (mAppPairLayout != null
                    && mAppPairLayout.updateConfiguration(mRootTaskInfo.configuration)) {
            if (mSplitLayout != null
                    && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)) {
                // Update bounds when root bounds or its orientation changed.
                final WindowContainerTransaction wct = new WindowContainerTransaction();
                final SurfaceControl dividerLeash = mAppPairLayout.getDividerLeash();
                final Rect dividerBounds = mAppPairLayout.getDividerBounds();
                final Rect bounds1 = mAppPairLayout.getBounds1();
                final Rect bounds2 = mAppPairLayout.getBounds2();
                final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
                final Rect dividerBounds = mSplitLayout.getDividerBounds();
                final Rect bounds1 = mSplitLayout.getBounds1();
                final Rect bounds2 = mSplitLayout.getBounds2();

                wct.setBounds(mTaskInfo1.token, bounds1)
                        .setBounds(mTaskInfo2.token, bounds2);
@@ -200,7 +204,9 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
                mSyncQueue.runInSync(t -> t
                        .setPosition(mTaskLeash1, bounds1.left, bounds1.top)
                        .setPosition(mTaskLeash2, bounds2.left, bounds2.top)
                        .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top));
                        .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)
                        // Resets layer to divider bar to make sure it is always on top.
                        .setLayer(dividerLeash, Integer.MAX_VALUE));
            }
        } else if (taskInfo.taskId == getTaskId1()) {
            mTaskInfo1 = taskInfo;
@@ -242,4 +248,39 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
    public String toString() {
        return TAG + "#" + getRootTaskId();
    }

    @Override
    public void onSnappedToDismiss(boolean snappedToEnd) {
        unpair(snappedToEnd ? mTaskInfo1.token : mTaskInfo2.token /* toTopToken */);
    }

    @Override
    public void onBoundsChanging(SplitLayout layout) {
        final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
        if (dividerLeash == null) return;
        final Rect dividerBounds = layout.getDividerBounds();
        final Rect bounds1 = layout.getBounds1();
        final Rect bounds2 = layout.getBounds2();
        mSyncQueue.runInSync(t -> t
                .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)
                .setPosition(mTaskLeash1, bounds1.left, bounds1.top)
                .setPosition(mTaskLeash2, bounds2.left, bounds2.top));
    }

    @Override
    public void onBoundsChanged(SplitLayout layout) {
        final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
        if (dividerLeash == null) return;
        final Rect dividerBounds = layout.getDividerBounds();
        final Rect bounds1 = layout.getBounds1();
        final Rect bounds2 = layout.getBounds2();
        final WindowContainerTransaction wct = new WindowContainerTransaction();
        wct.setBounds(mTaskInfo1.token, bounds1)
                .setBounds(mTaskInfo2.token, bounds2);
        mController.getTaskOrganizer().applyTransaction(wct);
        mSyncQueue.runInSync(t -> t
                .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)
                .setPosition(mTaskLeash1, bounds1.left, bounds1.top)
                .setPosition(mTaskLeash2, bounds2.left, bounds2.top));
    }
}
+0 −56
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.apppairs;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
 * Stack divider for app pair.
 */
public class DividerView extends FrameLayout {
    public DividerView(@NonNull Context context) {
        super(context);
    }

    public DividerView(@NonNull Context context,
            @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    void show() {
        post(() -> setVisibility(View.VISIBLE));
    }

    void hide() {
        post(() -> setVisibility(View.INVISIBLE));
    }
}
+171 −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.common.split;

import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceControlViewHost;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.internal.policy.DividerSnapAlgorithm;

/**
 * Stack divider for app pair.
 */
// TODO(b/172704238): add handle view to indicate touching status.
public class DividerView extends FrameLayout implements View.OnTouchListener {
    private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();

    private SplitLayout mSplitLayout;
    private SurfaceControlViewHost mViewHost;
    private DragListener mDragListener;

    private VelocityTracker mVelocityTracker;
    private boolean mMoving;
    private int mStartPos;

    public DividerView(@NonNull Context context) {
        super(context);
    }

    public DividerView(@NonNull Context context,
            @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    /** Sets up essential dependencies of the divider bar. */
    public void setup(
            SplitLayout layout,
            SurfaceControlViewHost viewHost,
            @Nullable DragListener dragListener) {
        mSplitLayout = layout;
        mViewHost = viewHost;
        mDragListener = dragListener;
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        setOnTouchListener(this);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (mSplitLayout == null) {
            return false;
        }

        final int action = event.getAction() & MotionEvent.ACTION_MASK;
        final boolean isLandscape = isLandscape();
        // Using raw xy to prevent lost track of motion events while moving divider bar.
        final int touchPos = isLandscape ? (int) event.getRawX() : (int) event.getRawY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mVelocityTracker = VelocityTracker.obtain();
                mVelocityTracker.addMovement(event);
                setSlippery(false);
                mStartPos = touchPos;
                mMoving = false;
                break;
            case MotionEvent.ACTION_MOVE:
                mVelocityTracker.addMovement(event);
                if (!mMoving && Math.abs(touchPos - mStartPos) > mTouchSlop) {
                    mStartPos = touchPos;
                    mMoving = true;
                    if (mDragListener != null) {
                        mDragListener.onDragStart();
                    }
                }
                if (mMoving) {
                    final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos;
                    mSplitLayout.updateDividePosition(position);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mVelocityTracker.addMovement(event);
                mVelocityTracker.computeCurrentVelocity(1000 /* units */);
                final float velocity = isLandscape
                        ? mVelocityTracker.getXVelocity()
                        : mVelocityTracker.getYVelocity();
                setSlippery(true);
                mMoving = false;
                if (mDragListener != null) {
                    mDragListener.onDragEnd();
                }

                final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos;
                final DividerSnapAlgorithm.SnapTarget snapTarget =
                        mSplitLayout.findSnapTarget(position, velocity);
                mSplitLayout.setSnapTarget(snapTarget);
                break;
        }
        return true;
    }

    private void setSlippery(boolean slippery) {
        if (mViewHost == null) {
            return;
        }

        final WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams();
        final boolean isSlippery = (lp.flags & FLAG_SLIPPERY) != 0;
        if (isSlippery == slippery) {
            return;
        }

        if (slippery) {
            lp.flags |= FLAG_SLIPPERY;
        } else {
            lp.flags &= ~FLAG_SLIPPERY;
        }
        mViewHost.relayout(lp);
    }

    private boolean isLandscape() {
        return getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE;
    }

    /** Monitors dragging action of the divider bar. */
    // TODO(b/172704238): add listeners to deal with resizing state of the app windows.
    public interface DragListener {
        /** Called when start dragging. */
        void onDragStart();
        /** Called when stop dragging. */
        void onDragEnd();
    }
}
+205 −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.common.split;

import static android.view.WindowManager.DOCKED_LEFT;
import static android.view.WindowManager.DOCKED_TOP;

import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END;
import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START;

import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
import android.view.SurfaceControl;

import androidx.annotation.Nullable;

import com.android.internal.policy.DividerSnapAlgorithm;

/**
 * Records and handles layout of splits. Helps to calculate proper bounds when configuration or
 * divide position changes.
 */
public class SplitLayout {
    private final int mDividerWindowWidth;
    private final int mDividerInsets;
    private final int mDividerSize;

    private final Rect mRootBounds = new Rect();
    private final Rect mDividerBounds = new Rect();
    private final Rect mBounds1 = new Rect();
    private final Rect mBounds2 = new Rect();
    private final LayoutChangeListener mLayoutChangeListener;
    private final SplitWindowManager mSplitWindowManager;

    private Context mContext;
    private DividerSnapAlgorithm mDividerSnapAlgorithm;
    private int mDividePosition;

    public SplitLayout(Context context, Configuration configuration,
            LayoutChangeListener layoutChangeListener, SurfaceControl rootLeash) {
        mContext = context.createConfigurationContext(configuration);
        mLayoutChangeListener = layoutChangeListener;
        mSplitWindowManager = new SplitWindowManager(mContext, configuration, rootLeash);

        mDividerWindowWidth = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.docked_stack_divider_thickness);
        mDividerInsets = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.docked_stack_divider_insets);
        mDividerSize = mDividerWindowWidth - mDividerInsets * 2;

        mRootBounds.set(configuration.windowConfiguration.getBounds());
        mDividerSnapAlgorithm = getSnapAlgorithm(context.getResources(), mRootBounds);
        mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position;
        updateBounds(mDividePosition);
    }

    /** Gets bounds of the primary split. */
    public Rect getBounds1() {
        return mBounds1;
    }

    /** Gets bounds of the secondary split. */
    public Rect getBounds2() {
        return mBounds2;
    }

    /** Gets bounds of divider window. */
    public Rect getDividerBounds() {
        return mDividerBounds;
    }

    /** Returns leash of the current divider bar. */
    @Nullable
    public SurfaceControl getDividerLeash() {
        return mSplitWindowManager == null ? null : mSplitWindowManager.getSurfaceControl();
    }

    int getDividePosition() {
        return mDividePosition;
    }

    /** Applies new configuration, returns {@code false} if there's no effect to the layout. */
    public boolean updateConfiguration(Configuration configuration) {
        final Rect rootBounds = configuration.windowConfiguration.getBounds();
        if (mRootBounds.equals(rootBounds)) {
            return false;
        }

        mContext = mContext.createConfigurationContext(configuration);
        mSplitWindowManager.setConfiguration(configuration);
        mRootBounds.set(rootBounds);
        mDividerSnapAlgorithm = getSnapAlgorithm(mContext.getResources(), mRootBounds);
        mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position;
        updateBounds(mDividePosition);
        release();
        init();
        return true;
    }

    /** Updates recording bounds of divider window and both of the splits. */
    private void updateBounds(int position) {
        mDividerBounds.set(mRootBounds);
        mBounds1.set(mRootBounds);
        mBounds2.set(mRootBounds);
        if (isLandscape(mRootBounds)) {
            mDividerBounds.left = position - mDividerInsets;
            mDividerBounds.right = mDividerBounds.left + mDividerWindowWidth;
            mBounds1.right = mBounds1.left + position;
            mBounds2.left = mBounds1.right + mDividerSize;
        } else {
            mDividerBounds.top = position - mDividerInsets;
            mDividerBounds.bottom = mDividerBounds.top + mDividerWindowWidth;
            mBounds1.bottom = mBounds1.top + position;
            mBounds2.top = mBounds1.bottom + mDividerSize;
        }
    }

    /** Inflates {@link DividerView} on the root surface. */
    public void init() {
        mSplitWindowManager.init(this);
    }

    /** Releases the surface holding the current {@link DividerView}. */
    public void release() {
        mSplitWindowManager.release();
    }

    /**
     * Updates bounds with the passing position. Usually used to update recording bounds while
     * performing animation or dragging divider bar to resize the splits.
     */
    public void updateDividePosition(int position) {
        updateBounds(position);
        mLayoutChangeListener.onBoundsChanging(this);
    }

    /**
     * Sets new divide position and updates bounds correspondingly. Notifies listener if the new
     * target indicates dismissing split.
     */
    public void setSnapTarget(DividerSnapAlgorithm.SnapTarget snapTarget) {
        switch(snapTarget.flag) {
            case FLAG_DISMISS_START:
                mLayoutChangeListener.onSnappedToDismiss(false /* snappedToEnd */);
                break;
            case FLAG_DISMISS_END:
                mLayoutChangeListener.onSnappedToDismiss(true /* snappedToEnd */);
                break;
            default:
                mDividePosition = snapTarget.position;
                updateBounds(mDividePosition);
                mLayoutChangeListener.onBoundsChanged(this);
                break;
        }
    }

    /**
     * Returns {@link DividerSnapAlgorithm.SnapTarget} which matches passing position and velocity.
     */
    public DividerSnapAlgorithm.SnapTarget findSnapTarget(int position, float velocity) {
        return mDividerSnapAlgorithm.calculateSnapTarget(position, velocity);
    }

    private DividerSnapAlgorithm getSnapAlgorithm(Resources resources, Rect rootBounds) {
        final boolean isLandscape = isLandscape(rootBounds);
        return new DividerSnapAlgorithm(
                resources,
                rootBounds.width(),
                rootBounds.height(),
                mDividerSize,
                !isLandscape,
                new Rect() /* insets */,
                isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */);
    }

    private static boolean isLandscape(Rect bounds) {
        return bounds.width() > bounds.height();
    }

    /** Listens layout change event. */
    public interface LayoutChangeListener {
        /** Calls when dismissing split. */
        void onSnappedToDismiss(boolean snappedToEnd);
        /** Calls when the bounds is changing due to animation or dragging divider bar. */
        void onBoundsChanging(SplitLayout layout);
        /** Calls when the target bounds changed. */
        void onBoundsChanged(SplitLayout layout);
    }
}
Loading