summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/res/drawable/dismiss_circle_background.xml28
-rw-r--r--packages/SystemUI/res/drawable/dismiss_target_x.xml28
-rw-r--r--packages/SystemUI/res/values/dimens.xml3
-rw-r--r--packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java282
-rw-r--r--packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java142
-rw-r--r--packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java127
-rw-r--r--packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java153
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java62
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java113
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java57
10 files changed, 470 insertions, 525 deletions
diff --git a/packages/SystemUI/res/drawable/dismiss_circle_background.xml b/packages/SystemUI/res/drawable/dismiss_circle_background.xml
new file mode 100644
index 000000000000..e311c520d3d6
--- /dev/null
+++ b/packages/SystemUI/res/drawable/dismiss_circle_background.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 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.
+ -->
+
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+
+ <stroke
+ android:width="1dp"
+ android:color="#66FFFFFF" />
+
+ <solid android:color="#B3000000" />
+
+</shape> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/dismiss_target_x.xml b/packages/SystemUI/res/drawable/dismiss_target_x.xml
new file mode 100644
index 000000000000..3672efffe139
--- /dev/null
+++ b/packages/SystemUI/res/drawable/dismiss_target_x.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 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.
+ -->
+
+<!-- 'X' icon. -->
+<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> \ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 7aaf6f9a778a..8bfba9e7b715 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1183,6 +1183,9 @@
<dimen name="bubble_dismiss_target_padding_x">40dp</dimen>
<dimen name="bubble_dismiss_target_padding_y">20dp</dimen>
+ <dimen name="dismiss_circle_size">52dp</dimen>
+ <dimen name="dismiss_target_x_size">24dp</dimen>
+
<!-- Bubbles user education views -->
<dimen name="bubbles_manage_education_width">160dp</dimen>
<!-- The inset from the top bound of the manage button to place the user education. -->
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 0cf6d89e8d74..8cc10d9d148f 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -32,7 +32,6 @@ import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
-import android.annotation.NonNull;
import android.app.Notification;
import android.content.Context;
import android.content.res.Configuration;
@@ -47,7 +46,6 @@ import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Bundle;
-import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.Log;
import android.view.Choreographer;
@@ -56,6 +54,7 @@ import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.WindowManager;
@@ -66,6 +65,7 @@ import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
@@ -81,7 +81,10 @@ 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.shared.system.SysUiStatsLog;
+import com.android.systemui.util.DismissCircleView;
import com.android.systemui.util.FloatingContentCoordinator;
+import com.android.systemui.util.animation.PhysicsAnimator;
+import com.android.systemui.util.magnetictarget.MagnetizedObject;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -227,8 +230,6 @@ public class BubbleStackView extends FrameLayout {
pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress);
pw.print(" showingDismiss: "); pw.println(mShowingDismiss);
pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating);
- pw.print(" draggingInDismiss: "); pw.println(mDraggingInDismissTarget);
- pw.print(" animatingMagnet: "); pw.println(mAnimatingMagnet);
mStackAnimationController.dump(fd, pw, args);
mExpandedAnimationController.dump(fd, pw, args);
}
@@ -240,16 +241,6 @@ public class BubbleStackView extends FrameLayout {
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;
@@ -331,8 +322,100 @@ public class BubbleStackView extends FrameLayout {
@NonNull
private final SurfaceSynchronizer mSurfaceSynchronizer;
- private BubbleDismissView mDismissContainer;
- private Runnable mAfterMagnet;
+ /**
+ * The currently magnetized object, which is being dragged and will be attracted to the magnetic
+ * dismiss target.
+ *
+ * This is either the stack itself, or an individual bubble.
+ */
+ private MagnetizedObject<?> mMagnetizedObject;
+
+ /**
+ * The action to run when the magnetized object is released in the dismiss target.
+ *
+ * This will actually perform the dismissal of either the stack or an individual bubble.
+ */
+ private Runnable mReleasedInDismissTargetAction;
+
+ /**
+ * The MagneticTarget instance for our circular dismiss view. This is added to the
+ * MagnetizedObject instances for the stack and any dragged-out bubbles.
+ */
+ private MagnetizedObject.MagneticTarget mMagneticTarget;
+
+ /** Magnet listener that handles animating and dismissing individual dragged-out bubbles. */
+ private final MagnetizedObject.MagnetListener mIndividualBubbleMagnetListener =
+ new MagnetizedObject.MagnetListener() {
+ @Override
+ public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+ animateDesaturateAndDarken(
+ mExpandedAnimationController.getDraggedOutBubble(), true);
+ }
+
+ @Override
+ public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
+ float velX, float velY, boolean wasFlungOut) {
+ animateDesaturateAndDarken(
+ mExpandedAnimationController.getDraggedOutBubble(), false);
+
+ if (wasFlungOut) {
+ mExpandedAnimationController.snapBubbleBack(
+ mExpandedAnimationController.getDraggedOutBubble(), velX, velY);
+ hideDismissTarget();
+ } else {
+ mExpandedAnimationController.onUnstuckFromTarget();
+ }
+ }
+
+ @Override
+ public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+ mExpandedAnimationController.dismissDraggedOutBubble(
+ mExpandedAnimationController.getDraggedOutBubble(),
+ mReleasedInDismissTargetAction);
+ hideDismissTarget();
+ }
+ };
+
+ /** Magnet listener that handles animating and dismissing the entire stack. */
+ private final MagnetizedObject.MagnetListener mStackMagnetListener =
+ new MagnetizedObject.MagnetListener() {
+ @Override
+ public void onStuckToTarget(
+ @NonNull MagnetizedObject.MagneticTarget target) {
+ animateDesaturateAndDarken(mBubbleContainer, true);
+ }
+
+ @Override
+ public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
+ float velX, float velY, boolean wasFlungOut) {
+ animateDesaturateAndDarken(mBubbleContainer, false);
+
+ if (wasFlungOut) {
+ mStackAnimationController.flingStackThenSpringToEdge(
+ mStackAnimationController.getStackPosition().x, velX, velY);
+ hideDismissTarget();
+ } else {
+ mStackAnimationController.onUnstuckFromTarget();
+ }
+ }
+
+ @Override
+ public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+ mStackAnimationController.implodeStack(
+ () -> {
+ resetDesaturationAndDarken();
+ mReleasedInDismissTargetAction.run();
+ }
+ );
+
+ hideDismissTarget();
+ }
+ };
+
+ private ViewGroup mDismissTargetContainer;
+ private PhysicsAnimator<View> mDismissTargetAnimator;
+ private PhysicsAnimator.SpringConfig mDismissTargetSpring = new PhysicsAnimator.SpringConfig(
+ SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
@@ -409,12 +492,31 @@ public class BubbleStackView extends FrameLayout {
.setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring);
- mDismissContainer = new BubbleDismissView(mContext);
- mDismissContainer.setLayoutParams(new FrameLayout.LayoutParams(
+ final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size);
+ final View targetView = new DismissCircleView(context);
+ final FrameLayout.LayoutParams newParams =
+ new FrameLayout.LayoutParams(targetSize, targetSize);
+ newParams.gravity = Gravity.CENTER;
+ targetView.setLayoutParams(newParams);
+ mDismissTargetAnimator = PhysicsAnimator.getInstance(targetView);
+
+ mDismissTargetContainer = new FrameLayout(context);
+ mDismissTargetContainer.setLayoutParams(new FrameLayout.LayoutParams(
MATCH_PARENT,
getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height),
Gravity.BOTTOM));
- addView(mDismissContainer);
+ mDismissTargetContainer.setClipChildren(false);
+ mDismissTargetContainer.addView(targetView);
+ mDismissTargetContainer.setVisibility(View.INVISIBLE);
+ addView(mDismissTargetContainer);
+
+ // Start translated down so the target springs up.
+ targetView.setTranslationY(
+ getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height));
+
+ // Save the MagneticTarget instance for the newly set up view - we'll add this to the
+ // MagnetizedObjects.
+ mMagneticTarget = new MagnetizedObject.MagneticTarget(targetView, mBubbleSize * 2);
mExpandedViewXAnim =
new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X);
@@ -1066,6 +1168,14 @@ public class BubbleStackView extends FrameLayout {
}
}
+ /*
+ * Sets the action to run to dismiss the currently dragging object (either the stack or an
+ * individual bubble).
+ */
+ public void setReleasedInDismissTargetAction(Runnable action) {
+ mReleasedInDismissTargetAction = action;
+ }
+
/**
* Dismiss the stack of bubbles.
*
@@ -1262,7 +1372,12 @@ public class BubbleStackView extends FrameLayout {
Log.d(TAG, "onBubbleDragStart: bubble=" + bubble);
}
maybeShowManageEducation(false);
- mExpandedAnimationController.prepareForBubbleDrag(bubble);
+ mExpandedAnimationController.prepareForBubbleDrag(bubble, mMagneticTarget);
+
+ // We're dragging an individual bubble, so set the magnetized object to the magnetized
+ // bubble.
+ mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut();
+ mMagnetizedObject.setMagnetListener(mIndividualBubbleMagnetListener);
}
/** Called with the coordinates to which an individual bubble has been dragged. */
@@ -1304,7 +1419,9 @@ public class BubbleStackView extends FrameLayout {
mBubbleContainer.setActiveController(mStackAnimationController);
hideFlyoutImmediate();
- mDraggingInDismissTarget = false;
+ // Since we're dragging the stack, set the magnetized object to the magnetized stack.
+ mMagnetizedObject = mStackAnimationController.getMagnetizedStack(mMagneticTarget);
+ mMagnetizedObject.setMagnetListener(mStackMagnetListener);
}
void onDragged(float x, float y) {
@@ -1425,6 +1542,11 @@ public class BubbleStackView extends FrameLayout {
}
}
+ /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */
+ boolean passEventToMagnetizedObject(MotionEvent event) {
+ return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
+ }
+
/** Prepares and starts the desaturate/darken animation on the bubble stack. */
private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
mDesaturateAndDarkenTargetView = targetView;
@@ -1455,102 +1577,6 @@ public class BubbleStackView extends FrameLayout {
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);
- }
- } 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);
- }
- }
-
- 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 View draggedOutBubble = mExpandedAnimationController.getDraggedOutBubble();
- final Runnable animateDismissal = () -> {
- mAfterMagnet = null;
-
- mVibrator.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
- mDismissContainer.springOut();
-
- // '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(draggedOutBubble, () -> {
- 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. */
private void springInDismissTarget() {
if (mShowingDismiss) {
@@ -1559,10 +1585,14 @@ public class BubbleStackView extends FrameLayout {
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);
+ mDismissTargetContainer.bringToFront();
+ mDismissTargetContainer.setZ(Short.MAX_VALUE - 1);
+ mDismissTargetContainer.setVisibility(VISIBLE);
+
+ mDismissTargetAnimator.cancel();
+ mDismissTargetAnimator
+ .spring(DynamicAnimation.TRANSLATION_Y, 0f, mDismissTargetSpring)
+ .start();
}
/**
@@ -1574,13 +1604,13 @@ public class BubbleStackView extends FrameLayout {
return;
}
- mDismissContainer.springOut();
mShowingDismiss = false;
- }
- /** Whether the location of the given MotionEvent is within the dismiss target area. */
- boolean isInDismissTarget(MotionEvent ev) {
- return isIntersecting(mDismissContainer.getDismissTarget(), ev.getRawX(), ev.getRawY());
+ mDismissTargetAnimator
+ .spring(DynamicAnimation.TRANSLATION_Y, mDismissTargetContainer.getHeight(),
+ mDismissTargetSpring)
+ .withEndActions(() -> mDismissTargetContainer.setVisibility(View.INVISIBLE))
+ .start();
}
/** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
index 46d1e0dfbab7..0c5bef4d2bde 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
@@ -30,28 +30,6 @@ import com.android.systemui.Dependency;
* dismissing, and flings.
*/
class BubbleTouchHandler implements View.OnTouchListener {
- /** 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;
-
- /**
- * 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();
@@ -66,8 +44,6 @@ class BubbleTouchHandler implements View.OnTouchListener {
/** View that was initially touched, when we received the first ACTION_DOWN event. */
private View mTouchedView;
- /** Whether the current touched view is in the dismiss target. */
- private boolean mInDismissTarget;
BubbleTouchHandler(BubbleStackView stackView,
BubbleData bubbleData, Context context) {
@@ -124,13 +100,33 @@ class BubbleTouchHandler implements View.OnTouchListener {
if (isStack) {
mViewPositionOnTouchDown.set(mStack.getStackPosition());
+
+ // Dismiss the entire stack if it's released in the dismiss target.
+ mStack.setReleasedInDismissTargetAction(
+ () -> mController.dismissStack(BubbleController.DISMISS_USER_GESTURE));
mStack.onDragStart();
+ mStack.passEventToMagnetizedObject(event);
} else if (isFlyout) {
mStack.onFlyoutDragStart();
} else {
mViewPositionOnTouchDown.set(
mTouchedView.getTranslationX(), mTouchedView.getTranslationY());
+
+ // Dismiss only the dragged-out bubble if it's released in the target.
+ final String individualBubbleKey = ((BadgedImageView) mTouchedView).getKey();
+ mStack.setReleasedInDismissTargetAction(() -> {
+ final Bubble bubble =
+ mBubbleData.getBubbleWithKey(individualBubbleKey);
+ // bubble can be null if the user is in the middle of
+ // dismissing the bubble, but the app also sent a cancel
+ if (bubble != null) {
+ mController.removeBubble(bubble.getEntry(),
+ BubbleController.DISMISS_USER_GESTURE);
+ }
+ });
+
mStack.onBubbleDragStart(mTouchedView);
+ mStack.passEventToMagnetizedObject(event);
}
break;
@@ -144,27 +140,16 @@ class BubbleTouchHandler implements View.OnTouchListener {
}
if (mMovedEnough) {
- if (isStack) {
- mStack.onDragged(viewX, viewY);
- } else if (isFlyout) {
+ if (isFlyout) {
mStack.onFlyoutDragged(deltaX);
- } else {
- mStack.onBubbleDragged(mTouchedView, viewX, viewY);
- }
- }
-
- 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.
- if (!isFlyout) {
- mStack.animateMagnetToDismissTarget(
- mTouchedView, mInDismissTarget, viewX, viewY, velX, velY);
+ } else if (!mStack.passEventToMagnetizedObject(event)) {
+ // If the magnetic target doesn't consume the event, drag the stack or
+ // bubble.
+ if (isStack) {
+ mStack.onDragged(viewX, viewY);
+ } else {
+ mStack.onBubbleDragged(mTouchedView, viewX, viewY);
+ }
}
}
break;
@@ -179,42 +164,21 @@ class BubbleTouchHandler implements View.OnTouchListener {
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 (isFlyout && mMovedEnough) {
mStack.onFlyoutDragFinished(rawX - mTouchDown.x /* deltaX */, velX);
- } else if (shouldDismiss) {
- final String individualBubbleKey =
- isStack ? null : ((BadgedImageView) mTouchedView).getKey();
- mStack.magnetToStackIfNeededThenAnimateDismissal(mTouchedView, velX, velY,
- () -> {
- if (isStack) {
- mController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
- } else {
- final Bubble bubble =
- mBubbleData.getBubbleWithKey(individualBubbleKey);
- // bubble can be null if the user is in the middle of
- // dismissing the bubble, but the app also sent a cancel
- if (bubble != null) {
- mController.removeBubble(bubble.getEntry(),
- BubbleController.DISMISS_USER_GESTURE);
- }
- }
- });
} else if (isFlyout) {
if (!mBubbleData.isExpanded() && !mMovedEnough) {
mStack.onFlyoutTapped();
}
} else if (mMovedEnough) {
- if (isStack) {
- mStack.onDragFinish(viewX, viewY, velX, velY);
- } else {
- mStack.onBubbleDragFinish(mTouchedView, viewX, viewY, velX, velY);
+ if (!mStack.passEventToMagnetizedObject(event)) {
+ // If the magnetic target didn't consume the event, tell the stack to finish
+ // the drag.
+ if (isStack) {
+ mStack.onDragFinish(viewX, viewY, velX, velY);
+ } else {
+ mStack.onBubbleDragFinish(mTouchedView, viewX, viewY, velX, velY);
+ }
}
} else if (mTouchedView == mStack.getExpandedBubbleView()) {
mBubbleData.setExpanded(false);
@@ -235,45 +199,15 @@ 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() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
+
mTouchedView = null;
mMovedEnough = false;
- mInDismissTarget = false;
mStack.onGestureFinished();
}
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 607b5ef9b2c2..3eaa90c985d7 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
@@ -25,13 +25,14 @@ import android.view.DisplayCutout;
import android.view.View;
import android.view.WindowInsets;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
-import com.android.systemui.bubbles.BubbleExperimentConfig;
+import com.android.systemui.util.magnetictarget.MagnetizedObject;
import com.google.android.collect.Sets;
@@ -62,6 +63,12 @@ public class ExpandedAnimationController
/** What percentage of the screen to use when centering the bubbles in landscape. */
private static final float CENTER_BUBBLES_LANDSCAPE_PERCENT = 0.66f;
+ /**
+ * Velocity required to dismiss an individual bubble without dragging it into the dismiss
+ * target.
+ */
+ private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f;
+
/** Horizontal offset between bubbles, which we need to know to re-stack them. */
private float mStackOffsetPx;
/** Space between status bar and bubbles in the expanded state. */
@@ -79,9 +86,6 @@ public class ExpandedAnimationController
/** What the current screen orientation is. */
private int mScreenOrientation;
- /** Whether the dragged-out bubble is in the dismiss target. */
- private boolean mIndividualBubbleWithinDismissTarget = false;
-
private boolean mAnimatingExpand = false;
private boolean mAnimatingCollapse = false;
private @Nullable Runnable mAfterExpand;
@@ -99,6 +103,17 @@ public class ExpandedAnimationController
*/
private boolean mSpringingBubbleToTouch = false;
+ /**
+ * Whether to spring the bubble to the next touch event coordinates. This is used to animate the
+ * bubble out of the magnetic dismiss target to the touch location.
+ *
+ * Once it 'catches up' and the animation ends, we'll revert to moving it directly.
+ */
+ private boolean mSpringToTouchOnNextMotionEvent = false;
+
+ /** The bubble currently being dragged out of the row (to potentially be dismissed). */
+ private MagnetizedObject<View> mMagnetizedBubbleDraggingOut;
+
private int mExpandedViewPadding;
public ExpandedAnimationController(Point displaySize, int expandedViewPadding,
@@ -113,9 +128,6 @@ public class ExpandedAnimationController
*/
private boolean mBubbleDraggedOutEnough = false;
- /** The bubble currently being dragged out of the row (to potentially be dismissed). */
- private View mBubbleDraggingOut;
-
/**
* Animates expanding the bubbles into a row along the top of the screen.
*/
@@ -235,12 +247,46 @@ public class ExpandedAnimationController
}).startAll(after);
}
+ /** Notifies the controller that the dragged-out bubble was unstuck from the magnetic target. */
+ public void onUnstuckFromTarget() {
+ mSpringToTouchOnNextMotionEvent = true;
+ }
+
/** Prepares the given bubble to be dragged out. */
- public void prepareForBubbleDrag(View bubble) {
+ public void prepareForBubbleDrag(View bubble, MagnetizedObject.MagneticTarget target) {
mLayout.cancelAnimationsOnView(bubble);
- mBubbleDraggingOut = bubble;
- mBubbleDraggingOut.setTranslationZ(Short.MAX_VALUE);
+ bubble.setTranslationZ(Short.MAX_VALUE);
+ mMagnetizedBubbleDraggingOut = new MagnetizedObject<View>(
+ mLayout.getContext(), bubble,
+ DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) {
+ @Override
+ public float getWidth(@NonNull View underlyingObject) {
+ return mBubbleSizePx;
+ }
+
+ @Override
+ public float getHeight(@NonNull View underlyingObject) {
+ return mBubbleSizePx;
+ }
+
+ @Override
+ public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) {
+ loc[0] = (int) bubble.getTranslationX();
+ loc[1] = (int) bubble.getTranslationY();
+ }
+ };
+ mMagnetizedBubbleDraggingOut.addTarget(target);
+ mMagnetizedBubbleDraggingOut.setHapticsEnabled(true);
+ mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
+ }
+
+ private void springBubbleTo(View bubble, float x, float y) {
+ animationForChild(bubble)
+ .translationX(x)
+ .translationY(y)
+ .withStiffness(SpringForce.STIFFNESS_HIGH)
+ .start();
}
/**
@@ -249,20 +295,20 @@ public class ExpandedAnimationController
* bubble is dragged back into the row.
*/
public void dragBubbleOut(View bubbleView, float x, float y) {
- if (mSpringingBubbleToTouch) {
+ if (mSpringToTouchOnNextMotionEvent) {
+ springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y);
+ mSpringToTouchOnNextMotionEvent = false;
+ mSpringingBubbleToTouch = true;
+ } else if (mSpringingBubbleToTouch) {
if (mLayout.arePropertiesAnimatingOnView(
bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) {
- animationForChild(mBubbleDraggingOut)
- .translationX(x)
- .translationY(y)
- .withStiffness(SpringForce.STIFFNESS_HIGH)
- .start();
+ springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y);
} else {
mSpringingBubbleToTouch = false;
}
}
- if (!mSpringingBubbleToTouch && !mIndividualBubbleWithinDismissTarget) {
+ if (!mSpringingBubbleToTouch && !mMagnetizedBubbleDraggingOut.getObjectStuckToTarget()) {
bubbleView.setTranslationX(x);
bubbleView.setTranslationY(y);
}
@@ -277,8 +323,6 @@ public class ExpandedAnimationController
/** Plays a dismiss animation on the dragged out bubble. */
public void dismissDraggedOutBubble(View bubble, Runnable after) {
- mIndividualBubbleWithinDismissTarget = false;
-
animationForChild(bubble)
.withStiffness(SpringForce.STIFFNESS_HIGH)
.scaleX(1.1f)
@@ -290,37 +334,14 @@ public class ExpandedAnimationController
}
@Nullable public View getDraggedOutBubble() {
- return mBubbleDraggingOut;
- }
-
- /** 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();
+ return mMagnetizedBubbleDraggingOut == null
+ ? null
+ : mMagnetizedBubbleDraggingOut.getUnderlyingObject();
}
- /**
- * 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();
+ /** Returns the MagnetizedObject instance for the dragging-out bubble. */
+ public MagnetizedObject<View> getMagnetizedBubbleDraggingOut() {
+ return mMagnetizedBubbleDraggingOut;
}
/**
@@ -335,13 +356,14 @@ public class ExpandedAnimationController
.withPositionStartVelocities(velX, velY)
.start(() -> bubbleView.setTranslationZ(0f) /* after */);
+ mMagnetizedBubbleDraggingOut = null;
+
updateBubblePositions();
}
/** Resets bubble drag out gesture flags. */
public void onGestureFinished() {
mBubbleDraggedOutEnough = false;
- mBubbleDraggingOut = null;
updateBubblePositions();
}
@@ -373,7 +395,6 @@ public class ExpandedAnimationController
pw.print(" isActive: "); pw.println(isActiveController());
pw.print(" animatingExpand: "); pw.println(mAnimatingExpand);
pw.print(" animatingCollapse: "); pw.println(mAnimatingCollapse);
- pw.print(" bubbleInDismiss: "); pw.println(mIndividualBubbleWithinDismissTarget);
pw.print(" springingBubble: "); pw.println(mSpringingBubbleToTouch);
}
@@ -453,8 +474,8 @@ public class ExpandedAnimationController
final PhysicsAnimationLayout.PhysicsPropertyAnimator animator = animationForChild(child);
// If we're removing the dragged-out bubble, that means it got dismissed.
- if (child.equals(mBubbleDraggingOut)) {
- mBubbleDraggingOut = null;
+ if (child.equals(getDraggedOutBubble())) {
+ mMagnetizedBubbleDraggingOut = null;
finishRemoval.run();
} else {
animator.alpha(0f, finishRemoval /* endAction */)
@@ -490,7 +511,7 @@ public class ExpandedAnimationController
// Don't animate the dragging out bubble, or it'll jump around while being dragged. It
// will be snapped to the correct X value after the drag (if it's not dismissed).
- if (bubble.equals(mBubbleDraggingOut)) {
+ if (bubble.equals(getDraggedOutBubble())) {
return;
}
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 f22c8faad95c..b81665cd186a 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
@@ -16,7 +16,6 @@
package com.android.systemui.bubbles.animation;
-import android.annotation.NonNull;
import android.content.res.Resources;
import android.graphics.PointF;
import android.graphics.Rect;
@@ -25,6 +24,7 @@ import android.util.Log;
import android.view.View;
import android.view.WindowInsets;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.FlingAnimation;
@@ -35,6 +35,7 @@ import androidx.dynamicanimation.animation.SpringForce;
import com.android.systemui.R;
import com.android.systemui.util.FloatingContentCoordinator;
import com.android.systemui.util.animation.PhysicsAnimator;
+import com.android.systemui.util.magnetictarget.MagnetizedObject;
import com.google.android.collect.Sets;
@@ -92,6 +93,9 @@ public class StackAnimationController extends
*/
private static final float ESCAPE_VELOCITY = 750f;
+ /** Velocity required to dismiss the stack without dragging it into the dismiss target. */
+ private static final float FLING_TO_DISMISS_MIN_VELOCITY = 4000f;
+
/**
* The canonical position of the stack. This is typically the position of the first bubble, but
* we need to keep track of it separately from the first bubble's translation in case there are
@@ -100,6 +104,12 @@ public class StackAnimationController extends
private PointF mStackPosition = new PointF(-1, -1);
/**
+ * MagnetizedObject instance for the stack, which is used by the touch handler for the magnetic
+ * dismiss target.
+ */
+ private MagnetizedObject<StackAnimationController> mMagnetizedStack;
+
+ /**
* The area that Bubbles will occupy after all animations end. This is used to move other
* floating content out of the way proactively.
*/
@@ -136,11 +146,6 @@ 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.
*
@@ -154,6 +159,14 @@ public class StackAnimationController extends
*/
private boolean mFirstBubbleSpringingToTouch = false;
+ /**
+ * Whether to spring the stack to the next touch event coordinates. This is used to animate the
+ * stack (including the first bubble) out of the magnetic dismiss target to the touch location.
+ * Once it 'catches up' and the animation ends, we'll revert to moving the first bubble directly
+ * and only animating the following bubbles.
+ */
+ private boolean mSpringToTouchOnNextMotionEvent = false;
+
/** Horizontal offset of bubbles in the stack. */
private float mStackOffset;
/** Diameter of the bubble icon. */
@@ -273,7 +286,8 @@ public class StackAnimationController extends
* Note that we need new SpringForce instances per animation despite identical configs because
* SpringAnimation uses SpringForce's internal (changing) velocity while the animation runs.
*/
- public void springStack(float destinationX, float destinationY, float stiffness) {
+ public void springStack(
+ float destinationX, float destinationY, float stiffness) {
notifyFloatingCoordinatorStackAnimatingTo(destinationX, destinationY);
springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X,
@@ -404,7 +418,7 @@ public class StackAnimationController extends
pw.println(mRestingStackPosition != null ? mRestingStackPosition.toString() : "null");
pw.print(" currentStackPos: "); pw.println(mStackPosition.toString());
pw.print(" isMovingFromFlinging: "); pw.println(mIsMovingFromFlinging);
- pw.print(" withinDismiss: "); pw.println(mWithinDismissTarget);
+ pw.print(" withinDismiss: "); pw.println(isStackStuckToTarget());
pw.print(" firstBubbleSpringing: "); pw.println(mFirstBubbleSpringingToTouch);
}
@@ -580,14 +594,18 @@ public class StackAnimationController extends
/** 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) {
+ // Begin the spring-to-touch catch up animation if needed.
+ if (mSpringToTouchOnNextMotionEvent) {
+ springStack(x, y, DEFAULT_STIFFNESS);
+ mSpringToTouchOnNextMotionEvent = false;
+ mFirstBubbleSpringingToTouch = true;
+ } else if (mFirstBubbleSpringingToTouch) {
final SpringAnimation springToTouchX =
- (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_X);
+ (SpringAnimation) mStackPositionAnimations.get(
+ DynamicAnimation.TRANSLATION_X);
final SpringAnimation springToTouchY =
- (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_Y);
+ (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()) {
@@ -600,56 +618,14 @@ public class StackAnimationController extends
}
}
- if (!mFirstBubbleSpringingToTouch && !mWithinDismissTarget) {
+ if (!mFirstBubbleSpringingToTouch && !isStackStuckToTarget()) {
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;
-
- springFirstBubbleWithStackFollowing(
- DynamicAnimation.TRANSLATION_X,
- new SpringForce()
- .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
- .setStiffness(SpringForce.STIFFNESS_MEDIUM),
- velX, mLayout.getWidth() / 2f - mBubbleBitmapSize / 2f);
-
- springFirstBubbleWithStackFollowing(
- DynamicAnimation.TRANSLATION_Y,
- new SpringForce()
- .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
- .setStiffness(SpringForce.STIFFNESS_MEDIUM),
- velY, destY, after);
+ /** Notify the controller that the stack has been unstuck from the dismiss target. */
+ public void onUnstuckFromTarget() {
+ mSpringToTouchOnNextMotionEvent = true;
}
/**
@@ -663,13 +639,7 @@ public class StackAnimationController extends
.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;
- });
+ .start(after);
}
/**
@@ -720,7 +690,7 @@ public class StackAnimationController extends
if (property.equals(DynamicAnimation.TRANSLATION_X)
|| property.equals(DynamicAnimation.TRANSLATION_Y)) {
return index + 1;
- } else if (mWithinDismissTarget) {
+ } else if (isStackStuckToTarget()) {
return index + 1; // Chain all animations in dismiss (scale, alpha, etc. are used).
} else {
return NONE;
@@ -733,7 +703,7 @@ public class StackAnimationController extends
if (property.equals(DynamicAnimation.TRANSLATION_X)) {
// If we're in the dismiss target, have the bubbles pile on top of each other with no
// offset.
- if (mWithinDismissTarget) {
+ if (isStackStuckToTarget()) {
return 0f;
} else {
// Offset to the left if we're on the left, or the right otherwise.
@@ -755,7 +725,7 @@ public class StackAnimationController extends
@Override
void onChildAdded(View child, int index) {
// Don't animate additions within the dismiss target.
- if (mWithinDismissTarget) {
+ if (isStackStuckToTarget()) {
return;
}
@@ -784,8 +754,6 @@ public class StackAnimationController extends
if (mLayout.getChildCount() > 0) {
animationForChildAtIndex(0).translationX(mStackPosition.x).start();
} else {
- // If there's no other bubbles, and we were in the dismiss target, reset the flag.
- mWithinDismissTarget = false;
// When all children are removed ensure stack position is sane
setStackPosition(mRestingStackPosition == null
? getDefaultStartPosition()
@@ -831,6 +799,9 @@ public class StackAnimationController extends
}
}
+ private boolean isStackStuckToTarget() {
+ return mMagnetizedStack != null && mMagnetizedStack.getObjectStuckToTarget();
+ }
/** Moves the stack, without any animation, to the starting position. */
private void moveStackToStartPosition() {
@@ -959,6 +930,44 @@ public class StackAnimationController extends
}
/**
+ * Returns the {@link MagnetizedObject} instance for the bubble stack, with the provided
+ * {@link MagnetizedObject.MagneticTarget} added as a target.
+ */
+ public MagnetizedObject<StackAnimationController> getMagnetizedStack(
+ MagnetizedObject.MagneticTarget target) {
+ if (mMagnetizedStack == null) {
+ mMagnetizedStack = new MagnetizedObject<StackAnimationController>(
+ mLayout.getContext(),
+ this,
+ new StackPositionProperty(DynamicAnimation.TRANSLATION_X),
+ new StackPositionProperty(DynamicAnimation.TRANSLATION_Y)
+ ) {
+ @Override
+ public float getWidth(@NonNull StackAnimationController underlyingObject) {
+ return mBubbleSize;
+ }
+
+ @Override
+ public float getHeight(@NonNull StackAnimationController underlyingObject) {
+ return mBubbleSize;
+ }
+
+ @Override
+ public void getLocationOnScreen(@NonNull StackAnimationController underlyingObject,
+ @NonNull int[] loc) {
+ loc[0] = (int) mStackPosition.x;
+ loc[1] = (int) mStackPosition.y;
+ }
+ };
+ mMagnetizedStack.addTarget(target);
+ mMagnetizedStack.setHapticsEnabled(true);
+ mMagnetizedStack.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
+ }
+
+ return mMagnetizedStack;
+ }
+
+ /**
* FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's
* translation and animate the rest of the stack with it. A DynamicAnimation can animate this
* property directly to move the first bubble and cause the stack to 'follow' to the new
diff --git a/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java b/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java
new file mode 100644
index 000000000000..6c3538cb6142
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2020 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.util;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.view.Gravity;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.systemui.R;
+
+/**
+ * Circular view with a semitransparent, circular background with an 'X' inside it.
+ *
+ * This is used by both Bubbles and PIP as the dismiss target.
+ */
+public class DismissCircleView extends FrameLayout {
+
+ private final ImageView mIconView = new ImageView(getContext());
+
+ public DismissCircleView(Context context) {
+ super(context);
+ final Resources res = getResources();
+
+ setBackground(res.getDrawable(R.drawable.dismiss_circle_background));
+
+ mIconView.setImageDrawable(res.getDrawable(R.drawable.dismiss_target_x));
+ addView(mIconView);
+
+ setViewSizes();
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ setViewSizes();
+ }
+
+ /** Retrieves the current dimensions for the icon and circle and applies them. */
+ private void setViewSizes() {
+ final Resources res = getResources();
+ final int iconSize = res.getDimensionPixelSize(R.dimen.dismiss_target_x_size);
+ mIconView.setLayoutParams(
+ new FrameLayout.LayoutParams(iconSize, iconSize, Gravity.CENTER));
+ }
+}
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 ae4581a80503..ec6d3e9d0dff 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,7 +17,6 @@
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.Configuration;
@@ -118,118 +117,6 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC
testBubblesInCorrectExpandedPositions();
}
- @Test
- @Ignore
- public void testBubbleDraggedNotDismissedSnapsBack() throws InterruptedException {
- expand();
-
- final View draggedBubble = mViews.get(0);
- mExpandedController.prepareForBubbleDrag(draggedBubble);
- mExpandedController.dragBubbleOut(draggedBubble, 500f, 500f);
-
- assertEquals(500f, draggedBubble.getTranslationX(), 1f);
- assertEquals(500f, draggedBubble.getTranslationY(), 1f);
-
- // Snap it back and make sure it made it back correctly.
- mExpandedController.snapBubbleBack(draggedBubble, 0f, 0f);
- waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
- testBubblesInCorrectExpandedPositions();
- }
-
- @Test
- @Ignore
- public void testBubbleDismissed() throws InterruptedException {
- expand();
-
- final View draggedBubble = mViews.get(0);
- mExpandedController.prepareForBubbleDrag(draggedBubble);
- mExpandedController.dragBubbleOut(draggedBubble, 500f, 500f);
-
- assertEquals(500f, draggedBubble.getTranslationX(), 1f);
- assertEquals(500f, draggedBubble.getTranslationY(), 1f);
-
- mLayout.removeView(draggedBubble);
- waitForLayoutMessageQueue();
- waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-
- assertEquals(-1, mLayout.indexOfChild(draggedBubble));
- testBubblesInCorrectExpandedPositions();
- }
-
- @Test
- @Ignore("Flaky")
- 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(draggedOutView, afterDismiss);
- waitForPropertyAnimations(DynamicAnimation.ALPHA);
- verify(after).run();
-
- waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-
- assertEquals(mBubblePaddingTop, mViews.get(1).getTranslationX(), 1f);
- }
-
- @Test
- @Ignore("Flaky")
- 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(Mockito.mock(Runnable.class));
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 9cc034996687..e3187cb9a6c5 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,7 +17,6 @@
package com.android.systemui.bubbles.animation;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@@ -41,7 +40,6 @@ import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.mockito.Mockito;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -242,61 +240,6 @@ public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase
}
@Test
- @Ignore("Flaky")
- 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
- @Ignore("Flaking")
- 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);
- }
-
- @Test
public void testFloatingCoordinator() {
// We should have called onContentAdded only once while adding all of the bubbles in
// setup().