Merge changes Ib5b3b032,I81147568,If9218d23 into rvc-dev

* changes:
  Fix LayoutParams issue with DismissCircleView.
  Modifies Bubbles to use the MagnetizedObject library.
  Adds DismissCircleView, a shared dismiss target for Bubbles/PIP.
This commit is contained in:
Josh Tsuji
2020-03-07 19:29:53 +00:00
committed by Android (Google) Code Review
10 changed files with 470 additions and 525 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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. -->

View File

@@ -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. */

View File

@@ -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();
}

View File

@@ -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;
return mMagnetizedBubbleDraggingOut == null
? null
: mMagnetizedBubbleDraggingOut.getUnderlyingObject();
}
/** 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();
/** 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;
}

View File

@@ -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
@@ -99,6 +103,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.
@@ -135,11 +145,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() {
@@ -958,6 +929,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

View File

@@ -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));
}
}

View File

@@ -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));

View File

@@ -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;
@@ -241,61 +239,6 @@ public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase
assertEquals(prevStackPos, mStackController.getStackPosition());
}
@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