summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/res/drawable/bubble_dismiss_circle.xml27
-rw-r--r--packages/SystemUI/res/drawable/bubble_dismiss_icon.xml26
-rw-r--r--packages/SystemUI/res/layout/bubble_dismiss_target.xml66
-rw-r--r--packages/SystemUI/res/values/dimens.xml9
-rw-r--r--packages/SystemUI/res/values/strings.xml2
-rw-r--r--packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/bubbles/BubbleDismissView.java227
-rw-r--r--packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java247
-rw-r--r--packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java132
-rw-r--r--packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java101
-rw-r--r--packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java73
-rw-r--r--packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java183
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java78
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java64
15 files changed, 1121 insertions, 122 deletions
diff --git a/packages/SystemUI/res/drawable/bubble_dismiss_circle.xml b/packages/SystemUI/res/drawable/bubble_dismiss_circle.xml
new file mode 100644
index 000000000000..1661bb22d148
--- /dev/null
+++ b/packages/SystemUI/res/drawable/bubble_dismiss_circle.xml
@@ -0,0 +1,27 @@
+<!--
+ 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.
+-->
+<!--
+ The transparent circle outline that encircles the bubbles when they're in the dismiss target.
+-->
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+
+ <stroke
+ android:width="1dp"
+ android:color="#66FFFFFF" />
+
+</shape> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/bubble_dismiss_icon.xml b/packages/SystemUI/res/drawable/bubble_dismiss_icon.xml
new file mode 100644
index 000000000000..5c8de581f8d1
--- /dev/null
+++ b/packages/SystemUI/res/drawable/bubble_dismiss_icon.xml
@@ -0,0 +1,26 @@
+<!--
+ 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.
+-->
+<!-- The 'X' bubble dismiss icon. This is just ic_close with a stroke. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24.0dp"
+ android:height="24.0dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z"
+ android:fillColor="#FFFFFFFF"
+ android:strokeColor="#FF000000"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/bubble_dismiss_target.xml b/packages/SystemUI/res/layout/bubble_dismiss_target.xml
new file mode 100644
index 000000000000..245177c8461b
--- /dev/null
+++ b/packages/SystemUI/res/layout/bubble_dismiss_target.xml
@@ -0,0 +1,66 @@
+<!--
+ ~ 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
+ -->
+<!-- Bubble dismiss target consisting of an X icon and the text 'Dismiss'. -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/pip_dismiss_gradient_height"
+ android:layout_gravity="bottom|center_horizontal">
+
+ <LinearLayout
+ android:id="@+id/bubble_dismiss_icon_container"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:paddingBottom="@dimen/bubble_dismiss_target_padding_y"
+ android:paddingTop="@dimen/bubble_dismiss_target_padding_y"
+ android:paddingLeft="@dimen/bubble_dismiss_target_padding_x"
+ android:paddingRight="@dimen/bubble_dismiss_target_padding_x"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/bubble_dismiss_close_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:src="@drawable/bubble_dismiss_icon" />
+
+ <TextView
+ android:id="@+id/bubble_dismiss_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="9dp"
+ android:layout_marginBottom="9dp"
+ android:layout_marginLeft="8dp"
+ android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body1"
+ android:textColor="@android:color/white"
+ android:shadowColor="@android:color/black"
+ android:shadowDx="-1"
+ android:shadowDy="1"
+ android:shadowRadius="0.01"
+ android:text="@string/bubble_dismiss_text" />
+
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/bubble_dismiss_circle"
+ android:layout_width="@dimen/bubble_dismiss_encircle_size"
+ android:layout_height="@dimen/bubble_dismiss_encircle_size"
+ android:layout_gravity="center"
+ android:alpha="0"
+ android:background="@drawable/bubble_dismiss_circle" />
+</FrameLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 0fa542c498bc..e53798d083fc 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1095,6 +1095,8 @@
<dimen name="bubble_padding">8dp</dimen>
<!-- Size of individual bubbles. -->
<dimen name="individual_bubble_size">52dp</dimen>
+ <!-- Size of the circle around the bubbles when they're in the dismiss target. -->
+ <dimen name="bubble_dismiss_encircle_size">56dp</dimen>
<!-- How much to inset the icon in the circle -->
<dimen name="bubble_icon_inset">16dp</dimen>
<!-- Padding around the view displayed when the bubble is expanded -->
@@ -1131,7 +1133,12 @@
<dimen name="bubble_header_icon_size">48dp</dimen>
<!-- Space between the pointer triangle and the bubble expanded view -->
<dimen name="bubble_pointer_margin">8dp</dimen>
-
+ <!-- Height of the permission prompt shown with bubbles -->
+ <dimen name="bubble_permission_height">120dp</dimen>
+ <!-- Padding applied to the bubble dismiss target. Touches in this padding cause the bubbles to
+ snap to the dismiss target. -->
+ <dimen name="bubble_dismiss_target_padding_x">40dp</dimen>
+ <dimen name="bubble_dismiss_target_padding_y">20dp</dimen>
<!-- Size of the RAT type for CellularTile -->
<dimen name="celltile_rat_type_size">10sp</dimen>
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 6ba72b6b85ad..13a20bf9bb7c 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2442,4 +2442,6 @@
<string name="bubble_accessibility_action_move_bottom_left">Move bottom left</string>
<!-- Action in accessibility menu to move the stack of bubbles to the bottom right of the screen. [CHAR LIMIT=30]-->
<string name="bubble_accessibility_action_move_bottom_right">Move bottom right</string>
+ <!-- Text used for the bubble dismiss area. Bubbles dragged to, or flung towards, this area will go away. [CHAR LIMIT=20] -->
+ <string name="bubble_dismiss_text">Dismiss</string>
</resources>
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 2d0944ad246f..48edf67a3ed4 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -108,7 +108,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
static final int DISMISS_ACCESSIBILITY_ACTION = 6;
static final int DISMISS_NO_LONGER_BUBBLE = 7;
- static final int MAX_BUBBLES = 5; // TODO: actually enforce this
+ public static final int MAX_BUBBLES = 5; // TODO: actually enforce this
// Enables some subset of notifs to automatically become bubbles
private static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false;
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDismissView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDismissView.java
new file mode 100644
index 000000000000..4db1e276f431
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDismissView.java
@@ -0,0 +1,227 @@
+/*
+ * 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.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.systemui.R;
+
+/** Dismiss view that contains a scrim gradient, as well as a dismiss icon, text, and circle. */
+public class BubbleDismissView extends FrameLayout {
+ /** Duration for animations involving the dismiss target text/icon/gradient. */
+ private static final int DISMISS_TARGET_ANIMATION_BASE_DURATION = 150;
+
+ private View mDismissGradient;
+
+ private LinearLayout mDismissTarget;
+ private ImageView mDismissIcon;
+ private TextView mDismissText;
+ private View mDismissCircle;
+
+ private SpringAnimation mDismissTargetAlphaSpring;
+ private SpringAnimation mDismissTargetVerticalSpring;
+
+ public BubbleDismissView(Context context) {
+ super(context);
+ setVisibility(GONE);
+
+ mDismissGradient = new FrameLayout(mContext);
+
+ FrameLayout.LayoutParams gradientParams =
+ new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
+ gradientParams.gravity = Gravity.BOTTOM;
+ mDismissGradient.setLayoutParams(gradientParams);
+
+ Drawable gradient = mContext.getResources().getDrawable(R.drawable.pip_dismiss_scrim);
+ gradient.setAlpha((int) (255 * 0.85f));
+ mDismissGradient.setBackground(gradient);
+
+ mDismissGradient.setVisibility(GONE);
+ addView(mDismissGradient);
+
+ LayoutInflater.from(context).inflate(R.layout.bubble_dismiss_target, this, true);
+ mDismissTarget = findViewById(R.id.bubble_dismiss_icon_container);
+ mDismissIcon = findViewById(R.id.bubble_dismiss_close_icon);
+ mDismissText = findViewById(R.id.bubble_dismiss_text);
+ mDismissCircle = findViewById(R.id.bubble_dismiss_circle);
+
+ // Set up the basic target area animations. These are very simple animations that don't need
+ // fancy interpolators.
+ final AccelerateDecelerateInterpolator interpolator =
+ new AccelerateDecelerateInterpolator();
+ mDismissGradient.animate()
+ .setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION)
+ .setInterpolator(interpolator);
+ mDismissText.animate()
+ .setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION)
+ .setInterpolator(interpolator);
+ mDismissIcon.animate()
+ .setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION)
+ .setInterpolator(interpolator);
+ mDismissCircle.animate()
+ .setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION / 2)
+ .setInterpolator(interpolator);
+
+ mDismissTargetAlphaSpring =
+ new SpringAnimation(mDismissTarget, DynamicAnimation.ALPHA)
+ .setSpring(new SpringForce()
+ .setStiffness(SpringForce.STIFFNESS_LOW)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
+ mDismissTargetVerticalSpring =
+ new SpringAnimation(mDismissTarget, DynamicAnimation.TRANSLATION_Y)
+ .setSpring(new SpringForce()
+ .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
+
+ mDismissTargetAlphaSpring.addEndListener((anim, canceled, alpha, velocity) -> {
+ // Since DynamicAnimations end when they're 'nearly' done, we can't rely on alpha being
+ // exactly zero when this listener is triggered. However, if it's less than 50% we can
+ // safely assume it was animating out rather than in.
+ if (alpha < 0.5f) {
+ // If the alpha spring was animating the view out, set it to GONE when it's done.
+ setVisibility(GONE);
+ }
+ });
+ }
+
+ /** Springs in the dismiss target and fades in the gradient. */
+ void springIn() {
+ setVisibility(View.VISIBLE);
+
+ // Fade in the dismiss target (icon + text).
+ mDismissTarget.setAlpha(0f);
+ mDismissTargetAlphaSpring.animateToFinalPosition(1f);
+
+ // Spring up the dismiss target (icon + text).
+ mDismissTarget.setTranslationY(mDismissTarget.getHeight() / 2f);
+ mDismissTargetVerticalSpring.animateToFinalPosition(0);
+
+ // Fade in the gradient.
+ mDismissGradient.setVisibility(VISIBLE);
+ mDismissGradient.animate().alpha(1f);
+
+ // Make sure the dismiss elements are in the separated position (in case we hid the target
+ // while they were condensed to cover the bubbles being in the target).
+ mDismissIcon.setAlpha(1f);
+ mDismissIcon.setScaleX(1f);
+ mDismissIcon.setScaleY(1f);
+ mDismissIcon.setTranslationX(0f);
+ mDismissText.setAlpha(1f);
+ mDismissText.setTranslationX(0f);
+ }
+
+ /** Springs out the dismiss target and fades out the gradient. */
+ void springOut() {
+ // Fade out the target.
+ mDismissTargetAlphaSpring.animateToFinalPosition(0f);
+
+ // Spring the target down a bit.
+ mDismissTargetVerticalSpring.animateToFinalPosition(mDismissTarget.getHeight() / 2f);
+
+ // Fade out the gradient and then set it to GONE so it's not in the SBV hierarchy.
+ mDismissGradient.animate().alpha(0f).withEndAction(
+ () -> mDismissGradient.setVisibility(GONE));
+
+ // Pop out the dismiss circle.
+ mDismissCircle.animate().alpha(0f).scaleX(1.2f).scaleY(1.2f);
+ }
+
+ /**
+ * Encircles the center of the dismiss target, pulling the X towards the center and hiding the
+ * text.
+ */
+ void animateEncircleCenterWithX(boolean encircle) {
+ // Pull the text towards the center if we're encircling (it'll be faded out, leaving only
+ // the X icon over the bubbles), or back to normal if we're un-encircling.
+ final float textTranslation = encircle
+ ? -mDismissIcon.getWidth() / 4f
+ : 0f;
+
+ // Center the icon if we're encircling, or put it back to normal if not.
+ final float iconTranslation = encircle
+ ? mDismissTarget.getWidth() / 2f
+ - mDismissIcon.getWidth() / 2f
+ - mDismissIcon.getLeft()
+ : 0f;
+
+ // Fade in/out the text and translate it.
+ mDismissText.animate()
+ .alpha(encircle ? 0f : 1f)
+ .translationX(textTranslation);
+
+ mDismissIcon.animate()
+ .setDuration(150)
+ .translationX(iconTranslation);
+
+ // Fade out the gradient if we're encircling (the bubbles will 'absorb' it by darkening
+ // themselves).
+ mDismissGradient.animate()
+ .alpha(encircle ? 0f : 1f);
+
+ // Prepare the circle to be 'dropped in'.
+ if (encircle) {
+ mDismissCircle.setAlpha(0f);
+ mDismissCircle.setScaleX(1.2f);
+ mDismissCircle.setScaleY(1.2f);
+ }
+
+ // Drop in the circle, or pull it back up.
+ mDismissCircle.animate()
+ .alpha(encircle ? 1f : 0f)
+ .scaleX(encircle ? 1f : 0f)
+ .scaleY(encircle ? 1f : 0f);
+ }
+
+ /** Animates the circle and the centered icon out. */
+ void animateEncirclingCircleDisappearance() {
+ // Pop out the dismiss icon and circle.
+ mDismissIcon.animate()
+ .setDuration(50)
+ .scaleX(0.9f)
+ .scaleY(0.9f)
+ .alpha(0f);
+ mDismissCircle.animate()
+ .scaleX(0.9f)
+ .scaleY(0.9f)
+ .alpha(0f);
+ }
+
+ /** Returns the Y value of the center of the dismiss target. */
+ float getDismissTargetCenterY() {
+ return getTop() + mDismissTarget.getTop() + mDismissTarget.getHeight() / 2f;
+ }
+
+ /** Returns the dismiss target, which contains the text/icon and any added padding. */
+ View getDismissTarget() {
+ return mDismissTarget;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 35dc1775cb7f..2b1742592fba 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -19,12 +19,18 @@ package com.android.systemui.bubbles;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+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;
+import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
@@ -32,6 +38,8 @@ 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.service.notification.StatusBarNotification;
import android.util.Log;
import android.util.StatsLog;
@@ -84,6 +92,9 @@ public class BubbleStackView extends FrameLayout {
/** 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;
+
/** How long to wait, in milliseconds, before hiding the flyout. */
@VisibleForTesting
static final int FLYOUT_HIDE_AFTER = 5000;
@@ -131,6 +142,10 @@ public class BubbleStackView extends FrameLayout {
private final SpringAnimation mExpandedViewYAnim;
private final BubbleData mBubbleData;
+ private final Vibrator mVibrator;
+ private final ValueAnimator mDesaturateAndDarkenAnimator;
+ private final Paint mDesaturateAndDarkenPaint = new Paint();
+
private PhysicsAnimationLayout mBubbleContainer;
private StackAnimationController mStackAnimationController;
private ExpandedAnimationController mExpandedAnimationController;
@@ -183,6 +198,20 @@ public class BubbleStackView extends FrameLayout {
private boolean mViewUpdatedRequested = false;
private boolean mIsExpansionAnimating = false;
+ private boolean mShowingDismiss = false;
+
+ /**
+ * Whether the user is currently dragging their finger within the dismiss target. In this state
+ * the stack will be magnetized to the center of the target, so we shouldn't move it until the
+ * touch exits the dismiss target area.
+ */
+ private boolean mDraggingInDismissTarget = false;
+
+ /** Whether the stack is magneting towards the dismiss target. */
+ private boolean mAnimatingMagnet = false;
+
+ /** The view to desaturate/darken when magneted to the dismiss target. */
+ private View mDesaturateAndDarkenTargetView;
private LayoutInflater mInflater;
@@ -222,6 +251,8 @@ public class BubbleStackView extends FrameLayout {
@NonNull private final SurfaceSynchronizer mSurfaceSynchronizer;
+ private BubbleDismissView mDismissContainer;
+ private Runnable mAfterMagnet;
public BubbleStackView(Context context, BubbleData data,
@Nullable SurfaceSynchronizer synchronizer) {
@@ -253,6 +284,8 @@ public class BubbleStackView extends FrameLayout {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
wm.getDefaultDisplay().getSize(mDisplaySize);
+ mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+
int padding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
@@ -286,6 +319,13 @@ public class BubbleStackView extends FrameLayout {
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);
+
mExpandedViewXAnim =
new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X);
mExpandedViewXAnim.setSpring(
@@ -342,6 +382,29 @@ public class BubbleStackView extends FrameLayout {
// This must be a separate OnDrawListener since it should be called for every draw.
getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
+
+ final ColorMatrix animatedMatrix = new ColorMatrix();
+ final ColorMatrix darkenMatrix = new ColorMatrix();
+
+ mDesaturateAndDarkenAnimator = ValueAnimator.ofFloat(1f, 0f);
+ mDesaturateAndDarkenAnimator.addUpdateListener(animation -> {
+ final float animatedValue = (float) animation.getAnimatedValue();
+ animatedMatrix.setSaturation(animatedValue);
+
+ final float animatedDarkenValue = (1f - animatedValue) * DARKEN_PERCENT;
+ darkenMatrix.setScale(
+ 1f - animatedDarkenValue /* red */,
+ 1f - animatedDarkenValue /* green */,
+ 1f - animatedDarkenValue /* blue */,
+ 1f /* alpha */);
+
+ // Concat the matrices so that the animatedMatrix both desaturates and darkens.
+ animatedMatrix.postConcat(darkenMatrix);
+
+ // Update the paint and apply it to the bubble container.
+ mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix));
+ mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint);
+ });
}
/**
@@ -838,23 +901,22 @@ public class BubbleStackView extends FrameLayout {
}
mExpandedAnimationController.dragBubbleOut(bubble, x, y);
+ springInDismissTarget();
}
/** Called when a drag operation on an individual bubble has finished. */
public void onBubbleDragFinish(
- View bubble, float x, float y, float velX, float velY, boolean dismissed) {
+ View bubble, float x, float y, float velX, float velY) {
if (DEBUG) {
- Log.d(TAG, "onBubbleDragFinish: bubble=" + bubble + ", dismissed=" + dismissed);
+ Log.d(TAG, "onBubbleDragFinish: bubble=" + bubble);
}
+
if (!mIsExpanded || mIsExpansionAnimating) {
return;
}
- if (dismissed) {
- mExpandedAnimationController.prepareForDismissalWithVelocity(bubble, velX, velY);
- } else {
- mExpandedAnimationController.snapBubbleBack(bubble, velX, velY);
- }
+ mExpandedAnimationController.snapBubbleBack(bubble, velX, velY);
+ springOutDismissTargetAndHideCircle();
}
void onDragStart() {
@@ -870,6 +932,7 @@ public class BubbleStackView extends FrameLayout {
hideFlyoutImmediate();
mIsDragging = true;
+ mDraggingInDismissTarget = false;
}
void onDragged(float x, float y) {
@@ -877,7 +940,8 @@ public class BubbleStackView extends FrameLayout {
return;
}
- mStackAnimationController.moveFirstBubbleWithStackFollowing(x, y);
+ springInDismissTarget();
+ mStackAnimationController.moveStackFromTouch(x, y);
}
void onDragFinish(float x, float y, float velX, float velY) {
@@ -894,10 +958,171 @@ public class BubbleStackView extends FrameLayout {
mStackAnimationController.flingStackThenSpringToEdge(x, velX, velY);
logBubbleEvent(null /* no bubble associated with bubble stack move */,
StatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
+
+ springOutDismissTargetAndHideCircle();
}
- void onDragFinishAsDismiss() {
- mIsDragging = false;
+ /** Prepares and starts the desaturate/darken animation on the bubble stack. */
+ private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
+ mDesaturateAndDarkenTargetView = targetView;
+
+ if (desaturateAndDarken) {
+ // Use the animated paint for the bubbles.
+ mDesaturateAndDarkenTargetView.setLayerType(
+ View.LAYER_TYPE_HARDWARE, mDesaturateAndDarkenPaint);
+ mDesaturateAndDarkenAnimator.removeAllListeners();
+ mDesaturateAndDarkenAnimator.start();
+ } else {
+ mDesaturateAndDarkenAnimator.removeAllListeners();
+ mDesaturateAndDarkenAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ // Stop using the animated paint.
+ resetDesaturationAndDarken();
+ }
+ });
+ mDesaturateAndDarkenAnimator.reverse();
+ }
+ }
+
+ private void resetDesaturationAndDarken() {
+ mDesaturateAndDarkenAnimator.removeAllListeners();
+ mDesaturateAndDarkenAnimator.cancel();
+ mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null);
+ }
+
+ /**
+ * Magnets the stack to the target, while also transforming the target to encircle the stack and
+ * desaturating/darkening the bubbles.
+ */
+ void animateMagnetToDismissTarget(
+ View magnetView, boolean toTarget, float x, float y, float velX, float velY) {
+ mDraggingInDismissTarget = toTarget;
+
+ if (toTarget) {
+ // The Y-value for the bubble stack to be positioned in the center of the dismiss target
+ final float destY = mDismissContainer.getDismissTargetCenterY() - mBubbleSize / 2f;
+
+ mAnimatingMagnet = true;
+
+ final Runnable afterMagnet = () -> {
+ mAnimatingMagnet = false;
+ if (mAfterMagnet != null) {
+ mAfterMagnet.run();
+ }
+ };
+
+ if (magnetView == this) {
+ mStackAnimationController.magnetToDismiss(velX, velY, destY, afterMagnet);
+ animateDesaturateAndDarken(mBubbleContainer, true);
+ } else {
+ mExpandedAnimationController.magnetBubbleToDismiss(
+ magnetView, velX, velY, destY, afterMagnet);
+
+ animateDesaturateAndDarken(magnetView, true);
+ }
+
+ mDismissContainer.animateEncircleCenterWithX(true);
+
+ } else {
+ mAnimatingMagnet = false;
+
+ if (magnetView == this) {
+ mStackAnimationController.demagnetizeFromDismissToPoint(x, y, velX, velY);
+ animateDesaturateAndDarken(mBubbleContainer, false);
+ } else {
+ mExpandedAnimationController.demagnetizeBubbleTo(x, y, velX, velY);
+ animateDesaturateAndDarken(magnetView, false);
+ }
+
+ mDismissContainer.animateEncircleCenterWithX(false);
+ }
+
+ mVibrator.vibrate(VibrationEffect.get(toTarget
+ ? VibrationEffect.EFFECT_CLICK
+ : VibrationEffect.EFFECT_TICK));
+ }
+
+ /**
+ * Magnets the stack to the dismiss target if it's not already there. Then, dismiss the stack
+ * using the 'implode' animation and animate out the target.
+ */
+ void magnetToStackIfNeededThenAnimateDismissal(
+ View touchedView, float velX, float velY, Runnable after) {
+ final Runnable animateDismissal = () -> {
+ mAfterMagnet = null;
+
+ mVibrator.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
+ mDismissContainer.animateEncirclingCircleDisappearance();
+
+ // 'Implode' the stack and then hide the dismiss target.
+ if (touchedView == this) {
+ mStackAnimationController.implodeStack(
+ () -> {
+ mAnimatingMagnet = false;
+ mShowingDismiss = false;
+ mDraggingInDismissTarget = false;
+ after.run();
+ resetDesaturationAndDarken();
+ });
+ } else {
+ mExpandedAnimationController.dismissDraggedOutBubble(() -> {
+ mAnimatingMagnet = false;
+ mShowingDismiss = false;
+ mDraggingInDismissTarget = false;
+ resetDesaturationAndDarken();
+ after.run();
+ });
+ }
+ };
+
+ if (mAnimatingMagnet) {
+ // If the magnet animation is currently playing, dismiss the stack after it's done. This
+ // happens if the stack is flung towards the target.
+ mAfterMagnet = animateDismissal;
+ } else if (mDraggingInDismissTarget) {
+ // If we're in the dismiss target, but not animating, we already magneted - dismiss
+ // immediately.
+ animateDismissal.run();
+ } else {
+ // Otherwise, we need to start the magnet animation and then dismiss afterward.
+ animateMagnetToDismissTarget(touchedView, true, -1 /* x */, -1 /* y */, velX, velY);
+ mAfterMagnet = animateDismissal;
+ }
+ }
+
+ /** Animates in the dismiss target, including the gradient behind it. */
+ private void springInDismissTarget() {
+ if (mShowingDismiss) {
+ return;
+ }
+
+ mShowingDismiss = true;
+
+ // Show the dismiss container and bring it to the front so the bubbles will go behind it.
+ mDismissContainer.springIn();
+ mDismissContainer.bringToFront();
+ mDismissContainer.setZ(Short.MAX_VALUE - 1);
+ }
+
+ /**
+ * Animates the dismiss target out, as well as the circle that encircles the bubbles, if they
+ * were dragged into the target and encircled.
+ */
+ private void springOutDismissTargetAndHideCircle() {
+ if (!mShowingDismiss) {
+ return;
+ }
+
+ mDismissContainer.springOut();
+ mShowingDismiss = false;
+ }
+
+
+ /** Whether the location of the given MotionEvent is within the dismiss target area. */
+ public boolean isInDismissTarget(MotionEvent ev) {
+ return isIntersecting(mDismissContainer.getDismissTarget(), ev.getRawX(), ev.getRawY());
}
/**
@@ -1066,7 +1291,7 @@ public class BubbleStackView extends FrameLayout {
private void setupFlyout() {
// Retrieve the styled floating background color.
TypedArray ta = mContext.obtainStyledAttributes(
- new int[] {android.R.attr.colorBackgroundFloating});
+ new int[]{android.R.attr.colorBackgroundFloating});
final int floatingBackgroundColor = ta.getColor(0, Color.WHITE);
ta.recycle();
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
index 82e6279772f4..f429c2c124b3 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
@@ -16,8 +16,6 @@
package com.android.systemui.bubbles;
-import static com.android.systemui.pip.phone.PipDismissViewController.SHOW_TARGET_DELAY;
-
import android.content.Context;
import android.graphics.PointF;
import android.os.Handler;
@@ -27,17 +25,35 @@ import android.view.View;
import android.view.ViewConfiguration;
import com.android.systemui.Dependency;
-import com.android.systemui.pip.phone.PipDismissViewController;
/**
* Handles interpreting touches on a {@link BubbleStackView}. This includes expanding, collapsing,
* dismissing, and flings.
*/
class BubbleTouchHandler implements View.OnTouchListener {
- /** Velocity required to dismiss a bubble without dragging it into the dismiss target. */
- private static final float DISMISS_MIN_VELOCITY = 4000f;
+ /** Velocity required to dismiss the stack without dragging it into the dismiss target. */
+ private static final float STACK_DISMISS_MIN_VELOCITY = 4000f;
+
+ /**
+ * Velocity required to dismiss an individual bubble without dragging it into the dismiss
+ * target.
+ *
+ * This is higher than the stack dismiss velocity since unlike the stack, a downward fling could
+ * also be an attempted gesture to return the bubble to the row of expanded bubbles, which would
+ * usually be below the dragged bubble. By increasing the required velocity, it's less likely
+ * that the user is trying to drop it back into the row vs. fling it away.
+ */
+ private static final float INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY = 6000f;
private static final String TAG = "BubbleTouchHandler";
+ /**
+ * When the stack is flung towards the bottom of the screen, it'll be dismissed if it's flung
+ * towards the center of the screen (where the dismiss target is). This value is the width of
+ * the target area to be considered 'towards the target'. For example 50% means that the stack
+ * needs to be flung towards the middle 50%, and the 25% on the left and right sides won't
+ * count.
+ */
+ private static final float DISMISS_FLING_TARGET_WIDTH_PERCENT = 0.5f;
private final PointF mTouchDown = new PointF();
private final PointF mViewPositionOnTouchDown = new PointF();
@@ -45,7 +61,6 @@ class BubbleTouchHandler implements View.OnTouchListener {
private final BubbleData mBubbleData;
private BubbleController mController = Dependency.get(BubbleController.class);
- private PipDismissViewController mDismissViewController;
private boolean mMovedEnough;
private int mTouchSlopSquared;
@@ -53,12 +68,6 @@ class BubbleTouchHandler implements View.OnTouchListener {
private boolean mInDismissTarget;
private Handler mHandler = new Handler();
- private Runnable mShowDismissAffordance = new Runnable() {
- @Override
- public void run() {
- mDismissViewController.showDismissTarget();
- }
- };
/** View that was initially touched, when we received the first ACTION_DOWN event. */
private View mTouchedView;
@@ -67,7 +76,6 @@ class BubbleTouchHandler implements View.OnTouchListener {
BubbleData bubbleData, Context context) {
final int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mTouchSlopSquared = touchSlop * touchSlop;
- mDismissViewController = new PipDismissViewController(context);
mBubbleData = bubbleData;
mStack = stackView;
}
@@ -104,11 +112,6 @@ class BubbleTouchHandler implements View.OnTouchListener {
mTouchDown.set(rawX, rawY);
- if (!isFlyout) {
- mDismissViewController.createDismissTarget();
- mHandler.postDelayed(mShowDismissAffordance, SHOW_TARGET_DELAY);
- }
-
if (isStack) {
mViewPositionOnTouchDown.set(mStack.getStackPosition());
mStack.onDragStart();
@@ -140,9 +143,18 @@ class BubbleTouchHandler implements View.OnTouchListener {
}
}
- // TODO - when we're in the target stick to it / animate in some way?
- mInDismissTarget = mDismissViewController.updateTarget(
- isStack ? mStack.getBubbleAt(0) : mTouchedView);
+ final boolean currentlyInDismissTarget = mStack.isInDismissTarget(event);
+ if (currentlyInDismissTarget != mInDismissTarget) {
+ mInDismissTarget = currentlyInDismissTarget;
+
+ mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
+ final float velX = mVelocityTracker.getXVelocity();
+ 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);
+ }
break;
case MotionEvent.ACTION_CANCEL:
@@ -151,28 +163,40 @@ class BubbleTouchHandler implements View.OnTouchListener {
case MotionEvent.ACTION_UP:
trackMovement(event);
- if (mInDismissTarget && isStack) {
- mController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
- mStack.onDragFinishAsDismiss();
+ mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
+ final float velX = mVelocityTracker.getXVelocity();
+ final float velY = mVelocityTracker.getYVelocity();
+
+ final boolean shouldDismiss =
+ isStack
+ ? mInDismissTarget
+ || isFastFlingTowardsDismissTarget(rawX, rawY, velX, velY)
+ : mInDismissTarget
+ || velY > INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY;
+
+ if (shouldDismiss) {
+ final String individualBubbleKey =
+ isStack ? null : ((BubbleView) mTouchedView).getKey();
+ mStack.magnetToStackIfNeededThenAnimateDismissal(mTouchedView, velX, velY,
+ () -> {
+ if (isStack) {
+ mController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
+ } else {
+ mController.removeBubble(
+ individualBubbleKey,
+ BubbleController.DISMISS_USER_GESTURE);
+ }
+ });
} else if (isFlyout) {
// TODO(b/129768381): Expand if tapped, dismiss if swiped away.
if (!mBubbleData.isExpanded() && !mMovedEnough) {
mBubbleData.setExpanded(true);
}
} else if (mMovedEnough) {
- mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
- final float velX = mVelocityTracker.getXVelocity();
- final float velY = mVelocityTracker.getYVelocity();
if (isStack) {
mStack.onDragFinish(viewX, viewY, velX, velY);
} else {
- final boolean dismissed = mInDismissTarget || velY > DISMISS_MIN_VELOCITY;
- mStack.onBubbleDragFinish(
- mTouchedView, viewX, viewY, velX, velY, /* dismissed */ dismissed);
- if (dismissed) {
- mController.removeBubble(((BubbleView) mTouchedView).getKey(),
- BubbleController.DISMISS_USER_GESTURE);
- }
+ mStack.onBubbleDragFinish(mTouchedView, viewX, viewY, velX, velY);
}
} else if (mTouchedView == mStack.getExpandedBubbleView()) {
mBubbleData.setExpanded(false);
@@ -191,9 +215,38 @@ class BubbleTouchHandler implements View.OnTouchListener {
return true;
}
+ /**
+ * Whether the given touch data represents a powerful fling towards the bottom-center of the
+ * screen (the dismiss target).
+ */
+ private boolean isFastFlingTowardsDismissTarget(
+ float rawX, float rawY, float velX, float velY) {
+ // Not a fling downward towards the target if velocity is zero or negative.
+ if (velY <= 0) {
+ return false;
+ }
+
+ float bottomOfScreenInterceptX = rawX;
+
+ // Only do math if the X velocity is non-zero, otherwise X won't change.
+ if (velX != 0) {
+ // Rise over run...
+ final float slope = velY / velX;
+ // ...y = mx + b, b = y / mx...
+ final float yIntercept = rawY - slope * rawX;
+ // ...calculate the x value when y = bottom of the screen.
+ bottomOfScreenInterceptX = (mStack.getHeight() - yIntercept) / slope;
+ }
+
+ final float dismissTargetWidth =
+ mStack.getWidth() * DISMISS_FLING_TARGET_WIDTH_PERCENT;
+ return velY > STACK_DISMISS_MIN_VELOCITY
+ && bottomOfScreenInterceptX > dismissTargetWidth / 2f
+ && bottomOfScreenInterceptX < mStack.getWidth() - dismissTargetWidth / 2f;
+ }
+
/** Clears all touch-related state. */
private void resetForNextGesture() {
- cleanUpDismissTarget();
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
@@ -203,15 +256,6 @@ class BubbleTouchHandler implements View.OnTouchListener {
mInDismissTarget = false;
}
- /**
- * Removes the dismiss target and cancels any pending callbacks to show it.
- */
- private void cleanUpDismissTarget() {
- mHandler.removeCallbacks(mShowDismissAffordance);
- mDismissViewController.destroyDismissTarget();
- }
-
-
private void trackMovement(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
index 95fbfe33ee71..a9ad464867a2 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
@@ -64,6 +64,20 @@ public class ExpandedAnimationController
/** Size of dismiss target at bottom of screen. */
private float mPipDismissHeight;
+ /** Whether the dragged-out bubble is in the dismiss target. */
+ private boolean mIndividualBubbleWithinDismissTarget = false;
+
+ /**
+ * Whether the dragged out bubble is springing towards the touch point, rather than using the
+ * default behavior of moving directly to the touch point.
+ *
+ * This happens when the user's finger exits the dismiss area while the bubble is magnetized to
+ * the center. Since the touch point differs from the bubble location, we need to animate the
+ * bubble back to the touch point to avoid a jarring instant location change from the center of
+ * the target to the touch point just outside the target bounds.
+ */
+ private boolean mSpringingBubbleToTouch = false;
+
public ExpandedAnimationController(Point displaySize) {
mDisplaySize = displaySize;
}
@@ -151,8 +165,23 @@ public class ExpandedAnimationController
* bubble is dragged back into the row.
*/
public void dragBubbleOut(View bubbleView, float x, float y) {
- bubbleView.setTranslationX(x);
- bubbleView.setTranslationY(y);
+ if (mSpringingBubbleToTouch) {
+ if (mLayout.arePropertiesAnimatingOnView(
+ bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) {
+ animationForChild(mBubbleDraggingOut)
+ .translationX(x)
+ .translationY(y)
+ .withStiffness(SpringForce.STIFFNESS_HIGH)
+ .start();
+ } else {
+ mSpringingBubbleToTouch = false;
+ }
+ }
+
+ if (!mSpringingBubbleToTouch && !mIndividualBubbleWithinDismissTarget) {
+ bubbleView.setTranslationX(x);
+ bubbleView.setTranslationY(y);
+ }
final boolean draggedOutEnough =
y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx;
@@ -164,6 +193,53 @@ public class ExpandedAnimationController
}
}
+ /** Plays a dismiss animation on the dragged out bubble. */
+ public void dismissDraggedOutBubble(Runnable after) {
+ mIndividualBubbleWithinDismissTarget = false;
+
+ // Fill the space from the soon to be dismissed bubble.
+ animateStackByBubbleWidthsStartingFrom(
+ /* numBubbleWidths */ -1,
+ /* startIndex */ mLayout.indexOfChild(mBubbleDraggingOut) + 1);
+
+ animationForChild(mBubbleDraggingOut)
+ .withStiffness(SpringForce.STIFFNESS_HIGH)
+ .scaleX(1.1f)
+ .scaleY(1.1f)
+ .alpha(0f, after)
+ .start();
+ }
+
+ /** Magnets the given bubble to the dismiss target. */
+ public void magnetBubbleToDismiss(
+ View bubbleView, float velX, float velY, float destY, Runnable after) {
+ mIndividualBubbleWithinDismissTarget = true;
+ mSpringingBubbleToTouch = false;
+ animationForChild(bubbleView)
+ .withStiffness(SpringForce.STIFFNESS_MEDIUM)
+ .withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+ .withPositionStartVelocities(velX, velY)
+ .translationX(mLayout.getWidth() / 2f - mBubbleSizePx / 2f)
+ .translationY(destY, after)
+ .start();
+ }
+
+ /**
+ * Springs the dragged-out bubble towards the given coordinates and sets flags to have touch
+ * events update the spring's final position until it's settled.
+ */
+ public void demagnetizeBubbleTo(float x, float y, float velX, float velY) {
+ mIndividualBubbleWithinDismissTarget = false;
+ mSpringingBubbleToTouch = true;
+
+ animationForChild(mBubbleDraggingOut)
+ .translationX(x)
+ .translationY(y)
+ .withPositionStartVelocities(velX, velY)
+ .withStiffness(SpringForce.STIFFNESS_HIGH)
+ .start();
+ }
+
/**
* Snaps a bubble back to its position within the bubble row, and animates the rest of the
* bubbles to accommodate it if it was previously dragged out past the threshold.
@@ -274,28 +350,21 @@ public class ExpandedAnimationController
@Override
void onChildRemoved(View child, int index, Runnable finishRemoval) {
- // Bubble pops out to the top.
- // TODO: Reverse this when bubbles are at the bottom.
-
final PhysicsAnimationLayout.PhysicsPropertyAnimator animator = animationForChild(child);
- animator.alpha(0f, finishRemoval /* endAction */);
// If we're removing the dragged-out bubble, that means it got dismissed.
if (child.equals(mBubbleDraggingOut)) {
- animator.position(
- mLayout.getWidth() / 2f - mBubbleSizePx / 2f,
- mLayout.getHeight() + mBubbleSizePx)
- .withPositionStartVelocities(mBubbleDraggingOutVelX, mBubbleDraggingOutVelY)
- .scaleX(ANIMATE_SCALE_PERCENT)
- .scaleY(ANIMATE_SCALE_PERCENT);
-
mBubbleDraggingOut = null;
+ finishRemoval.run();
} else {
- animator.translationY(getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR);
+ animator.alpha(0f, finishRemoval /* endAction */)
+ .withStiffness(SpringForce.STIFFNESS_HIGH)
+ .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
+ .scaleX(1.1f)
+ .scaleY(1.1f)
+ .start();
}
- animator.start();
-
// Animate all the other bubbles to their new positions sans this bubble.
animateBubblesAfterIndexToCorrectX(index);
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
index 460652612593..997d2c4627d8 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java
@@ -290,6 +290,10 @@ public class PhysicsAnimationLayout extends FrameLayout {
final Runnable checkIfAllFinished = () -> {
if (!arePropertiesAnimating(properties)) {
action.run();
+
+ for (DynamicAnimation.ViewProperty property : properties) {
+ removeEndActionForProperty(property);
+ }
}
};
@@ -379,10 +383,21 @@ public class PhysicsAnimationLayout extends FrameLayout {
/** Checks whether any animations of the given properties are still running. */
public boolean arePropertiesAnimating(DynamicAnimation.ViewProperty... properties) {
for (int i = 0; i < getChildCount(); i++) {
- for (DynamicAnimation.ViewProperty property : properties) {
- if (getAnimationAtIndex(property, i).isRunning()) {
- return true;
- }
+ if (arePropertiesAnimatingOnView(getChildAt(i), properties)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /** Checks whether any animations of the given properties are running on the given view. */
+ public boolean arePropertiesAnimatingOnView(
+ View view, DynamicAnimation.ViewProperty... properties) {
+ for (DynamicAnimation.ViewProperty property : properties) {
+ final SpringAnimation animation = getAnimationFromView(property, view);
+ if (animation != null && animation.isRunning()) {
+ return true;
}
}
@@ -556,7 +571,11 @@ public class PhysicsAnimationLayout extends FrameLayout {
DynamicAnimation anim, boolean canceled, float value, float velocity) {
if (!arePropertiesAnimating(mProperty)) {
if (mEndActionForProperty.containsKey(mProperty)) {
- mEndActionForProperty.get(mProperty).run();
+ final Runnable callback = mEndActionForProperty.get(mProperty);
+
+ if (callback != null) {
+ callback.run();
+ }
}
}
}
@@ -578,6 +597,12 @@ public class PhysicsAnimationLayout extends FrameLayout {
/** Start delay to use when start is called. */
private long mStartDelay = 0;
+ /** Damping ratio to use for the animations. */
+ private float mDampingRatio = -1;
+
+ /** Stiffness to use for the animations. */
+ private float mStiffness = -1;
+
/** End actions to call when animations for the given property complete. */
private Map<DynamicAnimation.ViewProperty, Runnable[]> mEndActionsForProperty =
new HashMap<>();
@@ -687,6 +712,24 @@ public class PhysicsAnimationLayout extends FrameLayout {
}
/**
+ * Set the damping ratio to use for this animation. If not supplied, will default to the
+ * value from {@link PhysicsAnimationController#getSpringForce}.
+ */
+ public PhysicsPropertyAnimator withDampingRatio(float dampingRatio) {
+ mDampingRatio = dampingRatio;
+ return this;
+ }
+
+ /**
+ * Set the stiffness to use for this animation. If not supplied, will default to the
+ * value from {@link PhysicsAnimationController#getSpringForce}.
+ */
+ public PhysicsPropertyAnimator withStiffness(float stiffness) {
+ mStiffness = stiffness;
+ return this;
+ }
+
+ /**
* Set the start velocities to use for TRANSLATION_X and TRANSLATION_Y animations. This
* overrides any value set via {@link #withStartVelocity(float)} for those properties.
*/
@@ -711,12 +754,14 @@ public class PhysicsAnimationLayout extends FrameLayout {
// If there are end actions, set an end listener on the layout for all the properties
// we're about to animate.
- if (after != null) {
+ if (after != null && after.length > 0) {
final DynamicAnimation.ViewProperty[] propertiesArray =
properties.toArray(new DynamicAnimation.ViewProperty[0]);
- for (Runnable callback : after) {
- setEndActionForMultipleProperties(callback, propertiesArray);
- }
+ setEndActionForMultipleProperties(() -> {
+ for (Runnable callback : after) {
+ callback.run();
+ }
+ }, propertiesArray);
}
// If we used position-specific end actions, we'll need to listen for both TRANSLATION_X
@@ -746,12 +791,15 @@ public class PhysicsAnimationLayout extends FrameLayout {
// Actually start the animations.
for (DynamicAnimation.ViewProperty property : properties) {
+ final SpringForce defaultSpringForce = mController.getSpringForce(property, mView);
animateValueForChild(
property,
mView,
mAnimatedProperties.get(property),
mPositionStartVelocities.getOrDefault(property, mDefaultStartVelocity),
mStartDelay,
+ mStiffness >= 0 ? mStiffness : defaultSpringForce.getStiffness(),
+ mDampingRatio >= 0 ? mDampingRatio : defaultSpringForce.getDampingRatio(),
mEndActionsForProperty.get(property));
}
@@ -760,6 +808,8 @@ public class PhysicsAnimationLayout extends FrameLayout {
mPositionStartVelocities.clear();
mDefaultStartVelocity = 0;
mStartDelay = 0;
+ mStiffness = -1;
+ mDampingRatio = -1;
mEndActionsForProperty.clear();
}
@@ -778,6 +828,8 @@ public class PhysicsAnimationLayout extends FrameLayout {
float value,
float startVel,
long startDelay,
+ float stiffness,
+ float dampingRatio,
Runnable[] afterCallbacks) {
if (view != null) {
final SpringAnimation animation =
@@ -795,6 +847,9 @@ public class PhysicsAnimationLayout extends FrameLayout {
});
}
+ animation.getSpring().setStiffness(stiffness);
+ animation.getSpring().setDampingRatio(dampingRatio);
+
if (startVel > 0) {
animation.setStartVelocity(startVel);
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
index bc249aedc605..f937525cf417 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
@@ -30,7 +30,6 @@ import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import com.android.systemui.R;
-import com.android.systemui.bubbles.BubbleController;
import com.google.android.collect.Sets;
@@ -116,6 +115,25 @@ public class StackAnimationController extends
*/
private boolean mIsMovingFromFlinging = false;
+ /**
+ * Whether the stack is within the dismiss target (either by being dragged, magnet'd, or flung).
+ */
+ private boolean mWithinDismissTarget = false;
+
+ /**
+ * Whether the first bubble is springing towards the touch point, rather than using the default
+ * behavior of moving directly to the touch point with the rest of the stack following it.
+ *
+ * This happens when the user's finger exits the dismiss area while the stack is magnetized to
+ * the center. Since the touch point differs from the stack location, we need to animate the
+ * stack back to the touch point to avoid a jarring instant location change from the center of
+ * the target to the touch point just outside the target bounds.
+ *
+ * This is reset once the spring animations end, since that means the first bubble has
+ * successfully 'caught up' to the touch.
+ */
+ private boolean mFirstBubbleSpringingToTouch = false;
+
/** Horizontal offset of bubbles in the stack. */
private float mStackOffset;
/** Diameter of the bubbles themselves. */
@@ -445,6 +463,120 @@ public class StackAnimationController extends
return allowableRegion;
}
+ /** Moves the stack in response to a touch event. */
+ public void moveStackFromTouch(float x, float y) {
+
+ // If we're springing to the touch point to 'catch up' after dragging out of the dismiss
+ // target, then update the stack position animations instead of moving the bubble directly.
+ if (mFirstBubbleSpringingToTouch) {
+ final SpringAnimation springToTouchX =
+ (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_X);
+ final SpringAnimation springToTouchY =
+ (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_Y);
+
+ // If either animation is still running, we haven't caught up. Update the animations.
+ if (springToTouchX.isRunning() || springToTouchY.isRunning()) {
+ springToTouchX.animateToFinalPosition(x);
+ springToTouchY.animateToFinalPosition(y);
+ } else {
+ // If the animations have finished, the stack is now at the touch point. We can
+ // resume moving the bubble directly.
+ mFirstBubbleSpringingToTouch = false;
+ }
+ }
+
+ if (!mFirstBubbleSpringingToTouch && !mWithinDismissTarget) {
+ moveFirstBubbleWithStackFollowing(x, y);
+ }
+ }
+
+ /**
+ * Demagnetizes the stack, springing it towards the given point. This also sets flags so that
+ * subsequent touch events will update the final position of the demagnetization spring instead
+ * of directly moving the bubbles, until demagnetization is complete.
+ */
+ public void demagnetizeFromDismissToPoint(float x, float y, float velX, float velY) {
+ mWithinDismissTarget = false;
+ mFirstBubbleSpringingToTouch = true;
+
+ springFirstBubbleWithStackFollowing(
+ DynamicAnimation.TRANSLATION_X,
+ new SpringForce()
+ .setDampingRatio(DEFAULT_BOUNCINESS)
+ .setStiffness(DEFAULT_STIFFNESS),
+ velX, x);
+
+ springFirstBubbleWithStackFollowing(
+ DynamicAnimation.TRANSLATION_Y,
+ new SpringForce()
+ .setDampingRatio(DEFAULT_BOUNCINESS)
+ .setStiffness(DEFAULT_STIFFNESS),
+ velY, y);
+ }
+
+ /**
+ * Spring the stack towards the dismiss target, respecting existing velocity. This also sets
+ * flags so that subsequent touch events will not move the stack until it's demagnetized.
+ */
+ public void magnetToDismiss(float velX, float velY, float destY, Runnable after) {
+ mWithinDismissTarget = true;
+ mFirstBubbleSpringingToTouch = false;
+
+ animationForChildAtIndex(0)
+ .translationX(mLayout.getWidth() / 2f - mIndividualBubbleSize / 2f)
+ .translationY(destY, after)
+ .withPositionStartVelocities(velX, velY)
+ .withStiffness(SpringForce.STIFFNESS_MEDIUM)
+ .withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+ .start();
+ }
+
+ /**
+ * 'Implode' the stack by shrinking the bubbles via chained animations and fading them out.
+ */
+ public void implodeStack(Runnable after) {
+ // Pop and fade the bubbles sequentially.
+ animationForChildAtIndex(0)
+ .scaleX(0.5f)
+ .scaleY(0.5f)
+ .alpha(0f)
+ .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
+ .withStiffness(SpringForce.STIFFNESS_HIGH)
+ .start(() -> {
+ // Run the callback and reset flags. The child translation animations might
+ // still be running, but that's fine. Once the alpha is at 0f they're no longer
+ // visible anyway.
+ after.run();
+ mWithinDismissTarget = false;
+ });
+ }
+
+ /**
+ * Springs the first bubble to the given final position, with the rest of the stack 'following'.
+ */
+ protected void springFirstBubbleWithStackFollowing(
+ DynamicAnimation.ViewProperty property, SpringForce spring,
+ float vel, float finalPosition) {
+
+ if (mLayout.getChildCount() == 0) {
+ return;
+ }
+
+ Log.d(TAG, String.format("Springing %s to final position %f.",
+ PhysicsAnimationLayout.getReadablePropertyName(property),
+ finalPosition));
+
+ StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
+ SpringAnimation springAnimation =
+ new SpringAnimation(this, firstBubbleProperty)
+ .setSpring(spring)
+ .setStartVelocity(vel);
+
+ cancelStackPositionAnimation(property);
+ mStackPositionAnimations.put(property, springAnimation);
+ springAnimation.animateToFinalPosition(finalPosition);
+ }
+
@Override
Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
return Sets.newHashSet(
@@ -459,7 +591,9 @@ public class StackAnimationController extends
int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
if (property.equals(DynamicAnimation.TRANSLATION_X)
|| property.equals(DynamicAnimation.TRANSLATION_Y)) {
- return index + 1; // Just chain them linearly.
+ return index + 1;
+ } else if (mWithinDismissTarget) {
+ return index + 1; // Chain all animations in dismiss (scale, alpha, etc. are used).
} else {
return NONE;
}
@@ -469,9 +603,15 @@ public class StackAnimationController extends
@Override
float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
if (property.equals(DynamicAnimation.TRANSLATION_X)) {
- // Offset to the left if we're on the left, or the right otherwise.
- return mLayout.isFirstChildXLeftOfCenter(mStackPosition.x)
- ? -mStackOffset : mStackOffset;
+ // If we're in the dismiss target, have the bubbles pile on top of each other with no
+ // offset.
+ if (mWithinDismissTarget) {
+ return 0f;
+ } else {
+ // Offset to the left if we're on the left, or the right otherwise.
+ return mLayout.isFirstChildXLeftOfCenter(mStackPosition.x)
+ ? -mStackOffset : mStackOffset;
+ }
} else {
return 0f;
}
@@ -480,11 +620,8 @@ public class StackAnimationController extends
@Override
SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
return new SpringForce()
- .setDampingRatio(BubbleController.getBubbleBounciness(
- mLayout.getContext(), DEFAULT_BOUNCINESS))
- .setStiffness(BubbleController.getBubbleStiffness(
- mLayout.getContext(),
- mIsMovingFromFlinging ? FLING_FOLLOW_STIFFNESS : DEFAULT_STIFFNESS));
+ .setDampingRatio(DEFAULT_BOUNCINESS)
+ .setStiffness(mIsMovingFromFlinging ? FLING_FOLLOW_STIFFNESS : DEFAULT_STIFFNESS);
}
@Override
@@ -594,32 +731,6 @@ public class StackAnimationController extends
}
/**
- * Springs the first bubble to the given final position, with the rest of the stack 'following'.
- */
- private void springFirstBubbleWithStackFollowing(
- DynamicAnimation.ViewProperty property, SpringForce spring,
- float vel, float finalPosition) {
-
- if (mLayout.getChildCount() == 0) {
- return;
- }
-
- Log.d(TAG, String.format("Springing %s to final position %f.",
- PhysicsAnimationLayout.getReadablePropertyName(property),
- finalPosition));
-
- StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
- SpringAnimation springAnimation =
- new SpringAnimation(this, firstBubbleProperty)
- .setSpring(spring)
- .setStartVelocity(vel);
-
- cancelStackPositionAnimation(property);
- mStackPositionAnimations.put(property, springAnimation);
- springAnimation.animateToFinalPosition(finalPosition);
- }
-
- /**
* Cancels any outstanding first bubble property animations that are running. This does not
* affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only
* cancels animations started from {@link #springFirstBubbleWithStackFollowing} and
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java
index cd8480505c04..567d192073b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java
@@ -17,6 +17,8 @@
package com.android.systemui.bubbles.animation;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.Mockito.verify;
import android.content.res.Resources;
import android.graphics.Point;
@@ -69,14 +71,14 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
testBubblesInCorrectExpandedPositions();
- Mockito.verify(afterExpand).run();
+ verify(afterExpand).run();
Runnable afterCollapse = Mockito.mock(Runnable.class);
mExpandedController.collapseBackToStack(afterCollapse);
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y, -1);
- Mockito.verify(afterExpand).run();
+ verify(afterExpand).run();
}
@Test
@@ -140,6 +142,78 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC
testBubblesInCorrectExpandedPositions();
}
+ @Test
+ public void testMagnetToDismiss_dismiss() throws InterruptedException {
+ expand();
+
+ final View draggedOutView = mViews.get(0);
+ final Runnable after = Mockito.mock(Runnable.class);
+
+ mExpandedController.prepareForBubbleDrag(draggedOutView);
+ mExpandedController.dragBubbleOut(draggedOutView, 25, 25);
+
+ // Magnet to dismiss, verify the bubble is at the dismiss target and the callback was
+ // called.
+ mExpandedController.magnetBubbleToDismiss(
+ mViews.get(0), 100 /* velX */, 100 /* velY */, 1000 /* destY */, after);
+ waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+ verify(after).run();
+ assertEquals(1000, mViews.get(0).getTranslationY(), .1f);
+
+ // Dismiss the now-magneted bubble, verify that the callback was called.
+ final Runnable afterDismiss = Mockito.mock(Runnable.class);
+ mExpandedController.dismissDraggedOutBubble(afterDismiss);
+ waitForPropertyAnimations(DynamicAnimation.ALPHA);
+ verify(after).run();
+
+ waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+
+ assertEquals(mBubblePadding, mViews.get(1).getTranslationX(), 1f);
+ }
+
+ @Test
+ public void testMagnetToDismiss_demagnetizeThenDrag() throws InterruptedException {
+ expand();
+
+ final View draggedOutView = mViews.get(0);
+ final Runnable after = Mockito.mock(Runnable.class);
+
+ mExpandedController.prepareForBubbleDrag(draggedOutView);
+ mExpandedController.dragBubbleOut(draggedOutView, 25, 25);
+
+ // Magnet to dismiss, verify the bubble is at the dismiss target and the callback was
+ // called.
+ mExpandedController.magnetBubbleToDismiss(
+ draggedOutView, 100 /* velX */, 100 /* velY */, 1000 /* destY */, after);
+ waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+ verify(after).run();
+ assertEquals(1000, mViews.get(0).getTranslationY(), .1f);
+
+ // Demagnetize the bubble towards (25, 25).
+ mExpandedController.demagnetizeBubbleTo(25 /* x */, 25 /* y */, 100, 100);
+
+ // Start dragging towards (20, 20).
+ mExpandedController.dragBubbleOut(draggedOutView, 20, 20);
+
+ // Since we just demagnetized, the bubble shouldn't be at (20, 20), it should be animating
+ // towards it.
+ assertNotEquals(20, draggedOutView.getTranslationX());
+ assertNotEquals(20, draggedOutView.getTranslationY());
+ waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+
+ // Waiting for the animations should result in the bubble ending at (20, 20) since the
+ // animation end value was updated.
+ assertEquals(20, draggedOutView.getTranslationX(), 1f);
+ assertEquals(20, draggedOutView.getTranslationY(), 1f);
+
+ // Drag to (30, 30).
+ mExpandedController.dragBubbleOut(draggedOutView, 30, 30);
+
+ // It should go there instantly since the animations finished.
+ assertEquals(30, draggedOutView.getTranslationX(), 1f);
+ assertEquals(30, draggedOutView.getTranslationY(), 1f);
+ }
+
/** Expand the stack and wait for animations to finish. */
private void expand() throws InterruptedException {
mExpandedController.expandFromStack(mExpansionPoint, Mockito.mock(Runnable.class));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java
index 9fce092ef7ce..a398fba008bb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java
@@ -195,9 +195,11 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase {
@Override
protected void animateValueForChild(DynamicAnimation.ViewProperty property, View view,
- float value, float startVel, long startDelay, Runnable[] afterCallbacks) {
+ float value, float startVel, long startDelay, float stiffness,
+ float dampingRatio, Runnable[] afterCallbacks) {
mMainThreadHandler.post(() -> super.animateValueForChild(
- property, view, value, startVel, startDelay, afterCallbacks));
+ property, view, value, startVel, startDelay, stiffness, dampingRatio,
+ afterCallbacks));
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
index 910cee3574dd..b83276bc93da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
@@ -17,6 +17,8 @@
package com.android.systemui.bubbles.animation;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.Mockito.verify;
import android.graphics.PointF;
import android.testing.AndroidTestingRunner;
@@ -33,6 +35,7 @@ import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mockito;
import org.mockito.Spy;
@SmallTest
@@ -223,6 +226,59 @@ public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase
assertEquals(prevStackPos, mStackController.getStackPosition());
}
+ @Test
+ public void testMagnetToDismiss_dismiss() throws InterruptedException {
+ final Runnable after = Mockito.mock(Runnable.class);
+
+ // Magnet to dismiss, verify the stack is at the dismiss target and the callback was
+ // called.
+ mStackController.magnetToDismiss(100 /* velX */, 100 /* velY */, 1000 /* destY */, after);
+ waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+ verify(after).run();
+ assertEquals(1000, mViews.get(0).getTranslationY(), .1f);
+
+ // Dismiss the stack, verify that the callback was called.
+ final Runnable afterImplode = Mockito.mock(Runnable.class);
+ mStackController.implodeStack(afterImplode);
+ waitForPropertyAnimations(
+ DynamicAnimation.ALPHA, DynamicAnimation.SCALE_X, DynamicAnimation.SCALE_Y);
+ verify(after).run();
+ }
+
+ @Test
+ public void testMagnetToDismiss_demagnetizeThenDrag() throws InterruptedException {
+ final Runnable after = Mockito.mock(Runnable.class);
+
+ // Magnet to dismiss, verify the stack is at the dismiss target and the callback was
+ // called.
+ mStackController.magnetToDismiss(100 /* velX */, 100 /* velY */, 1000 /* destY */, after);
+ waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+ verify(after).run();
+
+ assertEquals(1000, mViews.get(0).getTranslationY(), .1f);
+
+ // Demagnetize towards (25, 25) and then send a touch event.
+ mStackController.demagnetizeFromDismissToPoint(25, 25, 0, 0);
+ waitForLayoutMessageQueue();
+ mStackController.moveStackFromTouch(20, 20);
+
+ // Since the stack is demagnetizing, it shouldn't be at the stack position yet.
+ assertNotEquals(20, mStackController.getStackPosition().x, 1f);
+ assertNotEquals(20, mStackController.getStackPosition().y, 1f);
+
+ waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+
+ // Once the animation is done it should end at the touch position coordinates.
+ assertEquals(20, mStackController.getStackPosition().x, 1f);
+ assertEquals(20, mStackController.getStackPosition().y, 1f);
+
+ mStackController.moveStackFromTouch(30, 30);
+
+ // Touches after the animation are done should change the stack position instantly.
+ assertEquals(30, mStackController.getStackPosition().x, 1f);
+ assertEquals(30, mStackController.getStackPosition().y, 1f);
+ }
+
/**
* Checks every child view to make sure it's stacked at the given coordinates, off to the left
* or right side depending on offset multiplier.
@@ -249,5 +305,13 @@ public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase
super.flingThenSpringFirstBubbleWithStackFollowing(
property, vel, friction, spring, finalPosition));
}
+
+ @Override
+ protected void springFirstBubbleWithStackFollowing(DynamicAnimation.ViewProperty property,
+ SpringForce spring, float vel, float finalPosition) {
+ mMainThreadHandler.post(() ->
+ super.springFirstBubbleWithStackFollowing(
+ property, spring, vel, finalPosition));
+ }
}
}