Make the flyout dismissable with a gesture.
This adds the flyout-to-dot transition, and encapulates the flyout logic in a custom view. This also adds support for the 'new' dot changing sides (previously unimplemented) and animates it changing sides when the stack changes sides. Test: atest SystemUITests Fixes: 129768381 Bugs: 129768381 Change-Id: I90697b437cf14e5c5a211c87ed2c131a12e697e7
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
</merge>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -83,7 +83,7 @@ class Bubble {
|
||||
|
||||
public void updateDotVisibility() {
|
||||
if (iconView != null) {
|
||||
iconView.updateDotVisibility();
|
||||
iconView.updateDotVisibility(true /* animate */);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
mFlyout = mFlyoutContainer.findViewById(R.id.bubble_flyout);
|
||||
addView(mFlyoutContainer);
|
||||
setupFlyout();
|
||||
mFlyoutTransitionSpring.setSpring(new SpringForce()
|
||||
.setStiffness(SpringForce.STIFFNESS_MEDIUM)
|
||||
.setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
|
||||
mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring);
|
||||
|
||||
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();
|
||||
if (updateMessage != null
|
||||
&& !isExpanded()
|
||||
&& !mIsExpansionAnimating
|
||||
&& !mIsGestureInProgress) {
|
||||
if (bubble.iconView != null) {
|
||||
bubble.iconView.setSuppressDot(true /* suppressDot */, false /* animate */);
|
||||
mFlyoutDragDeltaX = 0f;
|
||||
mFlyout.setAlpha(0f);
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this bubble as "read", i.e. no badge should show.
|
||||
* Changes the dot's visibility to match the bubble view's state, running the provided callback
|
||||
* after animation if requested.
|
||||
*/
|
||||
public void updateDotVisibility() {
|
||||
boolean showDot = getEntry().showInShadeWhenBubble();
|
||||
animateDot(showDot);
|
||||
void updateDotVisibility(boolean animate, Runnable after) {
|
||||
boolean showDot = getEntry().showInShadeWhenBubble() && !mSuppressDot;
|
||||
|
||||
if (animate) {
|
||||
animateDot(showDot, after);
|
||||
} else {
|
||||
mBadgedImageView.setShowDot(showDot);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user