diff options
12 files changed, 795 insertions, 286 deletions
diff --git a/packages/SystemUI/res/drawable/bubble_flyout.xml b/packages/SystemUI/res/drawable/bubble_flyout.xml deleted file mode 100644 index afe5372d38d8..000000000000 --- a/packages/SystemUI/res/drawable/bubble_flyout.xml +++ /dev/null @@ -1,30 +0,0 @@ -<!-- - ~ Copyright (C) 2019 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 - --> -<layer-list xmlns:android="http://schemas.android.com/apk/res/android" > - <item> - <shape android:shape="rectangle"> - <solid android:color="?android:attr/colorBackgroundFloating" /> - <corners - android:bottomLeftRadius="?android:attr/dialogCornerRadius" - android:topLeftRadius="?android:attr/dialogCornerRadius" - android:bottomRightRadius="?android:attr/dialogCornerRadius" - android:topRightRadius="?android:attr/dialogCornerRadius" /> - <padding - android:left="@dimen/bubble_flyout_pointer_size" - android:right="@dimen/bubble_flyout_pointer_size" /> - </shape> - </item> -</layer-list>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/bubble_flyout.xml b/packages/SystemUI/res/layout/bubble_flyout.xml index 0e4d2985e775..5f773f462deb 100644 --- a/packages/SystemUI/res/layout/bubble_flyout.xml +++ b/packages/SystemUI/res/layout/bubble_flyout.xml @@ -13,18 +13,13 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<FrameLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:paddingLeft="@dimen/bubble_flyout_pointer_size" - android:paddingRight="@dimen/bubble_flyout_pointer_size"> +<merge xmlns:android="http://schemas.android.com/apk/res/android"> <FrameLayout - android:id="@+id/bubble_flyout" + android:id="@+id/bubble_flyout_text_container" android:layout_height="wrap_content" android:layout_width="wrap_content" - android:background="@drawable/bubble_flyout" + android:clipToPadding="false" android:paddingLeft="@dimen/bubble_flyout_padding_x" android:paddingRight="@dimen/bubble_flyout_padding_x" android:paddingTop="@dimen/bubble_flyout_padding_y" @@ -41,4 +36,4 @@ </FrameLayout> -</FrameLayout>
\ No newline at end of file +</merge>
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgeRenderer.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgeRenderer.java index 845b08483064..74ad0faca6d3 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BadgeRenderer.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BadgeRenderer.java @@ -18,12 +18,15 @@ package com.android.systemui.bubbles; import static android.graphics.Paint.ANTI_ALIAS_FLAG; import static android.graphics.Paint.FILTER_BITMAP_FLAG; +import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; import android.util.Log; +import com.android.systemui.R; + // XXX: Mostly opied from launcher code / can we share? /** * Contains parameters necessary to draw a badge for an icon (e.g. the size of the badge). @@ -32,20 +35,31 @@ public class BadgeRenderer { private static final String TAG = "BadgeRenderer"; - // The badge sizes are defined as percentages of the app icon size. + /** The badge sizes are defined as percentages of the app icon size. */ private static final float SIZE_PERCENTAGE = 0.38f; - // Extra scale down of the dot + /** Extra scale down of the dot. */ private static final float DOT_SCALE = 0.6f; private final float mDotCenterOffset; private final float mCircleRadius; private final Paint mCirclePaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG); - public BadgeRenderer(int iconSizePx) { - mDotCenterOffset = SIZE_PERCENTAGE * iconSizePx; - int size = (int) (DOT_SCALE * mDotCenterOffset); - mCircleRadius = size / 2f; + public BadgeRenderer(Context context) { + mDotCenterOffset = getDotCenterOffset(context); + mCircleRadius = getDotRadius(mDotCenterOffset); + } + + /** Space between the center of the dot and the top or left of the bubble stack. */ + static float getDotCenterOffset(Context context) { + final int iconSizePx = + context.getResources().getDimensionPixelSize(R.dimen.individual_bubble_size); + return SIZE_PERCENTAGE * iconSizePx; + } + + static float getDotRadius(float dotCenterOffset) { + int size = (int) (DOT_SCALE * dotCenterOffset); + return size / 2f; } /** diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java index f15e8e47649c..783780f8819c 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java @@ -57,7 +57,7 @@ public class BadgedImageView extends ImageView { int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mIconSize = getResources().getDimensionPixelSize(R.dimen.individual_bubble_size); - mDotRenderer = new BadgeRenderer(mIconSize); + mDotRenderer = new BadgeRenderer(getContext()); TypedArray ta = context.obtainStyledAttributes( new int[] {android.R.attr.colorBackgroundFloating}); @@ -83,6 +83,10 @@ public class BadgedImageView extends ImageView { invalidate(); } + public boolean getDotPosition() { + return mOnLeft; + } + /** * Set whether the dot should show or not. */ diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java index ac4a93ba7fb0..8aad0f8bd831 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java @@ -83,7 +83,7 @@ class Bubble { public void updateDotVisibility() { if (iconView != null) { - iconView.updateDotVisibility(); + iconView.updateDotVisibility(true /* animate */); } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java new file mode 100644 index 000000000000..71f68c16bd8d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2019 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.bubbles; + +import static android.graphics.Paint.ANTI_ALIAS_FLAG; +import static android.graphics.Paint.FILTER_BITMAP_FLAG; + +import android.animation.ArgbEvaluator; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PointF; +import android.graphics.RectF; +import android.graphics.drawable.ShapeDrawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.SpringAnimation; + +import com.android.systemui.R; +import com.android.systemui.recents.TriangleShape; + +/** + * Flyout view that appears as a 'chat bubble' alongside the bubble stack. The flyout can visually + * transform into the 'new' dot, which is used during flyout dismiss animations/gestures. + */ +public class BubbleFlyoutView extends FrameLayout { + /** Max width of the flyout, in terms of percent of the screen width. */ + private static final float FLYOUT_MAX_WIDTH_PERCENT = .6f; + + private final int mFlyoutPadding; + private final int mFlyoutSpaceFromBubble; + private final int mPointerSize; + private final int mBubbleSize; + private final int mFlyoutElevation; + private final int mBubbleElevation; + private final int mFloatingBackgroundColor; + private final float mCornerRadius; + + private final ViewGroup mFlyoutTextContainer; + private final TextView mFlyoutText; + /** Spring animation for the flyout. */ + private final SpringAnimation mFlyoutSpring = + new SpringAnimation(this, DynamicAnimation.TRANSLATION_X); + + /** 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 mNewDotOffsetFromBubbleBounds; + + /** + * The paint used to draw the background, whose color changes as the flyout transitions to the + * tinted 'new' dot. + */ + private final Paint mBgPaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG); + private final ArgbEvaluator mArgbEvaluator = new ArgbEvaluator(); + + /** + * Triangular ShapeDrawables used for the triangle that points from the flyout to the bubble + * stack (a chat-bubble effect). + */ + private final ShapeDrawable mLeftTriangleShape; + private final ShapeDrawable mRightTriangleShape; + + /** Whether the flyout arrow is on the left (pointing left) or right (pointing right). */ + private boolean mArrowPointingLeft = true; + + /** Color of the 'new' dot that the flyout will transform into. */ + private int mDotColor; + + /** The outline of the triangle, used for elevation shadows. */ + private final Outline mTriangleOutline = new Outline(); + + /** The bounds of the flyout background, kept up to date as it transitions to the 'new' dot. */ + private final RectF mBgRect = new RectF(); + + /** + * Percent progress in the transition from flyout to 'new' dot. These two values are the inverse + * of each other (if we're 40% transitioned to the dot, we're 60% flyout), but it makes the code + * much more readable. + */ + private float mPercentTransitionedToDot = 1f; + private float mPercentStillFlyout = 0f; + + /** + * The difference in values between the flyout and the dot. These differences are gradually + * added over the course of the animation to transform the flyout into the 'new' dot. + */ + private float mFlyoutToDotWidthDelta = 0f; + private float mFlyoutToDotHeightDelta = 0f; + private float mFlyoutToDotCornerRadiusDelta; + + /** The translation values when the flyout is completely transitioned into the dot. */ + private float mTranslationXWhenDot = 0f; + private float mTranslationYWhenDot = 0f; + + /** + * The current translation values applied to the flyout background as it transitions into the + * 'new' dot. + */ + private float mBgTranslationX; + private float mBgTranslationY; + + /** The flyout's X translation when at rest (not animating or dragging). */ + private float mRestingTranslationX = 0f; + + /** Callback to run when the flyout is hidden. */ + private Runnable mOnHide; + + public BubbleFlyoutView(Context context) { + super(context); + LayoutInflater.from(context).inflate(R.layout.bubble_flyout, this, true); + + mFlyoutTextContainer = findViewById(R.id.bubble_flyout_text_container); + mFlyoutText = mFlyoutTextContainer.findViewById(R.id.bubble_flyout_text); + + final Resources res = getResources(); + mFlyoutPadding = res.getDimensionPixelSize(R.dimen.bubble_flyout_padding_x); + 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); + mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); + mFlyoutElevation = res.getDimensionPixelSize(R.dimen.bubble_flyout_elevation); + mNewDotOffsetFromBubbleBounds = BadgeRenderer.getDotCenterOffset(context); + mNewDotRadius = BadgeRenderer.getDotRadius(mNewDotOffsetFromBubbleBounds); + mNewDotSize = mNewDotRadius * 2f; + + final TypedArray ta = mContext.obtainStyledAttributes( + new int[] { + android.R.attr.colorBackgroundFloating, + android.R.attr.dialogCornerRadius}); + mFloatingBackgroundColor = ta.getColor(0, Color.WHITE); + mCornerRadius = ta.getDimensionPixelSize(1, 0); + mFlyoutToDotCornerRadiusDelta = mNewDotRadius - mCornerRadius; + ta.recycle(); + + // Add padding for the pointer on either side, onDraw will draw it in this space. + setPadding(mPointerSize, 0, mPointerSize, 0); + setWillNotDraw(false); + setClipChildren(false); + setTranslationZ(mFlyoutElevation); + setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + BubbleFlyoutView.this.getOutline(outline); + } + }); + + mBgPaint.setColor(mFloatingBackgroundColor); + + mLeftTriangleShape = + new ShapeDrawable(TriangleShape.createHorizontal( + mPointerSize, mPointerSize, true /* isPointingLeft */)); + mLeftTriangleShape.setBounds(0, 0, mPointerSize, mPointerSize); + mLeftTriangleShape.getPaint().setColor(mFloatingBackgroundColor); + + mRightTriangleShape = + new ShapeDrawable(TriangleShape.createHorizontal( + mPointerSize, mPointerSize, false /* isPointingLeft */)); + mRightTriangleShape.setBounds(0, 0, mPointerSize, mPointerSize); + mRightTriangleShape.getPaint().setColor(mFloatingBackgroundColor); + } + + @Override + protected void onDraw(Canvas canvas) { + renderBackground(canvas); + invalidateOutline(); + super.onDraw(canvas); + } + + /** Configures the flyout and animates it in. */ + void showFlyout( + CharSequence updateMessage, PointF stackPos, float parentWidth, + boolean arrowPointingLeft, int dotColor, Runnable onHide) { + mArrowPointingLeft = arrowPointingLeft; + mDotColor = dotColor; + mOnHide = onHide; + + setCollapsePercent(0f); + setAlpha(0f); + setVisibility(VISIBLE); + + // Set the flyout TextView's max width in terms of percent, and then subtract out the + // padding so that the entire flyout view will be the desired width (rather than the + // TextView being the desired width + extra padding). + mFlyoutText.setMaxWidth( + (int) (parentWidth * FLYOUT_MAX_WIDTH_PERCENT) - mFlyoutPadding * 2); + mFlyoutText.setText(updateMessage); + + // Wait for the TextView to lay out so we know its line count. + post(() -> { + // Multi line flyouts get top-aligned to the bubble. + if (mFlyoutText.getLineCount() > 1) { + setTranslationY(stackPos.y); + } else { + // Single line flyouts are vertically centered with respect to the bubble. + setTranslationY( + stackPos.y + (mBubbleSize - mFlyoutTextContainer.getHeight()) / 2f); + } + + // Calculate the translation required to position the flyout next to the bubble stack, + // with the desired padding. + mRestingTranslationX = mArrowPointingLeft + ? stackPos.x + mBubbleSize + mFlyoutSpaceFromBubble + : stackPos.x - getWidth() - mFlyoutSpaceFromBubble; + + // Translate towards the stack slightly. + setTranslationX( + mRestingTranslationX + (arrowPointingLeft ? -mBubbleSize : mBubbleSize)); + + // Fade in the entire flyout and spring it to its normal position. + animate().alpha(1f); + mFlyoutSpring.animateToFinalPosition(mRestingTranslationX); + + // Calculate the difference in size between the flyout and the 'dot' so that we can + // transform into the dot later. + mFlyoutToDotWidthDelta = getWidth() - mNewDotSize; + mFlyoutToDotHeightDelta = getHeight() - mNewDotSize; + + // Calculate the translation values needed to be in the correct 'new dot' position. + final float distanceFromFlyoutLeftToDotCenterX = + mFlyoutSpaceFromBubble + mNewDotOffsetFromBubbleBounds / 2; + if (mArrowPointingLeft) { + mTranslationXWhenDot = -distanceFromFlyoutLeftToDotCenterX - mNewDotRadius; + } else { + mTranslationXWhenDot = + getWidth() + distanceFromFlyoutLeftToDotCenterX - mNewDotRadius; + } + + mTranslationYWhenDot = + getHeight() / 2f + - mNewDotRadius + - mBubbleSize / 2f + + mNewDotOffsetFromBubbleBounds / 2; + }); + } + + /** + * Hides the flyout and runs the optional callback passed into showFlyout. The flyout has been + * animated into the 'new' dot by the time we call this, so no animations are needed. + */ + void hideFlyout() { + if (mOnHide != null) { + mOnHide.run(); + mOnHide = null; + } + + setVisibility(GONE); + } + + /** Sets the percentage that the flyout should be collapsed into dot form. */ + void setCollapsePercent(float percentCollapsed) { + mPercentTransitionedToDot = Math.max(0f, Math.min(percentCollapsed, 1f)); + mPercentStillFlyout = (1f - mPercentTransitionedToDot); + + // Move and fade out the text. + mFlyoutText.setTranslationX( + (mArrowPointingLeft ? -getWidth() : getWidth()) * mPercentTransitionedToDot); + mFlyoutText.setAlpha(clampPercentage( + (mPercentStillFlyout - (1f - BubbleStackView.FLYOUT_DRAG_PERCENT_DISMISS)) + / BubbleStackView.FLYOUT_DRAG_PERCENT_DISMISS)); + + // Reduce the elevation towards that of the topmost bubble. + setTranslationZ( + mFlyoutElevation + - (mFlyoutElevation - mBubbleElevation) * mPercentTransitionedToDot); + invalidate(); + } + + /** Return the flyout's resting X translation (translation when not dragging or animating). */ + float getRestingTranslationX() { + return mRestingTranslationX; + } + + /** Clamps a float to between 0 and 1. */ + private float clampPercentage(float percent) { + return Math.min(1f, Math.max(0f, percent)); + } + + /** + * Renders the background, which is either the rounded 'chat bubble' flyout, or some state + * between that and the 'new' dot over the bubbles. + */ + private void renderBackground(Canvas canvas) { + // Calculate the width, height, and corner radius of the flyout given the current collapsed + // percentage. + final float width = getWidth() - (mFlyoutToDotWidthDelta * mPercentTransitionedToDot); + final float height = getHeight() - (mFlyoutToDotHeightDelta * mPercentTransitionedToDot); + final float cornerRadius = mCornerRadius + - (mFlyoutToDotCornerRadiusDelta * mPercentTransitionedToDot); + + // Translate the flyout background towards the collapsed 'dot' state. + mBgTranslationX = mTranslationXWhenDot * mPercentTransitionedToDot; + mBgTranslationY = mTranslationYWhenDot * mPercentTransitionedToDot; + + // Set the bounds of the rounded rectangle that serves as either the flyout background or + // the collapsed 'dot'. These bounds will also be used to provide the outline for elevation + // shadows. In the expanded flyout state, the left and right bounds leave space for the + // pointer triangle - as the flyout collapses, this space is reduced since the triangle + // retracts into the flyout. + mBgRect.set( + mPointerSize * mPercentStillFlyout /* left */, + 0 /* top */, + width - mPointerSize * mPercentStillFlyout /* right */, + height /* bottom */); + + mBgPaint.setColor( + (int) mArgbEvaluator.evaluate( + mPercentTransitionedToDot, mFloatingBackgroundColor, mDotColor)); + + canvas.save(); + canvas.translate(mBgTranslationX, mBgTranslationY); + renderPointerTriangle(canvas, width, height); + canvas.drawRoundRect(mBgRect, cornerRadius, cornerRadius, mBgPaint); + canvas.restore(); + } + + /** Renders the 'pointer' triangle that points from the flyout to the bubble stack. */ + private void renderPointerTriangle( + Canvas canvas, float currentFlyoutWidth, float currentFlyoutHeight) { + canvas.save(); + + // Translation to apply for the 'retraction' effect as the flyout collapses. + final float retractionTranslationX = + (mArrowPointingLeft ? 1 : -1) * (mPercentTransitionedToDot * mPointerSize * 2f); + + // Place the arrow either at the left side, or the far right, depending on whether the + // flyout is on the left or right side. + final float arrowTranslationX = + mArrowPointingLeft + ? retractionTranslationX + : currentFlyoutWidth - mPointerSize + retractionTranslationX; + + // Vertically center the arrow at all times. + final float arrowTranslationY = currentFlyoutHeight / 2f - mPointerSize / 2f; + + // Draw the appropriate direction of arrow. + final ShapeDrawable relevantTriangle = + mArrowPointingLeft ? mLeftTriangleShape : mRightTriangleShape; + canvas.translate(arrowTranslationX, arrowTranslationY); + relevantTriangle.setAlpha((int) (255f * mPercentStillFlyout)); + relevantTriangle.draw(canvas); + + // Save the triangle's outline for use in the outline provider, offsetting it to reflect its + // current position. + relevantTriangle.getOutline(mTriangleOutline); + mTriangleOutline.offset((int) arrowTranslationX, (int) arrowTranslationY); + + canvas.restore(); + } + + /** Builds an outline that includes the transformed flyout background and triangle. */ + private void getOutline(Outline outline) { + if (!mTriangleOutline.isEmpty()) { + // Draw the rect into the outline as a path so we can merge the triangle path into it. + final Path rectPath = new Path(); + rectPath.addRoundRect(mBgRect, mCornerRadius, mCornerRadius, Path.Direction.CW); + outline.setConvexPath(rectPath); + + // Get rid of the triangle path once it has disappeared behind the flyout. + if (mPercentStillFlyout > 0.5f) { + outline.mPath.addPath(mTriangleOutline.mPath); + } + + // Translate the outline to match the background's position. + final Matrix outlineMatrix = new Matrix(); + outlineMatrix.postTranslate(getLeft() + mBgTranslationX, getTop() + mBgTranslationY); + + // At the very end, retract the outline into the bubble so the shadow will be pulled + // into the flyout-dot as it (visually) becomes part of the bubble. We can't do this by + // animating translationZ to zero since then it'll go under the bubbles, which have + // elevation. + if (mPercentTransitionedToDot > 0.98f) { + final float percentBetween99and100 = (mPercentTransitionedToDot - 0.98f) / .02f; + final float percentShadowVisible = 1f - percentBetween99and100; + + // Keep it centered. + outlineMatrix.postTranslate( + mNewDotRadius * percentBetween99and100, + mNewDotRadius * percentBetween99and100); + outlineMatrix.preScale(percentShadowVisible, percentShadowVisible); + } + + outline.mPath.transform(outlineMatrix); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index 2b1742592fba..4fef157183c2 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -25,8 +25,6 @@ import android.animation.ValueAnimator; import android.annotation.NonNull; import android.content.Context; import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Color; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Outline; @@ -35,8 +33,6 @@ import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; -import android.graphics.drawable.LayerDrawable; -import android.graphics.drawable.ShapeDrawable; import android.os.Bundle; import android.os.VibrationEffect; import android.os.Vibrator; @@ -56,11 +52,11 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.FrameLayout; -import android.widget.TextView; import androidx.annotation.MainThread; import androidx.annotation.Nullable; import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.FloatPropertyCompat; import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; @@ -70,7 +66,6 @@ import com.android.systemui.R; import com.android.systemui.bubbles.animation.ExpandedAnimationController; import com.android.systemui.bubbles.animation.PhysicsAnimationLayout; import com.android.systemui.bubbles.animation.StackAnimationController; -import com.android.systemui.recents.TriangleShape; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import java.math.BigDecimal; @@ -86,12 +81,21 @@ public class BubbleStackView extends FrameLayout { private static final String TAG = "BubbleStackView"; private static final boolean DEBUG = false; + /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */ + static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f; + + /** Velocity required to dismiss the flyout via drag. */ + private static final float FLYOUT_DISMISS_VELOCITY = 2000f; + + /** + * Factor for attenuating translation when the flyout is overscrolled (8f = flyout moves 1 pixel + * for every 8 pixels overscrolled). + */ + private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f; + /** Duration of the flyout alpha animations. */ private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100; - /** Max width of the flyout, in terms of percent of the screen width. */ - private static final float FLYOUT_MAX_WIDTH_PERCENT = .6f; - /** Percent to darken the bubbles when they're in the dismiss target. */ private static final float DARKEN_PERCENT = 0.3f; @@ -152,17 +156,9 @@ public class BubbleStackView extends FrameLayout { private FrameLayout mExpandedViewContainer; - private FrameLayout mFlyoutContainer; - private FrameLayout mFlyout; - private TextView mFlyoutText; - private ShapeDrawable mLeftFlyoutTriangle; - private ShapeDrawable mRightFlyoutTriangle; - /** Spring animation for the flyout. */ - private SpringAnimation mFlyoutSpring; + private BubbleFlyoutView mFlyout; /** Runnable that fades out the flyout and then sets it to GONE. */ - private Runnable mHideFlyout = - () -> mFlyoutContainer.animate().alpha(0f).withEndAction( - () -> mFlyoutContainer.setVisibility(GONE)); + private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */); /** Layout change listener that moves the stack to the nearest valid position on rotation. */ private OnLayoutChangeListener mMoveStackToValidPositionOnLayoutListener; @@ -176,9 +172,6 @@ public class BubbleStackView extends FrameLayout { private int mBubbleSize; private int mBubblePadding; - private int mFlyoutPadding; - private int mFlyoutSpaceFromBubble; - private int mPointerSize; private int mExpandedAnimateXDistance; private int mExpandedAnimateYDistance; private int mStatusBarHeight; @@ -189,8 +182,11 @@ public class BubbleStackView extends FrameLayout { private boolean mIsExpanded; private boolean mImeVisible; - /** Whether the stack is currently being dragged. */ - private boolean mIsDragging = false; + /** Whether the stack is currently on the left side of the screen, or animating there. */ + private boolean mStackOnLeftOrWillBe = false; + + /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */ + private boolean mIsGestureInProgress = false; private BubbleTouchHandler mTouchHandler; private BubbleController.BubbleExpandListener mExpandListener; @@ -249,6 +245,40 @@ public class BubbleStackView extends FrameLayout { } }; + /** Float property that 'drags' the flyout. */ + private final FloatPropertyCompat mFlyoutCollapseProperty = + new FloatPropertyCompat("FlyoutCollapseSpring") { + @Override + public float getValue(Object o) { + return mFlyoutDragDeltaX; + } + + @Override + public void setValue(Object o, float v) { + onFlyoutDragged(v); + } + }; + + /** SpringAnimation that springs the flyout collapsed via onFlyoutDragged. */ + private final SpringAnimation mFlyoutTransitionSpring = + new SpringAnimation(this, mFlyoutCollapseProperty); + + /** Distance the flyout has been dragged in the X axis. */ + private float mFlyoutDragDeltaX = 0f; + + /** + * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides + * it immediately. + */ + private final DynamicAnimation.OnAnimationEndListener mAfterFlyoutTransitionSpring = + (dynamicAnimation, b, v, v1) -> { + if (mFlyoutDragDeltaX == 0) { + mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); + } else { + mFlyout.hideFlyout(); + } + }; + @NonNull private final SurfaceSynchronizer mSurfaceSynchronizer; private BubbleDismissView mDismissContainer; @@ -267,9 +297,6 @@ public class BubbleStackView extends FrameLayout { Resources res = getResources(); mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding); - mFlyoutPadding = res.getDimensionPixelSize(R.dimen.bubble_flyout_padding_x); - mFlyoutSpaceFromBubble = res.getDimensionPixelSize(R.dimen.bubble_flyout_space_from_bubble); - mPointerSize = res.getDimensionPixelSize(R.dimen.bubble_flyout_pointer_size); mExpandedAnimateXDistance = res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_x_distance); mExpandedAnimateYDistance = @@ -307,17 +334,24 @@ public class BubbleStackView extends FrameLayout { mExpandedViewContainer.setClipChildren(false); addView(mExpandedViewContainer); - mFlyoutContainer = (FrameLayout) mInflater.inflate(R.layout.bubble_flyout, this, false); - mFlyoutContainer.setVisibility(GONE); - mFlyoutContainer.setClipToPadding(false); - mFlyoutContainer.setClipChildren(false); - mFlyoutContainer.animate() + mFlyout = new BubbleFlyoutView(context); + mFlyout.setVisibility(GONE); + mFlyout.animate() .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION) .setInterpolator(new AccelerateDecelerateInterpolator()); + addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + + mFlyoutTransitionSpring.setSpring(new SpringForce() + .setStiffness(SpringForce.STIFFNESS_MEDIUM) + .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); + mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring); - mFlyout = mFlyoutContainer.findViewById(R.id.bubble_flyout); - addView(mFlyoutContainer); - setupFlyout(); + mDismissContainer = new BubbleDismissView(mContext); + mDismissContainer.setLayoutParams(new FrameLayout.LayoutParams( + MATCH_PARENT, + getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height), + Gravity.BOTTOM)); + addView(mDismissContainer); mDismissContainer = new BubbleDismissView(mContext); mDismissContainer.setLayoutParams(new FrameLayout.LayoutParams( @@ -742,7 +776,7 @@ public class BubbleStackView extends FrameLayout { } // Outside parts of view we care about. return null; - } else if (mFlyoutContainer.getVisibility() == VISIBLE && isIntersecting(mFlyout, x, y)) { + } else if (mFlyout.getVisibility() == VISIBLE && isIntersecting(mFlyout, x, y)) { return mFlyout; } @@ -931,7 +965,6 @@ public class BubbleStackView extends FrameLayout { mBubbleContainer.setController(mStackAnimationController); hideFlyoutImmediate(); - mIsDragging = true; mDraggingInDismissTarget = false; } @@ -948,20 +981,87 @@ public class BubbleStackView extends FrameLayout { if (DEBUG) { Log.d(TAG, "onDragFinish"); } - // TODO: Add fling to bottom to dismiss. - mIsDragging = false; if (mIsExpanded || mIsExpansionAnimating) { return; } - mStackAnimationController.flingStackThenSpringToEdge(x, velX, velY); + final float newStackX = mStackAnimationController.flingStackThenSpringToEdge(x, velX, velY); logBubbleEvent(null /* no bubble associated with bubble stack move */, StatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED); + mStackOnLeftOrWillBe = newStackX <= 0; + updateBubbleShadowsAndDotPosition(true /* animate */); springOutDismissTargetAndHideCircle(); } + void onFlyoutDragStart() { + mFlyout.removeCallbacks(mHideFlyout); + } + + void onFlyoutDragged(float deltaX) { + final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); + mFlyoutDragDeltaX = deltaX; + + final float collapsePercent = + onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth(); + mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent))); + + // Calculate how to translate the flyout if it has been dragged too far in etiher direction. + float overscrollTranslation = 0f; + if (collapsePercent < 0f || collapsePercent > 1f) { + // Whether we are more than 100% transitioned to the dot. + final boolean overscrollingPastDot = collapsePercent > 1f; + + // Whether we are overscrolling physically to the left - this can either be pulling the + // flyout away from the stack (if the stack is on the right) or pushing it to the left + // after it has already become the dot. + final boolean overscrollingLeft = + (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f); + + overscrollTranslation = + (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1) + * (overscrollingLeft ? -1 : 1) + * (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR + // Attenuate the smaller dot less than the larger flyout. + / (overscrollingPastDot ? 2 : 1))); + } + + mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation); + } + + /** + * Called when the flyout drag has finished, and returns true if the gesture successfully + * dismissed the flyout. + */ + void onFlyoutDragFinished(float deltaX, float velX) { + final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); + final boolean metRequiredVelocity = + onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY; + final boolean metRequiredDeltaX = + onLeft + ? deltaX < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS + : deltaX > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS; + final boolean isCancelFling = onLeft ? velX > 0 : velX < 0; + final boolean shouldDismiss = metRequiredVelocity || (metRequiredDeltaX && !isCancelFling); + + mFlyout.removeCallbacks(mHideFlyout); + animateFlyoutCollapsed(shouldDismiss, velX); + } + + /** + * Called when the first touch event of a gesture (stack drag, bubble drag, flyout drag, etc.) + * is received. + */ + void onGestureStart() { + mIsGestureInProgress = true; + } + + /** Called when a gesture is completed or cancelled. */ + void onGestureFinished() { + mIsGestureInProgress = false; + } + /** Prepares and starts the desaturate/darken animation on the bubble stack. */ private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) { mDesaturateAndDarkenTargetView = targetView; @@ -1119,12 +1219,22 @@ public class BubbleStackView extends FrameLayout { mShowingDismiss = false; } - /** Whether the location of the given MotionEvent is within the dismiss target area. */ - public boolean isInDismissTarget(MotionEvent ev) { + boolean isInDismissTarget(MotionEvent ev) { return isIntersecting(mDismissContainer.getDismissTarget(), ev.getRawX(), ev.getRawY()); } + /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */ + private void animateFlyoutCollapsed(boolean collapsed, float velX) { + final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); + mFlyoutTransitionSpring + .setStartValue(mFlyoutDragDeltaX) + .setStartVelocity(velX) + .animateToFinalPosition(collapsed + ? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth()) + : 0f); + } + /** * Calculates how large the expanded view of the bubble can be. This takes into account the * y position when the bubbles are expanded as well as the bounds of the dismiss target. @@ -1161,55 +1271,27 @@ public class BubbleStackView extends FrameLayout { final CharSequence updateMessage = bubble.entry.getUpdateMessage(getContext()); // Show the message if one exists, and we're not expanded or animating expansion. - if (updateMessage != null && !isExpanded() && !mIsExpansionAnimating && !mIsDragging) { - final PointF stackPos = mStackAnimationController.getStackPosition(); - - // Set the flyout TextView's max width in terms of percent, and then subtract out the - // padding so that the entire flyout view will be the desired width (rather than the - // TextView being the desired width + extra padding). - mFlyoutText.setMaxWidth( - (int) (getWidth() * FLYOUT_MAX_WIDTH_PERCENT) - mFlyoutPadding * 2); - - mFlyoutContainer.setAlpha(0f); - mFlyoutContainer.setVisibility(VISIBLE); - - mFlyoutText.setText(updateMessage); - - final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); - - if (onLeft) { - mLeftFlyoutTriangle.setAlpha(255); - mRightFlyoutTriangle.setAlpha(0); - } else { - mLeftFlyoutTriangle.setAlpha(0); - mRightFlyoutTriangle.setAlpha(255); + if (updateMessage != null + && !isExpanded() + && !mIsExpansionAnimating + && !mIsGestureInProgress) { + if (bubble.iconView != null) { + bubble.iconView.setSuppressDot(true /* suppressDot */, false /* animate */); + mFlyoutDragDeltaX = 0f; + mFlyout.setAlpha(0f); + + // Post in case layout isn't complete and getWidth returns 0. + post(() -> mFlyout.showFlyout( + updateMessage, mStackAnimationController.getStackPosition(), getWidth(), + mStackAnimationController.isStackOnLeftSide(), + bubble.iconView.getBadgeColor(), + () -> { + bubble.iconView.setSuppressDot( + false /* suppressDot */, false /* animate */); + })); } - - mFlyoutContainer.post(() -> { - // Multi line flyouts get top-aligned to the bubble. - if (mFlyoutText.getLineCount() > 1) { - mFlyoutContainer.setTranslationY(stackPos.y); - } else { - // Single line flyouts are vertically centered with respect to the bubble. - mFlyoutContainer.setTranslationY( - stackPos.y + (mBubbleSize - mFlyout.getHeight()) / 2f); - } - - final float destinationX = onLeft - ? stackPos.x + mBubbleSize + mFlyoutSpaceFromBubble - : stackPos.x - mFlyoutContainer.getWidth() - mFlyoutSpaceFromBubble; - - // Translate towards the stack slightly, then spring out from the stack. - mFlyoutContainer.setTranslationX( - destinationX + (onLeft ? -mBubblePadding : mBubblePadding)); - - mFlyoutContainer.animate().alpha(1f); - mFlyoutSpring.animateToFinalPosition(destinationX); - - mFlyout.removeCallbacks(mHideFlyout); - mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); - }); - + mFlyout.removeCallbacks(mHideFlyout); + mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT); } } @@ -1217,7 +1299,7 @@ public class BubbleStackView extends FrameLayout { /** Hide the flyout immediately and cancel any pending hide runnables. */ private void hideFlyoutImmediate() { mFlyout.removeCallbacks(mHideFlyout); - mHideFlyout.run(); + mFlyout.hideFlyout(); } @Override @@ -1230,7 +1312,7 @@ public class BubbleStackView extends FrameLayout { mBubbleContainer.getBoundsOnScreen(outRect); } - if (mFlyoutContainer.getVisibility() == View.VISIBLE) { + if (mFlyout.getVisibility() == View.VISIBLE) { final Rect flyoutBounds = new Rect(); mFlyout.getBoundsOnScreen(flyoutBounds); outRect.union(flyoutBounds); @@ -1287,78 +1369,11 @@ public class BubbleStackView extends FrameLayout { } } - /** Sets up the flyout views and drawables. */ - private void setupFlyout() { - // Retrieve the styled floating background color. - TypedArray ta = mContext.obtainStyledAttributes( - new int[]{android.R.attr.colorBackgroundFloating}); - final int floatingBackgroundColor = ta.getColor(0, Color.WHITE); - ta.recycle(); - - // Retrieve the flyout background, which is currently a rounded white rectangle with a - // shadow but no triangular arrow pointing anywhere. - final LayerDrawable flyoutBackground = (LayerDrawable) mFlyout.getBackground(); - - // Create the triangle drawables and set their color. - mLeftFlyoutTriangle = - new ShapeDrawable(TriangleShape.createHorizontal( - mPointerSize, mPointerSize, true /* isPointingLeft */)); - mRightFlyoutTriangle = - new ShapeDrawable(TriangleShape.createHorizontal( - mPointerSize, mPointerSize, false /* isPointingLeft */)); - mLeftFlyoutTriangle.getPaint().setColor(floatingBackgroundColor); - mRightFlyoutTriangle.getPaint().setColor(floatingBackgroundColor); - - // Add both triangles to the drawable. We'll show and hide the appropriate ones when we show - // the flyout. - final int leftTriangleIndex = flyoutBackground.addLayer(mLeftFlyoutTriangle); - flyoutBackground.setLayerSize(leftTriangleIndex, mPointerSize, mPointerSize); - flyoutBackground.setLayerGravity(leftTriangleIndex, Gravity.LEFT | Gravity.CENTER_VERTICAL); - flyoutBackground.setLayerInsetLeft(leftTriangleIndex, -mPointerSize); - - final int rightTriangleIndex = flyoutBackground.addLayer(mRightFlyoutTriangle); - flyoutBackground.setLayerSize(rightTriangleIndex, mPointerSize, mPointerSize); - flyoutBackground.setLayerGravity( - rightTriangleIndex, Gravity.RIGHT | Gravity.CENTER_VERTICAL); - flyoutBackground.setLayerInsetRight(rightTriangleIndex, -mPointerSize); - - // Append the appropriate triangle's outline to the view's outline so that the shadows look - // correct. - mFlyout.setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - final boolean leftPointing = mStackAnimationController.isStackOnLeftSide(); - - // Get the outline from the appropriate triangle. - final Outline triangleOutline = new Outline(); - if (leftPointing) { - mLeftFlyoutTriangle.getOutline(triangleOutline); - } else { - mRightFlyoutTriangle.getOutline(triangleOutline); - } - - // Offset it to the correct position, since it has no intrinsic position since - // that is maintained by the parent LayerDrawable. - triangleOutline.offset( - leftPointing ? -mPointerSize : mFlyout.getWidth(), - mFlyout.getHeight() / 2 - mPointerSize / 2); - - // Merge the outlines. - final Outline compoundOutline = new Outline(); - flyoutBackground.getOutline(compoundOutline); - compoundOutline.mPath.addPath(triangleOutline.mPath); - outline.set(compoundOutline); - } - }); - - mFlyoutText = mFlyout.findViewById(R.id.bubble_flyout_text); - mFlyoutSpring = new SpringAnimation(mFlyoutContainer, DynamicAnimation.TRANSLATION_X); - } - private void applyCurrentState() { if (DEBUG) { Log.d(TAG, "applyCurrentState: mIsExpanded=" + mIsExpanded); } + mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE); if (mIsExpanded) { // First update the view so that it calculates a new height (ensuring the y position @@ -1376,10 +1391,15 @@ public class BubbleStackView extends FrameLayout { } } + mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); + updateBubbleShadowsAndDotPosition(false); + } + + /** Sets the appropriate Z-order and dot position for each bubble in the stack. */ + private void updateBubbleShadowsAndDotPosition(boolean animate) { int bubbsCount = mBubbleContainer.getChildCount(); for (int i = 0; i < bubbsCount; i++) { BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i); - bv.updateDotVisibility(); bv.setZ((BubbleController.MAX_BUBBLES * getResources().getDimensionPixelSize(R.dimen.bubble_elevation)) - i); @@ -1393,6 +1413,11 @@ public class BubbleStackView extends FrameLayout { } }); bv.setClipToOutline(false); + + // If the dot is on the left, and so is the stack, we need to change the dot position. + if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) { + bv.setDotPosition(!mStackOnLeftOrWillBe, animate); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java index f429c2c124b3..8fe8bd305707 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java @@ -111,12 +111,13 @@ class BubbleTouchHandler implements View.OnTouchListener { trackMovement(event); mTouchDown.set(rawX, rawY); + mStack.onGestureStart(); if (isStack) { mViewPositionOnTouchDown.set(mStack.getStackPosition()); mStack.onDragStart(); } else if (isFlyout) { - // TODO(b/129768381): Make the flyout dismissable with a gesture. + mStack.onFlyoutDragStart(); } else { mViewPositionOnTouchDown.set( mTouchedView.getTranslationX(), mTouchedView.getTranslationY()); @@ -137,7 +138,7 @@ class BubbleTouchHandler implements View.OnTouchListener { if (isStack) { mStack.onDragged(viewX, viewY); } else if (isFlyout) { - // TODO(b/129768381): Make the flyout dismissable with a gesture. + mStack.onFlyoutDragged(deltaX); } else { mStack.onBubbleDragged(mTouchedView, viewX, viewY); } @@ -152,8 +153,10 @@ class BubbleTouchHandler implements View.OnTouchListener { final float velY = mVelocityTracker.getYVelocity(); // If the touch event is within the dismiss target, magnet the stack to it. - mStack.animateMagnetToDismissTarget( - mTouchedView, mInDismissTarget, viewX, viewY, velX, velY); + if (!isFlyout) { + mStack.animateMagnetToDismissTarget( + mTouchedView, mInDismissTarget, viewX, viewY, velX, velY); + } } break; @@ -174,7 +177,9 @@ class BubbleTouchHandler implements View.OnTouchListener { : mInDismissTarget || velY > INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY; - if (shouldDismiss) { + if (isFlyout && mMovedEnough) { + mStack.onFlyoutDragFinished(rawX - mTouchDown.x /* deltaX */, velX); + } else if (shouldDismiss) { final String individualBubbleKey = isStack ? null : ((BubbleView) mTouchedView).getKey(); mStack.magnetToStackIfNeededThenAnimateDismissal(mTouchedView, velX, velY, @@ -200,7 +205,7 @@ class BubbleTouchHandler implements View.OnTouchListener { } } else if (mTouchedView == mStack.getExpandedBubbleView()) { mBubbleData.setExpanded(false); - } else if (isStack) { + } else if (isStack || isFlyout) { // Toggle expansion mBubbleData.setExpanded(!mBubbleData.isExpanded()); } else { @@ -251,9 +256,12 @@ class BubbleTouchHandler implements View.OnTouchListener { mVelocityTracker.recycle(); mVelocityTracker = null; } + mTouchedView = null; mMovedEnough = false; mInDismissTarget = false; + + mStack.onGestureFinished(); } private void trackMovement(MotionEvent event) { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java index 2681b6d0c891..aa32b9456cbc 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java @@ -48,9 +48,12 @@ public class BubbleView extends FrameLayout { private Context mContext; private BadgedImageView mBadgedImageView; + private int mBadgeColor; private int mPadding; private int mIconInset; + private boolean mSuppressDot = false; + private NotificationEntry mEntry; public BubbleView(Context context) { @@ -130,18 +133,54 @@ public class BubbleView extends FrameLayout { return (mEntry != null) ? mEntry.getRow() : null; } + /** Changes the dot's visibility to match the bubble view's state. */ + void updateDotVisibility(boolean animate) { + updateDotVisibility(animate, null /* after */); + } + + /** + * Changes the dot's visibility to match the bubble view's state, running the provided callback + * after animation if requested. + */ + void updateDotVisibility(boolean animate, Runnable after) { + boolean showDot = getEntry().showInShadeWhenBubble() && !mSuppressDot; + + if (animate) { + animateDot(showDot, after); + } else { + mBadgedImageView.setShowDot(showDot); + } + } + /** - * Marks this bubble as "read", i.e. no badge should show. + * Sets whether or not to hide the dot even if we'd otherwise show it. This is used while the + * flyout is visible or animating, to hide the dot until the flyout visually transforms into it. */ - public void updateDotVisibility() { - boolean showDot = getEntry().showInShadeWhenBubble(); - animateDot(showDot); + void setSuppressDot(boolean suppressDot, boolean animate) { + mSuppressDot = suppressDot; + updateDotVisibility(animate); + } + + /** Sets the position of the 'new' dot, animating it out and back in if requested. */ + void setDotPosition(boolean onLeft, boolean animate) { + if (animate && onLeft != mBadgedImageView.getDotPosition() && !mSuppressDot) { + animateDot(false /* showDot */, () -> { + mBadgedImageView.setDotPosition(onLeft); + animateDot(true /* showDot */, null); + }); + } else { + mBadgedImageView.setDotPosition(onLeft); + } + } + + boolean getDotPositionOnLeft() { + return mBadgedImageView.getDotPosition(); } /** * Animates the badge to show or hide. */ - private void animateDot(boolean showDot) { + private void animateDot(boolean showDot, Runnable after) { if (mBadgedImageView.isShowingDot() != showDot) { mBadgedImageView.setShowDot(showDot); mBadgedImageView.clearAnimation(); @@ -152,9 +191,13 @@ public class BubbleView extends FrameLayout { fraction = showDot ? fraction : 1 - fraction; mBadgedImageView.setDotScale(fraction); }).withEndAction(() -> { - if (!showDot) { - mBadgedImageView.setShowDot(false); - } + if (!showDot) { + mBadgedImageView.setShowDot(false); + } + + if (after != null) { + after.run(); + } }).start(); } } @@ -181,8 +224,13 @@ public class BubbleView extends FrameLayout { mBadgedImageView.setImageDrawable(iconDrawable); } int badgeColor = determineDominateColor(iconDrawable, n.color); + mBadgeColor = badgeColor; mBadgedImageView.setDotColor(badgeColor); - animateDot(mEntry.showInShadeWhenBubble() /* showDot */); + animateDot(mEntry.showInShadeWhenBubble() /* showDot */, null /* after */); + } + + int getBadgeColor() { + return mBadgeColor; } private Drawable buildIconWithTint(Drawable iconDrawable, int backgroundColor) { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java index f937525cf417..8529ed42cf0a 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java @@ -225,8 +225,10 @@ public class StackAnimationController extends /** * Flings the stack starting with the given velocities, springing it to the nearest edge * afterward. + * + * @return The X value that the stack will end up at after the fling/spring. */ - public void flingStackThenSpringToEdge(float x, float velX, float velY) { + public float flingStackThenSpringToEdge(float x, float velX, float velY) { final boolean stackOnLeftSide = x - mIndividualBubbleSize / 2 < mLayout.getWidth() / 2; final boolean stackShouldFlingLeft = stackOnLeftSide @@ -281,6 +283,7 @@ public class StackAnimationController extends DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); mIsMovingFromFlinging = true; + return destinationRelativeX; } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java new file mode 100644 index 000000000000..173237f7b311 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2019 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.bubbles; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotSame; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.Mockito.verify; + +import android.graphics.Color; +import android.graphics.PointF; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.View; +import android.widget.TextView; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class BubbleFlyoutViewTest extends SysuiTestCase { + private BubbleFlyoutView mFlyout; + private TextView mFlyoutText; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mFlyout = new BubbleFlyoutView(getContext()); + + mFlyoutText = mFlyout.findViewById(R.id.bubble_flyout_text); + } + + @Test + public void testShowFlyout_isVisible() { + mFlyout.showFlyout("Hello", new PointF(100, 100), 500, true, Color.WHITE, null); + assertEquals("Hello", mFlyoutText.getText()); + assertEquals(View.VISIBLE, mFlyout.getVisibility()); + assertEquals(1f, mFlyoutText.getAlpha(), .01f); + } + + @Test + public void testFlyoutHide_runsCallback() { + Runnable after = Mockito.mock(Runnable.class); + mFlyout.showFlyout("Hello", new PointF(100, 100), 500, true, Color.WHITE, after); + mFlyout.hideFlyout(); + + verify(after).run(); + } + + @Test + public void testSetCollapsePercent() { + mFlyout.showFlyout("Hello", new PointF(100, 100), 500, true, Color.WHITE, null); + + float initialTranslationZ = mFlyout.getTranslationZ(); + + mFlyout.setCollapsePercent(1f); + assertEquals(0f, mFlyoutText.getAlpha(), 0.01f); + assertNotSame(0f, mFlyoutText.getTranslationX()); // Should have moved to collapse. + assertTrue(mFlyout.getTranslationZ() < initialTranslationZ); // Should be descending. + + mFlyout.setCollapsePercent(0f); + assertEquals(1f, mFlyoutText.getAlpha(), 0.01f); + assertEquals(0f, mFlyoutText.getTranslationX()); + assertEquals(initialTranslationZ, mFlyout.getTranslationZ()); + + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleStackViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleStackViewTest.java deleted file mode 100644 index bafae6ce737a..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleStackViewTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2019 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.bubbles; - -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.widget.TextView; - -import androidx.test.filters.SmallTest; - -import com.android.systemui.R; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper(setAsMainLooper = true) -public class BubbleStackViewTest extends SysuiTestCase { - private BubbleStackView mStackView; - @Mock private Bubble mBubble; - @Mock private NotificationEntry mNotifEntry; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - mStackView = new BubbleStackView(mContext, new BubbleData(getContext()), null); - mBubble.entry = mNotifEntry; - } - - @Test - public void testAnimateInFlyoutForBubble() { - when(mNotifEntry.getUpdateMessage(any())).thenReturn("Test Flyout Message."); - mStackView.animateInFlyoutForBubble(mBubble); - - assertEquals("Test Flyout Message.", - ((TextView) mStackView.findViewById(R.id.bubble_flyout_text)).getText()); - } -} |