Flexible bubble sizing & fixed position

This will enable some stuff for taskbar & hopefully make customizations by
ARC++ easier??

- BubblePositioner now tracks the size of the bubble & bitmap within it.
  Have to use a multiplier to derive the size of a couple of things now
  (e.g. bitmap size)
- BubblePositioner now tracks the position of the bubble stack & allows
  a way to override / set a fixed position. If something is in fixed
  location, it is not draggable (however when expanded you can still
  drag individual bubbles to dismiss)
- BubblePositioner tracks some taskbar related info
- Rather than checking for "isLandscape" we want "showBubblesVertically"
  (e.g. device might not be landscape but should be vertical if taskbar)
- Fixes an animation issue where bubbles super-overshoot the y position
  when expanding vertically.

Bug: 167413172
Test: a lot of manual testing
Test: atest WMShellTest SystemUITests
Change-Id: I79948a4bd6b58fb3aa2f52e2553f3dfc31630cf1
This commit is contained in:
Mady Mellor
2020-11-06 13:44:05 -08:00
parent cef78df671
commit 97fcd7a082
24 changed files with 514 additions and 222 deletions

View File

@@ -16,14 +16,16 @@
-->
<com.android.wm.shell.bubbles.BubbleMenuView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="#66000000"
android:visibility="gone"
android:id="@+id/bubble_menu_container">
android:id="@+id/bubble_menu_container"
tools:ignore="MissingClass">
<FrameLayout
android:layout_height="@dimen/individual_bubble_size"
android:layout_height="@dimen/bubble_menu_item_height"
android:layout_width="wrap_content"
android:background="#FFFFFF"
android:id="@+id/bubble_menu_view">

View File

@@ -17,6 +17,6 @@
<com.android.wm.shell.bubbles.BadgedImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/bubble_overflow_button"
android:layout_width="@dimen/individual_bubble_size"
android:layout_height="@dimen/individual_bubble_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/bubble_ic_overflow_button"/>

View File

@@ -17,5 +17,5 @@
<com.android.wm.shell.bubbles.BadgedImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/bubble_view"
android:layout_width="@dimen/individual_bubble_size"
android:layout_height="@dimen/individual_bubble_size"/>
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

View File

@@ -99,12 +99,12 @@
<dimen name="bubble_flyout_avatar_message_space">6dp</dimen>
<!-- Padding between status bar and bubbles when displayed in expanded state -->
<dimen name="bubble_padding_top">16dp</dimen>
<!-- Max amount of space between bubbles when expanded. -->
<dimen name="bubble_max_spacing">16dp</dimen>
<!-- Size of individual bubbles. -->
<dimen name="individual_bubble_size">60dp</dimen>
<!-- Size of bubble bitmap. -->
<dimen name="bubble_bitmap_size">52dp</dimen>
<!-- Size of bubble icon bitmap. -->
<dimen name="bubble_overflow_icon_bitmap_size">24dp</dimen>
<!-- Extra padding added to the touchable rect for bubbles so they are easier to grab. -->
<dimen name="bubble_touch_padding">12dp</dimen>
<!-- Size of the circle around the bubbles when they're in the dismiss target. -->
@@ -136,6 +136,8 @@
<dimen name="bubble_dismiss_slop">16dp</dimen>
<!-- Height of button allowing users to adjust settings for bubbles. -->
<dimen name="bubble_manage_button_height">48dp</dimen>
<!-- Height of an item in the bubble manage menu. -->
<dimen name="bubble_menu_item_height">60dp</dimen>
<!-- Max width of the message bubble-->
<dimen name="bubble_message_max_width">144dp</dimen>
<!-- Min width of the message bubble -->

View File

