blob: 4c85d2090ec53b6d21f8302bd96a763388ee71e8 [file] [log] [blame]
/*
* 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.SCREEN_HEIGHT_DP_UNDEFINED;
import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED;
import static android.view.WindowManager.DOCKED_BOTTOM;
import static android.view.WindowManager.DOCKED_INVALID;
import static android.view.WindowManager.DOCKED_LEFT;
import static android.view.WindowManager.DOCKED_RIGHT;
import static android.view.WindowManager.DOCKED_TOP;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE;
import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END;
import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START;
import static com.android.wm.shell.animation.Interpolators.DIM_INTERPOLATOR;
import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.view.Display;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.RoundedCorner;
import android.view.SurfaceControl;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.DividerSnapAlgorithm;
import com.android.internal.policy.DockedDividerUtils;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.animation.Interpolators;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.InteractionJankMonitorUtils;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
import java.io.PrintWriter;
/**
* Records and handles layout of splits. Helps to calculate proper bounds when configuration or
* divide position changes.
*/
public final class SplitLayout implements DisplayInsetsController.OnInsetsChangedListener {
public static final int PARALLAX_NONE = 0;
public static final int PARALLAX_DISMISSING = 1;
public static final int PARALLAX_ALIGN_CENTER = 2;
private static final int FLING_RESIZE_DURATION = 250;
private static final int FLING_SWITCH_DURATION = 350;
private static final int FLING_ENTER_DURATION = 350;
private static final int FLING_EXIT_DURATION = 350;
private final int mDividerWindowWidth;
private final int mDividerInsets;
private final int mDividerSize;
private final Rect mTempRect = new Rect();
private final Rect mRootBounds = new Rect();
private final Rect mDividerBounds = new Rect();
// Bounds1 final position should be always at top or left
private final Rect mBounds1 = new Rect();
// Bounds2 final position should be always at bottom or right
private final Rect mBounds2 = new Rect();
// The temp bounds outside of display bounds for side stage when split screen inactive to avoid
// flicker next time active split screen.
private final Rect mInvisibleBounds = new Rect();
private final Rect mWinBounds1 = new Rect();
private final Rect mWinBounds2 = new Rect();
private final SplitLayoutHandler mSplitLayoutHandler;
private final SplitWindowManager mSplitWindowManager;
private final DisplayImeController mDisplayImeController;
private final ImePositionProcessor mImePositionProcessor;
private final ResizingEffectPolicy mSurfaceEffectPolicy;
private final ShellTaskOrganizer mTaskOrganizer;
private final InsetsState mInsetsState = new InsetsState();
private Context mContext;
@VisibleForTesting DividerSnapAlgorithm mDividerSnapAlgorithm;
private WindowContainerToken mWinToken1;
private WindowContainerToken mWinToken2;
private int mDividePosition;
private boolean mInitialized = false;
private boolean mFreezeDividerWindow = false;
private int mOrientation;
private int mRotation;
private final boolean mDimNonImeSide;
public SplitLayout(String windowName, Context context, Configuration configuration,
SplitLayoutHandler splitLayoutHandler,
SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks,
DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer,
int parallaxType) {
mContext = context.createConfigurationContext(configuration);
mOrientation = configuration.orientation;
mRotation = configuration.windowConfiguration.getRotation();
mSplitLayoutHandler = splitLayoutHandler;
mDisplayImeController = displayImeController;
mSplitWindowManager = new SplitWindowManager(windowName, mContext, configuration,
parentContainerCallbacks);
mTaskOrganizer = taskOrganizer;
mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId());
mSurfaceEffectPolicy = new ResizingEffectPolicy(parallaxType);
final Resources resources = context.getResources();
mDividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width);
mDividerInsets = getDividerInsets(resources, context.getDisplay());
mDividerWindowWidth = mDividerSize + 2 * mDividerInsets;
mRootBounds.set(configuration.windowConfiguration.getBounds());
mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null);
resetDividerPosition();
mDimNonImeSide = resources.getBoolean(R.bool.config_dimNonImeAttachedSide);
updateInvisibleRect();
}
private int getDividerInsets(Resources resources, Display display) {
final int dividerInset = resources.getDimensionPixelSize(
com.android.internal.R.dimen.docked_stack_divider_insets);
int radius = 0;
RoundedCorner corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT);
radius = corner != null ? Math.max(radius, corner.getRadius()) : radius;
corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT);
radius = corner != null ? Math.max(radius, corner.getRadius()) : radius;
corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT);
radius = corner != null ? Math.max(radius, corner.getRadius()) : radius;
corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT);
radius = corner != null ? Math.max(radius, corner.getRadius()) : radius;
return Math.max(dividerInset, radius);
}
/** Gets bounds of the primary split with screen based coordinate. */
public Rect getBounds1() {
return new Rect(mBounds1);
}
/** Gets bounds of the primary split with parent based coordinate. */
public Rect getRefBounds1() {
Rect outBounds = getBounds1();
outBounds.offset(-mRootBounds.left, -mRootBounds.top);
return outBounds;
}
/** Gets bounds of the secondary split with screen based coordinate. */
public Rect getBounds2() {
return new Rect(mBounds2);
}
/** Gets bounds of the secondary split with parent based coordinate. */
public Rect getRefBounds2() {
final Rect outBounds = getBounds2();
outBounds.offset(-mRootBounds.left, -mRootBounds.top);
return outBounds;
}
/** Gets root bounds of the whole split layout */
public Rect getRootBounds() {
return new Rect(mRootBounds);
}
/** Gets bounds of divider window with screen based coordinate. */
public Rect getDividerBounds() {
return new Rect(mDividerBounds);
}
/** Gets bounds of divider window with parent based coordinate. */
public Rect getRefDividerBounds() {
final Rect outBounds = getDividerBounds();
outBounds.offset(-mRootBounds.left, -mRootBounds.top);
return outBounds;
}
/** Gets bounds of the primary split with screen based coordinate on the param Rect. */
public void getBounds1(Rect rect) {
rect.set(mBounds1);
}
/** Gets bounds of the primary split with parent based coordinate on the param Rect. */
public void getRefBounds1(Rect rect) {
getBounds1(rect);
rect.offset(-mRootBounds.left, -mRootBounds.top);
}
/** Gets bounds of the secondary split with screen based coordinate on the param Rect. */
public void getBounds2(Rect rect) {
rect.set(mBounds2);
}
/** Gets bounds of the secondary split with parent based coordinate on the param Rect. */
public void getRefBounds2(Rect rect) {
getBounds2(rect);
rect.offset(-mRootBounds.left, -mRootBounds.top);
}
/** Gets root bounds of the whole split layout on the param Rect. */
public void getRootBounds(Rect rect) {
rect.set(mRootBounds);
}
/** Gets bounds of divider window with screen based coordinate on the param Rect. */
public void getDividerBounds(Rect rect) {
rect.set(mDividerBounds);
}
/** Gets bounds of divider window with parent based coordinate on the param Rect. */
public void getRefDividerBounds(Rect rect) {
getDividerBounds(rect);
rect.offset(-mRootBounds.left, -mRootBounds.top);
}
/** Gets bounds size equal to root bounds but outside of screen, used for position side stage
* when split inactive to avoid flicker when next time active. */
public void getInvisibleBounds(Rect rect) {
rect.set(mInvisibleBounds);
}
/** Returns leash of the current divider bar. */
@Nullable
public SurfaceControl getDividerLeash() {
return mSplitWindowManager == null ? null : mSplitWindowManager.getSurfaceControl();
}
int getDividePosition() {
return mDividePosition;
}
/**
* Returns the divider position as a fraction from 0 to 1.
*/
public float getDividerPositionAsFraction() {
return Math.min(1f, Math.max(0f, isLandscape()
? (float) ((mBounds1.right + mBounds2.left) / 2f) / mBounds2.right
: (float) ((mBounds1.bottom + mBounds2.top) / 2f) / mBounds2.bottom));
}
private void updateInvisibleRect() {
mInvisibleBounds.set(mRootBounds.left, mRootBounds.top,
isLandscape() ? mRootBounds.right / 2 : mRootBounds.right,
isLandscape() ? mRootBounds.bottom : mRootBounds.bottom / 2);
mInvisibleBounds.offset(isLandscape() ? mRootBounds.right : 0,
isLandscape() ? 0 : mRootBounds.bottom);
}
/** Applies new configuration, returns {@code false} if there's no effect to the layout. */
public boolean updateConfiguration(Configuration configuration) {
// Update the split bounds when necessary. Besides root bounds changed, split bounds need to
// be updated when the rotation changed to cover the case that users rotated the screen 180
// degrees.
// Make sure to render the divider bar with proper resources that matching the screen
// orientation.
final int rotation = configuration.windowConfiguration.getRotation();
final Rect rootBounds = configuration.windowConfiguration.getBounds();
final int orientation = configuration.orientation;
if (mOrientation == orientation
&& mRotation == rotation
&& mRootBounds.equals(rootBounds)) {
return false;
}
mContext = mContext.createConfigurationContext(configuration);
mSplitWindowManager.setConfiguration(configuration);
mOrientation = orientation;
mTempRect.set(mRootBounds);
mRootBounds.set(rootBounds);
mRotation = rotation;
mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null);
initDividerPosition(mTempRect);
updateInvisibleRect();
return true;
}
/** Rotate the layout to specific rotation and calculate new bounds. The stable insets value
* should be calculated by display layout. */
public void rotateTo(int newRotation, Rect stableInsets) {
final int rotationDelta = (newRotation - mRotation + 4) % 4;
final boolean changeOrient = (rotationDelta % 2) != 0;
mRotation = newRotation;
Rect tmpRect = new Rect(mRootBounds);
if (changeOrient) {
tmpRect.set(mRootBounds.top, mRootBounds.left, mRootBounds.bottom, mRootBounds.right);
}
// We only need new bounds here, other configuration should be update later.
mTempRect.set(mRootBounds);
mRootBounds.set(tmpRect);
mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, stableInsets);
initDividerPosition(mTempRect);
}
private void initDividerPosition(Rect oldBounds) {
final float snapRatio = (float) mDividePosition
/ (float) (isLandscape(oldBounds) ? oldBounds.width() : oldBounds.height());
// Estimate position by previous ratio.
final float length =
(float) (isLandscape() ? mRootBounds.width() : mRootBounds.height());
final int estimatePosition = (int) (length * snapRatio);
// Init divider position by estimated position using current bounds snap algorithm.
mDividePosition = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget(
estimatePosition).position;
updateBounds(mDividePosition);
}
private void updateBounds(int position) {
updateBounds(position, mBounds1, mBounds2, mDividerBounds, true /* setEffectBounds */);
}
/** Updates recording bounds of divider window and both of the splits. */
private void updateBounds(int position, Rect bounds1, Rect bounds2, Rect dividerBounds,
boolean setEffectBounds) {
dividerBounds.set(mRootBounds);
bounds1.set(mRootBounds);
bounds2.set(mRootBounds);
final boolean isLandscape = isLandscape(mRootBounds);
if (isLandscape) {
position += mRootBounds.left;
dividerBounds.left = position - mDividerInsets;
dividerBounds.right = dividerBounds.left + mDividerWindowWidth;
bounds1.right = position;
bounds2.left = bounds1.right + mDividerSize;
} else {
position += mRootBounds.top;
dividerBounds.top = position - mDividerInsets;
dividerBounds.bottom = dividerBounds.top + mDividerWindowWidth;
bounds1.bottom = position;
bounds2.top = bounds1.bottom + mDividerSize;
}
DockedDividerUtils.sanitizeStackBounds(bounds1, true /** topLeft */);
DockedDividerUtils.sanitizeStackBounds(bounds2, false /** topLeft */);
if (setEffectBounds) {
mSurfaceEffectPolicy.applyDividerPosition(position, isLandscape);
}
}
/** Inflates {@link DividerView} on the root surface. */
public void init() {
if (mInitialized) return;
mInitialized = true;
mSplitWindowManager.init(this, mInsetsState);
mDisplayImeController.addPositionProcessor(mImePositionProcessor);
}
/** Releases the surface holding the current {@link DividerView}. */
public void release(SurfaceControl.Transaction t) {
if (!mInitialized) return;
mInitialized = false;
mSplitWindowManager.release(t);
mDisplayImeController.removePositionProcessor(mImePositionProcessor);
mImePositionProcessor.reset();
}
public void release() {
release(null /* t */);
}
/** Releases and re-inflates {@link DividerView} on the root surface. */
public void update(SurfaceControl.Transaction t) {
if (!mInitialized) return;
mSplitWindowManager.release(t);
mImePositionProcessor.reset();
mSplitWindowManager.init(this, mInsetsState);
}
@Override
public void insetsChanged(InsetsState insetsState) {
mInsetsState.set(insetsState);
if (!mInitialized) {
return;
}
if (mFreezeDividerWindow) {
// DO NOT change its layout before transition actually run because it might cause
// flicker.
return;
}
mSplitWindowManager.onInsetsChanged(insetsState);
}
@Override
public void insetsControlChanged(InsetsState insetsState,
InsetsSourceControl[] activeControls) {
if (!mInsetsState.equals(insetsState)) {
insetsChanged(insetsState);
}
}
public void setFreezeDividerWindow(boolean freezeDividerWindow) {
mFreezeDividerWindow = freezeDividerWindow;
}
/** Update current layout as divider put on start or end position. */
public void setDividerAtBorder(boolean start) {
final int pos = start ? mDividerSnapAlgorithm.getDismissStartTarget().position
: mDividerSnapAlgorithm.getDismissEndTarget().position;
setDividePosition(pos, false /* applyLayoutChange */);
}
/**
* Updates bounds with the passing position. Usually used to update recording bounds while
* performing animation or dragging divider bar to resize the splits.
*/
void updateDivideBounds(int position) {
updateBounds(position);
mSplitLayoutHandler.onLayoutSizeChanging(this);
}
void setDividePosition(int position, boolean applyLayoutChange) {
mDividePosition = position;
updateBounds(mDividePosition);
if (applyLayoutChange) {
mSplitLayoutHandler.onLayoutSizeChanged(this);
}
}
/** Updates divide position and split bounds base on the ratio within root bounds. */
public void setDivideRatio(float ratio) {
final int position = isLandscape()
? mRootBounds.left + (int) (mRootBounds.width() * ratio)
: mRootBounds.top + (int) (mRootBounds.height() * ratio);
final DividerSnapAlgorithm.SnapTarget snapTarget =
mDividerSnapAlgorithm.calculateNonDismissingSnapTarget(position);
setDividePosition(snapTarget.position, false /* applyLayoutChange */);
}
/** Resets divider position. */
public void resetDividerPosition() {
mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position;
updateBounds(mDividePosition);
mWinToken1 = null;
mWinToken2 = null;
mWinBounds1.setEmpty();
mWinBounds2.setEmpty();
}
/**
* Sets new divide position and updates bounds correspondingly. Notifies listener if the new
* target indicates dismissing split.
*/
public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) {
switch (snapTarget.flag) {
case FLAG_DISMISS_START:
flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
() -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */,
EXIT_REASON_DRAG_DIVIDER));
break;
case FLAG_DISMISS_END:
flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
() -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */,
EXIT_REASON_DRAG_DIVIDER));
break;
default:
flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
() -> setDividePosition(snapTarget.position, true /* applyLayoutChange */));
break;
}
}
void onStartDragging() {
InteractionJankMonitorUtils.beginTracing(CUJ_SPLIT_SCREEN_RESIZE, mContext,
getDividerLeash(), null /* tag */);
}
void onDraggingCancelled() {
InteractionJankMonitorUtils.cancelTracing(CUJ_SPLIT_SCREEN_RESIZE);
}
void onDoubleTappedDivider() {
mSplitLayoutHandler.onDoubleTappedDivider();
}
/**
* Returns {@link DividerSnapAlgorithm.SnapTarget} which matches passing position and velocity.
* If hardDismiss is set to {@code true}, it will be harder to reach dismiss target.
*/
public DividerSnapAlgorithm.SnapTarget findSnapTarget(int position, float velocity,
boolean hardDismiss) {
return mDividerSnapAlgorithm.calculateSnapTarget(position, velocity, hardDismiss);
}
private DividerSnapAlgorithm getSnapAlgorithm(Context context, Rect rootBounds,
@Nullable Rect stableInsets) {
final boolean isLandscape = isLandscape(rootBounds);
final Rect insets = stableInsets != null ? stableInsets : getDisplayInsets(context);
// Make split axis insets value same as the larger one to avoid bounds1 and bounds2
// have difference for avoiding size-compat mode when switching unresizable apps in
// landscape while they are letterboxed.
if (!isLandscape) {
final int largerInsets = Math.max(insets.top, insets.bottom);
insets.set(insets.left, largerInsets, insets.right, largerInsets);
}
return new DividerSnapAlgorithm(
context.getResources(),
rootBounds.width(),
rootBounds.height(),
mDividerSize,
!isLandscape,
insets,
isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */);
}
/** Fling divider from current position to end or start position then exit */
public void flingDividerToDismiss(boolean toEnd, int reason) {
final int target = toEnd ? mDividerSnapAlgorithm.getDismissEndTarget().position
: mDividerSnapAlgorithm.getDismissStartTarget().position;
flingDividePosition(getDividePosition(), target, FLING_EXIT_DURATION,
() -> mSplitLayoutHandler.onSnappedToDismiss(toEnd, reason));
}
/** Fling divider from current position to center position. */
public void flingDividerToCenter() {
final int pos = mDividerSnapAlgorithm.getMiddleTarget().position;
flingDividePosition(getDividePosition(), pos, FLING_ENTER_DURATION,
() -> setDividePosition(pos, true /* applyLayoutChange */));
}
@VisibleForTesting
void flingDividePosition(int from, int to, int duration,
@Nullable Runnable flingFinishedCallback) {
if (from == to) {
// No animation run, still callback to stop resizing.
mSplitLayoutHandler.onLayoutSizeChanged(this);
InteractionJankMonitorUtils.endTracing(
CUJ_SPLIT_SCREEN_RESIZE);
return;
}
ValueAnimator animator = ValueAnimator
.ofInt(from, to)
.setDuration(duration);
animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
animator.addUpdateListener(
animation -> updateDivideBounds((int) animation.getAnimatedValue()));
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (flingFinishedCallback != null) {
flingFinishedCallback.run();
}
InteractionJankMonitorUtils.endTracing(
CUJ_SPLIT_SCREEN_RESIZE);
}
@Override
public void onAnimationCancel(Animator animation) {
setDividePosition(to, true /* applyLayoutChange */);
}
});
animator.start();
}
/** Swich both surface position with animation. */
public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1,
SurfaceControl leash2, Runnable finishCallback) {
final boolean isLandscape = isLandscape();
final Rect insets = getDisplayInsets(mContext);
insets.set(isLandscape ? insets.left : 0, isLandscape ? 0 : insets.top,
isLandscape ? insets.right : 0, isLandscape ? 0 : insets.bottom);
final int dividerPos = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget(
isLandscape ? mBounds2.width() : mBounds2.height()).position;
final Rect distBounds1 = new Rect();
final Rect distBounds2 = new Rect();
final Rect distDividerBounds = new Rect();
// Compute dist bounds.
updateBounds(dividerPos, distBounds2, distBounds1, distDividerBounds,
false /* setEffectBounds */);
// Offset to real position under root container.
distBounds1.offset(-mRootBounds.left, -mRootBounds.top);
distBounds2.offset(-mRootBounds.left, -mRootBounds.top);
distDividerBounds.offset(-mRootBounds.left, -mRootBounds.top);
// DO NOT move to insets area for smooth animation.
distBounds1.set(distBounds1.left, distBounds1.top,
distBounds1.right - insets.right, distBounds1.bottom - insets.bottom);
distBounds2.set(distBounds2.left + insets.left, distBounds2.top + insets.top,
distBounds2.right, distBounds2.bottom);
ValueAnimator animator1 = moveSurface(t, leash1, getRefBounds1(), distBounds1,
false /* alignStart */);
ValueAnimator animator2 = moveSurface(t, leash2, getRefBounds2(), distBounds2,
true /* alignStart */);
ValueAnimator animator3 = moveSurface(t, getDividerLeash(), getRefDividerBounds(),
distDividerBounds, true /* alignStart */);
AnimatorSet set = new AnimatorSet();
set.playTogether(animator1, animator2, animator3);
set.setDuration(FLING_SWITCH_DURATION);
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mDividePosition = dividerPos;
updateBounds(mDividePosition);
finishCallback.run();
}
});
set.start();
}
private ValueAnimator moveSurface(SurfaceControl.Transaction t, SurfaceControl leash,
Rect start, Rect end, boolean alignStart) {
Rect tempStart = new Rect(start);
Rect tempEnd = new Rect(end);
final float diffX = tempEnd.left - tempStart.left;
final float diffY = tempEnd.top - tempStart.top;
final float diffWidth = tempEnd.width() - tempStart.width();
final float diffHeight = tempEnd.height() - tempStart.height();
ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.addUpdateListener(animation -> {
if (leash == null) return;
final float scale = (float) animation.getAnimatedValue();
final float distX = tempStart.left + scale * diffX;
final float distY = tempStart.top + scale * diffY;
final int width = (int) (tempStart.width() + scale * diffWidth);
final int height = (int) (tempStart.height() + scale * diffHeight);
if (alignStart) {
t.setPosition(leash, distX, distY);
t.setWindowCrop(leash, width, height);
} else {
final int offsetX = width - tempStart.width();
final int offsetY = height - tempStart.height();
t.setPosition(leash, distX + offsetX, distY + offsetY);
mTempRect.set(0, 0, width, height);
mTempRect.offsetTo(-offsetX, -offsetY);
t.setCrop(leash, mTempRect);
}
t.apply();
});
return animator;
}
private static Rect getDisplayInsets(Context context) {
return context.getSystemService(WindowManager.class)
.getMaximumWindowMetrics()
.getWindowInsets()
.getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout())
.toRect();
}
private static boolean isLandscape(Rect bounds) {
return bounds.width() > bounds.height();
}
/** Reverse the split position. */
@SplitPosition
public static int reversePosition(@SplitPosition int position) {
switch (position) {
case SPLIT_POSITION_TOP_OR_LEFT:
return SPLIT_POSITION_BOTTOM_OR_RIGHT;
case SPLIT_POSITION_BOTTOM_OR_RIGHT:
return SPLIT_POSITION_TOP_OR_LEFT;
default:
return SPLIT_POSITION_UNDEFINED;
}
}
/**
* Return if this layout is landscape.
*/
public boolean isLandscape() {
return isLandscape(mRootBounds);
}
/** Apply recorded surface layout to the {@link SurfaceControl.Transaction}. */
public void applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1,
SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2,
boolean applyResizingOffset) {
final SurfaceControl dividerLeash = getDividerLeash();
if (dividerLeash != null) {
getRefDividerBounds(mTempRect);
t.setPosition(dividerLeash, mTempRect.left, mTempRect.top);
// Resets layer of divider bar to make sure it is always on top.
t.setLayer(dividerLeash, Integer.MAX_VALUE);
}
getRefBounds1(mTempRect);
t.setPosition(leash1, mTempRect.left, mTempRect.top)
.setWindowCrop(leash1, mTempRect.width(), mTempRect.height());
getRefBounds2(mTempRect);
t.setPosition(leash2, mTempRect.left, mTempRect.top)
.setWindowCrop(leash2, mTempRect.width(), mTempRect.height());
if (mImePositionProcessor.adjustSurfaceLayoutForIme(
t, dividerLeash, leash1, leash2, dimLayer1, dimLayer2)) {
return;
}
mSurfaceEffectPolicy.adjustDimSurface(t, dimLayer1, dimLayer2);
if (applyResizingOffset) {
mSurfaceEffectPolicy.adjustRootSurface(t, leash1, leash2);
}
}
/** Apply recorded task layout to the {@link WindowContainerTransaction}. */
public void applyTaskChanges(WindowContainerTransaction wct,
ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) {
if (!mBounds1.equals(mWinBounds1) || !task1.token.equals(mWinToken1)) {
wct.setBounds(task1.token, mBounds1);
wct.setSmallestScreenWidthDp(task1.token, getSmallestWidthDp(mBounds1));
mWinBounds1.set(mBounds1);
mWinToken1 = task1.token;
}
if (!mBounds2.equals(mWinBounds2) || !task2.token.equals(mWinToken2)) {
wct.setBounds(task2.token, mBounds2);
wct.setSmallestScreenWidthDp(task2.token, getSmallestWidthDp(mBounds2));
mWinBounds2.set(mBounds2);
mWinToken2 = task2.token;
}
}
private int getSmallestWidthDp(Rect bounds) {
mTempRect.set(bounds);
mTempRect.inset(getDisplayInsets(mContext));
final int minWidth = Math.min(mTempRect.width(), mTempRect.height());
final float density = mContext.getResources().getDisplayMetrics().density;
return (int) (minWidth / density);
}
/**
* Shift configuration bounds to prevent client apps get configuration changed or relaunch. And
* restore shifted configuration bounds if it's no longer shifted.
*/
public void applyLayoutOffsetTarget(WindowContainerTransaction wct, int offsetX, int offsetY,
ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2) {
if (offsetX == 0 && offsetY == 0) {
wct.setBounds(taskInfo1.token, mBounds1);
wct.setScreenSizeDp(taskInfo1.token,
SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
wct.setBounds(taskInfo2.token, mBounds2);
wct.setScreenSizeDp(taskInfo2.token,
SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
} else {
getBounds1(mTempRect);
mTempRect.offset(offsetX, offsetY);
wct.setBounds(taskInfo1.token, mTempRect);
wct.setScreenSizeDp(taskInfo1.token,
taskInfo1.configuration.screenWidthDp,
taskInfo1.configuration.screenHeightDp);
getBounds2(mTempRect);
mTempRect.offset(offsetX, offsetY);
wct.setBounds(taskInfo2.token, mTempRect);
wct.setScreenSizeDp(taskInfo2.token,
taskInfo2.configuration.screenWidthDp,
taskInfo2.configuration.screenHeightDp);
}
}
/** Dumps the current split bounds recorded in this layout. */
public void dump(@NonNull PrintWriter pw, String prefix) {
pw.println(prefix + "bounds1=" + mBounds1.toShortString());
pw.println(prefix + "dividerBounds=" + mDividerBounds.toShortString());
pw.println(prefix + "bounds2=" + mBounds2.toShortString());
}
/** Handles layout change event. */
public interface SplitLayoutHandler {
/** Calls when dismissing split. */
void onSnappedToDismiss(boolean snappedToEnd, int reason);
/**
* Calls when resizing the split bounds.
*
* @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl,
* SurfaceControl, SurfaceControl, boolean)
*/
void onLayoutSizeChanging(SplitLayout layout);
/**
* Calls when finish resizing the split bounds.
*
* @see #applyTaskChanges(WindowContainerTransaction, ActivityManager.RunningTaskInfo,
* ActivityManager.RunningTaskInfo)
* @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl,
* SurfaceControl, SurfaceControl, boolean)
*/
void onLayoutSizeChanged(SplitLayout layout);
/**
* Calls when re-positioning the split bounds. Like moving split bounds while showing IME
* panel.
*
* @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl,
* SurfaceControl, SurfaceControl, boolean)
*/
void onLayoutPositionChanging(SplitLayout layout);
/**
* Notifies the target offset for shifting layout. So layout handler can shift configuration
* bounds correspondingly to make sure client apps won't get configuration changed or
* relaunched. If the layout is no longer shifted, layout handler should restore shifted
* configuration bounds.
*
* @see #applyLayoutOffsetTarget(WindowContainerTransaction, int, int,
* ActivityManager.RunningTaskInfo, ActivityManager.RunningTaskInfo)
*/
void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout);
/** Calls when user double tapped on the divider bar. */
default void onDoubleTappedDivider() {
}
/** Returns split position of the token. */
@SplitPosition
int getSplitItemPosition(WindowContainerToken token);
}
/**
* Calculates and applies proper dismissing parallax offset and dimming value to hint users
* dismissing gesture.
*/
private class ResizingEffectPolicy {
/** Indicates whether to offset splitting bounds to hint dismissing progress or not. */
private final int mParallaxType;
int mShrinkSide = DOCKED_INVALID;
// The current dismissing side.
int mDismissingSide = DOCKED_INVALID;
// The parallax offset to hint the dismissing side and progress.
final Point mParallaxOffset = new Point();
// The dimming value to hint the dismissing side and progress.
float mDismissingDimValue = 0.0f;
final Rect mContentBounds = new Rect();
final Rect mSurfaceBounds = new Rect();
ResizingEffectPolicy(int parallaxType) {
mParallaxType = parallaxType;
}
/**
* Applies a parallax to the task to hint dismissing progress.
*
* @param position the split position to apply dismissing parallax effect
* @param isLandscape indicates whether it's splitting horizontally or vertically
*/
void applyDividerPosition(int position, boolean isLandscape) {
mDismissingSide = DOCKED_INVALID;
mParallaxOffset.set(0, 0);
mDismissingDimValue = 0;
int totalDismissingDistance = 0;
if (position < mDividerSnapAlgorithm.getFirstSplitTarget().position) {
mDismissingSide = isLandscape ? DOCKED_LEFT : DOCKED_TOP;
totalDismissingDistance = mDividerSnapAlgorithm.getDismissStartTarget().position
- mDividerSnapAlgorithm.getFirstSplitTarget().position;
} else if (position > mDividerSnapAlgorithm.getLastSplitTarget().position) {
mDismissingSide = isLandscape ? DOCKED_RIGHT : DOCKED_BOTTOM;
totalDismissingDistance = mDividerSnapAlgorithm.getLastSplitTarget().position
- mDividerSnapAlgorithm.getDismissEndTarget().position;
}
final boolean topLeftShrink = isLandscape
? position < mWinBounds1.right : position < mWinBounds1.bottom;
if (topLeftShrink) {
mShrinkSide = isLandscape ? DOCKED_LEFT : DOCKED_TOP;
mContentBounds.set(mWinBounds1);
mSurfaceBounds.set(mBounds1);
} else {
mShrinkSide = isLandscape ? DOCKED_RIGHT : DOCKED_BOTTOM;
mContentBounds.set(mWinBounds2);
mSurfaceBounds.set(mBounds2);
}
if (mDismissingSide != DOCKED_INVALID) {
float fraction = Math.max(0,
Math.min(mDividerSnapAlgorithm.calculateDismissingFraction(position), 1f));
mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction);
if (mParallaxType == PARALLAX_DISMISSING) {
fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide);
if (isLandscape) {
mParallaxOffset.x = (int) (fraction * totalDismissingDistance);
} else {
mParallaxOffset.y = (int) (fraction * totalDismissingDistance);
}
}
}
if (mParallaxType == PARALLAX_ALIGN_CENTER) {
if (isLandscape) {
mParallaxOffset.x =
(mSurfaceBounds.width() - mContentBounds.width()) / 2;
} else {
mParallaxOffset.y =
(mSurfaceBounds.height() - mContentBounds.height()) / 2;
}
}
}
/**
* @return for a specified {@code fraction}, this returns an adjusted value that simulates a
* slowing down parallax effect
*/
private float calculateParallaxDismissingFraction(float fraction, int dockSide) {
float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f;
// Less parallax at the top, just because.
if (dockSide == WindowManager.DOCKED_TOP) {
result /= 2f;
}
return result;
}
/** Applies parallax offset and dimming value to the root surface at the dismissing side. */
void adjustRootSurface(SurfaceControl.Transaction t,
SurfaceControl leash1, SurfaceControl leash2) {
SurfaceControl targetLeash = null;
if (mParallaxType == PARALLAX_DISMISSING) {
switch (mDismissingSide) {
case DOCKED_TOP:
case DOCKED_LEFT:
targetLeash = leash1;
mTempRect.set(mBounds1);
break;
case DOCKED_BOTTOM:
case DOCKED_RIGHT:
targetLeash = leash2;
mTempRect.set(mBounds2);
break;
}
} else if (mParallaxType == PARALLAX_ALIGN_CENTER) {
switch (mShrinkSide) {
case DOCKED_TOP:
case DOCKED_LEFT:
targetLeash = leash1;
mTempRect.set(mBounds1);
break;
case DOCKED_BOTTOM:
case DOCKED_RIGHT:
targetLeash = leash2;
mTempRect.set(mBounds2);
break;
}
}
if (mParallaxType != PARALLAX_NONE && targetLeash != null) {
t.setPosition(targetLeash,
mTempRect.left + mParallaxOffset.x, mTempRect.top + mParallaxOffset.y);
// Transform the screen-based split bounds to surface-based crop bounds.
mTempRect.offsetTo(-mParallaxOffset.x, -mParallaxOffset.y);
t.setWindowCrop(targetLeash, mTempRect);
}
}
void adjustDimSurface(SurfaceControl.Transaction t,
SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
SurfaceControl targetDimLayer;
switch (mDismissingSide) {
case DOCKED_TOP:
case DOCKED_LEFT:
targetDimLayer = dimLayer1;
break;
case DOCKED_BOTTOM:
case DOCKED_RIGHT:
targetDimLayer = dimLayer2;
break;
case DOCKED_INVALID:
default:
t.setAlpha(dimLayer1, 0).hide(dimLayer1);
t.setAlpha(dimLayer2, 0).hide(dimLayer2);
return;
}
t.setAlpha(targetDimLayer, mDismissingDimValue)
.setVisibility(targetDimLayer, mDismissingDimValue > 0.001f);
}
}
/** Records IME top offset changes and updates SplitLayout correspondingly. */
private class ImePositionProcessor implements DisplayImeController.ImePositionProcessor {
/**
* Maximum size of an adjusted split bounds relative to original stack bounds. Used to
* restrict IME adjustment so that a min portion of top split remains visible.
*/
private static final float ADJUSTED_SPLIT_FRACTION_MAX = 0.7f;
private static final float ADJUSTED_NONFOCUS_DIM = 0.3f;
private final int mDisplayId;
private boolean mHasImeFocus;
private boolean mImeShown;
private int mYOffsetForIme;
private float mDimValue1;
private float mDimValue2;
private int mStartImeTop;
private int mEndImeTop;
private int mTargetYOffset;
private int mLastYOffset;
private float mTargetDim1;
private float mTargetDim2;
private float mLastDim1;
private float mLastDim2;
private ImePositionProcessor(int displayId) {
mDisplayId = displayId;
}
@Override
public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop,
boolean showing, boolean isFloating, SurfaceControl.Transaction t) {
if (displayId != mDisplayId || !mInitialized) {
return 0;
}
final int imeTargetPosition = getImeTargetPosition();
mHasImeFocus = imeTargetPosition != SPLIT_POSITION_UNDEFINED;
if (!mHasImeFocus) {
return 0;
}
mStartImeTop = showing ? hiddenTop : shownTop;
mEndImeTop = showing ? shownTop : hiddenTop;
mImeShown = showing;
// Update target dim values
mLastDim1 = mDimValue1;
mTargetDim1 = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT && mImeShown
&& mDimNonImeSide ? ADJUSTED_NONFOCUS_DIM : 0.0f;
mLastDim2 = mDimValue2;
mTargetDim2 = imeTargetPosition == SPLIT_POSITION_TOP_OR_LEFT && mImeShown
&& mDimNonImeSide ? ADJUSTED_NONFOCUS_DIM : 0.0f;
// Calculate target bounds offset for IME
mLastYOffset = mYOffsetForIme;
final boolean needOffset = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
&& !isFloating && !isLandscape(mRootBounds) && mImeShown;
mTargetYOffset = needOffset ? getTargetYOffset() : 0;
if (mTargetYOffset != mLastYOffset) {
// Freeze the configuration size with offset to prevent app get a configuration
// changed or relaunch. This is required to make sure client apps will calculate
// insets properly after layout shifted.
if (mTargetYOffset == 0) {
mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this);
} else {
mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset - mLastYOffset,
SplitLayout.this);
}
}
// Make {@link DividerView} non-interactive while IME showing in split mode. Listen to
// ImePositionProcessor#onImeVisibilityChanged directly in DividerView is not enough
// because DividerView won't receive onImeVisibilityChanged callback after it being
// re-inflated.
mSplitWindowManager.setInteractive(!mImeShown || !mHasImeFocus);
return needOffset ? IME_ANIMATION_NO_ALPHA : 0;
}
@Override
public void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) {
if (displayId != mDisplayId || !mHasImeFocus) return;
onProgress(getProgress(imeTop));
mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
}
@Override
public void onImeEndPositioning(int displayId, boolean cancel,
SurfaceControl.Transaction t) {
if (displayId != mDisplayId || !mHasImeFocus || cancel) return;
onProgress(1.0f);
mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
}
@Override
public void onImeControlTargetChanged(int displayId, boolean controlling) {
if (displayId != mDisplayId) return;
// Restore the split layout when wm-shell is not controlling IME insets anymore.
if (!controlling && mImeShown) {
reset();
mSplitWindowManager.setInteractive(true);
mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this);
mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
}
}
private int getTargetYOffset() {
final int desireOffset = Math.abs(mEndImeTop - mStartImeTop);
// Make sure to keep at least 30% visible for the top split.
final int maxOffset = (int) (mBounds1.height() * ADJUSTED_SPLIT_FRACTION_MAX);
return -Math.min(desireOffset, maxOffset);
}
@SplitPosition
private int getImeTargetPosition() {
final WindowContainerToken token = mTaskOrganizer.getImeTarget(mDisplayId);
return mSplitLayoutHandler.getSplitItemPosition(token);
}
private float getProgress(int currImeTop) {
return ((float) currImeTop - mStartImeTop) / (mEndImeTop - mStartImeTop);
}
private void onProgress(float progress) {
mDimValue1 = getProgressValue(mLastDim1, mTargetDim1, progress);
mDimValue2 = getProgressValue(mLastDim2, mTargetDim2, progress);
mYOffsetForIme =
(int) getProgressValue((float) mLastYOffset, (float) mTargetYOffset, progress);
}
private float getProgressValue(float start, float end, float progress) {
return start + (end - start) * progress;
}
void reset() {
mHasImeFocus = false;
mImeShown = false;
mYOffsetForIme = mLastYOffset = mTargetYOffset = 0;
mDimValue1 = mLastDim1 = mTargetDim1 = 0.0f;
mDimValue2 = mLastDim2 = mTargetDim2 = 0.0f;
}
/**
* Adjusts surface layout while showing IME.
*
* @return {@code false} if there's no need to adjust, otherwise {@code true}
*/
boolean adjustSurfaceLayoutForIme(SurfaceControl.Transaction t,
SurfaceControl dividerLeash, SurfaceControl leash1, SurfaceControl leash2,
SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
final boolean showDim = mDimValue1 > 0.001f || mDimValue2 > 0.001f;
boolean adjusted = false;
if (mYOffsetForIme != 0) {
if (dividerLeash != null) {
getRefDividerBounds(mTempRect);
mTempRect.offset(0, mYOffsetForIme);
t.setPosition(dividerLeash, mTempRect.left, mTempRect.top);
}
getRefBounds1(mTempRect);
mTempRect.offset(0, mYOffsetForIme);
t.setPosition(leash1, mTempRect.left, mTempRect.top);
getRefBounds2(mTempRect);
mTempRect.offset(0, mYOffsetForIme);
t.setPosition(leash2, mTempRect.left, mTempRect.top);
adjusted = true;
}
if (showDim) {
t.setAlpha(dimLayer1, mDimValue1).setVisibility(dimLayer1, mDimValue1 > 0.001f);
t.setAlpha(dimLayer2, mDimValue2).setVisibility(dimLayer2, mDimValue2 > 0.001f);
adjusted = true;
}
return adjusted;
}
}
}