+++ /dev/null
-<!--
- ~ 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
~ 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"
</FrameLayout>
-</FrameLayout>
\ No newline at end of file
+</merge>
\ No newline at end of file
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).
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;
}
/**
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});
invalidate();
}
+ public boolean getDotPosition() {
+ return mOnLeft;
+ }
+
/**
* Set whether the dot should show or not.
*/
public void updateDotVisibility() {
if (iconView != null) {
- iconView.updateDotVisibility();
+ iconView.updateDotVisibility(true /* animate */);
}
}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
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;
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;
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;
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;
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;
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;
private int mBubbleSize;
private int mBubblePadding;
- private int mFlyoutPadding;
- private int mFlyoutSpaceFromBubble;
- private int mPointerSize;
private int mExpandedAnimateXDistance;
private int mExpandedAnimateYDistance;
private int mStatusBarHeight;
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;
}
};
+ /** 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;
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 =
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(
}
// 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;
}
mBubbleContainer.setController(mStackAnimationController);
hideFlyoutImmediate();
- mIsDragging = true;
mDraggingInDismissTarget = false;
}
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;
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.
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);
}
}
/** Hide the flyout immediately and cancel any pending hide runnables. */
private void hideFlyoutImmediate() {
mFlyout.removeCallbacks(mHideFlyout);
- mHideFlyout.run();
+ mFlyout.hideFlyout();
}
@Override
mBubbleContainer.getBoundsOnScreen(outRect);
}
- if (mFlyoutContainer.getVisibility() == View.VISIBLE) {
+ if (mFlyout.getVisibility() == View.VISIBLE) {
final Rect flyoutBounds = new Rect();
mFlyout.getBoundsOnScreen(flyoutBounds);
outRect.union(flyoutBounds);
}
}
- /** 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
}
}
+ 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);
}
});
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);
+ }
}
}
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());
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);
}
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;
: 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,
}
} else if (mTouchedView == mStack.getExpandedBubbleView()) {
mBubbleData.setExpanded(false);
- } else if (isStack) {
+ } else if (isStack || isFlyout) {
// Toggle expansion
mBubbleData.setExpanded(!mBubbleData.isExpanded());
} else {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
+
mTouchedView = null;
mMovedEnough = false;
mInDismissTarget = false;
+
+ mStack.onGestureFinished();
}
private void trackMovement(MotionEvent event) {
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) {
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();
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();
}
}
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) {
/**
* 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
DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
mIsMovingFromFlinging = true;
+ return destinationRelativeX;
}
/**
--- /dev/null
+/*
+ * 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());
+
+ }
+}
+++ /dev/null
-/*
- * 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());
- }
-}