Loading libs/WindowManager/Shell/res/layout/split_divider.xml +2 −2 Original line number Diff line number Diff line Loading @@ -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"> Loading @@ -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> libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java +66 −25 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading @@ -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); Loading @@ -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(); Loading @@ -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 Loading @@ -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) Loading @@ -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); Loading @@ -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; Loading Loading @@ -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)); } } libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/DividerView.javadeleted 100644 → 0 +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)); } } libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java 0 → 100644 +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(); } } libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java 0 → 100644 +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
libs/WindowManager/Shell/res/layout/split_divider.xml +2 −2 Original line number Diff line number Diff line Loading @@ -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"> Loading @@ -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>
libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java +66 −25 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading @@ -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); Loading @@ -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(); Loading @@ -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 Loading @@ -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) Loading @@ -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); Loading @@ -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; Loading Loading @@ -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)); } }
libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/DividerView.javadeleted 100644 → 0 +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)); } }
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java 0 → 100644 +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(); } }
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java 0 → 100644 +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); } }