diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java index c3053fa6152ec..d92e2ccc77bdc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java @@ -23,15 +23,19 @@ import android.annotation.Nullable; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Outline; import android.graphics.Paint; import android.graphics.PaintFlagsDrawFilter; import android.graphics.Path; import android.graphics.Rect; import android.util.AttributeSet; import android.util.PathParser; +import android.view.View; +import android.view.ViewOutlineProvider; import android.widget.ImageView; import com.android.launcher3.icons.DotRenderer; +import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.animation.Interpolators; import java.util.EnumSet; @@ -104,6 +108,19 @@ public class BadgedImageView extends ImageView { setFocusable(true); setClickable(true); + setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + BadgedImageView.this.getOutline(outline); + } + }); + } + + private void getOutline(Outline outline) { + final int bubbleSize = mPositioner.getBubbleSize(); + final int normalizedSize = IconNormalizer.getNormalizedCircleSize(bubbleSize); + final int inset = (bubbleSize - normalizedSize) / 2; + outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize); } public void initialize(BubblePositioner positioner) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index 70167d7ff8300..62df2cb69da99 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -56,11 +56,8 @@ public class BubblePositioner { 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; + /** 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; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index 51fd914f2e0c8..ee822109f40ca 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -22,6 +22,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -632,6 +633,7 @@ public class BubbleStackView extends FrameLayout // First, see if the magnetized object consumes the event - if so, we shouldn't move the // bubble since it's stuck to the target. if (!passEventToMagnetizedObject(ev)) { + updateBubbleShadows(true /* showForAllBubbles */); if (mBubbleData.isExpanded() || mPositioner.showingInTaskbar()) { mExpandedAnimationController.dragBubbleOut( v, viewInitialX + dx, viewInitialY + dy); @@ -671,7 +673,7 @@ public class BubbleStackView extends FrameLayout mStackAnimationController.flingStackThenSpringToEdge( viewInitialX + dx, velX, velY) <= 0; final boolean updateForCollapsedStack = oldOnLeft != mStackOnLeftOrWillBe; - updateBadgesAndZOrder(updateForCollapsedStack); + updateBadges(updateForCollapsedStack); logBubbleEvent(null /* no bubble associated with bubble stack move */, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED); } @@ -783,9 +785,9 @@ public class BubbleStackView extends FrameLayout mBubbleController.onAllBubblesAnimatedOut(); } }; - mStackAnimationController = new StackAnimationController( - floatingContentCoordinator, this::getBubbleCount, onBubbleAnimatedOut, mPositioner); + floatingContentCoordinator, this::getBubbleCount, onBubbleAnimatedOut, + this::animateShadows /* onStackAnimationFinished */, mPositioner); mExpandedAnimationController = new ExpandedAnimationController( mPositioner, mExpandedViewPadding, onBubbleAnimatedOut); @@ -1505,13 +1507,12 @@ public class BubbleStackView extends FrameLayout // Set the dot position to the opposite of the side the stack is resting on, since the stack // resting slightly off-screen would result in the dot also being off-screen. bubble.getIconView().setDotBadgeOnLeft(!mStackOnLeftOrWillBe /* onLeft */); - bubble.getIconView().setOnClickListener(mBubbleClickListener); bubble.getIconView().setOnTouchListener(mBubbleTouchListener); - mBubbleContainer.addView(bubble.getIconView(), 0, new FrameLayout.LayoutParams(mPositioner.getBubbleSize(), mPositioner.getBubbleSize())); + updateBubbleShadows(false /* showForAllBubbles */); animateInFlyoutForBubble(bubble); requestUpdate(); logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__POSTED); @@ -1566,7 +1567,8 @@ public class BubbleStackView extends FrameLayout }; if (mIsExpanded || isExpansionAnimating()) { reorder.run(); - updateBadgesAndZOrder(false /* setBadgeForCollapsedStack */); + updateBadges(false /* setBadgeForCollapsedStack */); + updateZOrder(); } else if (!isExpansionAnimating()) { List bubbleViews = bubbles.stream() .map(b -> b.getIconView()).collect(Collectors.toList()); @@ -1803,7 +1805,8 @@ public class BubbleStackView extends FrameLayout } beforeExpandedViewAnimation(); - updateBadgesAndZOrder(false /* setBadgeForCollapsedStack */); + updateZOrder(); + updateBadges(false /* setBadgeForCollapsedStack */); mBubbleContainer.setActiveController(mExpandedAnimationController); updateOverflowVisibility(); updatePointerPosition(); @@ -2014,7 +2017,8 @@ public class BubbleStackView extends FrameLayout mExpandedBubble)); } updateOverflowVisibility(); - updateBadgesAndZOrder(true /* setBadgeForCollapsedStack */); + updateZOrder(); + updateBadges(true /* setBadgeForCollapsedStack */); afterExpandedViewAnimation(); if (previouslySelected != null) { previouslySelected.setTaskViewVisibility(false); @@ -2714,14 +2718,56 @@ public class BubbleStackView extends FrameLayout } /** - * Sets the appropriate Z-order, badge, and dot position for each bubble in the stack. - * Animate dot and badge changes. + * Updates whether each of the bubbles should show shadows. When collapsed & resting, only the + * visible bubbles (top 2) will show a shadow. When the stack is being dragged, everything + * shows a shadow. When an individual bubble is dragged out, it should show a shadow. */ - private void updateBadgesAndZOrder(boolean setBadgeForCollapsedStack) { + private void updateBubbleShadows(boolean showForAllBubbles) { + int bubbleCount = getBubbleCount(); + for (int i = 0; i < bubbleCount; i++) { + final float z = (mMaxBubbles * mBubbleElevation) - i; + BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); + boolean isDraggedOut = mMagnetizedObject != null + && mMagnetizedObject.getUnderlyingObject().equals(bv); + if (showForAllBubbles || isDraggedOut) { + bv.setZ(z); + } else { + final float tz = i < NUM_VISIBLE_WHEN_RESTING ? z : 0f; + bv.setZ(tz); + } + } + } + + /** + * When the bubbles are flung and then rest, the shadows stack up for the bubbles hidden + * beneath the top two bubbles, to avoid this we animate the Z translations once the stack + * is resting so that they fade away nicely. + */ + private void animateShadows() { + int bubbleCount = getBubbleCount(); + for (int i = 0; i < bubbleCount; i++) { + BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); + boolean fullShadow = i < NUM_VISIBLE_WHEN_RESTING; + if (!fullShadow) { + bv.animate().translationZ(0).start(); + } + } + } + + private void updateZOrder() { + int bubbleCount = getBubbleCount(); + for (int i = 0; i < bubbleCount; i++) { + BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); + bv.setZ(i < NUM_VISIBLE_WHEN_RESTING + ? (mMaxBubbles * mBubbleElevation) - i + : 0f); + } + } + + private void updateBadges(boolean setBadgeForCollapsedStack) { int bubbleCount = getBubbleCount(); for (int i = 0; i < bubbleCount; i++) { BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); - bv.setZ((mMaxBubbles * mBubbleElevation) - i); if (mIsExpanded) { // If we're not displaying vertically, we always show the badge on the left. boolean onLeft = mPositioner.showBubblesVertically() && !mStackOnLeftOrWillBe; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java index f960e5271dca9..9dfd8c591db55 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java @@ -16,6 +16,8 @@ package com.android.wm.shell.bubbles.animation; +import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING; + import android.content.res.Resources; import android.graphics.Path; import android.graphics.PointF; @@ -289,7 +291,8 @@ public class ExpandedAnimationController path.lineTo(stackedX, expandedY); // Then, draw a line down to the stack position. - path.lineTo(stackedX, mCollapsePoint.y + Math.min(index, 1) * mStackOffsetPx); + path.lineTo(stackedX, mCollapsePoint.y + + Math.min(index, NUM_VISIBLE_WHEN_RESTING - 1) * mStackOffsetPx); } // The lead bubble should be the bubble with the longest distance to travel when we're diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java index 443e297afb885..8043d2b8b2f92 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java @@ -16,6 +16,8 @@ package com.android.wm.shell.bubbles.animation; +import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING; + import android.content.ContentResolver; import android.content.res.Resources; import android.graphics.PointF; @@ -255,14 +257,21 @@ public class StackAnimationController extends */ private Runnable mOnBubbleAnimatedOutAction; + /** + * Callback to run whenever the stack is finished being flung somewhere. + */ + private Runnable mOnStackAnimationFinished; + public StackAnimationController( FloatingContentCoordinator floatingContentCoordinator, IntSupplier bubbleCountSupplier, Runnable onBubbleAnimatedOutAction, + Runnable onStackAnimationFinished, BubblePositioner positioner) { mFloatingContentCoordinator = floatingContentCoordinator; mBubbleCountSupplier = bubbleCountSupplier; mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction; + mOnStackAnimationFinished = onStackAnimationFinished; mPositioner = positioner; } @@ -672,6 +681,10 @@ public class StackAnimationController extends mPositioner.setRestingPosition(mStackPosition); } + if (mOnStackAnimationFinished != null) { + mOnStackAnimationFinished.run(); + } + if (after != null) { for (Runnable callback : after) { callback.run(); @@ -716,7 +729,7 @@ public class StackAnimationController extends } else { // We only show the first two bubbles in the stack & the rest hide behind them // so they don't need an offset. - return index > 1 ? 0f : mStackOffset; + return index > (NUM_VISIBLE_WHEN_RESTING - 1) ? 0f : mStackOffset; } } else { return 0f; @@ -825,7 +838,8 @@ public class StackAnimationController extends private void moveToFinalIndex(View view, int newIndex, Runnable finishReorder) { final ViewPropertyAnimator animator = view.animate() - .translationY(getStackPosition().y + Math.min(newIndex, 1) * mStackOffset) + .translationY(getStackPosition().y + + Math.min(newIndex, NUM_VISIBLE_WHEN_RESTING - 1) * mStackOffset) .setDuration(BUBBLE_SWAP_DURATION) .withEndAction(() -> { view.setTag(R.id.reorder_animator_tag, null); @@ -834,8 +848,9 @@ public class StackAnimationController extends view.setTag(R.id.reorder_animator_tag, animator); } + // TODO: do we need this & BubbleStackView#updateBadgesAndZOrder? private void updateBadgesAndZOrder(View v, int index) { - v.setZ((mMaxBubbles * mElevation) - index); + v.setZ(index < NUM_VISIBLE_WHEN_RESTING ? (mMaxBubbles * mElevation) - index : 0f); BadgedImageView bv = (BadgedImageView) v; if (index == 0) { bv.showDotAndBadge(!isStackOnLeftSide()); @@ -939,8 +954,9 @@ public class StackAnimationController extends final float yOffset = getOffsetForChainedPropertyAnimation( DynamicAnimation.TRANSLATION_Y, 0); for (int i = 0; i < mLayout.getChildCount(); i++) { - mLayout.getChildAt(i).setTranslationX(pos.x + (Math.min(i, 1) * xOffset)); - mLayout.getChildAt(i).setTranslationY(pos.y + (Math.min(i, 1) * yOffset)); + float index = Math.min(i, NUM_VISIBLE_WHEN_RESTING - 1); + mLayout.getChildAt(i).setTranslationX(pos.x + (index * xOffset)); + mLayout.getChildAt(i).setTranslationY(pos.y + (index * yOffset)); } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/StackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/StackAnimationControllerTest.java index f36dcbe7bf4b6..31fafcaab7cce 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/StackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/StackAnimationControllerTest.java @@ -69,7 +69,7 @@ public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase public int getAsInt() { return mLayout.getChildCount(); } - }, mock(Runnable.class))); + }, mock(Runnable.class), mock(Runnable.class))); mLayout.setActiveController(mStackController); addOneMoreThanBubbleLimitBubbles(); mStackOffset = mLayout.getResources().getDimensionPixelSize(R.dimen.bubble_stack_offset); @@ -307,10 +307,12 @@ public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase TestableStackController( FloatingContentCoordinator floatingContentCoordinator, IntSupplier bubbleCountSupplier, - Runnable onBubbleAnimatedOutAction) { + Runnable onBubbleAnimatedOutAction, + Runnable onStackAnimationFinished) { super(floatingContentCoordinator, bubbleCountSupplier, onBubbleAnimatedOutAction, + onStackAnimationFinished, new TestableBubblePositioner(mContext, mock(WindowManager.class))); }