Merge "[Divider] Implement Divider Dragging" into main
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
index cae232e..b8ac191 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
@@ -23,9 +23,11 @@
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE;
import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_DECOR_SURFACE_BOOSTED;
import static androidx.window.extensions.embedding.DividerAttributes.RATIO_UNSET;
import static androidx.window.extensions.embedding.DividerAttributes.WIDTH_UNSET;
+import static androidx.window.extensions.embedding.SplitAttributesHelper.isReversedLayout;
import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM;
import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT;
import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT;
@@ -45,8 +47,10 @@
import android.os.IBinder;
import android.util.TypedValue;
import android.view.Gravity;
+import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
+import android.view.View;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
import android.widget.FrameLayout;
@@ -56,23 +60,30 @@
import android.window.TaskFragmentParentInfo;
import android.window.WindowContainerTransaction;
+import androidx.annotation.GuardedBy;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
+import androidx.window.extensions.core.util.function.Consumer;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.window.flags.Flags;
import java.util.Objects;
+import java.util.concurrent.Executor;
/**
* Manages the rendering and interaction of the divider.
*/
-class DividerPresenter {
+class DividerPresenter implements View.OnTouchListener {
private static final String WINDOW_NAME = "AE Divider";
+ private static final int VEIL_LAYER = 0;
+ private static final int DIVIDER_LAYER = 1;
// TODO(b/327067596) Update based on UX guidance.
private static final Color DEFAULT_DIVIDER_COLOR = Color.valueOf(Color.BLACK);
+ private static final Color DEFAULT_PRIMARY_VEIL_COLOR = Color.valueOf(Color.BLACK);
+ private static final Color DEFAULT_SECONDARY_VEIL_COLOR = Color.valueOf(Color.GRAY);
@VisibleForTesting
static final float DEFAULT_MIN_RATIO = 0.35f;
@VisibleForTesting
@@ -80,11 +91,23 @@
@VisibleForTesting
static final int DEFAULT_DIVIDER_WIDTH_DP = 24;
+ private final int mTaskId;
+
+ @NonNull
+ private final Object mLock = new Object();
+
+ @NonNull
+ private final DragEventCallback mDragEventCallback;
+
+ @NonNull
+ private final Executor mCallbackExecutor;
+
/**
* The {@link Properties} of the divider. This field is {@code null} when no divider should be
* drawn, e.g. when the split doesn't have {@link DividerAttributes} or when the decor surface
* is not available.
*/
+ @GuardedBy("mLock")
@Nullable
@VisibleForTesting
Properties mProperties;
@@ -94,6 +117,7 @@
* drawn, i.e. when {@link #mProperties} is {@code null}. The {@link Renderer} is recreated or
* updated when {@link #mProperties} is changed.
*/
+ @GuardedBy("mLock")
@Nullable
@VisibleForTesting
Renderer mRenderer;
@@ -102,10 +126,26 @@
* The owner TaskFragment token of the decor surface. The decor surface is placed right above
* the owner TaskFragment surface and is removed if the owner TaskFragment is destroyed.
*/
+ @GuardedBy("mLock")
@Nullable
@VisibleForTesting
IBinder mDecorSurfaceOwner;
+ /**
+ * The current divider position relative to the Task bounds. For vertical split (left-to-right
+ * or right-to-left), it is the x coordinate in the task window, and for horizontal split
+ * (top-to-bottom or bottom-to-top), it is the y coordinate in the task window.
+ */
+ @GuardedBy("mLock")
+ private int mDividerPosition;
+
+ DividerPresenter(int taskId, @NonNull DragEventCallback dragEventCallback,
+ @NonNull Executor callbackExecutor) {
+ mTaskId = taskId;
+ mDragEventCallback = dragEventCallback;
+ mCallbackExecutor = callbackExecutor;
+ }
+
/** Updates the divider when external conditions are changed. */
void updateDivider(
@NonNull WindowContainerTransaction wct,
@@ -115,58 +155,65 @@
return;
}
- // Clean up the decor surface if top SplitContainer is null.
- if (topSplitContainer == null) {
- removeDecorSurfaceAndDivider(wct);
- return;
- }
+ synchronized (mLock) {
+ // Clean up the decor surface if top SplitContainer is null.
+ if (topSplitContainer == null) {
+ removeDecorSurfaceAndDivider(wct);
+ return;
+ }
- // Clean up the decor surface if DividerAttributes is null.
- final DividerAttributes dividerAttributes =
- topSplitContainer.getCurrentSplitAttributes().getDividerAttributes();
- if (dividerAttributes == null) {
- removeDecorSurfaceAndDivider(wct);
- return;
- }
+ // Clean up the decor surface if DividerAttributes is null.
+ final DividerAttributes dividerAttributes =
+ topSplitContainer.getCurrentSplitAttributes().getDividerAttributes();
+ if (dividerAttributes == null) {
+ removeDecorSurfaceAndDivider(wct);
+ return;
+ }
- if (topSplitContainer.getCurrentSplitAttributes().getSplitType()
- instanceof SplitAttributes.SplitType.ExpandContainersSplitType) {
- // No divider is needed for ExpandContainersSplitType.
- removeDivider();
- return;
- }
+ if (topSplitContainer.getCurrentSplitAttributes().getSplitType()
+ instanceof SplitAttributes.SplitType.ExpandContainersSplitType) {
+ // No divider is needed for ExpandContainersSplitType.
+ removeDivider();
+ return;
+ }
- // Skip updating when the TFs have not been updated to match the SplitAttributes.
- if (topSplitContainer.getPrimaryContainer().getLastRequestedBounds().isEmpty()
- || topSplitContainer.getSecondaryContainer().getLastRequestedBounds().isEmpty()) {
- return;
- }
+ // Skip updating when the TFs have not been updated to match the SplitAttributes.
+ if (topSplitContainer.getPrimaryContainer().getLastRequestedBounds().isEmpty()
+ || topSplitContainer.getSecondaryContainer().getLastRequestedBounds()
+ .isEmpty()) {
+ return;
+ }
- final SurfaceControl decorSurface = parentInfo.getDecorSurface();
- if (decorSurface == null) {
- // Clean up when the decor surface is currently unavailable.
- removeDivider();
- // Request to create the decor surface
- createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer());
- return;
- }
+ final SurfaceControl decorSurface = parentInfo.getDecorSurface();
+ if (decorSurface == null) {
+ // Clean up when the decor surface is currently unavailable.
+ removeDivider();
+ // Request to create the decor surface
+ createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer());
+ return;
+ }
- // make the top primary container the owner of the decor surface.
- if (!Objects.equals(mDecorSurfaceOwner,
- topSplitContainer.getPrimaryContainer().getTaskFragmentToken())) {
- createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer());
- }
+ // make the top primary container the owner of the decor surface.
+ if (!Objects.equals(mDecorSurfaceOwner,
+ topSplitContainer.getPrimaryContainer().getTaskFragmentToken())) {
+ createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer());
+ }
- updateProperties(
- new Properties(
- parentInfo.getConfiguration(),
- dividerAttributes,
- decorSurface,
- getInitialDividerPosition(topSplitContainer),
- isVerticalSplit(topSplitContainer),
- parentInfo.getDisplayId()));
+ updateProperties(
+ new Properties(
+ parentInfo.getConfiguration(),
+ dividerAttributes,
+ decorSurface,
+ getInitialDividerPosition(topSplitContainer),
+ isVerticalSplit(topSplitContainer),
+ isReversedLayout(
+ topSplitContainer.getCurrentSplitAttributes(),
+ parentInfo.getConfiguration()),
+ parentInfo.getDisplayId()));
+ }
}
+ @GuardedBy("mLock")
private void updateProperties(@NonNull Properties properties) {
if (Properties.equalsForDivider(mProperties, properties)) {
return;
@@ -176,16 +223,16 @@
if (mRenderer == null) {
// Create a new renderer when a renderer doesn't exist yet.
- mRenderer = new Renderer();
+ mRenderer = new Renderer(mProperties, this);
} else if (!Properties.areSameSurfaces(
previousProperties.mDecorSurface, mProperties.mDecorSurface)
|| previousProperties.mDisplayId != mProperties.mDisplayId) {
// Release and recreate the renderer if the decor surface or the display has changed.
mRenderer.release();
- mRenderer = new Renderer();
+ mRenderer = new Renderer(mProperties, this);
} else {
// Otherwise, update the renderer for the new properties.
- mRenderer.update();
+ mRenderer.update(mProperties);
}
}
@@ -195,6 +242,7 @@
*
* See {@link TaskFragmentOperation#OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE}.
*/
+ @GuardedBy("mLock")
private void createOrMoveDecorSurface(
@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) {
final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
@@ -204,6 +252,7 @@
mDecorSurfaceOwner = container.getTaskFragmentToken();
}
+ @GuardedBy("mLock")
private void removeDecorSurfaceAndDivider(@NonNull WindowContainerTransaction wct) {
if (mDecorSurfaceOwner != null) {
final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
@@ -215,6 +264,7 @@
removeDivider();
}
+ @GuardedBy("mLock")
private void removeDivider() {
if (mRenderer != null) {
mRenderer.release();
@@ -238,7 +288,7 @@
private static boolean isVerticalSplit(@NonNull SplitContainer splitContainer) {
final int layoutDirection = splitContainer.getCurrentSplitAttributes().getLayoutDirection();
- switch(layoutDirection) {
+ switch (layoutDirection) {
case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT:
case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT:
case SplitAttributes.LayoutDirection.LOCALE:
@@ -251,12 +301,6 @@
}
}
- private static void safeReleaseSurfaceControl(@Nullable SurfaceControl sc) {
- if (sc != null) {
- sc.release();
- }
- }
-
private static int getDividerWidthPx(@NonNull DividerAttributes dividerAttributes) {
int dividerWidthDp = dividerAttributes.getWidthDp();
return convertDpToPixel(dividerWidthDp);
@@ -388,6 +432,227 @@
.build();
}
+ @Override
+ public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) {
+ synchronized (mLock) {
+ final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+ mDividerPosition = calculateDividerPosition(
+ event, taskBounds, mRenderer.mDividerWidthPx, mProperties.mDividerAttributes,
+ mProperties.mIsVerticalSplit, mProperties.mIsReversedLayout);
+ mRenderer.setDividerPosition(mDividerPosition);
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ onStartDragging();
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ onFinishDragging();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ onDrag();
+ break;
+ default:
+ break;
+ }
+ }
+
+ // Returns false so that the default button click callback is still triggered, i.e. the
+ // button UI transitions into the "pressed" state.
+ return false;
+ }
+
+ @GuardedBy("mLock")
+ private void onStartDragging() {
+ mRenderer.mIsDragging = true;
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ mRenderer.updateSurface(t);
+ mRenderer.showVeils(t);
+ final IBinder decorSurfaceOwner = mDecorSurfaceOwner;
+
+ // Callbacks must be executed on the executor to release mLock and prevent deadlocks.
+ mCallbackExecutor.execute(() -> {
+ mDragEventCallback.onStartDragging(
+ wct -> setDecorSurfaceBoosted(wct, decorSurfaceOwner, true /* boosted */, t));
+ });
+ }
+
+ @GuardedBy("mLock")
+ private void onDrag() {
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ mRenderer.updateSurface(t);
+ t.apply();
+ }
+
+ @GuardedBy("mLock")
+ private void onFinishDragging() {
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ mRenderer.updateSurface(t);
+ mRenderer.hideVeils(t);
+ final IBinder decorSurfaceOwner = mDecorSurfaceOwner;
+
+ // Callbacks must be executed on the executor to release mLock and prevent deadlocks.
+ mCallbackExecutor.execute(() -> {
+ mDragEventCallback.onFinishDragging(
+ mTaskId,
+ wct -> setDecorSurfaceBoosted(wct, decorSurfaceOwner, false /* boosted */, t));
+ });
+ mRenderer.mIsDragging = false;
+ }
+
+ private static void setDecorSurfaceBoosted(
+ @NonNull WindowContainerTransaction wct,
+ @Nullable IBinder decorSurfaceOwner,
+ boolean boosted,
+ @NonNull SurfaceControl.Transaction clientTransaction) {
+ if (decorSurfaceOwner == null) {
+ return;
+ }
+ wct.addTaskFragmentOperation(
+ decorSurfaceOwner,
+ new TaskFragmentOperation.Builder(OP_TYPE_SET_DECOR_SURFACE_BOOSTED)
+ .setBooleanValue(boosted)
+ .setSurfaceTransaction(clientTransaction)
+ .build()
+ );
+ }
+
+ /** Calculates the new divider position based on the touch event and divider attributes. */
+ @VisibleForTesting
+ static int calculateDividerPosition(@NonNull MotionEvent event, @NonNull Rect taskBounds,
+ int dividerWidthPx, @NonNull DividerAttributes dividerAttributes,
+ boolean isVerticalSplit, boolean isReversedLayout) {
+ // The touch event is in display space. Converting it into the task window space.
+ final int touchPositionInTaskSpace = isVerticalSplit
+ ? (int) (event.getRawX()) - taskBounds.left
+ : (int) (event.getRawY()) - taskBounds.top;
+
+ // Assuming that the touch position is at the center of the divider bar, so the divider
+ // position is offset by half of the divider width.
+ int dividerPosition = touchPositionInTaskSpace - dividerWidthPx / 2;
+
+ // Limit the divider position to the min and max ratios set in DividerAttributes.
+ // TODO(b/327536303) Handle when the divider is dragged to the edge.
+ dividerPosition = Math.max(dividerPosition, calculateMinPosition(
+ taskBounds, dividerWidthPx, dividerAttributes, isVerticalSplit, isReversedLayout));
+ dividerPosition = Math.min(dividerPosition, calculateMaxPosition(
+ taskBounds, dividerWidthPx, dividerAttributes, isVerticalSplit, isReversedLayout));
+ return dividerPosition;
+ }
+
+ /** Calculates the min position of the divider that the user is allowed to drag to. */
+ @VisibleForTesting
+ static int calculateMinPosition(@NonNull Rect taskBounds, int dividerWidthPx,
+ @NonNull DividerAttributes dividerAttributes, boolean isVerticalSplit,
+ boolean isReversedLayout) {
+ // The usable size is the task window size minus the divider bar width. This is shared
+ // between the primary and secondary containers based on the split ratio.
+ final int usableSize = isVerticalSplit
+ ? taskBounds.width() - dividerWidthPx
+ : taskBounds.height() - dividerWidthPx;
+ return (int) (isReversedLayout
+ ? usableSize - usableSize * dividerAttributes.getPrimaryMaxRatio()
+ : usableSize * dividerAttributes.getPrimaryMinRatio());
+ }
+
+ /** Calculates the max position of the divider that the user is allowed to drag to. */
+ @VisibleForTesting
+ static int calculateMaxPosition(@NonNull Rect taskBounds, int dividerWidthPx,
+ @NonNull DividerAttributes dividerAttributes, boolean isVerticalSplit,
+ boolean isReversedLayout) {
+ // The usable size is the task window size minus the divider bar width. This is shared
+ // between the primary and secondary containers based on the split ratio.
+ final int usableSize = isVerticalSplit
+ ? taskBounds.width() - dividerWidthPx
+ : taskBounds.height() - dividerWidthPx;
+ return (int) (isReversedLayout
+ ? usableSize - usableSize * dividerAttributes.getPrimaryMinRatio()
+ : usableSize * dividerAttributes.getPrimaryMaxRatio());
+ }
+
+ /**
+ * Returns the new split ratio of the {@link SplitContainer} based on the current divider
+ * position.
+ */
+ float calculateNewSplitRatio(@NonNull SplitContainer topSplitContainer) {
+ synchronized (mLock) {
+ return calculateNewSplitRatio(
+ topSplitContainer,
+ mDividerPosition,
+ mProperties.mConfiguration.windowConfiguration.getBounds(),
+ mRenderer.mDividerWidthPx,
+ mProperties.mIsVerticalSplit,
+ mProperties.mIsReversedLayout);
+ }
+ }
+
+ /**
+ * Returns the new split ratio of the {@link SplitContainer} based on the current divider
+ * position.
+ * @param topSplitContainer the {@link SplitContainer} for which to compute the split ratio.
+ * @param dividerPosition the divider position. See {@link #mDividerPosition}.
+ * @param taskBounds the task bounds
+ * @param dividerWidthPx the width of the divider in pixels.
+ * @param isVerticalSplit if {@code true}, the split is a vertical split. If {@code false}, the
+ * split is a horizontal split. See
+ * {@link #isVerticalSplit(SplitContainer)}.
+ * @param isReversedLayout if {@code true}, the split layout is reversed, i.e. right-to-left or
+ * bottom-to-top. If {@code false}, the split is not reversed, i.e.
+ * left-to-right or top-to-bottom. See
+ * {@link SplitAttributesHelper#isReversedLayout}
+ * @return the computed split ratio of the primary container.
+ */
+ @VisibleForTesting
+ static float calculateNewSplitRatio(
+ @NonNull SplitContainer topSplitContainer,
+ int dividerPosition,
+ @NonNull Rect taskBounds,
+ int dividerWidthPx,
+ boolean isVerticalSplit,
+ boolean isReversedLayout) {
+ final int usableSize = isVerticalSplit
+ ? taskBounds.width() - dividerWidthPx
+ : taskBounds.height() - dividerWidthPx;
+
+ final TaskFragmentContainer primaryContainer = topSplitContainer.getPrimaryContainer();
+ final Rect origPrimaryBounds = primaryContainer.getLastRequestedBounds();
+
+ float newRatio;
+ if (isVerticalSplit) {
+ final int newPrimaryWidth = isReversedLayout
+ ? (origPrimaryBounds.right - (dividerPosition + dividerWidthPx))
+ : (dividerPosition - origPrimaryBounds.left);
+ newRatio = 1.0f * newPrimaryWidth / usableSize;
+ } else {
+ final int newPrimaryHeight = isReversedLayout
+ ? (origPrimaryBounds.bottom - (dividerPosition + dividerWidthPx))
+ : (dividerPosition - origPrimaryBounds.top);
+ newRatio = 1.0f * newPrimaryHeight / usableSize;
+ }
+ return newRatio;
+ }
+
+ /** Callbacks for drag events */
+ interface DragEventCallback {
+ /**
+ * Called when the user starts dragging the divider. Callbacks are executed on
+ * {@link #mCallbackExecutor}.
+ *
+ * @param action additional action that should be applied to the
+ * {@link WindowContainerTransaction}
+ */
+ void onStartDragging(@NonNull Consumer<WindowContainerTransaction> action);
+
+ /**
+ * Called when the user finishes dragging the divider. Callbacks are executed on
+ * {@link #mCallbackExecutor}.
+ *
+ * @param taskId the Task id of the {@link TaskContainer} that this divider belongs to.
+ * @param action additional action that should be applied to the
+ * {@link WindowContainerTransaction}
+ */
+ void onFinishDragging(int taskId, @NonNull Consumer<WindowContainerTransaction> action);
+ }
+
/**
* Properties for the {@link DividerPresenter}. The rendering of the divider solely depends on
* these properties. When any value is updated, the divider is re-rendered. The Properties
@@ -411,6 +676,7 @@
private final boolean mIsVerticalSplit;
private final int mDisplayId;
+ private final boolean mIsReversedLayout;
@VisibleForTesting
Properties(
@@ -419,12 +685,14 @@
@NonNull SurfaceControl decorSurface,
int initialDividerPosition,
boolean isVerticalSplit,
+ boolean isReversedLayout,
int displayId) {
mConfiguration = configuration;
mDividerAttributes = dividerAttributes;
mDecorSurface = decorSurface;
mInitialDividerPosition = initialDividerPosition;
mIsVerticalSplit = isVerticalSplit;
+ mIsReversedLayout = isReversedLayout;
mDisplayId = displayId;
}
@@ -445,7 +713,8 @@
&& areConfigurationsEqualForDivider(a.mConfiguration, b.mConfiguration)
&& a.mInitialDividerPosition == b.mInitialDividerPosition
&& a.mIsVerticalSplit == b.mIsVerticalSplit
- && a.mDisplayId == b.mDisplayId;
+ && a.mDisplayId == b.mDisplayId
+ && a.mIsReversedLayout == b.mIsReversedLayout;
}
private static boolean areSameSurfaces(
@@ -472,7 +741,7 @@
* recreated. When other fields in the Properties are changed, the renderer is updated.
*/
@VisibleForTesting
- class Renderer {
+ static class Renderer {
@NonNull
private final SurfaceControl mDividerSurface;
@NonNull
@@ -481,10 +750,21 @@
private final SurfaceControlViewHost mViewHost;
@NonNull
private final FrameLayout mDividerLayout;
- private final int mDividerWidthPx;
+ @NonNull
+ private final View.OnTouchListener mListener;
+ @NonNull
+ private Properties mProperties;
+ private int mDividerWidthPx;
+ @Nullable
+ private SurfaceControl mPrimaryVeil;
+ @Nullable
+ private SurfaceControl mSecondaryVeil;
+ private boolean mIsDragging;
+ private int mDividerPosition;
- private Renderer() {
- mDividerWidthPx = getDividerWidthPx(mProperties.mDividerAttributes);
+ private Renderer(@NonNull Properties properties, @NonNull View.OnTouchListener listener) {
+ mProperties = properties;
+ mListener = listener;
mDividerSurface = createChildSurface("DividerSurface", true /* visible */);
mWindowlessWindowManager = new WindowlessWindowManager(
@@ -503,36 +783,63 @@
}
/** Updates the divider when properties are changed */
+ private void update(@NonNull Properties newProperties) {
+ mProperties = newProperties;
+ update();
+ }
+
+ /** Updates the divider when initializing or when properties are changed */
@VisibleForTesting
void update() {
+ mDividerWidthPx = getDividerWidthPx(mProperties.mDividerAttributes);
+ mDividerPosition = mProperties.mInitialDividerPosition;
mWindowlessWindowManager.setConfiguration(mProperties.mConfiguration);
- updateSurface();
+ // TODO handle synchronization between surface transactions and WCT.
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ updateSurface(t);
updateLayout();
- updateDivider();
+ updateDivider(t);
+ t.apply();
}
@VisibleForTesting
void release() {
mViewHost.release();
// TODO handle synchronization between surface transactions and WCT.
- new SurfaceControl.Transaction().remove(mDividerSurface).apply();
- safeReleaseSurfaceControl(mDividerSurface);
- }
-
- private void updateSurface() {
- final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
- // TODO handle synchronization between surface transactions and WCT.
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
- if (mProperties.mIsVerticalSplit) {
- t.setPosition(mDividerSurface, mProperties.mInitialDividerPosition, 0.0f);
- t.setWindowCrop(mDividerSurface, mDividerWidthPx, taskBounds.height());
- } else {
- t.setPosition(mDividerSurface, 0.0f, mProperties.mInitialDividerPosition);
- t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerWidthPx);
- }
+ t.remove(mDividerSurface);
+ removeVeils(t);
t.apply();
}
+ private void setDividerPosition(int dividerPosition) {
+ mDividerPosition = dividerPosition;
+ }
+
+ /**
+ * Updates the positions and crops of the divider surface and veil surfaces. This method
+ * should be called when {@link #mProperties} is changed or while dragging to update the
+ * position of the divider surface and the veil surfaces.
+ */
+ private void updateSurface(@NonNull SurfaceControl.Transaction t) {
+ final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+ if (mProperties.mIsVerticalSplit) {
+ t.setPosition(mDividerSurface, mDividerPosition, 0.0f);
+ t.setWindowCrop(mDividerSurface, mDividerWidthPx, taskBounds.height());
+ } else {
+ t.setPosition(mDividerSurface, 0.0f, mDividerPosition);
+ t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerWidthPx);
+ }
+ if (mIsDragging) {
+ updateVeils(t);
+ }
+ }
+
+ /**
+ * Updates the layout parameters of the layout used to host the divider. This method should
+ * be called only when {@link #mProperties} is changed. This should not be called while
+ * dragging, because the layout parameters are not changed during dragging.
+ */
private void updateLayout() {
final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
final WindowManager.LayoutParams lp = mProperties.mIsVerticalSplit
@@ -552,12 +859,21 @@
mViewHost.setView(mDividerLayout, lp);
}
- private void updateDivider() {
+ /**
+ * Updates the UI component of the divider, including the drag handle and the veils. This
+ * method should be called only when {@link #mProperties} is changed. This should not be
+ * called while dragging, because the UI components are not changed during dragging and
+ * only their surface positions are changed.
+ */
+ private void updateDivider(@NonNull SurfaceControl.Transaction t) {
mDividerLayout.removeAllViews();
mDividerLayout.setBackgroundColor(DEFAULT_DIVIDER_COLOR.toArgb());
if (mProperties.mDividerAttributes.getDividerType()
== DividerAttributes.DIVIDER_TYPE_DRAGGABLE) {
+ createVeils();
drawDragHandle();
+ } else {
+ removeVeils(t);
}
mViewHost.getView().invalidate();
}
@@ -580,7 +896,7 @@
button.setLayoutParams(params);
button.setBackgroundColor(R.color.transparent);
- final Drawable handle = context.getResources().getDrawable(
+ final Drawable handle = context.getResources().getDrawable(
R.drawable.activity_embedding_divider_handle, context.getTheme());
if (mProperties.mIsVerticalSplit) {
button.setImageDrawable(handle);
@@ -598,6 +914,8 @@
button.setImageDrawable(rotatedHandle);
}
+
+ button.setOnTouchListener(mListener);
mDividerLayout.addView(button);
}
@@ -613,5 +931,69 @@
.setColorLayer()
.build();
}
+
+ private void createVeils() {
+ if (mPrimaryVeil == null) {
+ mPrimaryVeil = createChildSurface("DividerPrimaryVeil", false /* visible */);
+ }
+ if (mSecondaryVeil == null) {
+ mSecondaryVeil = createChildSurface("DividerSecondaryVeil", false /* visible */);
+ }
+ }
+
+ private void removeVeils(@NonNull SurfaceControl.Transaction t) {
+ if (mPrimaryVeil != null) {
+ t.remove(mPrimaryVeil);
+ }
+ if (mSecondaryVeil != null) {
+ t.remove(mSecondaryVeil);
+ }
+ mPrimaryVeil = null;
+ mSecondaryVeil = null;
+ }
+
+ private void showVeils(@NonNull SurfaceControl.Transaction t) {
+ t.setColor(mPrimaryVeil, colorToFloatArray(DEFAULT_PRIMARY_VEIL_COLOR))
+ .setColor(mSecondaryVeil, colorToFloatArray(DEFAULT_SECONDARY_VEIL_COLOR))
+ .setLayer(mDividerSurface, DIVIDER_LAYER)
+ .setLayer(mPrimaryVeil, VEIL_LAYER)
+ .setLayer(mSecondaryVeil, VEIL_LAYER)
+ .setVisibility(mPrimaryVeil, true)
+ .setVisibility(mSecondaryVeil, true);
+ updateVeils(t);
+ }
+
+ private void hideVeils(@NonNull SurfaceControl.Transaction t) {
+ t.setVisibility(mPrimaryVeil, false).setVisibility(mSecondaryVeil, false);
+ }
+
+ private void updateVeils(@NonNull SurfaceControl.Transaction t) {
+ final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+
+ // Relative bounds of the primary and secondary containers in the Task.
+ Rect primaryBounds;
+ Rect secondaryBounds;
+ if (mProperties.mIsVerticalSplit) {
+ final Rect boundsLeft = new Rect(0, 0, mDividerPosition, taskBounds.height());
+ final Rect boundsRight = new Rect(mDividerPosition + mDividerWidthPx, 0,
+ taskBounds.width(), taskBounds.height());
+ primaryBounds = mProperties.mIsReversedLayout ? boundsRight : boundsLeft;
+ secondaryBounds = mProperties.mIsReversedLayout ? boundsLeft : boundsRight;
+ } else {
+ final Rect boundsTop = new Rect(0, 0, taskBounds.width(), mDividerPosition);
+ final Rect boundsBottom = new Rect(0, mDividerPosition + mDividerWidthPx,
+ taskBounds.width(), taskBounds.height());
+ primaryBounds = mProperties.mIsReversedLayout ? boundsBottom : boundsTop;
+ secondaryBounds = mProperties.mIsReversedLayout ? boundsTop : boundsBottom;
+ }
+ t.setWindowCrop(mPrimaryVeil, primaryBounds.width(), primaryBounds.height());
+ t.setWindowCrop(mSecondaryVeil, secondaryBounds.width(), secondaryBounds.height());
+ t.setPosition(mPrimaryVeil, primaryBounds.left, primaryBounds.top);
+ t.setPosition(mSecondaryVeil, secondaryBounds.left, secondaryBounds.top);
+ }
+
+ private static float[] colorToFloatArray(@NonNull Color color) {
+ return new float[]{color.red(), color.green(), color.blue()};
+ }
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 3f4dddf..32f2d67 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -165,7 +165,7 @@
/**
* Expands an existing TaskFragment to fill parent.
* @param wct WindowContainerTransaction in which the task fragment should be resized.
- * @param fragmentToken token of an existing TaskFragment.
+ * @param container the {@link TaskFragmentContainer} to be expanded.
*/
void expandTaskFragment(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container) {
@@ -174,8 +174,6 @@
clearAdjacentTaskFragments(wct, fragmentToken);
updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED);
updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT);
-
- container.getTaskContainer().updateDivider(wct);
}
/**
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitAttributesHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitAttributesHelper.java
new file mode 100644
index 0000000..042a68a6
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitAttributesHelper.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 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 androidx.window.extensions.embedding;
+
+import android.content.res.Configuration;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+/** Helper functions for {@link SplitAttributes} */
+class SplitAttributesHelper {
+ /**
+ * Returns whether the split layout direction is reversed. Right-to-left and bottom-to-top are
+ * considered reversed.
+ */
+ static boolean isReversedLayout(
+ @NonNull SplitAttributes splitAttributes, @NonNull Configuration configuration) {
+ switch (splitAttributes.getLayoutDirection()) {
+ case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT:
+ case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM:
+ return false;
+ case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT:
+ case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP:
+ return true;
+ case SplitAttributes.LayoutDirection.LOCALE:
+ return configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ default:
+ throw new IllegalArgumentException(
+ "Invalid layout direction:" + splitAttributes.getLayoutDirection());
+ }
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 1bc8264..b9b86f0 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -110,7 +110,7 @@
* Main controller class that manages split states and presentation.
*/
public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback,
- ActivityEmbeddingComponent {
+ ActivityEmbeddingComponent, DividerPresenter.DragEventCallback {
static final String TAG = "SplitController";
static final boolean ENABLE_SHELL_TRANSITIONS =
SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
@@ -163,6 +163,10 @@
@GuardedBy("mLock")
final SparseArray<TaskContainer> mTaskContainers = new SparseArray<>();
+ /** Map from Task id to {@link DividerPresenter} which manages the divider in the Task. */
+ @GuardedBy("mLock")
+ private final SparseArray<DividerPresenter> mDividerPresenters = new SparseArray<>();
+
/** Callback to Jetpack to notify about changes to split states. */
@GuardedBy("mLock")
@Nullable
@@ -195,15 +199,16 @@
: null;
private final Handler mHandler;
+ private final MainThreadExecutor mExecutor;
final Object mLock = new Object();
private final ActivityStartMonitor mActivityStartMonitor;
public SplitController(@NonNull WindowLayoutComponentImpl windowLayoutComponent,
@NonNull DeviceStateManagerFoldingFeatureProducer foldingFeatureProducer) {
Log.i(TAG, "Initializing Activity Embedding Controller.");
- final MainThreadExecutor executor = new MainThreadExecutor();
- mHandler = executor.mHandler;
- mPresenter = new SplitPresenter(executor, windowLayoutComponent, this);
+ mExecutor = new MainThreadExecutor();
+ mHandler = mExecutor.mHandler;
+ mPresenter = new SplitPresenter(mExecutor, windowLayoutComponent, this);
mTransactionManager = new TransactionManager(mPresenter);
final ActivityThread activityThread = ActivityThread.currentActivityThread();
final Application application = activityThread.getApplication();
@@ -844,7 +849,11 @@
// Checks if container should be updated before apply new parentInfo.
final boolean shouldUpdateContainer = taskContainer.shouldUpdateContainer(parentInfo);
taskContainer.updateTaskFragmentParentInfo(parentInfo);
- taskContainer.updateDivider(wct);
+
+ // The divider need to be updated even if shouldUpdateContainer is false, because the decor
+ // surface may change in TaskFragmentParentInfo, which requires divider update but not
+ // container update.
+ updateDivider(wct, taskContainer);
// If the last direct activity of the host task is dismissed and the overlay container is
// the only taskFragment, the overlay container should also be dismissed.
@@ -1007,6 +1016,7 @@
if (taskContainer.isEmpty()) {
// Cleanup the TaskContainer if it becomes empty.
mTaskContainers.remove(taskContainer.getTaskId());
+ mDividerPresenters.remove(taskContainer.getTaskId());
}
return;
}
@@ -1759,6 +1769,7 @@
}
if (!mTaskContainers.contains(taskId)) {
mTaskContainers.put(taskId, new TaskContainer(taskId, activityInTask));
+ mDividerPresenters.put(taskId, new DividerPresenter(taskId, this, mExecutor));
}
final TaskContainer taskContainer = mTaskContainers.get(taskId);
final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity,
@@ -3065,4 +3076,46 @@
return configuration != null
&& configuration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_PINNED;
}
+
+ @GuardedBy("mLock")
+ void updateDivider(
+ @NonNull WindowContainerTransaction wct, @NonNull TaskContainer taskContainer) {
+ final DividerPresenter dividerPresenter = mDividerPresenters.get(taskContainer.getTaskId());
+ final TaskFragmentParentInfo parentInfo = taskContainer.getTaskFragmentParentInfo();
+ if (parentInfo != null) {
+ dividerPresenter.updateDivider(
+ wct, parentInfo, taskContainer.getTopNonFinishingSplitContainer());
+ }
+ }
+
+ @Override
+ public void onStartDragging(@NonNull Consumer<WindowContainerTransaction> action) {
+ synchronized (mLock) {
+ final TransactionRecord transactionRecord =
+ mTransactionManager.startNewTransaction();
+ final WindowContainerTransaction wct = transactionRecord.getTransaction();
+ action.accept(wct);
+ transactionRecord.apply(false /* shouldApplyIndependently */);
+ }
+ }
+
+ @Override
+ public void onFinishDragging(
+ int taskId,
+ @NonNull Consumer<WindowContainerTransaction> action) {
+ synchronized (mLock) {
+ final TransactionRecord transactionRecord =
+ mTransactionManager.startNewTransaction();
+ final WindowContainerTransaction wct = transactionRecord.getTransaction();
+ final TaskContainer taskContainer = mTaskContainers.get(taskId);
+ if (taskContainer != null) {
+ final DividerPresenter dividerPresenter =
+ mDividerPresenters.get(taskContainer.getTaskId());
+ taskContainer.updateTopSplitContainerForDivider(dividerPresenter);
+ updateContainersInTask(wct, taskContainer);
+ }
+ action.accept(wct);
+ transactionRecord.apply(false /* shouldApplyIndependently */);
+ }
+ }
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 20bc820..0d31266 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -19,6 +19,7 @@
import static android.content.pm.PackageManager.MATCH_ALL;
import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider;
+import static androidx.window.extensions.embedding.SplitAttributesHelper.isReversedLayout;
import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
import android.app.Activity;
@@ -33,7 +34,6 @@
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
-import android.util.LayoutDirection;
import android.util.Pair;
import android.util.Size;
import android.view.View;
@@ -368,7 +368,7 @@
updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode);
updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes);
updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes);
- taskContainer.updateDivider(wct);
+ mController.updateDivider(wct, taskContainer);
}
private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
@@ -697,6 +697,17 @@
return RESULT_NOT_EXPANDED;
}
+ /**
+ * Expands an existing TaskFragment to fill parent.
+ * @param wct WindowContainerTransaction in which the task fragment should be resized.
+ * @param container the {@link TaskFragmentContainer} to be expanded.
+ */
+ void expandTaskFragment(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentContainer container) {
+ super.expandTaskFragment(wct, container);
+ mController.updateDivider(wct, container.getTaskContainer());
+ }
+
static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) {
return shouldShowSplit(splitContainer.getCurrentSplitAttributes());
}
@@ -1108,7 +1119,6 @@
*/
private SplitType computeSplitType(@NonNull SplitAttributes splitAttributes,
@NonNull Configuration taskConfiguration, @Nullable FoldingFeature foldingFeature) {
- final int layoutDirection = splitAttributes.getLayoutDirection();
final SplitType splitType = splitAttributes.getSplitType();
if (splitType instanceof ExpandContainersSplitType) {
return splitType;
@@ -1117,19 +1127,9 @@
// Reverse the ratio for RIGHT_TO_LEFT and BOTTOM_TO_TOP to make the boundary
// computation have the same direction, which is from (top, left) to (bottom, right).
final SplitType reversedSplitType = new RatioSplitType(1 - splitRatio.getRatio());
- switch (layoutDirection) {
- case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT:
- case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM:
- return splitType;
- case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT:
- case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP:
- return reversedSplitType;
- case LayoutDirection.LOCALE: {
- boolean isLtr = taskConfiguration.getLayoutDirection()
- == View.LAYOUT_DIRECTION_LTR;
- return isLtr ? splitType : reversedSplitType;
- }
- }
+ return isReversedLayout(splitAttributes, taskConfiguration)
+ ? reversedSplitType
+ : splitType;
} else if (splitType instanceof HingeSplitType) {
final HingeSplitType hinge = (HingeSplitType) splitType;
@WindowingMode
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index e75a317..a215bdf 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -88,10 +88,6 @@
*/
final Set<IBinder> mFinishedContainer = new ArraySet<>();
- // TODO(b/293654166): move DividerPresenter to SplitController.
- @NonNull
- final DividerPresenter mDividerPresenter;
-
/**
* The {@link TaskContainer} constructor
*
@@ -113,7 +109,6 @@
// the host task is visible and has an activity in the task.
mIsVisible = true;
mHasDirectActivity = true;
- mDividerPresenter = new DividerPresenter();
}
int getTaskId() {
@@ -151,6 +146,11 @@
mTaskFragmentParentInfo = info;
}
+ @Nullable
+ TaskFragmentParentInfo getTaskFragmentParentInfo() {
+ return mTaskFragmentParentInfo;
+ }
+
/**
* Returns {@code true} if the container should be updated with {@code info}.
*/
@@ -398,16 +398,22 @@
return mContainers;
}
- void updateDivider(@NonNull WindowContainerTransaction wct) {
- if (mTaskFragmentParentInfo != null) {
- // Update divider only if TaskFragmentParentInfo is available.
- mDividerPresenter.updateDivider(
- wct, mTaskFragmentParentInfo, getTopNonFinishingSplitContainer());
+ void updateTopSplitContainerForDivider(@NonNull DividerPresenter dividerPresenter) {
+ final SplitContainer topSplitContainer = getTopNonFinishingSplitContainer();
+ if (topSplitContainer == null) {
+ return;
}
+
+ final float newRatio = dividerPresenter.calculateNewSplitRatio(topSplitContainer);
+ topSplitContainer.updateDefaultSplitAttributes(
+ new SplitAttributes.Builder(topSplitContainer.getDefaultSplitAttributes())
+ .setSplitType(new SplitAttributes.SplitType.RatioSplitType(newRatio))
+ .build()
+ );
}
@Nullable
- private SplitContainer getTopNonFinishingSplitContainer() {
+ SplitContainer getTopNonFinishingSplitContainer() {
for (int i = mSplitContainers.size() - 1; i >= 0; i--) {
final SplitContainer splitContainer = mSplitContainers.get(i);
if (!splitContainer.getPrimaryContainer().isFinished()
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
index 4d1d807..47d01da 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
@@ -42,6 +42,7 @@
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
import android.view.Display;
+import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.window.TaskFragmentOperation;
import android.window.TaskFragmentParentInfo;
@@ -60,6 +61,8 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.concurrent.Executor;
+
/**
* Test class for {@link DividerPresenter}.
*
@@ -73,6 +76,8 @@
@Rule
public final SetFlagsRule mSetFlagRule = new SetFlagsRule();
+ private static final int MOCK_TASK_ID = 1234;
+
@Mock
private DividerPresenter.Renderer mRenderer;
@@ -83,6 +88,12 @@
private TaskFragmentParentInfo mParentInfo;
@Mock
+ private TaskContainer mTaskContainer;
+
+ @Mock
+ private DividerPresenter.DragEventCallback mDragEventCallback;
+
+ @Mock
private SplitContainer mSplitContainer;
@Mock
@@ -110,6 +121,8 @@
MockitoAnnotations.initMocks(this);
mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_INTERACTIVE_DIVIDER_FLAG);
+ when(mTaskContainer.getTaskId()).thenReturn(MOCK_TASK_ID);
+
when(mParentInfo.getDisplayId()).thenReturn(Display.DEFAULT_DISPLAY);
when(mParentInfo.getConfiguration()).thenReturn(new Configuration());
when(mParentInfo.getDecorSurface()).thenReturn(mSurfaceControl);
@@ -133,9 +146,11 @@
mSurfaceControl,
getInitialDividerPosition(mSplitContainer),
true /* isVerticalSplit */,
+ false /* isReversedLayout */,
Display.DEFAULT_DISPLAY);
- mDividerPresenter = new DividerPresenter();
+ mDividerPresenter = new DividerPresenter(
+ MOCK_TASK_ID, mDragEventCallback, mock(Executor.class));
mDividerPresenter.mProperties = mProperties;
mDividerPresenter.mRenderer = mRenderer;
mDividerPresenter.mDecorSurfaceOwner = mPrimaryContainerToken;
@@ -311,6 +326,184 @@
dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset);
}
+ @Test
+ public void testCalculateDividerPosition() {
+ final MotionEvent event = mock(MotionEvent.class);
+ final Rect taskBounds = new Rect(100, 200, 1000, 2000);
+ final int dividerWidthPx = 50;
+ final DividerAttributes dividerAttributes =
+ new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+ .setPrimaryMinRatio(0.3f)
+ .setPrimaryMaxRatio(0.8f)
+ .build();
+
+ // Left-to-right split
+ when(event.getRawX()).thenReturn(500f); // Touch event is in display space
+ assertEquals(
+ // Touch position is in task space is 400, then minus half of divider width.
+ 375,
+ DividerPresenter.calculateDividerPosition(
+ event,
+ taskBounds,
+ dividerWidthPx,
+ dividerAttributes,
+ true /* isVerticalSplit */,
+ false /* isReversedLayout */));
+
+ // Top-to-bottom split
+ when(event.getRawY()).thenReturn(1000f); // Touch event is in display space
+ assertEquals(
+ // Touch position is in task space is 800, then minus half of divider width.
+ 775,
+ DividerPresenter.calculateDividerPosition(
+ event,
+ taskBounds,
+ dividerWidthPx,
+ dividerAttributes,
+ false /* isVerticalSplit */,
+ false /* isReversedLayout */));
+ }
+
+ @Test
+ public void testCalculateMinPosition() {
+ final Rect taskBounds = new Rect(100, 200, 1000, 2000);
+ final int dividerWidthPx = 50;
+ final DividerAttributes dividerAttributes =
+ new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+ .setPrimaryMinRatio(0.3f)
+ .setPrimaryMaxRatio(0.8f)
+ .build();
+
+ // Left-to-right split
+ assertEquals(
+ 255, /* (1000 - 100 - 50) * 0.3 */
+ DividerPresenter.calculateMinPosition(
+ taskBounds,
+ dividerWidthPx,
+ dividerAttributes,
+ true /* isVerticalSplit */,
+ false /* isReversedLayout */));
+
+ // Top-to-bottom split
+ assertEquals(
+ 525, /* (2000 - 200 - 50) * 0.3 */
+ DividerPresenter.calculateMinPosition(
+ taskBounds,
+ dividerWidthPx,
+ dividerAttributes,
+ false /* isVerticalSplit */,
+ false /* isReversedLayout */));
+
+ // Right-to-left split
+ assertEquals(
+ 170, /* (1000 - 100 - 50) * (1 - 0.8) */
+ DividerPresenter.calculateMinPosition(
+ taskBounds,
+ dividerWidthPx,
+ dividerAttributes,
+ true /* isVerticalSplit */,
+ true /* isReversedLayout */));
+ }
+
+ @Test
+ public void testCalculateMaxPosition() {
+ final Rect taskBounds = new Rect(100, 200, 1000, 2000);
+ final int dividerWidthPx = 50;
+ final DividerAttributes dividerAttributes =
+ new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+ .setPrimaryMinRatio(0.3f)
+ .setPrimaryMaxRatio(0.8f)
+ .build();
+
+ // Left-to-right split
+ assertEquals(
+ 680, /* (1000 - 100 - 50) * 0.8 */
+ DividerPresenter.calculateMaxPosition(
+ taskBounds,
+ dividerWidthPx,
+ dividerAttributes,
+ true /* isVerticalSplit */,
+ false /* isReversedLayout */));
+
+ // Top-to-bottom split
+ assertEquals(
+ 1400, /* (2000 - 200 - 50) * 0.8 */
+ DividerPresenter.calculateMaxPosition(
+ taskBounds,
+ dividerWidthPx,
+ dividerAttributes,
+ false /* isVerticalSplit */,
+ false /* isReversedLayout */));
+
+ // Right-to-left split
+ assertEquals(
+ 595, /* (1000 - 100 - 50) * (1 - 0.3) */
+ DividerPresenter.calculateMaxPosition(
+ taskBounds,
+ dividerWidthPx,
+ dividerAttributes,
+ true /* isVerticalSplit */,
+ true /* isReversedLayout */));
+ }
+
+ @Test
+ public void testCalculateNewSplitRatio_leftToRight() {
+ // primary=500px; secondary=500px; divider=100px; total=1100px.
+ final Rect taskBounds = new Rect(0, 0, 1100, 2000);
+ final Rect primaryBounds = new Rect(0, 0, 500, 2000);
+ final Rect secondaryBounds = new Rect(600, 0, 1100, 2000);
+ final int dividerWidthPx = 100;
+ final int dividerPosition = 300;
+
+ final TaskFragmentContainer mockPrimaryContainer =
+ createMockTaskFragmentContainer(mPrimaryContainerToken, primaryBounds);
+ final TaskFragmentContainer mockSecondaryContainer =
+ createMockTaskFragmentContainer(mSecondaryContainerToken, secondaryBounds);
+ when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer);
+ when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer);
+
+ assertEquals(
+ 0.3f, // Primary is 300px after dragging.
+ DividerPresenter.calculateNewSplitRatio(
+ mSplitContainer,
+ dividerPosition,
+ taskBounds,
+ dividerWidthPx,
+ true /* isVerticalSplit */,
+ false /* isReversedLayout */),
+ 0.0001 /* delta */);
+ }
+
+ @Test
+ public void testCalculateNewSplitRatio_bottomToTop() {
+ // Primary is at bottom. Secondary is at top.
+ // primary=500px; secondary=500px; divider=100px; total=1100px.
+ final Rect taskBounds = new Rect(0, 0, 2000, 1100);
+ final Rect primaryBounds = new Rect(0, 0, 2000, 1100);
+ final Rect secondaryBounds = new Rect(0, 0, 2000, 500);
+ final int dividerWidthPx = 100;
+ final int dividerPosition = 300;
+
+ final TaskFragmentContainer mockPrimaryContainer =
+ createMockTaskFragmentContainer(mPrimaryContainerToken, primaryBounds);
+ final TaskFragmentContainer mockSecondaryContainer =
+ createMockTaskFragmentContainer(mSecondaryContainerToken, secondaryBounds);
+ when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer);
+ when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer);
+
+ assertEquals(
+ // After dragging, secondary is [0, 0, 2000, 300]. Primary is [0, 400, 2000, 1100].
+ 0.7f,
+ DividerPresenter.calculateNewSplitRatio(
+ mSplitContainer,
+ dividerPosition,
+ taskBounds,
+ dividerWidthPx,
+ false /* isVerticalSplit */,
+ true /* isReversedLayout */),
+ 0.0001 /* delta */);
+ }
+
private TaskFragmentContainer createMockTaskFragmentContainer(
@NonNull IBinder token, @NonNull Rect bounds) {
final TaskFragmentContainer container = mock(TaskFragmentContainer.class);