blob: c600f56ba0c5af7882cac0049ab392902f9391bf [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 java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.IntDef;
import android.content.Context;
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.View;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import androidx.annotation.VisibleForTesting;
import com.android.wm.shell.R;
import java.lang.annotation.Retention;
/**
* 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;
@Retention(SOURCE)
@IntDef({TASKBAR_POSITION_NONE, TASKBAR_POSITION_RIGHT, TASKBAR_POSITION_LEFT,
TASKBAR_POSITION_BOTTOM})
@interface TaskbarPosition {}
public static final int TASKBAR_POSITION_NONE = -1;
public static final int TASKBAR_POSITION_RIGHT = 0;
public static final int TASKBAR_POSITION_LEFT = 1;
public static final int TASKBAR_POSITION_BOTTOM = 2;
/** 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;
private Context mContext;
private WindowManager mWindowManager;
private Rect mPositionRect;
private @Surface.Rotation int mRotation = Surface.ROTATION_0;
private Insets mInsets;
private int mDefaultMaxBubbles;
private int mMaxBubbles;
private int mBubbleSize;
private int mBubbleBadgeSize;
private int mSpacingBetweenBubbles;
private int mExpandedViewLargeScreenWidth;
private int mExpandedViewPadding;
private int mPointerMargin;
private float mPointerWidth;
private float mPointerHeight;
private PointF mPinLocation;
private PointF mRestingStackPosition;
private int[] mPaddings = new int[4];
private boolean mIsLargeScreen;
private boolean mShowingInTaskbar;
private @TaskbarPosition int mTaskbarPosition = TASKBAR_POSITION_NONE;
private int mTaskbarIconSize;
private int mTaskbarSize;
public BubblePositioner(Context context, WindowManager windowManager) {
mContext = context;
mWindowManager = windowManager;
update();
}
public void setRotation(int rotation) {
mRotation = rotation;
}
/**
* 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());
mIsLargeScreen = mContext.getResources().getConfiguration().smallestScreenWidthDp >= 600;
if (BubbleDebugConfig.DEBUG_POSITIONER) {
Log.w(TAG, "update positioner:"
+ " rotation: " + mRotation
+ " insets: " + insets
+ " isLargeScreen: " + mIsLargeScreen
+ " bounds: " + windowMetrics.getBounds()
+ " showingInTaskbar: " + mShowingInTaskbar);
}
updateInternal(mRotation, insets, windowMetrics.getBounds());
}
/**
* Updates position information to account for taskbar state.
*
* @param taskbarPosition which position the taskbar is displayed in.
* @param showingInTaskbar whether the taskbar is being shown.
*/
public void updateForTaskbar(int iconSize,
@TaskbarPosition int taskbarPosition, boolean showingInTaskbar, int taskbarSize) {
mShowingInTaskbar = showingInTaskbar;
mTaskbarIconSize = iconSize;
mTaskbarPosition = taskbarPosition;
mTaskbarSize = taskbarSize;
update();
}
@VisibleForTesting
public void updateInternal(int rotation, Insets insets, Rect bounds) {
mRotation = rotation;
mInsets = insets;
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);
mBubbleBadgeSize = res.getDimensionPixelSize(R.dimen.bubble_badge_size);
mSpacingBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing);
mDefaultMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
mExpandedViewLargeScreenWidth = res.getDimensionPixelSize(
R.dimen.bubble_expanded_view_tablet_width);
mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin);
mMaxBubbles = calculateMaxBubbles();
if (mShowingInTaskbar) {
adjustForTaskbar();
}
}
/**
* @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;
}
/**
* Taskbar insets appear as navigationBar insets, however, unlike navigationBar this should
* not inset bubbles UI as bubbles floats above the taskbar. This adjust the available space
* and insets to account for the taskbar.
*/
// TODO(b/171559950): When the insets are reported correctly we can remove this logic
private void adjustForTaskbar() {
// When bar is showing on edges... subtract that inset because we appear on top
if (mShowingInTaskbar && mTaskbarPosition != TASKBAR_POSITION_BOTTOM) {
WindowInsets metricInsets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
Insets navBarInsets = metricInsets.getInsetsIgnoringVisibility(
WindowInsets.Type.navigationBars());
int newInsetLeft = mInsets.left;
int newInsetRight = mInsets.right;
if (mTaskbarPosition == TASKBAR_POSITION_LEFT) {
mPositionRect.left -= navBarInsets.left;
newInsetLeft -= navBarInsets.left;
} else if (mTaskbarPosition == TASKBAR_POSITION_RIGHT) {
mPositionRect.right += navBarInsets.right;
newInsetRight -= navBarInsets.right;
}
mInsets = Insets.of(newInsetLeft, mInsets.top, newInsetRight, mInsets.bottom);
}
}
/**
* @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 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 mRotation == Surface.ROTATION_90 || mRotation == Surface.ROTATION_270;
}
/** @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() || mShowingInTaskbar || mIsLargeScreen;
}
/** Size of the bubble. */
public int getBubbleSize() {
return (mShowingInTaskbar && mTaskbarIconSize > 0)
? mTaskbarIconSize
: mBubbleSize;
}
/** The maximum number of bubbles that can be displayed comfortably on screen. */
public int getMaxBubbles() {
return mMaxBubbles;
}
/**
* Calculates the left & right padding for the bubble expanded view.
*
* On larger screens the width of the expanded view is restricted via this padding.
* On landscape the bubble overflow expanded view is also restricted via this padding.
*/
public int[] getExpandedViewPadding(boolean onLeft, boolean isOverflow) {
int leftPadding = mInsets.left + mExpandedViewPadding;
int rightPadding = mInsets.right + mExpandedViewPadding;
final boolean isLargeOrOverflow = mIsLargeScreen || isOverflow;
if (showBubblesVertically()) {
if (!onLeft) {
rightPadding += mBubbleSize - mPointerHeight;
leftPadding += isLargeOrOverflow
? (mPositionRect.width() - rightPadding - mExpandedViewLargeScreenWidth)
: 0;
} else {
leftPadding += mBubbleSize - mPointerHeight;
rightPadding += isLargeOrOverflow
? (mPositionRect.width() - leftPadding - mExpandedViewLargeScreenWidth)
: 0;
}
}
// [left, top, right, bottom]
mPaddings[0] = leftPadding;
mPaddings[1] = showBubblesVertically() ? 0 : mPointerMargin;
mPaddings[2] = rightPadding;
mPaddings[3] = 0;
return mPaddings;
}
/** Calculates the y position of the expanded view when it is expanded. */
public float getExpandedViewY() {
final int top = getAvailableRect().top;
if (showBubblesVertically()) {
return top - mPointerWidth;
} else {
return top + mBubbleSize + mPointerMargin;
}
}
/**
* 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 (mPinLocation != null) {
return mPinLocation;
}
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()
!= View.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(new RectF(mPositionRect));
}
/**
* @return whether the bubble stack is pinned to the taskbar.
*/
public boolean showingInTaskbar() {
return mShowingInTaskbar;
}
/**
* @return the taskbar position if set.
*/
public int getTaskbarPosition() {
return mTaskbarPosition;
}
public int getTaskbarSize() {
return mTaskbarSize;
}
/**
* In some situations bubbles will be pinned to a specific onscreen location. This sets the
* location to anchor the stack to.
*/
public void setPinnedLocation(PointF point) {
mPinLocation = point;
}
}