@@ -31,7 +31,6 @@ import android.util.PathParser;
import android.widget.ImageView;
import com.android.launcher3.icons.DotRenderer;
import com.android.wm.shell.R;
import com.android.wm.shell.animation.Interpolators;
import java.util.EnumSet;
@@ -75,9 +74,8 @@ public class BadgedImageView extends ImageView {
private boolean mDotIsAnimating = false;
private BubbleViewProvider mBubble;
private BubblePositioner mPositioner;
private int mBubbleBitmapSize;
private int mBubbleSize;
private DotRenderer mDotRenderer;
private DotRenderer.DrawParams mDrawParams;
private boolean mOnLeft;
@@ -101,18 +99,21 @@ public class BadgedImageView extends ImageView {
public BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mBubbleBitmapSize = getResources().getDimensionPixelSize(R.dimen.bubble_bitmap_size);
mBubbleSize = getResources().getDimensionPixelSize(R.dimen.individual_bubble_size);
mDrawParams = new DotRenderer.DrawParams();
Path iconPath = PathParser.createPathFromPathData(
getResources().getString(com.android.internal.R.string.config_icon_mask));
mDotRenderer = new DotRenderer(mBubbleBitmapSize, iconPath, DEFAULT_PATH_SIZE);
setFocusable(true);
setClickable(true);
}
public void initialize(BubblePositioner positioner) {
mPositioner = positioner;
Path iconPath = PathParser.createPathFromPathData(
getResources().getString(com.android.internal.R.string.config_icon_mask));
mDotRenderer = new DotRenderer(mPositioner.getBubbleBitmapSize(),
iconPath, DEFAULT_PATH_SIZE);
}
public void showDotAndBadge(boolean onLeft) {
removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.BEHIND_STACK);
animateDotBadgePositions(onLeft);
@@ -186,7 +187,8 @@ public class BadgedImageView extends ImageView {
* @param iconPath The new icon path to use when calculating dot position.
*/
void drawDot(Path iconPath) {
mDotRenderer = new DotRenderer(mBubbleBitmapSize, iconPath, DEFAULT_PATH_SIZE);
mDotRenderer = new DotRenderer(mPositioner.getBubbleBitmapSize(),
iconPath, DEFAULT_PATH_SIZE);
invalidate();
}
@@ -310,13 +312,13 @@ public class BadgedImageView extends ImageView {
bubbleCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG));
bubbleCanvas.setBitmap(bubble);
final int badgeSize = (int) (ICON_BADGE_SCALE * mBubbleSize);
final int bubbleSize = bubble.getWidth();
final int badgeSize = (int) (ICON_BADGE_SCALE * bubbleSize);
if (mOnLeft) {
badge.setBounds(0, mBubbleSize - badgeSize, badgeSize, mBubbleSize);
badge.setBounds(0, bubbleSize - badgeSize, badgeSize, bubbleSize);
} else {
badge.setBounds(mBubbleSize - badgeSize, mBubbleSize - badgeSize,
mBubbleSize, mBubbleSize);
badge.setBounds(bubbleSize - badgeSize, bubbleSize - badgeSize,
bubbleSize, bubbleSize);
}
badge.draw(bubbleCanvas);
bubbleCanvas.setBitmap(null);

View File

@@ -310,7 +310,7 @@ public class Bubble implements BubbleViewProvider {
*
* @param callback the callback to notify one the bubble is ready to be displayed.
* @param context the context for the bubble.
* @param controller
* @param controller the bubble controller.
* @param stackView the stackView the bubble is eventually added to.
* @param iconFactory the iconfactory use to create badged images for the bubble.
*/

View File

@@ -95,12 +95,6 @@ public class BubbleController implements Bubbles {
private BubblePositioner mBubblePositioner;
private SysuiProxy mSysuiProxy;
/**
* The relative position of the stack when we removed it and nulled it out. If the stack is
* re-created, it will re-appear at this position.
*/
@Nullable private BubbleStackView.RelativeStackPosition mPositionFromRemovedStack;
// Tracks the id of the current (foreground) user.
private int mCurrentUserId;
// Saves notification keys of active bubbles when users are switched.
@@ -176,12 +170,12 @@ public class BubbleController implements Bubbles {
Handler mainHandler,
ShellTaskOrganizer organizer) {
BubbleLogger logger = new BubbleLogger(uiEventLogger);
return new BubbleController(context,
new BubbleData(context, logger), synchronizer,
floatingContentCoordinator, new BubbleDataRepository(context, launcherApps),
statusBarService, windowManager,
windowManagerShellWrapper, launcherApps, logger, mainHandler, organizer,
new BubblePositioner(context, windowManager));
BubblePositioner positioner = new BubblePositioner(context, windowManager);
BubbleData data = new BubbleData(context, logger);
return new BubbleController(context, data, synchronizer, floatingContentCoordinator,
new BubbleDataRepository(context, launcherApps),
statusBarService, windowManager, windowManagerShellWrapper, launcherApps,
logger, mainHandler, organizer, positioner);
}
/**
@@ -207,6 +201,7 @@ public class BubbleController implements Bubbles {
mLogger = bubbleLogger;
mMainHandler = mainHandler;
mBubblePositioner = positioner;
mBubbleData = data;
mBubbleData.setListener(mBubbleDataListener);
mBubbleData.setSuppressionChangedListener(bubble -> {
@@ -249,7 +244,6 @@ public class BubbleController implements Bubbles {
mBubbleIconFactory = new BubbleIconFactory(context);
mTaskOrganizer = organizer;
mBubblePositioner = positioner;
launcherApps.registerCallback(new LauncherApps.Callback() {
@Override
@@ -388,7 +382,6 @@ public class BubbleController implements Bubbles {
if (mStackView == null) {
mStackView = new BubbleStackView(
mContext, this, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator);
mStackView.setStackStartPosition(mPositionFromRemovedStack);
mStackView.addView(mBubbleScrim);
mStackView.onOrientationChanged();
if (mExpandListener != null) {
@@ -430,6 +423,8 @@ public class BubbleController implements Bubbles {
try {
mAddedToWindowManager = true;
mWindowManager.addView(mStackView, mWmLayoutParams);
// Position info is dependent on us being attached to a window
mBubblePositioner.update(mOrientation);
} catch (IllegalStateException e) {
// This means the stack has already been added. This shouldn't happen...
e.printStackTrace();
@@ -449,7 +444,6 @@ public class BubbleController implements Bubbles {
try {
mAddedToWindowManager = false;
if (mStackView != null) {
mPositionFromRemovedStack = mStackView.getRelativeStackPosition();
mWindowManager.removeView(mStackView);
mStackView.removeView(mBubbleScrim);
mStackView = null;
@@ -544,7 +538,7 @@ public class BubbleController implements Bubbles {
}
if (newConfig.fontScale != mFontScale) {
mFontScale = newConfig.fontScale;
mStackView.updateFlyout(mFontScale);
mStackView.updateFontScale(mFontScale);
}
if (newConfig.getLayoutDirection() != mLayoutDirection) {
mLayoutDirection = newConfig.getLayoutDirection();

View File

@@ -45,6 +45,7 @@ public class BubbleDebugConfig {
static final boolean DEBUG_EXPERIMENTS = true;
static final boolean DEBUG_OVERFLOW = false;
static final boolean DEBUG_USER_EDUCATION = false;
static final boolean DEBUG_POSITIONER = false;
private static final boolean FORCE_SHOW_USER_EDUCATION = false;
private static final String FORCE_SHOW_USER_EDUCATION_SETTING =

View File

@@ -639,7 +639,9 @@ public class BubbleExpandedView extends LinearLayout {
}
/**
* Cleans up anything related to the task and TaskView.
* Cleans up anything related to the task and TaskView. If this view should be reused after this
* method is called, then {@link #initialize(BubbleController, BubbleStackView)} must be invoked
* first.
*/
public void cleanUpExpandedState() {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {

View File

@@ -68,9 +68,8 @@ public class BubbleFlyoutView extends FrameLayout {
private final int mFlyoutPadding;
private final int mFlyoutSpaceFromBubble;
private final int mPointerSize;
private final int mBubbleSize;
private final int mBubbleBitmapSize;
private final float mBubbleIconTopPadding;
private int mBubbleSize;
private int mBubbleBitmapSize;
private final int mFlyoutElevation;
private final int mBubbleElevation;
@@ -83,9 +82,9 @@ public class BubbleFlyoutView extends FrameLayout {
private final TextView mMessageText;
/** Values related to the 'new' dot which we use to figure out where to collapse the flyout. */
private final float mNewDotRadius;
private final float mNewDotSize;
private final float mOriginalDotSize;
private float mNewDotRadius;
private float mNewDotSize;
private float mOriginalDotSize;
/**
* The paint used to draw the background, whose color changes as the flyout transitions to the
@@ -169,17 +168,9 @@ public class BubbleFlyoutView extends FrameLayout {
mFlyoutSpaceFromBubble = res.getDimensionPixelSize(R.dimen.bubble_flyout_space_from_bubble);
mPointerSize = res.getDimensionPixelSize(R.dimen.bubble_flyout_pointer_size);
mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
mBubbleBitmapSize = res.getDimensionPixelSize(R.dimen.bubble_bitmap_size);
mBubbleIconTopPadding = (mBubbleSize - mBubbleBitmapSize) / 2f;
mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
mFlyoutElevation = res.getDimensionPixelSize(R.dimen.bubble_flyout_elevation);
mOriginalDotSize = SIZE_PERCENTAGE * mBubbleBitmapSize;
mNewDotRadius = (DOT_SCALE * mOriginalDotSize) / 2f;
mNewDotSize = mNewDotRadius * 2f;
final TypedArray ta = mContext.obtainStyledAttributes(
new int[] {
android.R.attr.colorBackgroundFloating,
@@ -306,7 +297,15 @@ public class BubbleFlyoutView extends FrameLayout {
@Nullable Runnable onLayoutComplete,
@Nullable Runnable onHide,
float[] dotCenter,
boolean hideDot) {
boolean hideDot,
BubblePositioner positioner) {
mBubbleBitmapSize = positioner.getBubbleBitmapSize();
mBubbleSize = positioner.getBubbleSize();
mOriginalDotSize = SIZE_PERCENTAGE * mBubbleBitmapSize;
mNewDotRadius = (DOT_SCALE * mOriginalDotSize) / 2f;
mNewDotSize = mNewDotRadius * 2f;
updateFlyoutMessage(flyoutMessage, parentWidth);

View File

@@ -29,14 +29,18 @@ import android.graphics.drawable.InsetDrawable
import android.util.PathParser
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import com.android.wm.shell.R
/**
* The icon in the bubble overflow is scaled down, this is the percent of the normal bubble bitmap
* size to use.
*/
const val ICON_BITMAP_SIZE_PERCENT = 0.46f
class BubbleOverflow(
private val context: Context,
private val controller: BubbleController,
private val stack: BubbleStackView
private val positioner: BubblePositioner
) : BubbleViewProvider {
private lateinit var bitmap: Bitmap
@@ -48,41 +52,36 @@ class BubbleOverflow(
private var showDot = false
private val inflater: LayoutInflater = LayoutInflater.from(context)
private val expandedView: BubbleExpandedView = inflater
.inflate(R.layout.bubble_expanded_view, null /* root */, false /* attachToRoot */)
as BubbleExpandedView
private val overflowBtn: BadgedImageView = inflater
.inflate(R.layout.bubble_overflow_button, null /* root */, false /* attachToRoot */)
as BadgedImageView
private var expandedView: BubbleExpandedView?
private var overflowBtn: BadgedImageView?
init {
updateResources()
with(expandedView) {
initialize(controller, stack)
setOverflow(true)
applyThemeAttrs()
}
with(overflowBtn) {
setContentDescription(context.resources.getString(
R.string.bubble_overflow_button_content_description))
updateBtnTheme()
}
bitmapSize = positioner.bubbleBitmapSize
iconBitmapSize = (bitmapSize * ICON_BITMAP_SIZE_PERCENT).toInt()
expandedView = null
overflowBtn = null
}
fun initialize(controller: BubbleController) {
getExpandedView()?.initialize(controller, controller.stackView)
getExpandedView()?.setOverflow(true)
}
fun update() {
updateResources()
expandedView.applyThemeAttrs()
getExpandedView()?.applyThemeAttrs()
// Apply inset and new style to fresh icon drawable.
overflowBtn.setImageResource(R.drawable.bubble_ic_overflow_button)
getIconView()?.setImageResource(R.drawable.bubble_ic_overflow_button)
updateBtnTheme()
}
fun updateResources() {
bitmapSize = context.resources.getDimensionPixelSize(R.dimen.bubble_bitmap_size)
iconBitmapSize = context.resources.getDimensionPixelSize(
R.dimen.bubble_overflow_icon_bitmap_size)
val bubbleSize = context.resources.getDimensionPixelSize(R.dimen.individual_bubble_size)
overflowBtn.setLayoutParams(FrameLayout.LayoutParams(bubbleSize, bubbleSize))
expandedView.updateDimensions()
bitmapSize = positioner.bubbleBitmapSize
iconBitmapSize = (bitmapSize * 0.46f).toInt()
val bubbleSize = positioner.bubbleSize
overflowBtn?.setLayoutParams(FrameLayout.LayoutParams(bubbleSize, bubbleSize))
expandedView?.updateDimensions()
}
private fun updateBtnTheme() {
@@ -92,7 +91,7 @@ class BubbleOverflow(
val typedValue = TypedValue()
context.theme.resolveAttribute(android.R.attr.colorAccent, typedValue, true)
val colorAccent = res.getColor(typedValue.resourceId)
overflowBtn.drawable?.setTint(colorAccent)
overflowBtn?.drawable?.setTint(colorAccent)
dotColor = colorAccent
val iconFactory = BubbleIconFactory(context)
@@ -103,7 +102,7 @@ class BubbleOverflow(
val bg = ColorDrawable(res.getColor(
if (nightMode) R.color.bubbles_dark else R.color.bubbles_light))
val fg = InsetDrawable(overflowBtn.drawable,
val fg = InsetDrawable(overflowBtn?.drawable,
bitmapSize - iconBitmapSize /* inset */)
bitmap = iconFactory.createBadgedIconBitmap(AdaptiveIconDrawable(bg, fg),
null /* user */, true /* shrinkNonAdaptiveIcons */).icon
@@ -111,7 +110,7 @@ class BubbleOverflow(
// Update dot path
dotPath = PathParser.createPathFromPathData(
res.getString(com.android.internal.R.string.config_icon_mask))
val scale = iconFactory.normalizer.getScale(overflowBtn.getDrawable(),
val scale = iconFactory.normalizer.getScale(getIconView()!!.getDrawable(),
null /* outBounds */, null /* path */, null /* outMaskShape */)
val radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f
val matrix = Matrix()
@@ -120,20 +119,31 @@ class BubbleOverflow(
dotPath.transform(matrix)
// Attach BubbleOverflow to BadgedImageView
overflowBtn.setRenderedBubble(this)
overflowBtn.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE)
overflowBtn?.setRenderedBubble(this)
overflowBtn?.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE)
}
fun setVisible(visible: Int) {
overflowBtn.visibility = visible
overflowBtn?.visibility = visible
}
fun setShowDot(show: Boolean) {
showDot = show
overflowBtn.updateDotVisibility(true /* animate */)
overflowBtn?.updateDotVisibility(true /* animate */)
}
fun cleanUpExpandedState() {
expandedView?.cleanUpExpandedState()
expandedView = null
}
override fun getExpandedView(): BubbleExpandedView? {
if (expandedView == null) {
expandedView = inflater.inflate(R.layout.bubble_expanded_view,
null /* root */, false /* attachToRoot */) as BubbleExpandedView
expandedView?.applyThemeAttrs()
updateResources()
}
return expandedView
}
@@ -158,10 +168,20 @@ class BubbleOverflow(
}
override fun setContentVisibility(visible: Boolean) {
expandedView.setContentVisibility(visible)
expandedView?.setContentVisibility(visible)
}
override fun getIconView(): View? {
override fun getIconView(): BadgedImageView? {
if (overflowBtn == null) {
overflowBtn = inflater.inflate(R.layout.bubble_overflow_button,
null /* root */, false /* attachToRoot */) as BadgedImageView
overflowBtn?.initialize(positioner)
overflowBtn?.setContentDescription(context.resources.getString(
R.string.bubble_overflow_button_content_description))
val bubbleSize = positioner.bubbleSize
overflowBtn?.setLayoutParams(FrameLayout.LayoutParams(bubbleSize, bubbleSize))
updateBtnTheme()
}
return overflowBtn
}
@@ -170,7 +190,7 @@ class BubbleOverflow(
}
override fun getTaskId(): Int {
return if (expandedView != null) expandedView.getTaskId() else INVALID_TASK_ID
return if (expandedView != null) expandedView!!.getTaskId() else INVALID_TASK_ID
}
companion object {

View File

@@ -111,11 +111,11 @@ public class BubbleOverflowActivity extends Activity {
IBinder binder = intent.getExtras().getBinder(EXTRA_BUBBLE_CONTROLLER);
if (binder instanceof ObjectWrapper) {
mController = ((ObjectWrapper<BubbleController>) binder).get();
updateOverflow();
}
} else {
Log.w(TAG, "Bubble overflow activity created without bubble controller!");
}
updateOverflow();
}
void updateOverflow() {
@@ -138,7 +138,9 @@ public class BubbleOverflowActivity extends Activity {
final int viewHeight = recyclerViewHeight / rows;
mAdapter = new BubbleOverflowAdapter(getApplicationContext(), mOverflowBubbles,
mController::promoteBubbleFromOverflow, viewWidth, viewHeight);
mController::promoteBubbleFromOverflow,
mController.getPositioner(),
viewWidth, viewHeight);
mRecyclerView.setAdapter(mAdapter);
mOverflowBubbles.clear();
@@ -257,15 +259,20 @@ class BubbleOverflowAdapter extends RecyclerView.Adapter<BubbleOverflowAdapter.V
private Context mContext;
private Consumer<Bubble> mPromoteBubbleFromOverflow;
private BubblePositioner mPositioner;
private List<Bubble> mBubbles;
private int mWidth;
private int mHeight;
public BubbleOverflowAdapter(Context context, List<Bubble> list, Consumer<Bubble> promoteBubble,
BubbleOverflowAdapter(Context context,
List<Bubble> list,
Consumer<Bubble> promoteBubble,
BubblePositioner positioner,
int width, int height) {
mContext = context;
mBubbles = list;
mPromoteBubbleFromOverflow = promoteBubble;
mPositioner = positioner;
mWidth = width;
mHeight = height;
}
@@ -295,7 +302,7 @@ class BubbleOverflowAdapter extends RecyclerView.Adapter<BubbleOverflowAdapter.V
TextView viewName = overflowView.findViewById(R.id.bubble_view_name);
viewName.setTextColor(textColor);
return new ViewHolder(overflowView);
return new ViewHolder(overflowView, mPositioner);
}
@Override
@@ -348,9 +355,10 @@ class BubbleOverflowAdapter extends RecyclerView.Adapter<BubbleOverflowAdapter.V
public BadgedImageView iconView;
public TextView textView;
public ViewHolder(LinearLayout v) {
ViewHolder(LinearLayout v, BubblePositioner positioner) {
super(v);
iconView = v.findViewById(R.id.bubble_view);
iconView.initialize(positioner);
textView = v.findViewById(R.id.bubble_view_name);
}
}

View File

@@ -16,45 +16,99 @@
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.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.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_RIGHT, TASKBAR_POSITION_LEFT, TASKBAR_POSITION_BOTTOM})
@interface TaskbarPosition {}
public static final int TASKBAR_POSITION_RIGHT = 0;
public static final int TASKBAR_POSITION_LEFT = 1;
public static final int TASKBAR_POSITION_BOTTOM = 2;
/**
* The bitmap in the bubble is slightly smaller than the overall size of the bubble.
* This is the percentage to scale the image down based on the overall bubble size.
*/
private static final float BUBBLE_BITMAP_SIZE_PERCENT = 0.86f;
private Context mContext;
private WindowManager mWindowManager;
private Rect mPositionRect;
private int mOrientation;
private Insets mInsets;
private int mBubbleSize;
private int mBubbleBitmapSize;
private PointF mPinLocation;
private PointF mRestingStackPosition;
private boolean mShowingInTaskbar;
private @TaskbarPosition int mTaskbarPosition;
private int mTaskbarIconSize;
public BubblePositioner(Context context, WindowManager windowManager) {
mContext = context;
mWindowManager = windowManager;
update(Configuration.ORIENTATION_UNDEFINED);
}
/**
* Updates orientation, available space, and inset information. Call this when config changes
* occur or when added to a window.
*/
public void update(int orientation) {
WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
mPositionRect = new Rect(windowMetrics.getBounds());
if (windowMetrics == null) {
return;
}
WindowInsets metricInsets = windowMetrics.getWindowInsets();
Insets insets = metricInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()
| WindowInsets.Type.statusBars()
| WindowInsets.Type.displayCutout());
update(orientation, insets, windowMetrics.getBounds());
if (BubbleDebugConfig.DEBUG_POSITIONER) {
Log.w(TAG, "update positioner:"
+ " landscape= " + (orientation == Configuration.ORIENTATION_LANDSCAPE)
+ " insets: " + insets
+ " bounds: " + windowMetrics.getBounds()
+ " showingInTaskbar: " + mShowingInTaskbar);
}
updateInternal(orientation, insets, windowMetrics.getBounds());
}
@VisibleForTesting
public void update(int orientation, Insets insets, Rect bounds) {
public void updateInternal(int orientation, Insets insets, Rect bounds) {
mOrientation = orientation;
mInsets = insets;
@@ -63,27 +117,156 @@ public class BubblePositioner {
mPositionRect.top += mInsets.top;
mPositionRect.right -= mInsets.right;
mPositionRect.bottom -= mInsets.bottom;
Resources res = mContext.getResources();
mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
mBubbleBitmapSize = res.getDimensionPixelSize(R.dimen.bubble_bitmap_size);
adjustForTaskbar();
}
/**
* @return a rect of available screen space for displaying bubbles in the correct orientation,
* accounting for system bars and cutouts.
* 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) {
mShowingInTaskbar = showingInTaskbar;
mTaskbarIconSize = iconSize;
mTaskbarPosition = taskbarPosition;
update(mOrientation);
}
/**
* 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 current orientation.
*/
public int getOrientation() {
return mOrientation;
}
/**
* @return the relevant insets (status bar, nav bar, cutouts).
* @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 mOrientation == Configuration.ORIENTATION_LANDSCAPE;
}
/**
* 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 mOrientation == Configuration.ORIENTATION_LANDSCAPE
|| mShowingInTaskbar;
}
/** Size of the bubble account for badge & dot. */
public int getBubbleSize() {
int bsize = (mShowingInTaskbar && mTaskbarIconSize > 0)
? mTaskbarIconSize
: mBubbleSize;
return bsize;
}
/** Size of the bitmap within the bubble */
public int getBubbleBitmapSize() {
float size = (mShowingInTaskbar && mTaskbarIconSize > 0)
? (mTaskbarIconSize * BUBBLE_BITMAP_SIZE_PERCENT)
: mBubbleBitmapSize;
return (int) size;
}
/**
* 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;
}
/**
* 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;
}
}

View File

@@ -30,7 +30,6 @@ import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.ColorMatrix;
@@ -701,7 +700,6 @@ public class BubbleStackView extends FrameLayout
}
};
@Nullable
private BubbleOverflow mBubbleOverflow;
private StackEducationView mStackEduView;
private ManageEducationView mManageEduView;
@@ -818,11 +816,12 @@ public class BubbleStackView extends FrameLayout
setFocusable(true);
mBubbleContainer.bringToFront();
mBubbleOverflow = new BubbleOverflow(getContext(), bubbleController, this);
mBubbleOverflow = new BubbleOverflow(getContext(), mPositioner);
mBubbleOverflow.initialize(mBubbleController);
mBubbleContainer.addView(mBubbleOverflow.getIconView(),
mBubbleContainer.getChildCount() /* index */,
new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
new FrameLayout.LayoutParams(mPositioner.getBubbleSize(),
mPositioner.getBubbleSize()));
updateOverflow();
mBubbleOverflow.getIconView().setOnClickListener((View v) -> {
setSelectedBubble(mBubbleOverflow);
@@ -1067,7 +1066,7 @@ public class BubbleStackView extends FrameLayout
mStackEduView = new StackEducationView(mContext);
addView(mStackEduView);
}
return mStackEduView.show(mStackAnimationController.getStartPosition());
return mStackEduView.show(mPositioner.getDefaultStartPosition());
}
private void updateUserEdu() {
@@ -1093,7 +1092,7 @@ public class BubbleStackView extends FrameLayout
addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
}
void updateFlyout(float fontScale) {
void updateFontScale(float fontScale) {
mFlyout.updateFontSize(fontScale);
}
@@ -1130,7 +1129,9 @@ public class BubbleStackView extends FrameLayout
Resources res = getContext().getResources();
mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
mRelativeStackPositionBeforeRotation = mStackAnimationController.getRelativeStackPosition();
mRelativeStackPositionBeforeRotation = new RelativeStackPosition(
mPositioner.getRestingPosition(),
mStackAnimationController.getAllowableStackPositionRegion());
mManageMenu.setVisibility(View.INVISIBLE);
mShowingManage = false;
@@ -1157,7 +1158,7 @@ public class BubbleStackView extends FrameLayout
Resources res = getContext().getResources();
mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
mBubbleSize = getResources().getDimensionPixelSize(R.dimen.individual_bubble_size);
mBubbleSize = mPositioner.getBubbleSize();
for (Bubble b : mBubbleData.getBubbles()) {
if (b.getIconView() == null) {
Log.d(TAG, "Display size changed. Icon null: " + b);
@@ -1165,6 +1166,7 @@ public class BubbleStackView extends FrameLayout
}
b.getIconView().setLayoutParams(new LayoutParams(mBubbleSize, mBubbleSize));
}
mBubbleOverflow.getIconView().setLayoutParams(new LayoutParams(mBubbleSize, mBubbleSize));
mExpandedAnimationController.updateResources();
mStackAnimationController.updateResources();
mDismissView.updateResources();
@@ -1199,8 +1201,8 @@ public class BubbleStackView extends FrameLayout
super.onDetachedFromWindow();
getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
if (mBubbleOverflow != null && mBubbleOverflow.getExpandedView() != null) {
mBubbleOverflow.getExpandedView().cleanUpExpandedState();
if (mBubbleOverflow != null) {
mBubbleOverflow.cleanUpExpandedState();
}
}
@@ -1390,8 +1392,7 @@ public class BubbleStackView extends FrameLayout
if (getBubbleCount() == 0 && shouldShowStackEdu()) {
// Override the default stack position if we're showing user education.
mStackAnimationController.setStackPosition(
mStackAnimationController.getStartPosition());
mStackAnimationController.setStackPosition(mPositioner.getDefaultStartPosition());
}
if (getBubbleCount() == 0) {
@@ -1410,7 +1411,8 @@ public class BubbleStackView extends FrameLayout
bubble.getIconView().setOnTouchListener(mBubbleTouchListener);
mBubbleContainer.addView(bubble.getIconView(), 0,
new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
new FrameLayout.LayoutParams(mPositioner.getBubbleSize(),
mPositioner.getBubbleSize()));
animateInFlyoutForBubble(bubble);
requestUpdate();
logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
@@ -1614,6 +1616,11 @@ public class BubbleStackView extends FrameLayout
mBubbleController.hideCurrentInputMethod();
}
/** Set the stack position to whatever the positioner says. */
void updateStackPosition() {
mStackAnimationController.setStackPosition(mPositioner.getRestingPosition());
}
private void beforeExpandedViewAnimation() {
mIsExpansionAnimating = true;
hideFlyoutImmediate();
@@ -1629,8 +1636,7 @@ public class BubbleStackView extends FrameLayout
private void animateExpansion() {
cancelDelayedExpandCollapseSwitchAnimations();
final boolean isLandscape =
mPositioner.getOrientation() == Configuration.ORIENTATION_LANDSCAPE;
final boolean showVertically = mPositioner.showBubblesVertically();
mIsExpanded = true;
if (mStackEduView != null) {
mStackEduView.hide(true /* fromExpansion */);
@@ -1650,14 +1656,19 @@ public class BubbleStackView extends FrameLayout
mExpandedViewContainer.setTranslationY(getExpandedViewY());
mExpandedViewContainer.setAlpha(1f);
// X-value of the bubble we're expanding, once it's settled in its row.
int index;
if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) {
index = mBubbleData.getBubbles().size();
} else {
index = getBubbleIndex(mExpandedBubble);
}
// Position of the bubble we're expanding, once it's settled in its row.
final float bubbleWillBeAt =
mExpandedAnimationController.getBubbleXOrYForOrientation(
mBubbleData.getBubbles().indexOf(mExpandedBubble));
mExpandedAnimationController.getBubbleXOrYForOrientation(index);
// How far horizontally the bubble will be animating. We'll wait a bit longer for bubbles
// that are animating farther, so that the expanded view doesn't move as much.
final float relevantStackPosition = isLandscape
final float relevantStackPosition = showVertically
? mStackAnimationController.getStackPosition().y
: mStackAnimationController.getStackPosition().x;
final float distanceAnimated = Math.abs(bubbleWillBeAt - relevantStackPosition);
@@ -1674,7 +1685,7 @@ public class BubbleStackView extends FrameLayout
}
// Set the pivot point for the scale, so the expanded view animates out from the bubble.
if (isLandscape) {
if (showVertically) {
float pivotX;
float pivotY = bubbleWillBeAt + mBubbleSize / 2f;
if (mStackOnLeftOrWillBe) {
@@ -1709,7 +1720,7 @@ public class BubbleStackView extends FrameLayout
if (mExpandedBubble == null || mExpandedBubble.getIconView() == null) {
return;
}
float translation = isLandscape
float translation = showVertically
? mExpandedBubble.getIconView().getTranslationY()
: mExpandedBubble.getIconView().getTranslationX();
mExpandedViewContainerMatrix.postTranslate(
@@ -1761,13 +1772,17 @@ public class BubbleStackView extends FrameLayout
// We want to visually collapse into this bubble during the animation.
final View expandingFromBubble = mExpandedBubble.getIconView();
int index;
if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) {
index = mBubbleData.getBubbles().size();
} else {
index = mBubbleData.getBubbles().indexOf(mExpandedBubble);
}
// Value the bubble is animating from (back into the stack).
final float expandingFromBubbleAt =
mExpandedAnimationController.getBubbleXOrYForOrientation(
mBubbleData.getBubbles().indexOf(mExpandedBubble));
final boolean isLandscape =
mPositioner.getOrientation() == Configuration.ORIENTATION_LANDSCAPE;
if (isLandscape) {
mExpandedAnimationController.getBubbleXOrYForOrientation(index);
final boolean showVertically = mPositioner.showBubblesVertically();
if (mPositioner.showBubblesVertically()) {
float pivotX;
float pivotY = expandingFromBubbleAt + mBubbleSize / 2f;
if (mStackOnLeftOrWillBe) {
@@ -1792,7 +1807,7 @@ public class BubbleStackView extends FrameLayout
.addUpdateListener((target, values) -> {
if (expandingFromBubble != null) {
// Follow the bubble as it translates!
if (isLandscape) {
if (showVertically) {
mExpandedViewContainerMatrix.postTranslate(
0f, expandingFromBubble.getTranslationY()
- expandingFromBubbleAt);
@@ -1849,7 +1864,7 @@ public class BubbleStackView extends FrameLayout
.spring(DynamicAnimation.SCALE_Y, 0f, mScaleOutSpringConfig)
.withEndActions(this::releaseAnimatingOutBubbleBuffer);
if (mPositioner.getOrientation() == Configuration.ORIENTATION_LANDSCAPE) {
if (mPositioner.showBubblesVertically()) {
float translationX = mStackAnimationController.isStackOnLeftSide()
? mAnimatingOutSurfaceContainer.getTranslationX() + mBubbleSize * 2
: mAnimatingOutSurfaceContainer.getTranslationX();
@@ -1874,7 +1889,7 @@ public class BubbleStackView extends FrameLayout
mExpandedViewContainer.setAlpha(1f);
mExpandedViewContainer.setVisibility(View.VISIBLE);
if (mPositioner.getOrientation() == Configuration.ORIENTATION_LANDSCAPE) {
if (mPositioner.showBubblesVertically()) {
float pivotX;
float pivotY = expandingFromBubbleDestination + mBubbleSize / 2f;
if (mStackOnLeftOrWillBe) {
@@ -2180,7 +2195,7 @@ public class BubbleStackView extends FrameLayout
*/
float getExpandedViewY() {
final int top = mPositioner.getAvailableRect().top;
if (mPositioner.getOrientation() == Configuration.ORIENTATION_LANDSCAPE) {
if (mPositioner.showBubblesVertically()) {
return top + mExpandedViewPadding;
} else {
return top + mBubbleSize + mBubblePaddingTop;
@@ -2278,7 +2293,8 @@ public class BubbleStackView extends FrameLayout
expandFlyoutAfterDelay /* onLayoutComplete */,
mAfterFlyoutHidden,
bubble.getIconView().getDotCenter(),
!bubble.showDot());
!bubble.showDot(),
mPositioner);
}
mFlyout.bringToFront();
});
@@ -2550,7 +2566,7 @@ public class BubbleStackView extends FrameLayout
Insets insets = mPositioner.getInsets();
int leftPadding = insets.left + mExpandedViewPadding;
int rightPadding = insets.right + mExpandedViewPadding;
if (mPositioner.getOrientation() == Configuration.ORIENTATION_LANDSCAPE) {
if (mPositioner.showBubblesVertically()) {
if (!mStackAnimationController.isStackOnLeftSide()) {
rightPadding += mPointerHeight + mBubbleSize;
} else {
@@ -2581,7 +2597,9 @@ public class BubbleStackView extends FrameLayout
bv.setZ((mMaxBubbles * mBubbleElevation) - i);
if (mIsExpanded) {
bv.showDotAndBadge(false /* onLeft */);
// If we're not displaying vertically, we always show the badge on the left.
boolean onLeft = mPositioner.showBubblesVertically() && !mStackOnLeftOrWillBe;
bv.showDotAndBadge(onLeft);
} else if (i == 0) {
bv.showDotAndBadge(!mStackOnLeftOrWillBe);
} else {
@@ -2599,7 +2617,7 @@ public class BubbleStackView extends FrameLayout
return;
}
float bubblePosition = mExpandedAnimationController.getBubbleXOrYForOrientation(index);
if (mPositioner.getOrientation() == Configuration.ORIENTATION_LANDSCAPE) {
if (mPositioner.showBubblesVertically()) {
float x = mStackOnLeftOrWillBe
? mPositioner.getAvailableRect().left
: mPositioner.getAvailableRect().right
@@ -2661,21 +2679,11 @@ public class BubbleStackView extends FrameLayout
.floatValue();
}
/** Set the start position of the bubble stack. */
public void setStackStartPosition(RelativeStackPosition position) {
mStackAnimationController.setStackStartPosition(position);
}
/** @return the position of the bubble stack. */
public PointF getStackPosition() {
return mStackAnimationController.getStackPosition();
}
/** @return the relative position of the bubble stack. */
public RelativeStackPosition getRelativeStackPosition() {
return mStackAnimationController.getRelativeStackPosition();
}
/**
* Logs the bubble UI event.
*

View File

@@ -134,6 +134,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
LayoutInflater inflater = LayoutInflater.from(c);
info.imageView = (BadgedImageView) inflater.inflate(
R.layout.bubble_view, stackView, false /* attachToRoot */);
info.imageView.initialize(controller.getPositioner());
info.expandedView = (BubbleExpandedView) inflater.inflate(
R.layout.bubble_expanded_view, stackView, false /* attachToRoot */);

View File

@@ -84,7 +84,11 @@ public class ExpandedAnimationController
private float mBubbleSizePx;
/** Max number of bubbles shown in row above expanded view. */
private int mBubblesMaxRendered;
/** Max amount of space to have between bubbles when expanded. */
private int mBubblesMaxSpace;
/** Amount of space between the bubbles when expanded. */
private float mSpaceBetweenBubbles;
/** Whether the expand / collapse animation is running. */
private boolean mAnimatingExpand = false;
/**
@@ -205,8 +209,17 @@ public class ExpandedAnimationController
mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
mBubbleSizePx = mPositioner.getBubbleSize();
mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered);
mBubblesMaxSpace = res.getDimensionPixelSize(R.dimen.bubble_max_spacing);
final float availableSpace = mPositioner.isLandscape()
? mPositioner.getAvailableRect().height()
: mPositioner.getAvailableRect().width();
final float spaceForMaxBubbles = (mExpandedViewPadding * 2)
+ (mBubblesMaxRendered + 1) * mBubbleSizePx;
float spaceBetweenBubbles =
(availableSpace - spaceForMaxBubbles) / mBubblesMaxRendered;
mSpaceBetweenBubbles = Math.min(spaceBetweenBubbles, mBubblesMaxSpace);
}
/**
@@ -250,13 +263,15 @@ public class ExpandedAnimationController
final Path path = new Path();
path.moveTo(bubble.getTranslationX(), bubble.getTranslationY());
final float expandedY = getExpandedY();
final float expandedY = mPositioner.showBubblesVertically()
? getBubbleXOrYForOrientation(index)
: getExpandedY();
if (expanding) {
// If we're expanding, first draw a line from the bubble's current position to the
// top of the screen.
path.lineTo(bubble.getTranslationX(), expandedY);
// Then, draw a line across the screen to the bubble's resting position.
if (mPositioner.getOrientation() == Configuration.ORIENTATION_LANDSCAPE) {
if (mPositioner.showBubblesVertically()) {
Rect availableRect = mPositioner.getAvailableRect();
boolean onLeft = mCollapsePoint != null
&& mCollapsePoint.x < (availableRect.width() / 2f);
@@ -422,6 +437,9 @@ public class ExpandedAnimationController
* bubbles to accommodate it if it was previously dragged out past the threshold.
*/
public void snapBubbleBack(View bubbleView, float velX, float velY) {
if (mLayout == null) {
return;
}
final int index = mLayout.indexOfChild(bubbleView);
animationForChildAtIndex(index)
@@ -580,7 +598,7 @@ public class ExpandedAnimationController
return;
}
if (mPositioner.getOrientation() == Configuration.ORIENTATION_LANDSCAPE) {
if (mPositioner.showBubblesVertically()) {
Rect availableRect = mPositioner.getAvailableRect();
boolean onLeft = mCollapsePoint != null
&& mCollapsePoint.x < (availableRect.width() / 2f);
@@ -613,23 +631,15 @@ public class ExpandedAnimationController
if (mLayout == null) {
return 0;
}
final float positionInBar = index * (mBubbleSizePx + mSpaceBetweenBubbles);
Rect availableRect = mPositioner.getAvailableRect();
final boolean isLandscape =
mPositioner.getOrientation() == Configuration.ORIENTATION_LANDSCAPE;
final float availableSpace = isLandscape
? availableRect.height()
: availableRect.width();
final float spaceForMaxBubbles = (mExpandedViewPadding * 2)
+ (mBubblesMaxRendered + 1) * mBubbleSizePx;
final float spaceBetweenBubbles =
(availableSpace - spaceForMaxBubbles) / mBubblesMaxRendered;
final boolean isLandscape = mPositioner.showBubblesVertically();
final float expandedStackSize = (mLayout.getChildCount() * mBubbleSizePx)
+ ((mLayout.getChildCount() - 1) * spaceBetweenBubbles);
+ ((mLayout.getChildCount() - 1) * mSpaceBetweenBubbles);
final float centerPosition = isLandscape
? availableRect.centerY()
: availableRect.centerX();
final float rowStart = centerPosition - (expandedStackSize / 2f);
final float positionInBar = index * (mBubbleSizePx + spaceBetweenBubbles);
return rowStart + positionInBar;
}
}

View File

@@ -133,12 +133,6 @@ public class StackAnimationController extends
/** Whether or not the stack's start position has been set. */
private boolean mStackMovedToStartPosition = false;
/**
* 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.
*/
private PointF mRestingStackPosition;
/** The height of the most recently visible IME. */
private float mImeHeight = 0f;
@@ -434,6 +428,9 @@ public class StackAnimationController extends
* Where the stack would be if it were snapped to the nearest horizontal edge (left or right).
*/
public PointF getStackPositionAlongNearestHorizontalEdge() {
if (mPositioner.showingInTaskbar()) {
return mPositioner.getRestingPosition();
}
final PointF stackPos = getStackPosition();
final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x);
final RectF bounds = getAllowableStackPositionRegion();
@@ -447,7 +444,7 @@ public class StackAnimationController extends
pw.println("StackAnimationController state:");
pw.print(" isActive: "); pw.println(isActiveController());
pw.print(" restingStackPos: ");
pw.println(mRestingStackPosition != null ? mRestingStackPosition.toString() : "null");
pw.println(mPositioner.getRestingPosition().toString());
pw.print(" currentStackPos: "); pw.println(mStackPosition.toString());
pw.print(" isMovingFromFlinging: "); pw.println(mIsMovingFromFlinging);
pw.print(" withinDismiss: "); pw.println(isStackStuckToTarget());
@@ -501,7 +498,7 @@ public class StackAnimationController extends
.addEndListener((animation, canceled, endValue, endVelocity) -> {
if (!canceled) {
mRestingStackPosition.set(mStackPosition);
mPositioner.setRestingPosition(mStackPosition);
springFirstBubbleWithStackFollowing(property, spring, endVelocity,
finalPosition != null
@@ -679,7 +676,7 @@ public class StackAnimationController extends
// resting position - the touch location is not a valid resting
// position. We'll set this when the stack springs to the left or
// right side of the screen after the touch gesture ends.
mRestingStackPosition.set(mStackPosition);
mPositioner.setRestingPosition(mStackPosition);
}
if (after != null) {
@@ -772,10 +769,9 @@ public class StackAnimationController extends
if (getBubbleCount() > 0) {
animationForChildAtIndex(0).translationX(mStackPosition.x).start();
} else {
// TODO: still needed with positioner?
// When all children are removed ensure stack position is sane
setStackPosition(mRestingStackPosition == null
? getStartPosition()
: mRestingStackPosition);
mPositioner.setRestingPosition(mPositioner.getRestingPosition());
// Remove the stack from the coordinator since we don't have any bubbles and aren't
// visible.
@@ -848,8 +844,8 @@ public class StackAnimationController extends
mSwapAnimationOffset = res.getDimensionPixelSize(R.dimen.bubble_swap_animation_offset);
mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
mElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
mBubbleBitmapSize = res.getDimensionPixelSize(R.dimen.bubble_bitmap_size);
mBubbleSize = mPositioner.getBubbleSize();
mBubbleBitmapSize = mPositioner.getBubbleBitmapSize();
mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
}
@@ -873,9 +869,8 @@ public class StackAnimationController extends
// Post to ensure that the layout's width and height have been calculated.
mLayout.setVisibility(View.INVISIBLE);
mLayout.post(() -> {
setStackPosition(mRestingStackPosition == null
? getStartPosition()
: mRestingStackPosition);
setStackPosition(mPositioner.getRestingPosition());
mStackMovedToStartPosition = true;
mLayout.setVisibility(View.VISIBLE);
@@ -919,11 +914,7 @@ public class StackAnimationController extends
Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
mStackPosition.set(pos.x, pos.y);
if (mRestingStackPosition == null) {
mRestingStackPosition = new PointF();
}
mRestingStackPosition.set(mStackPosition);
mPositioner.setRestingPosition(mStackPosition);
// If we're not the active controller, we don't want to physically move the bubble views.
if (isActiveController()) {

View File

@@ -19,7 +19,9 @@ package com.android.wm.shell.bubbles;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.graphics.Color;
import android.graphics.PointF;
@@ -36,7 +38,7 @@ import com.android.wm.shell.ShellTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@@ -48,11 +50,16 @@ public class BubbleFlyoutViewTest extends ShellTestCase {
private TextView mSenderName;
private float[] mDotCenter = new float[2];
private Bubble.FlyoutMessage mFlyoutMessage;
@Mock
private BubblePositioner mPositioner;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
when(mPositioner.getBubbleBitmapSize()).thenReturn(40);
when(mPositioner.getBubbleSize()).thenReturn(60);
mFlyoutMessage = new Bubble.FlyoutMessage();
mFlyoutMessage.senderName = "Josh";
mFlyoutMessage.message = "Hello";
@@ -70,7 +77,8 @@ public class BubbleFlyoutViewTest extends ShellTestCase {
mFlyout.setupFlyoutStartingAsDot(
mFlyoutMessage,
new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
false);
false,
mPositioner);
mFlyout.setVisibility(View.VISIBLE);
assertEquals("Hello", mFlyoutText.getText());
@@ -80,10 +88,11 @@ public class BubbleFlyoutViewTest extends ShellTestCase {
@Test
public void testFlyoutHide_runsCallback() {
Runnable after = Mockito.mock(Runnable.class);
Runnable after = mock(Runnable.class);
mFlyout.setupFlyoutStartingAsDot(mFlyoutMessage,
new PointF(100, 100), 500, true, Color.WHITE, null, after, mDotCenter,
false);
false,
mPositioner);
mFlyout.hideFlyout();
verify(after).run();
@@ -93,7 +102,8 @@ public class BubbleFlyoutViewTest extends ShellTestCase {
public void testSetCollapsePercent() {
mFlyout.setupFlyoutStartingAsDot(mFlyoutMessage,
new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
false);
false,
mPositioner);
mFlyout.setVisibility(View.VISIBLE);
mFlyout.setCollapsePercent(1f);

View File

@@ -0,0 +1,35 @@
/*
* 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 android.content.Context;
import android.content.res.Configuration;
import android.graphics.Insets;
import android.graphics.Rect;
import android.view.WindowManager;
public class TestableBubblePositioner extends BubblePositioner {
public TestableBubblePositioner(Context context,
WindowManager windowManager) {
super(context, windowManager);
updateInternal(Configuration.ORIENTATION_PORTRAIT,
Insets.of(0, 0, 0, 0),
new Rect(0, 0, 500, 1000));
}
}

View File

@@ -64,7 +64,7 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC
super.setUp();
BubblePositioner positioner = new BubblePositioner(getContext(), mock(WindowManager.class));
positioner.update(Configuration.ORIENTATION_PORTRAIT,
positioner.updateInternal(Configuration.ORIENTATION_PORTRAIT,
Insets.of(0, 0, 0, 0),
new Rect(0, 0, mDisplayWidth, mDisplayHeight));
mExpandedController = new ExpandedAnimationController(positioner, mExpandedViewPadding,

View File

@@ -27,6 +27,7 @@ import static org.mockito.Mockito.verify;
import android.graphics.PointF;
import android.testing.AndroidTestingRunner;
import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;
import androidx.dynamicanimation.animation.DynamicAnimation;
@@ -34,7 +35,7 @@ import androidx.dynamicanimation.animation.SpringForce;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.R;
import com.android.wm.shell.bubbles.BubblePositioner;
import com.android.wm.shell.bubbles.TestableBubblePositioner;
import com.android.wm.shell.common.FloatingContentCoordinator;
import org.junit.Before;
@@ -310,7 +311,7 @@ public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase
super(floatingContentCoordinator,
bubbleCountSupplier,
onBubbleAnimatedOutAction,
mock(BubblePositioner.class));
new TestableBubblePositioner(mContext, mock(WindowManager.class)));
}
@Override

View File

@@ -45,9 +45,6 @@ import android.app.INotificationManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.pm.LauncherApps;
import android.content.res.Configuration;
import android.graphics.Insets;
import android.graphics.Rect;
import android.hardware.display.AmbientDisplayConfiguration;
import android.hardware.face.FaceManager;
import android.os.Handler;
@@ -99,7 +96,6 @@ import com.android.wm.shell.bubbles.BubbleData;
import com.android.wm.shell.bubbles.BubbleDataRepository;
import com.android.wm.shell.bubbles.BubbleEntry;
import com.android.wm.shell.bubbles.BubbleLogger;
import com.android.wm.shell.bubbles.BubblePositioner;
import com.android.wm.shell.bubbles.BubbleStackView;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.common.FloatingContentCoordinator;
@@ -206,8 +202,8 @@ public class BubblesTest extends SysuiTestCase {
private WindowManagerShellWrapper mWindowManagerShellWrapper;
@Mock
private BubbleLogger mBubbleLogger;
@Mock
private BubblePositioner mPositioner;
private TestableBubblePositioner mPositioner;
private BubbleData mBubbleData;
@@ -257,10 +253,7 @@ public class BubblesTest extends SysuiTestCase {
mBubbleData = new BubbleData(mContext, mBubbleLogger);
Rect availableRect = new Rect(0, 0, 1000, 5000);
when(mPositioner.getAvailableRect()).thenReturn(availableRect);
when(mPositioner.getOrientation()).thenReturn(Configuration.ORIENTATION_PORTRAIT);
when(mPositioner.getInsets()).thenReturn(Insets.of(0, 0, 0, 0));
mPositioner = new TestableBubblePositioner(mContext, mWindowManager);
TestableNotificationInterruptStateProviderImpl interruptionStateProvider =
new TestableNotificationInterruptStateProviderImpl(mContext.getContentResolver(),

View File

@@ -42,9 +42,6 @@ import android.app.INotificationManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.pm.LauncherApps;
import android.content.res.Configuration;
import android.graphics.Insets;
import android.graphics.Rect;
import android.hardware.display.AmbientDisplayConfiguration;
import android.os.Handler;
import android.os.PowerManager;
@@ -92,7 +89,6 @@ import com.android.wm.shell.bubbles.BubbleData;
import com.android.wm.shell.bubbles.BubbleDataRepository;
import com.android.wm.shell.bubbles.BubbleEntry;
import com.android.wm.shell.bubbles.BubbleLogger;
import com.android.wm.shell.bubbles.BubblePositioner;
import com.android.wm.shell.bubbles.BubbleStackView;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.common.FloatingContentCoordinator;
@@ -188,8 +184,8 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
private WindowManagerShellWrapper mWindowManagerShellWrapper;
@Mock
private BubbleLogger mBubbleLogger;
@Mock
private BubblePositioner mPositioner;
private TestableBubblePositioner mPositioner;
private BubbleData mBubbleData;
@@ -226,10 +222,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase {
mBubbleData = new BubbleData(mContext, mBubbleLogger);
Rect availableRect = new Rect(0, 0, 1000, 5000);
when(mPositioner.getAvailableRect()).thenReturn(availableRect);
when(mPositioner.getOrientation()).thenReturn(Configuration.ORIENTATION_PORTRAIT);
when(mPositioner.getInsets()).thenReturn(Insets.of(0, 0, 0, 0));
mPositioner = new TestableBubblePositioner(mContext, mWindowManager);
TestableNotificationInterruptStateProviderImpl interruptionStateProvider =
new TestableNotificationInterruptStateProviderImpl(mContext.getContentResolver(),

View File

@@ -0,0 +1,37 @@
/*
* 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.systemui.wmshell;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Insets;
import android.graphics.Rect;
import android.view.WindowManager;
import com.android.wm.shell.bubbles.BubblePositioner;
public class TestableBubblePositioner extends BubblePositioner {
public TestableBubblePositioner(Context context,
WindowManager windowManager) {
super(context, windowManager);
updateInternal(Configuration.ORIENTATION_PORTRAIT,
Insets.of(0, 0, 0, 0),
new Rect(0, 0, 500, 1000));
}
}