blob: 575a5320bbd3ce4d43fe25c8615ffff24bb4d442 [file] [log] [blame]
/*
* Copyright (C) 2015 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.internal.policy;
import static android.view.WindowManager.DOCKED_INVALID;
import static android.view.WindowManager.DOCKED_LEFT;
import static android.view.WindowManager.DOCKED_RIGHT;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.view.Display;
import android.view.DisplayInfo;
import java.util.ArrayList;
/**
* Calculates the snap targets and the snap position given a position and a velocity. All positions
* here are to be interpreted as the left/top edge of the divider rectangle.
*
* @hide
*/
public class DividerSnapAlgorithm {
private static final int MIN_FLING_VELOCITY_DP_PER_SECOND = 400;
private static final int MIN_DISMISS_VELOCITY_DP_PER_SECOND = 600;
/**
* 3 snap targets: left/top has 16:9 ratio (for videos), 1:1, and right/bottom has 16:9 ratio
*/
private static final int SNAP_MODE_16_9 = 0;
/**
* 3 snap targets: fixed ratio, 1:1, (1 - fixed ratio)
*/
private static final int SNAP_FIXED_RATIO = 1;
/**
* 1 snap target: 1:1
*/
private static final int SNAP_ONLY_1_1 = 2;
/**
* 1 snap target: minimized height, (1 - minimized height)
*/
private static final int SNAP_MODE_MINIMIZED = 3;
private final float mMinFlingVelocityPxPerSecond;
private final float mMinDismissVelocityPxPerSecond;
private final int mDisplayWidth;
private final int mDisplayHeight;
private final int mDividerSize;
private final ArrayList<SnapTarget> mTargets = new ArrayList<>();
private final Rect mInsets = new Rect();
private final int mSnapMode;
private final boolean mFreeSnapMode;
private final int mMinimalSizeResizableTask;
private final int mTaskHeightInMinimizedMode;
private final float mFixedRatio;
private boolean mIsHorizontalDivision;
/** The first target which is still splitting the screen */
private final SnapTarget mFirstSplitTarget;
/** The last target which is still splitting the screen */
private final SnapTarget mLastSplitTarget;
private final SnapTarget mDismissStartTarget;
private final SnapTarget mDismissEndTarget;
private final SnapTarget mMiddleTarget;
public static DividerSnapAlgorithm create(Context ctx, Rect insets) {
DisplayInfo displayInfo = new DisplayInfo();
ctx.getSystemService(DisplayManager.class).getDisplay(
Display.DEFAULT_DISPLAY).getDisplayInfo(displayInfo);
int dividerWindowWidth = ctx.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.docked_stack_divider_thickness);
int dividerInsets = ctx.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.docked_stack_divider_insets);
return new DividerSnapAlgorithm(ctx.getResources(),
displayInfo.logicalWidth, displayInfo.logicalHeight,
dividerWindowWidth - 2 * dividerInsets,
ctx.getApplicationContext().getResources().getConfiguration().orientation
== Configuration.ORIENTATION_PORTRAIT,
insets);
}
public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
boolean isHorizontalDivision, Rect insets) {
this(res, displayWidth, displayHeight, dividerSize, isHorizontalDivision, insets,
DOCKED_INVALID, false);
}
public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
boolean isHorizontalDivision, Rect insets, int dockSide) {
this(res, displayWidth, displayHeight, dividerSize, isHorizontalDivision, insets,
dockSide, false);
}
public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
boolean isHorizontalDivision, Rect insets, int dockSide, boolean isMinimizedMode) {
mMinFlingVelocityPxPerSecond =
MIN_FLING_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density;
mMinDismissVelocityPxPerSecond =
MIN_DISMISS_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density;
mDividerSize = dividerSize;
mDisplayWidth = displayWidth;
mDisplayHeight = displayHeight;
mIsHorizontalDivision = isHorizontalDivision;
mInsets.set(insets);
mSnapMode = isMinimizedMode ? SNAP_MODE_MINIMIZED :
res.getInteger(com.android.internal.R.integer.config_dockedStackDividerSnapMode);
mFreeSnapMode = res.getBoolean(
com.android.internal.R.bool.config_dockedStackDividerFreeSnapMode);
mFixedRatio = res.getFraction(
com.android.internal.R.fraction.docked_stack_divider_fixed_ratio, 1, 1);
mMinimalSizeResizableTask = res.getDimensionPixelSize(
com.android.internal.R.dimen.default_minimal_size_resizable_task);
mTaskHeightInMinimizedMode = res.getDimensionPixelSize(
com.android.internal.R.dimen.task_height_of_minimized_mode);
calculateTargets(isHorizontalDivision, dockSide);
mFirstSplitTarget = mTargets.get(1);
mLastSplitTarget = mTargets.get(mTargets.size() - 2);
mDismissStartTarget = mTargets.get(0);
mDismissEndTarget = mTargets.get(mTargets.size() - 1);
mMiddleTarget = mTargets.get(mTargets.size() / 2);
mMiddleTarget.isMiddleTarget = true;
}
/**
* @return whether it's feasible to enable split screen in the current configuration, i.e. when
* snapping in the middle both tasks are larger than the minimal task size.
*/
public boolean isSplitScreenFeasible() {
int statusBarSize = mInsets.top;
int navBarSize = mIsHorizontalDivision ? mInsets.bottom : mInsets.right;
int size = mIsHorizontalDivision
? mDisplayHeight
: mDisplayWidth;
int availableSpace = size - navBarSize - statusBarSize - mDividerSize;
return availableSpace / 2 >= mMinimalSizeResizableTask;
}
public SnapTarget calculateSnapTarget(int position, float velocity) {
return calculateSnapTarget(position, velocity, true /* hardDismiss */);
}
/**
* @param position the top/left position of the divider
* @param velocity current dragging velocity
* @param hardDismiss if set, make it a bit harder to get reach the dismiss targets
*/
public SnapTarget calculateSnapTarget(int position, float velocity, boolean hardDismiss) {
if (position < mFirstSplitTarget.position && velocity < -mMinDismissVelocityPxPerSecond) {
return mDismissStartTarget;
}
if (position > mLastSplitTarget.position && velocity > mMinDismissVelocityPxPerSecond) {
return mDismissEndTarget;
}
if (Math.abs(velocity) < mMinFlingVelocityPxPerSecond) {
return snap(position, hardDismiss);
}
if (velocity < 0) {
return mFirstSplitTarget;
} else {
return mLastSplitTarget;
}
}
public SnapTarget calculateNonDismissingSnapTarget(int position) {
SnapTarget target = snap(position, false /* hardDismiss */);
if (target == mDismissStartTarget) {
return mFirstSplitTarget;
} else if (target == mDismissEndTarget) {
return mLastSplitTarget;
} else {
return target;
}
}
public float calculateDismissingFraction(int position) {
if (position < mFirstSplitTarget.position) {
return 1f - (float) (position - getStartInset())
/ (mFirstSplitTarget.position - getStartInset());
} else if (position > mLastSplitTarget.position) {
return (float) (position - mLastSplitTarget.position)
/ (mDismissEndTarget.position - mLastSplitTarget.position - mDividerSize);
}
return 0f;
}
public SnapTarget getClosestDismissTarget(int position) {
if (position < mFirstSplitTarget.position) {
return mDismissStartTarget;
} else if (position > mLastSplitTarget.position) {
return mDismissEndTarget;
} else if (position - mDismissStartTarget.position
< mDismissEndTarget.position - position) {
return mDismissStartTarget;
} else {
return mDismissEndTarget;
}
}
public SnapTarget getFirstSplitTarget() {
return mFirstSplitTarget;
}
public SnapTarget getLastSplitTarget() {
return mLastSplitTarget;
}
public SnapTarget getDismissStartTarget() {
return mDismissStartTarget;
}
public SnapTarget getDismissEndTarget() {
return mDismissEndTarget;
}
private int getStartInset() {
if (mIsHorizontalDivision) {
return mInsets.top;
} else {
return mInsets.left;
}
}
private int getEndInset() {
if (mIsHorizontalDivision) {
return mInsets.bottom;
} else {
return mInsets.right;
}
}
private boolean shouldApplyFreeSnapMode(int position) {
if (!mFreeSnapMode) {
return false;
}
if (!isFirstSplitTargetAvailable() || !isLastSplitTargetAvailable()) {
return false;
}
return mFirstSplitTarget.position < position && position < mLastSplitTarget.position;
}
private SnapTarget snap(int position, boolean hardDismiss) {
if (shouldApplyFreeSnapMode(position)) {
return new SnapTarget(position, position, SnapTarget.FLAG_NONE);
}
int minIndex = -1;
float minDistance = Float.MAX_VALUE;
int size = mTargets.size();
for (int i = 0; i < size; i++) {
SnapTarget target = mTargets.get(i);
float distance = Math.abs(position - target.position);
if (hardDismiss) {
distance /= target.distanceMultiplier;
}
if (distance < minDistance) {
minIndex = i;
minDistance = distance;
}
}
return mTargets.get(minIndex);
}
private void calculateTargets(boolean isHorizontalDivision, int dockedSide) {
mTargets.clear();
int dividerMax = isHorizontalDivision
? mDisplayHeight
: mDisplayWidth;
int navBarSize = isHorizontalDivision ? mInsets.bottom : mInsets.right;
int startPos = -mDividerSize;
if (dockedSide == DOCKED_RIGHT) {
startPos += mInsets.left;
}
mTargets.add(new SnapTarget(startPos, startPos, SnapTarget.FLAG_DISMISS_START,
0.35f));
switch (mSnapMode) {
case SNAP_MODE_16_9:
addRatio16_9Targets(isHorizontalDivision, dividerMax);
break;
case SNAP_FIXED_RATIO:
addFixedDivisionTargets(isHorizontalDivision, dividerMax);
break;
case SNAP_ONLY_1_1:
addMiddleTarget(isHorizontalDivision);
break;
case SNAP_MODE_MINIMIZED:
addMinimizedTarget(isHorizontalDivision, dockedSide);
break;
}
mTargets.add(new SnapTarget(dividerMax - navBarSize, dividerMax,
SnapTarget.FLAG_DISMISS_END, 0.35f));
}
private void addNonDismissingTargets(boolean isHorizontalDivision, int topPosition,
int bottomPosition, int dividerMax) {
maybeAddTarget(topPosition, topPosition - getStartInset());
addMiddleTarget(isHorizontalDivision);
maybeAddTarget(bottomPosition,
dividerMax - getEndInset() - (bottomPosition + mDividerSize));
}
private void addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax) {
int start = isHorizontalDivision ? mInsets.top : mInsets.left;
int end = isHorizontalDivision
? mDisplayHeight - mInsets.bottom
: mDisplayWidth - mInsets.right;
int size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2;
int topPosition = start + size;
int bottomPosition = end - size - mDividerSize;
addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax);
}
private void addRatio16_9Targets(boolean isHorizontalDivision, int dividerMax) {
int start = isHorizontalDivision ? mInsets.top : mInsets.left;
int end = isHorizontalDivision
? mDisplayHeight - mInsets.bottom
: mDisplayWidth - mInsets.right;
int startOther = isHorizontalDivision ? mInsets.left : mInsets.top;
int endOther = isHorizontalDivision
? mDisplayWidth - mInsets.right
: mDisplayHeight - mInsets.bottom;
float size = 9.0f / 16.0f * (endOther - startOther);
int sizeInt = (int) Math.floor(size);
int topPosition = start + sizeInt;
int bottomPosition = end - sizeInt - mDividerSize;
addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax);
}
/**
* Adds a target at {@param position} but only if the area with size of {@param smallerSize}
* meets the minimal size requirement.
*/
private void maybeAddTarget(int position, int smallerSize) {
if (smallerSize >= mMinimalSizeResizableTask) {
mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
}
}
private void addMiddleTarget(boolean isHorizontalDivision) {
int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision,
mInsets, mDisplayWidth, mDisplayHeight, mDividerSize);
mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
}
private void addMinimizedTarget(boolean isHorizontalDivision, int dockedSide) {
// In portrait offset the position by the statusbar height, in landscape add the statusbar
// height as well to match portrait offset
int position = mTaskHeightInMinimizedMode + mInsets.top;
if (!isHorizontalDivision) {
if (dockedSide == DOCKED_LEFT) {
position += mInsets.left;
} else if (dockedSide == DOCKED_RIGHT) {
position = mDisplayWidth - position - mInsets.right - mDividerSize;
}
}
mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
}
public SnapTarget getMiddleTarget() {
return mMiddleTarget;
}
public SnapTarget getNextTarget(SnapTarget snapTarget) {
int index = mTargets.indexOf(snapTarget);
if (index != -1 && index < mTargets.size() - 1) {
return mTargets.get(index + 1);
}
return snapTarget;
}
public SnapTarget getPreviousTarget(SnapTarget snapTarget) {
int index = mTargets.indexOf(snapTarget);
if (index != -1 && index > 0) {
return mTargets.get(index - 1);
}
return snapTarget;
}
/**
* @return whether or not there are more than 1 split targets that do not include the two
* dismiss targets, used in deciding to display the middle target for accessibility
*/
public boolean showMiddleSplitTargetForAccessibility() {
return (mTargets.size() - 2) > 1;
}
public boolean isFirstSplitTargetAvailable() {
return mFirstSplitTarget != mMiddleTarget;
}
public boolean isLastSplitTargetAvailable() {
return mLastSplitTarget != mMiddleTarget;
}
/**
* Cycles through all non-dismiss targets with a stepping of {@param increment}. It moves left
* if {@param increment} is negative and moves right otherwise.
*/
public SnapTarget cycleNonDismissTarget(SnapTarget snapTarget, int increment) {
int index = mTargets.indexOf(snapTarget);
if (index != -1) {
SnapTarget newTarget = mTargets.get((index + mTargets.size() + increment)
% mTargets.size());
if (newTarget == mDismissStartTarget) {
return mLastSplitTarget;
} else if (newTarget == mDismissEndTarget) {
return mFirstSplitTarget;
} else {
return newTarget;
}
}
return snapTarget;
}
/**
* Represents a snap target for the divider.
*/
public static class SnapTarget {
public static final int FLAG_NONE = 0;
/** If the divider reaches this value, the left/top task should be dismissed. */
public static final int FLAG_DISMISS_START = 1;
/** If the divider reaches this value, the right/bottom task should be dismissed */
public static final int FLAG_DISMISS_END = 2;
/** Position of this snap target. The right/bottom edge of the top/left task snaps here. */
public final int position;
/**
* Like {@link #position}, but used to calculate the task bounds which might be different
* from the stack bounds.
*/
public final int taskPosition;
public final int flag;
public boolean isMiddleTarget;
/**
* Multiplier used to calculate distance to snap position. The lower this value, the harder
* it's to snap on this target
*/
private final float distanceMultiplier;
public SnapTarget(int position, int taskPosition, int flag) {
this(position, taskPosition, flag, 1f);
}
public SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier) {
this.position = position;
this.taskPosition = taskPosition;
this.flag = flag;
this.distanceMultiplier = distanceMultiplier;
}
}
}