blob: d101b0c4d7e82af7a6ec733f398f2c02bfd26de2 [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.bubbles;
import static android.view.View.LAYOUT_DIRECTION_RTL;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.Log;
import android.view.Surface;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.R;
/**
* Keeps track of display size, configuration, and specific bubble sizes. One place for all
* placement and positioning calculations to refer to.
*/
public class BubblePositioner {
private static final String TAG = BubbleDebugConfig.TAG_WITH_CLASS_NAME
? "BubblePositioner"
: BubbleDebugConfig.TAG_BUBBLES;
/** When the bubbles are collapsed in a stack only some of them are shown, this is how many. **/
public static final int NUM_VISIBLE_WHEN_RESTING = 2;
/** Indicates a bubble's height should be the maximum available space. **/
public static final int MAX_HEIGHT = -1;
/** The max percent of screen width to use for the flyout on large screens. */
public static final float FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN = 0.3f;
/** The max percent of screen width to use for the flyout on phone. */
public static final float FLYOUT_MAX_WIDTH_PERCENT = 0.6f;
/** The percent of screen width for the expanded view on a large screen. **/
private static final float EXPANDED_VIEW_LARGE_SCREEN_LANDSCAPE_WIDTH_PERCENT = 0.48f;
/** The percent of screen width for the expanded view on a large screen. **/
private static final float EXPANDED_VIEW_LARGE_SCREEN_PORTRAIT_WIDTH_PERCENT = 0.70f;
/** The percent of screen width for the expanded view on a small tablet. **/
private static final float EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT = 0.72f;
/** The percent of screen width for the expanded view when shown in the bubble bar. **/
private static final float EXPANDED_VIEW_BUBBLE_BAR_PORTRAIT_WIDTH_PERCENT = 0.7f;
/** The percent of screen width for the expanded view when shown in the bubble bar. **/
private static final float EXPANDED_VIEW_BUBBLE_BAR_LANDSCAPE_WIDTH_PERCENT = 0.4f;
private Context mContext;
private WindowManager mWindowManager;
private Rect mScreenRect;
private @Surface.Rotation int mRotation = Surface.ROTATION_0;
private Insets mInsets;
private boolean mImeVisible;
private int mImeHeight;
private boolean mIsLargeScreen;
private boolean mIsSmallTablet;
private Rect mPositionRect;
private int mDefaultMaxBubbles;
private int mMaxBubbles;
private int mBubbleSize;
private int mSpacingBetweenBubbles;
private int mBubblePaddingTop;
private int mBubbleOffscreenAmount;
private int mStackOffset;
private int mExpandedViewMinHeight;
private int mExpandedViewLargeScreenWidth;
private int mExpandedViewLargeScreenInsetClosestEdge;
private int mExpandedViewLargeScreenInsetFurthestEdge;
private int mOverflowWidth;
private int mExpandedViewPadding;
private int mPointerMargin;
private int mPointerWidth;
private int mPointerHeight;
private int mPointerOverlap;
private int mManageButtonHeight;
private int mOverflowHeight;
private int mMinimumFlyoutWidthLargeScreen;
private PointF mRestingStackPosition;
private int[] mPaddings = new int[4];
private boolean mShowingInBubbleBar;
private boolean mBubblesOnHome;
private int mBubbleBarSize;
private int mBubbleBarHomeAdjustment;
private final PointF mBubbleBarPosition = new PointF();
public BubblePositioner(Context context, WindowManager windowManager) {
mContext = context;
mWindowManager = windowManager;
update();
}
/**
* Available space and inset information. Call this when config changes
* occur or when added to a window.
*/
public void update() {
WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
if (windowMetrics == null) {
return;
}
WindowInsets metricInsets = windowMetrics.getWindowInsets();
Insets insets = metricInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()
| WindowInsets.Type.statusBars()
| WindowInsets.Type.displayCutout());
final Rect bounds = windowMetrics.getBounds();
Configuration config = mContext.getResources().getConfiguration();
mIsLargeScreen = config.smallestScreenWidthDp >= 600;
if (mIsLargeScreen) {
float largestEdgeDp = Math.max(config.screenWidthDp, config.screenHeightDp);
mIsSmallTablet = largestEdgeDp < 960;
} else {
mIsSmallTablet = false;
}
if (BubbleDebugConfig.DEBUG_POSITIONER) {
Log.w(TAG, "update positioner:"
+ " rotation: " + mRotation
+ " insets: " + insets
+ " isLargeScreen: " + mIsLargeScreen
+ " isSmallTablet: " + mIsSmallTablet
+ " showingInBubbleBar: " + mShowingInBubbleBar
+ " bounds: " + bounds);
}
updateInternal(mRotation, insets, bounds);
}
@VisibleForTesting
public void updateInternal(int rotation, Insets insets, Rect bounds) {
mRotation = rotation;
mInsets = insets;
mScreenRect = new Rect(bounds);
mPositionRect = new Rect(bounds);
mPositionRect.left += mInsets.left;
mPositionRect.top += mInsets.top;
mPositionRect.right -= mInsets.right;
mPositionRect.bottom -= mInsets.bottom;
Resources res = mContext.getResources();
mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size);
mSpacingBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing);
mDefaultMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
mBubbleBarHomeAdjustment = mExpandedViewPadding / 2;
mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
mBubbleOffscreenAmount = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
mBubbleBarSize = res.getDimensionPixelSize(R.dimen.bubblebar_size);
if (mShowingInBubbleBar) {
mExpandedViewLargeScreenWidth = isLandscape()
? (int) (bounds.width() * EXPANDED_VIEW_BUBBLE_BAR_LANDSCAPE_WIDTH_PERCENT)
: (int) (bounds.width() * EXPANDED_VIEW_BUBBLE_BAR_PORTRAIT_WIDTH_PERCENT);
} else if (mIsSmallTablet) {
mExpandedViewLargeScreenWidth = (int) (bounds.width()
* EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT);
} else {
mExpandedViewLargeScreenWidth = isLandscape()
? (int) (bounds.width() * EXPANDED_VIEW_LARGE_SCREEN_LANDSCAPE_WIDTH_PERCENT)
: (int) (bounds.width() * EXPANDED_VIEW_LARGE_SCREEN_PORTRAIT_WIDTH_PERCENT);
}
if (mIsLargeScreen) {
if (isLandscape() && !mIsSmallTablet) {
mExpandedViewLargeScreenInsetClosestEdge = res.getDimensionPixelSize(
R.dimen.bubble_expanded_view_largescreen_landscape_padding);
mExpandedViewLargeScreenInsetFurthestEdge = bounds.width()
- mExpandedViewLargeScreenInsetClosestEdge
- mExpandedViewLargeScreenWidth;
} else {
final int centeredInset = (bounds.width() - mExpandedViewLargeScreenWidth) / 2;
mExpandedViewLargeScreenInsetClosestEdge = centeredInset;
mExpandedViewLargeScreenInsetFurthestEdge = centeredInset;
}
} else {
mExpandedViewLargeScreenInsetClosestEdge = mExpandedViewPadding;
mExpandedViewLargeScreenInsetFurthestEdge = mExpandedViewPadding;
}
mOverflowWidth = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_overflow_width);
mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin);
mPointerOverlap = res.getDimensionPixelSize(R.dimen.bubble_pointer_overlap);
mManageButtonHeight = res.getDimensionPixelSize(R.dimen.bubble_manage_button_total_height);
mExpandedViewMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height);
mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height);
mMinimumFlyoutWidthLargeScreen = res.getDimensionPixelSize(
R.dimen.bubbles_flyout_min_width_large_screen);
mMaxBubbles = calculateMaxBubbles();
}
/**
* @return the maximum number of bubbles that can fit on the screen when expanded. If the
* screen size / screen density is too small to support the default maximum number, then
* the number will be adjust to something lower to ensure everything is presented nicely.
*/
private int calculateMaxBubbles() {
// Use the shortest edge.
// In portrait the bubbles should align with the expanded view so subtract its padding.
// We always show the overflow so subtract one bubble size.
int padding = showBubblesVertically() ? 0 : (mExpandedViewPadding * 2);
int availableSpace = Math.min(mPositionRect.width(), mPositionRect.height())
- padding
- mBubbleSize;
// Each of the bubbles have spacing because the overflow is at the end.
int howManyFit = availableSpace / (mBubbleSize + mSpacingBetweenBubbles);
if (howManyFit < mDefaultMaxBubbles) {
// Not enough space for the default.
return howManyFit;
}
return mDefaultMaxBubbles;
}
/**
* @return a rect of available screen space accounting for orientation, system bars and cutouts.
* Does not account for IME.
*/
public Rect getAvailableRect() {
return mPositionRect;
}
/**
* @return a rect of the screen size.
*/
public Rect getScreenRect() {
return mScreenRect;
}
/**
* @return the relevant insets (status bar, nav bar, cutouts). If taskbar is showing, its
* inset is not included here.
*/
public Insets getInsets() {
return mInsets;
}
/** @return whether the device is in landscape orientation. */
public boolean isLandscape() {
return mContext.getResources().getConfiguration().orientation
== Configuration.ORIENTATION_LANDSCAPE;
}
/** @return whether the screen is considered large. */
public boolean isLargeScreen() {
return mIsLargeScreen;
}
/**
* Indicates how bubbles appear when expanded.
*
* When false, bubbles display at the top of the screen with the expanded view
* below them. When true, bubbles display at the edges of the screen with the expanded view
* to the left or right side.
*/
public boolean showBubblesVertically() {
return isLandscape() || mIsLargeScreen;
}
/** Size of the bubble. */
public int getBubbleSize() {
return mBubbleSize;
}
/** The amount of padding at the top of the screen that the bubbles avoid when being placed. */
public int getBubblePaddingTop() {
return mBubblePaddingTop;
}
/** The amount the stack hang off of the screen when collapsed. */
public int getStackOffScreenAmount() {
return mBubbleOffscreenAmount;
}
/** Offset of bubbles in the stack (i.e. how much they overlap). */
public int getStackOffset() {
return mStackOffset;
}
/** Size of the visible (non-overlapping) part of the pointer. */
public int getPointerSize() {
return mPointerHeight - mPointerOverlap;
}
/** The maximum number of bubbles that can be displayed comfortably on screen. */
public int getMaxBubbles() {
return mMaxBubbles;
}
/** The height for the IME if it's visible. **/
public int getImeHeight() {
return mImeVisible ? mImeHeight : 0;
}
/** Return top position of the IME if it's visible */
public int getImeTop() {
if (mImeVisible) {
return getScreenRect().bottom - getImeHeight() - getInsets().bottom;
}
return 0;
}
/** Sets whether the IME is visible. **/
public void setImeVisible(boolean visible, int height) {
mImeVisible = visible;
mImeHeight = height;
}
private int getExpandedViewLargeScreenInsetFurthestEdge(boolean isOverflow) {
if (isOverflow && mIsLargeScreen) {
return mScreenRect.width()
- mExpandedViewLargeScreenInsetClosestEdge
- mOverflowWidth;
}
return mExpandedViewLargeScreenInsetFurthestEdge;
}
/**
* Calculates the padding for the bubble expanded view.
*
* Some specifics:
* On large screens the width of the expanded view is restricted via this padding.
* On phone landscape the bubble overflow expanded view is also restricted via this padding.
* On large screens & landscape no top padding is set, the top position is set via translation.
* On phone portrait top padding is set as the space between the tip of the pointer and the
* bubble.
* When the overflow is shown it doesn't have the manage button to pad out the bottom so
* padding is added.
*/
public int[] getExpandedViewContainerPadding(boolean onLeft, boolean isOverflow) {
final int pointerTotalHeight = getPointerSize();
final int expandedViewLargeScreenInsetFurthestEdge =
getExpandedViewLargeScreenInsetFurthestEdge(isOverflow);
if (mIsLargeScreen) {
// Note:
// If we're in portrait OR if we're a small tablet, then the two insets values will
// be equal. If we're landscape and a large tablet, the two values will be different.
// [left, top, right, bottom]
mPaddings[0] = onLeft
? mExpandedViewLargeScreenInsetClosestEdge - pointerTotalHeight
: expandedViewLargeScreenInsetFurthestEdge;
mPaddings[1] = 0;
mPaddings[2] = onLeft
? expandedViewLargeScreenInsetFurthestEdge
: mExpandedViewLargeScreenInsetClosestEdge - pointerTotalHeight;
// Overflow doesn't show manage button / get padding from it so add padding here
mPaddings[3] = isOverflow ? mExpandedViewPadding : 0;
return mPaddings;
} else {
int leftPadding = mInsets.left + mExpandedViewPadding;
int rightPadding = mInsets.right + mExpandedViewPadding;
final float expandedViewWidth = isOverflow
? mOverflowWidth
: mExpandedViewLargeScreenWidth;
if (showBubblesVertically()) {
if (!onLeft) {
rightPadding += mBubbleSize - pointerTotalHeight;
leftPadding += isOverflow
? (mPositionRect.width() - rightPadding - expandedViewWidth)
: 0;
} else {
leftPadding += mBubbleSize - pointerTotalHeight;
rightPadding += isOverflow
? (mPositionRect.width() - leftPadding - expandedViewWidth)
: 0;
}
}
// [left, top, right, bottom]
mPaddings[0] = leftPadding;
mPaddings[1] = showBubblesVertically() ? 0 : mPointerMargin;
mPaddings[2] = rightPadding;
mPaddings[3] = 0;
return mPaddings;
}
}
/** Gets the y position of the expanded view if it was top-aligned. */
public float getExpandedViewYTopAligned() {
final int top = getAvailableRect().top;
if (showBubblesVertically()) {
return top - mPointerWidth + mExpandedViewPadding;
} else {
return top + mBubbleSize + mPointerMargin;
}
}
/**
* Calculate the maximum height the expanded view can be depending on where it's placed on
* the screen and the size of the elements around it (e.g. padding, pointer, manage button).
*/
public int getMaxExpandedViewHeight(boolean isOverflow) {
// Subtract top insets because availableRect.height would account for that
int expandedContainerY = (int) getExpandedViewYTopAligned() - getInsets().top;
int paddingTop = showBubblesVertically()
? 0
: mPointerHeight;
// Subtract pointer size because it's laid out in LinearLayout with the expanded view.
int pointerSize = showBubblesVertically()
? mPointerWidth
: (mPointerHeight + mPointerMargin);
int bottomPadding = isOverflow ? mExpandedViewPadding : mManageButtonHeight;
return getAvailableRect().height()
- expandedContainerY
- paddingTop
- pointerSize
- bottomPadding;
}
/**
* Determines the height for the bubble, ensuring a minimum height. If the height should be as
* big as available, returns {@link #MAX_HEIGHT}.
*/
public float getExpandedViewHeight(BubbleViewProvider bubble) {
boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey());
if (isOverflow && showBubblesVertically() && !mIsLargeScreen) {
// overflow in landscape on phone is max
return MAX_HEIGHT;
}
float desiredHeight = isOverflow
? mOverflowHeight
: ((Bubble) bubble).getDesiredHeight(mContext);
desiredHeight = Math.max(desiredHeight, mExpandedViewMinHeight);
if (desiredHeight > getMaxExpandedViewHeight(isOverflow)) {
return MAX_HEIGHT;
}
return desiredHeight;
}
/**
* Gets the y position for the expanded view. This is the position on screen of the top
* horizontal line of the expanded view.
*
* @param bubble the bubble being positioned.
* @param bubblePosition the x position of the bubble if showing on top, the y position of the
* bubble if showing vertically.
* @return the y position for the expanded view.
*/
public float getExpandedViewY(BubbleViewProvider bubble, float bubblePosition) {
boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey());
float expandedViewHeight = getExpandedViewHeight(bubble);
float topAlignment = getExpandedViewYTopAligned();
if (!showBubblesVertically() || expandedViewHeight == MAX_HEIGHT) {
// Top-align when bubbles are shown at the top or are max size.
return topAlignment;
}
// If we're here, we're showing vertically & developer has made height less than maximum.
int manageButtonHeight = isOverflow ? mExpandedViewPadding : mManageButtonHeight;
float pointerPosition = getPointerPosition(bubblePosition);
float bottomIfCentered = pointerPosition + (expandedViewHeight / 2) + manageButtonHeight;
float topIfCentered = pointerPosition - (expandedViewHeight / 2);
if (topIfCentered > mPositionRect.top && mPositionRect.bottom > bottomIfCentered) {
// Center it
return pointerPosition - mPointerWidth - (expandedViewHeight / 2f);
} else if (topIfCentered <= mPositionRect.top) {
// Top align
return topAlignment;
} else {
// Bottom align
return mPositionRect.bottom - manageButtonHeight - expandedViewHeight - mPointerWidth;
}
}
/**
* The position the pointer points to, the center of the bubble.
*
* @param bubblePosition the x position of the bubble if showing on top, the y position of the
* bubble if showing vertically.
* @return the position the tip of the pointer points to. The x position if showing on top, the
* y position if showing vertically.
*/
public float getPointerPosition(float bubblePosition) {
// TODO: I don't understand why it works but it does - why normalized in portrait
// & not in landscape? Am I missing ~2dp in the portrait expandedViewY calculation?
final float normalizedSize = IconNormalizer.getNormalizedCircleSize(
getBubbleSize());
return showBubblesVertically()
? bubblePosition + (getBubbleSize() / 2f)
: bubblePosition + (normalizedSize / 2f) - mPointerWidth;
}
private int getExpandedStackSize(int numberOfBubbles) {
return (numberOfBubbles * mBubbleSize)
+ ((numberOfBubbles - 1) * mSpacingBetweenBubbles);
}
/**
* Returns the position of the bubble on-screen when the stack is expanded.
*
* @param index the index of the bubble in the stack.
* @param state state information about the stack to help with calculations.
* @return the position of the bubble on-screen when the stack is expanded.
*/
public PointF getExpandedBubbleXY(int index, BubbleStackView.StackViewState state) {
boolean showBubblesVertically = showBubblesVertically();
boolean isRtl = mContext.getResources().getConfiguration().getLayoutDirection()
== LAYOUT_DIRECTION_RTL;
int onScreenIndex;
if (showBubblesVertically || !isRtl) {
onScreenIndex = index;
} else {
// If bubbles are shown horizontally, check if RTL language is used.
// If RTL is active, position first bubble on the right and last on the left.
// Last bubble has screen index 0 and first bubble has max screen index value.
onScreenIndex = state.numberOfBubbles - 1 - index;
}
final float positionInRow = onScreenIndex * (mBubbleSize + mSpacingBetweenBubbles);
final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles);
final float centerPosition = showBubblesVertically
? mPositionRect.centerY()
: mPositionRect.centerX();
// alignment - centered on the edge
final float rowStart = centerPosition - (expandedStackSize / 2f);
float x;
float y;
if (showBubblesVertically) {
int inset = mExpandedViewLargeScreenInsetClosestEdge;
y = rowStart + positionInRow;
int left = mIsLargeScreen
? inset - mExpandedViewPadding - mBubbleSize
: mPositionRect.left;
int right = mIsLargeScreen
? mPositionRect.right - inset + mExpandedViewPadding
: mPositionRect.right - mBubbleSize;
x = state.onLeft
? left
: right;
} else {
y = mPositionRect.top + mExpandedViewPadding;
x = rowStart + positionInRow;
}
if (showBubblesVertically && mImeVisible) {
return new PointF(x, getExpandedBubbleYForIme(onScreenIndex, state));
}
return new PointF(x, y);
}
/**
* Returns the position of the bubble on-screen when the stack is expanded and the IME
* is showing.
*
* @param index the index of the bubble in the stack.
* @param state information about the stack state (# of bubbles, selected bubble).
* @return y position of the bubble on-screen when the stack is expanded.
*/
private float getExpandedBubbleYForIme(int index, BubbleStackView.StackViewState state) {
final float top = getAvailableRect().top + mExpandedViewPadding;
if (!showBubblesVertically()) {
// Showing horizontally: align to top
return top;
}
// Showing vertically: might need to translate the bubbles above the IME.
// Add spacing here to provide a margin between top of IME and bottom of bubble row.
final float bottomHeight = getImeHeight() + mInsets.bottom + (mSpacingBetweenBubbles * 2);
final float bottomInset = mScreenRect.bottom - bottomHeight;
final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles);
final float centerPosition = mPositionRect.centerY();
final float rowBottom = centerPosition + (expandedStackSize / 2f);
final float rowTop = centerPosition - (expandedStackSize / 2f);
float rowTopForIme = rowTop;
if (rowBottom > bottomInset) {
// We overlap with IME, must shift the bubbles
float translationY = rowBottom - bottomInset;
rowTopForIme = Math.max(rowTop - translationY, top);
if (rowTop - translationY < top) {
// Even if we shift the bubbles, they will still overlap with the IME.
// Hide the overflow for a lil more space:
final float expandedStackSizeNoO = getExpandedStackSize(state.numberOfBubbles - 1);
final float centerPositionNoO = showBubblesVertically()
? mPositionRect.centerY()
: mPositionRect.centerX();
final float rowBottomNoO = centerPositionNoO + (expandedStackSizeNoO / 2f);
final float rowTopNoO = centerPositionNoO - (expandedStackSizeNoO / 2f);
translationY = rowBottomNoO - bottomInset;
rowTopForIme = rowTopNoO - translationY;
}
}
// Check if the selected bubble is within the appropriate space
final float selectedPosition = rowTopForIme
+ (state.selectedIndex * (mBubbleSize + mSpacingBetweenBubbles));
if (selectedPosition < top) {
// We must always keep the selected bubble in view so we'll have to allow more overlap.
rowTopForIme = top;
}
return rowTopForIme + (index * (mBubbleSize + mSpacingBetweenBubbles));
}
/**
* @return the width of the bubble flyout (message originating from the bubble).
*/
public float getMaxFlyoutSize() {
if (isLargeScreen()) {
return Math.max(mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN,
mMinimumFlyoutWidthLargeScreen);
}
return mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT;
}
/**
* @return whether the stack is considered on the left side of the screen.
*/
public boolean isStackOnLeft(PointF currentStackPosition) {
if (currentStackPosition == null) {
currentStackPosition = getRestingPosition();
}
final int stackCenter = (int) currentStackPosition.x + mBubbleSize / 2;
return stackCenter < mScreenRect.width() / 2;
}
/**
* Sets the stack's most recent position along the edge of the screen. This is saved when the
* last bubble is removed, so that the stack can be restored in its previous position.
*/
public void setRestingPosition(PointF position) {
if (mRestingStackPosition == null) {
mRestingStackPosition = new PointF(position);
} else {
mRestingStackPosition.set(position);
}
}
/** The position the bubble stack should rest at when collapsed. */
public PointF getRestingPosition() {
if (mRestingStackPosition == null) {
return getDefaultStartPosition();
}
return mRestingStackPosition;
}
/**
* @return the stack position to use if we don't have a saved location or if user education
* is being shown.
*/
public PointF getDefaultStartPosition() {
// Start on the left if we're in LTR, right otherwise.
final boolean startOnLeft =
mContext.getResources().getConfiguration().getLayoutDirection()
!= LAYOUT_DIRECTION_RTL;
final float startingVerticalOffset = mContext.getResources().getDimensionPixelOffset(
R.dimen.bubble_stack_starting_offset_y);
// TODO: placement bug here because mPositionRect doesn't handle the overhanging edge
return new BubbleStackView.RelativeStackPosition(
startOnLeft,
startingVerticalOffset / mPositionRect.height())
.getAbsolutePositionInRegion(getAllowableStackPositionRegion(
1 /* default starts with 1 bubble */));
}
/**
* Returns the region that the stack position must stay within. This goes slightly off the left
* and right sides of the screen, below the status bar/cutout and above the navigation bar.
* While the stack position is not allowed to rest outside of these bounds, it can temporarily
* be animated or dragged beyond them.
*/
public RectF getAllowableStackPositionRegion(int bubbleCount) {
final RectF allowableRegion = new RectF(getAvailableRect());
final int imeHeight = getImeHeight();
final float bottomPadding = bubbleCount > 1
? mBubblePaddingTop + mStackOffset
: mBubblePaddingTop;
allowableRegion.left -= mBubbleOffscreenAmount;
allowableRegion.top += mBubblePaddingTop;
allowableRegion.right += mBubbleOffscreenAmount - mBubbleSize;
allowableRegion.bottom -= imeHeight + bottomPadding + mBubbleSize;
return allowableRegion;
}
/**
* Navigation bar has an area where system gestures can be started from.
*
* @return {@link Rect} for system navigation bar gesture zone
*/
public Rect getNavBarGestureZone() {
// Gesture zone height from the bottom
int gestureZoneHeight = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.navigation_bar_gesture_height);
Rect screen = getScreenRect();
return new Rect(
screen.left,
screen.bottom - gestureZoneHeight,
screen.right,
screen.bottom);
}
//
// Bubble bar specific sizes below.
//
/**
* Sets whether bubbles are showing in the bubble bar from launcher.
*/
public void setShowingInBubbleBar(boolean showingInBubbleBar) {
mShowingInBubbleBar = showingInBubbleBar;
}
/**
* Sets whether bubbles are showing on launcher home, in which case positions are different.
*/
public void setBubblesOnHome(boolean bubblesOnHome) {
mBubblesOnHome = bubblesOnHome;
}
/**
* How wide the expanded view should be when showing from the bubble bar.
*/
public int getExpandedViewWidthForBubbleBar() {
return mExpandedViewLargeScreenWidth;
}
/**
* How tall the expanded view should be when showing from the bubble bar.
*/
public int getExpandedViewHeightForBubbleBar() {
return getAvailableRect().height()
- mBubbleBarSize
- mExpandedViewPadding * 2
- getBubbleBarHomeAdjustment();
}
/**
* The amount of padding from the edge of the screen to the expanded view when in bubble bar.
*/
public int getBubbleBarExpandedViewPadding() {
return mExpandedViewPadding;
}
/**
* Returns the on screen co-ordinates of the bubble bar.
*/
public PointF getBubbleBarPosition() {
mBubbleBarPosition.set(getAvailableRect().width() - mBubbleBarSize,
getAvailableRect().height() - mBubbleBarSize
- mExpandedViewPadding - getBubbleBarHomeAdjustment());
return mBubbleBarPosition;
}
/**
* When bubbles are shown on launcher home, there's an extra bit of padding that needs to
* be applied between the expanded view and the bubble bar. This returns the adjustment value
* if bubbles are showing on home.
*/
private int getBubbleBarHomeAdjustment() {
return mBubblesOnHome ? mBubbleBarHomeAdjustment : 0;
}
}