Merge "Animation for bubbles home gesture" into tm-qpr-dev

This commit is contained in:
Ats Jenk
2022-05-13 16:32:46 +00:00
committed by Android (Google) Code Review
18 changed files with 1869 additions and 51 deletions

View File

@@ -52,6 +52,19 @@ public class Interpolators {
*/
public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
/**
* The accelerated emphasized interpolator. Used for hero / emphasized movement of content that
* is disappearing e.g. when moving off screen.
*/
public static final Interpolator EMPHASIZED_ACCELERATE = new PathInterpolator(
0.3f, 0f, 0.8f, 0.15f);
/**
* The decelerating emphasized interpolator. Used for hero / emphasized movement of content that
* is appearing e.g. when coming from off screen
*/
public static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator(
0.05f, 0.7f, 0.1f, 1f);
/**
* Interpolator to be used when animating a move based on a click. Pair with enough duration.
*/

View File

@@ -1512,7 +1512,7 @@ public class BubbleController {
public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
mBubblePositioner.setImeVisible(imeVisible, imeHeight);
if (mStackView != null) {
mStackView.animateForIme(imeVisible);
mStackView.setImeVisible(imeVisible);
}
}
}

View File

@@ -46,6 +46,9 @@ public class BubbleDebugConfig {
static final boolean DEBUG_OVERFLOW = false;
static final boolean DEBUG_USER_EDUCATION = false;
static final boolean DEBUG_POSITIONER = false;
public static final boolean DEBUG_COLLAPSE_ANIMATOR = false;
static final boolean DEBUG_BUBBLE_GESTURE = false;
public static boolean DEBUG_EXPANDED_VIEW_DRAGGING = false;
private static final boolean FORCE_SHOW_USER_EDUCATION = false;
private static final String FORCE_SHOW_USER_EDUCATION_SETTING =

View File

@@ -43,10 +43,13 @@ import android.graphics.CornerPathEffect;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Picture;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.ShapeDrawable;
import android.os.RemoteException;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.IntProperty;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
@@ -75,6 +78,48 @@ import java.io.PrintWriter;
public class BubbleExpandedView extends LinearLayout {
private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES;
/** {@link IntProperty} for updating bottom clip */
public static final IntProperty<BubbleExpandedView> BOTTOM_CLIP_PROPERTY =
new IntProperty<BubbleExpandedView>("bottomClip") {
@Override
public void setValue(BubbleExpandedView expandedView, int value) {
expandedView.setBottomClip(value);
}
@Override
public Integer get(BubbleExpandedView expandedView) {
return expandedView.mBottomClip;
}
};
/** {@link FloatProperty} for updating taskView or overflow alpha */
public static final FloatProperty<BubbleExpandedView> CONTENT_ALPHA =
new FloatProperty<BubbleExpandedView>("contentAlpha") {
@Override
public void setValue(BubbleExpandedView expandedView, float value) {
expandedView.setContentAlpha(value);
}
@Override
public Float get(BubbleExpandedView expandedView) {
return expandedView.getContentAlpha();
}
};
/** {@link FloatProperty} for updating manage button alpha */
public static final FloatProperty<BubbleExpandedView> MANAGE_BUTTON_ALPHA =
new FloatProperty<BubbleExpandedView>("manageButtonAlpha") {
@Override
public void setValue(BubbleExpandedView expandedView, float value) {
expandedView.mManageButton.setAlpha(value);
}
@Override
public Float get(BubbleExpandedView expandedView) {
return expandedView.mManageButton.getAlpha();
}
};
// The triangle pointing to the expanded view
private View mPointerView;
@Nullable private int[] mExpandedViewContainerLocation;
@@ -90,7 +135,7 @@ public class BubbleExpandedView extends LinearLayout {
/**
* Whether we want the {@code TaskView}'s content to be visible (alpha = 1f). If
* {@link #mIsAlphaAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha
* {@link #mIsAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha
* value until the animation ends.
*/
private boolean mIsContentVisible = false;
@@ -99,12 +144,13 @@ public class BubbleExpandedView extends LinearLayout {
* Whether we're animating the {@code TaskView}'s alpha value. If so, we will hold off on
* applying alpha changes from {@link #setContentVisibility} until the animation ends.
*/
private boolean mIsAlphaAnimating = false;
private boolean mIsAnimating = false;
private int mPointerWidth;
private int mPointerHeight;
private float mPointerRadius;
private float mPointerOverlap;
private final PointF mPointerPos = new PointF();
private CornerPathEffect mPointerEffect;
private ShapeDrawable mCurrentPointer;
private ShapeDrawable mTopPointer;
@@ -113,11 +159,13 @@ public class BubbleExpandedView extends LinearLayout {
private float mCornerRadius = 0f;
private int mBackgroundColorFloating;
private boolean mUsingMaxHeight;
private int mTopClip = 0;
private int mBottomClip = 0;
@Nullable private Bubble mBubble;
private PendingIntent mPendingIntent;
// TODO(b/170891664): Don't use a flag, set the BubbleOverflow object instead
private boolean mIsOverflow;
private boolean mIsClipping;
private BubbleController mController;
private BubbleStackView mStackView;
@@ -268,7 +316,8 @@ public class BubbleExpandedView extends LinearLayout {
mExpandedViewContainer.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
Rect clip = new Rect(0, mTopClip, view.getWidth(), view.getHeight() - mBottomClip);
outline.setRoundRect(clip, mCornerRadius);
}
});
mExpandedViewContainer.setClipToOutline(true);
@@ -300,9 +349,9 @@ public class BubbleExpandedView extends LinearLayout {
// they should not collapse the stack (which all other touches on areas around the AV
// would do).
if (motionEvent.getRawY() >= avBounds.top
&& motionEvent.getRawY() <= avBounds.bottom
&& (motionEvent.getRawX() < avBounds.left
|| motionEvent.getRawX() > avBounds.right)) {
&& motionEvent.getRawY() <= avBounds.bottom
&& (motionEvent.getRawX() < avBounds.left
|| motionEvent.getRawX() > avBounds.right)) {
return true;
}
@@ -384,7 +433,7 @@ public class BubbleExpandedView extends LinearLayout {
}
void applyThemeAttrs() {
final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
final TypedArray ta = mContext.obtainStyledAttributes(new int[]{
android.R.attr.dialogCornerRadius,
android.R.attr.colorBackgroundFloating});
boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
@@ -429,7 +478,7 @@ public class BubbleExpandedView extends LinearLayout {
* ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble
* being dragged out, the manage menu) this is set to false, otherwise it should be true.
*/
void setSurfaceZOrderedOnTop(boolean onTop) {
public void setSurfaceZOrderedOnTop(boolean onTop) {
if (mTaskView == null) {
return;
}
@@ -510,12 +559,12 @@ public class BubbleExpandedView extends LinearLayout {
}
/**
* Whether we are currently animating the {@code TaskView}'s alpha value. If this is set to
* Whether we are currently animating the {@code TaskView}. If this is set to
* true, calls to {@link #setContentVisibility} will not be applied until this is set to false
* again.
*/
void setAlphaAnimating(boolean animating) {
mIsAlphaAnimating = animating;
public void setAnimating(boolean animating) {
mIsAnimating = animating;
// If we're done animating, apply the correct
if (!animating) {
@@ -524,15 +573,128 @@ public class BubbleExpandedView extends LinearLayout {
}
/**
* Sets the alpha of the underlying {@code TaskView}, since changing the expanded view's alpha
* does not affect the {@code TaskView} since it uses a Surface.
* Get alpha from underlying {@code TaskView} if this view is for a bubble.
* Or get alpha for the overflow view if this view is for overflow.
*
* @return alpha for the content being shown
*/
void setTaskViewAlpha(float alpha) {
public float getContentAlpha() {
if (mIsOverflow) {
return mOverflowView.getAlpha();
}
if (mTaskView != null) {
return mTaskView.getAlpha();
}
return 1f;
}
/**
* Set alpha of the underlying {@code TaskView} if this view is for a bubble.
* Or set alpha for the overflow view if this view is for overflow.
*
* Changing expanded view's alpha does not affect the {@code TaskView} since it uses a Surface.
*/
public void setContentAlpha(float alpha) {
if (mIsOverflow) {
mOverflowView.setAlpha(alpha);
} else if (mTaskView != null) {
mTaskView.setAlpha(alpha);
}
mPointerView.setAlpha(alpha);
setAlpha(alpha);
}
/**
* Set translation Y for the expanded view content.
* Excludes manage button and pointer.
*/
public void setContentTranslationY(float translationY) {
mExpandedViewContainer.setTranslationY(translationY);
// Left or right pointer can become detached when moving the view up
if (translationY <= 0 && (isShowingLeftPointer() || isShowingRightPointer())) {
// Y coordinate where the pointer would start to get detached from the expanded view.
// Takes into account bottom clipping and rounded corners
float detachPoint =
mExpandedViewContainer.getBottom() - mBottomClip - mCornerRadius + translationY;
float pointerBottom = mPointerPos.y + mPointerHeight;
// If pointer bottom is past detach point, move it in by that many pixels
float horizontalShift = 0;
if (pointerBottom > detachPoint) {
horizontalShift = pointerBottom - detachPoint;
}
if (isShowingLeftPointer()) {
// Move left pointer right
movePointerBy(horizontalShift, 0);
} else {
// Move right pointer left
movePointerBy(-horizontalShift, 0);
}
// Hide pointer if it is moved by entire width
mPointerView.setVisibility(
horizontalShift > mPointerWidth ? View.INVISIBLE : View.VISIBLE);
}
}
/**
* Update alpha value for the manage button
*/
public void setManageButtonAlpha(float alpha) {
mManageButton.setAlpha(alpha);
}
/**
* Set {@link #setTranslationY(float) translationY} for the manage button
*/
public void setManageButtonTranslationY(float translationY) {
mManageButton.setTranslationY(translationY);
}
/**
* Set top clipping for the view
*/
public void setTopClip(int clip) {
mTopClip = clip;
onContainerClipUpdate();
}
/**
* Set bottom clipping for the view
*/
public void setBottomClip(int clip) {
mBottomClip = clip;
onContainerClipUpdate();
}
private void onContainerClipUpdate() {
if (mTopClip == 0 && mBottomClip == 0) {
if (mIsClipping) {
mIsClipping = false;
if (mTaskView != null) {
mTaskView.setClipBounds(null);
mTaskView.setEnableSurfaceClipping(false);
}
mExpandedViewContainer.invalidateOutline();
}
} else {
if (!mIsClipping) {
mIsClipping = true;
if (mTaskView != null) {
mTaskView.setEnableSurfaceClipping(true);
}
}
mExpandedViewContainer.invalidateOutline();
if (mTaskView != null) {
mTaskView.setClipBounds(new Rect(0, mTopClip, mTaskView.getWidth(),
mTaskView.getHeight() - mBottomClip));
}
}
}
/**
* Move pointer from base position
*/
public void movePointerBy(float x, float y) {
mPointerView.setTranslationX(mPointerPos.x + x);
mPointerView.setTranslationY(mPointerPos.y + y);
}
/**
@@ -543,13 +705,13 @@ public class BubbleExpandedView extends LinearLayout {
* Note that this contents visibility doesn't affect visibility at {@link android.view.View},
* and setting {@code false} actually means rendering the contents in transparent.
*/
void setContentVisibility(boolean visibility) {
public void setContentVisibility(boolean visibility) {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "setContentVisibility: visibility=" + visibility
+ " bubble=" + getBubbleKey());
}
mIsContentVisible = visibility;
if (mTaskView != null && !mIsAlphaAnimating) {
if (mTaskView != null && !mIsAnimating) {
mTaskView.setAlpha(visibility ? 1f : 0f);
mPointerView.setAlpha(visibility ? 1f : 0f);
}
@@ -560,6 +722,44 @@ public class BubbleExpandedView extends LinearLayout {
return mTaskView;
}
@VisibleForTesting
public BubbleOverflowContainerView getOverflow() {
return mOverflowView;
}
/**
* Return content height: taskView or overflow.
* Takes into account clippings set by {@link #setTopClip(int)} and {@link #setBottomClip(int)}
*
* @return if bubble is for overflow, return overflow height, otherwise return taskView height
*/
public int getContentHeight() {
if (mIsOverflow) {
return mOverflowView.getHeight() - mTopClip - mBottomClip;
}
if (mTaskView != null) {
return mTaskView.getHeight() - mTopClip - mBottomClip;
}
return 0;
}
/**
* Return bottom position of the content on screen
*
* @return if bubble is for overflow, return value for overflow, otherwise taskView
*/
public int getContentBottomOnScreen() {
Rect out = new Rect();
if (mIsOverflow) {
mOverflowView.getBoundsOnScreen(out);
}
if (mTaskView != null) {
mTaskView.getBoundsOnScreen(out);
}
return out.bottom;
}
int getTaskId() {
return mTaskId;
}
@@ -728,27 +928,47 @@ public class BubbleExpandedView extends LinearLayout {
post(() -> {
mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer;
updatePointerView();
float pointerY;
float pointerX;
if (showVertically) {
pointerY = bubbleCenter - (mPointerWidth / 2f);
pointerX = onLeft
mPointerPos.y = bubbleCenter - (mPointerWidth / 2f);
mPointerPos.x = onLeft
? -mPointerHeight + mPointerOverlap
: getWidth() - mPaddingRight - mPointerOverlap;
} else {
pointerY = mPointerOverlap;
pointerX = bubbleCenter - (mPointerWidth / 2f);
mPointerPos.y = mPointerOverlap;
mPointerPos.x = bubbleCenter - (mPointerWidth / 2f);
}
if (animate) {
mPointerView.animate().translationX(pointerX).translationY(pointerY).start();
mPointerView.animate().translationX(mPointerPos.x).translationY(
mPointerPos.y).start();
} else {
mPointerView.setTranslationY(pointerY);
mPointerView.setTranslationX(pointerX);
mPointerView.setTranslationY(mPointerPos.y);
mPointerView.setTranslationX(mPointerPos.x);
mPointerView.setVisibility(VISIBLE);
}
});
}
/**
* Return true if pointer is shown on the left
*/
public boolean isShowingLeftPointer() {
return mCurrentPointer == mLeftPointer;
}
/**
* Return true if pointer is shown on the right
*/
public boolean isShowingRightPointer() {
return mCurrentPointer == mRightPointer;
}
/**
* Return width of the current pointer
*/
public int getPointerWidth() {
return mPointerWidth;
}
/**
* Position of the manage button displayed in the expanded view. Used for placing user
* education about the manage button.

View File

@@ -344,6 +344,14 @@ public class BubblePositioner {
return mImeVisible ? mImeHeight : 0;
}
/** Return top position of the IME if it's visible */
public int getImeTop() {
if (mImeVisible) {
return getScreenRect().bottom - getImeHeight() - getInsets().bottom;
}
return 0;
}
/** Sets whether the IME is visible. **/
public void setImeVisible(boolean visible, int height) {
mImeVisible = visible;
@@ -706,4 +714,21 @@ public class BubblePositioner {
public void setPinnedLocation(PointF point) {
mPinLocation = point;
}
/**
* Navigation bar has an area where system gestures can be started from.
*
* @return {@link Rect} for system navigation bar gesture zone
*/
public Rect getNavBarGestureZone() {
// Gesture zone height from the bottom
int gestureZoneHeight = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.navigation_bar_gesture_height);
Rect screen = getScreenRect();
return new Rect(
screen.left,
screen.bottom - gestureZoneHeight,
screen.right,
screen.bottom);
}
}

View File

@@ -44,6 +44,7 @@ import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.os.SystemProperties;
import android.provider.Settings;
import android.util.Log;
import android.view.Choreographer;
@@ -56,6 +57,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.WindowManagerPolicyConstants;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.widget.FrameLayout;
@@ -75,8 +77,12 @@ import com.android.internal.util.FrameworkStatsLog;
import com.android.wm.shell.R;
import com.android.wm.shell.animation.Interpolators;
import com.android.wm.shell.animation.PhysicsAnimator;
import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener;
import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix;
import com.android.wm.shell.bubbles.animation.ExpandedAnimationController;
import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationController;
import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationControllerImpl;
import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationControllerStub;
import com.android.wm.shell.bubbles.animation.PhysicsAnimationLayout;
import com.android.wm.shell.bubbles.animation.StackAnimationController;
import com.android.wm.shell.common.FloatingContentCoordinator;
@@ -97,6 +103,12 @@ import java.util.stream.Collectors;
*/
public class BubbleStackView extends FrameLayout
implements ViewTreeObserver.OnComputeInternalInsetsListener {
/**
* Set to {@code true} to enable home gesture handling in bubbles
*/
public static final boolean HOME_GESTURE_ENABLED =
SystemProperties.getBoolean("persist.wm.debug.bubbles_home_gesture", false);
private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES;
/** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */
@@ -148,7 +160,7 @@ public class BubbleStackView extends FrameLayout
* Handler to use for all delayed animations - this way, we can easily cancel them before
* starting a new animation.
*/
private final ShellExecutor mDelayedAnimationExecutor;
private final ShellExecutor mMainExecutor;
private Runnable mDelayedAnimation;
/**
@@ -197,6 +209,7 @@ public class BubbleStackView extends FrameLayout
private PhysicsAnimationLayout mBubbleContainer;
private StackAnimationController mStackAnimationController;
private ExpandedAnimationController mExpandedAnimationController;
private ExpandedViewAnimationController mExpandedViewAnimationController;
private View mScrim;
private View mManageMenuScrim;
@@ -276,6 +289,9 @@ public class BubbleStackView extends FrameLayout
*/
private int mPointerIndexDown = -1;
@Nullable
private BubblesNavBarGestureTracker mBubblesNavBarGestureTracker;
/** Description of current animation controller state. */
public void dump(PrintWriter pw, String[] args) {
pw.println("Stack view state:");
@@ -693,6 +709,67 @@ public class BubbleStackView extends FrameLayout
}
};
/** Touch listener set on the whole view that forwards event to the swipe up listener. */
private final RelativeTouchListener mContainerSwipeListener = new RelativeTouchListener() {
@Override
public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
// Pass move event on to swipe listener
mSwipeUpListener.onDown(ev.getX(), ev.getY());
return true;
}
@Override
public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
float viewInitialY, float dx, float dy) {
// Pass move event on to swipe listener
mSwipeUpListener.onMove(dx, dy);
}
@Override
public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
float viewInitialY, float dx, float dy, float velX, float velY) {
// Pass up even on to swipe listener
mSwipeUpListener.onUp(velX, velY);
}
};
/** MotionEventListener that listens from home gesture swipe event. */
private final MotionEventListener mSwipeUpListener = new MotionEventListener() {
@Override
public void onDown(float x, float y) {}
@Override
public void onMove(float dx, float dy) {
if ((mManageEduView != null && mManageEduView.getVisibility() == VISIBLE)
|| isStackEduShowing()) {
return;
}
if (mShowingManage) {
showManageMenu(false /* show */);
}
// Only allow up
float collapsed = Math.min(dy, 0);
mExpandedViewAnimationController.updateDrag((int) -collapsed);
}
@Override
public void onCancel() {
mExpandedViewAnimationController.animateBackToExpanded();
}
@Override
public void onUp(float velX, float velY) {
mExpandedViewAnimationController.setSwipeVelocity(velY);
if (mExpandedViewAnimationController.shouldCollapse()) {
// Update data first and start the animation when we are processing change
mBubbleData.setExpanded(false);
} else {
mExpandedViewAnimationController.animateBackToExpanded();
}
}
};
/** Click listener set on the flyout, which expands the stack when the flyout is tapped. */
private OnClickListener mFlyoutClickListener = new OnClickListener() {
@Override
@@ -766,7 +843,7 @@ public class BubbleStackView extends FrameLayout
ShellExecutor mainExecutor) {
super(context);
mDelayedAnimationExecutor = mainExecutor;
mMainExecutor = mainExecutor;
mBubbleController = bubbleController;
mBubbleData = data;
@@ -796,6 +873,14 @@ public class BubbleStackView extends FrameLayout
mExpandedAnimationController = new ExpandedAnimationController(mPositioner,
onBubbleAnimatedOut, this);
if (HOME_GESTURE_ENABLED) {
mExpandedViewAnimationController =
new ExpandedViewAnimationControllerImpl(context, mPositioner);
} else {
mExpandedViewAnimationController = new ExpandedViewAnimationControllerStub();
}
mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
// Force LTR by default since most of the Bubbles UI is positioned manually by the user, or
@@ -971,7 +1056,7 @@ public class BubbleStackView extends FrameLayout
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
// We need to be Z ordered on top in order for alpha animations to work.
mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true);
mExpandedBubble.getExpandedView().setAlphaAnimating(true);
mExpandedBubble.getExpandedView().setAnimating(true);
}
}
@@ -985,14 +1070,15 @@ public class BubbleStackView extends FrameLayout
// = 0f remains in effect.
&& !mExpandedViewTemporarilyHidden) {
mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false);
mExpandedBubble.getExpandedView().setAlphaAnimating(false);
mExpandedBubble.getExpandedView().setAnimating(false);
}
}
});
mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> {
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
mExpandedBubble.getExpandedView().setTaskViewAlpha(
(float) valueAnimator.getAnimatedValue());
float alpha = (float) valueAnimator.getAnimatedValue();
mExpandedBubble.getExpandedView().setContentAlpha(alpha);
mExpandedBubble.getExpandedView().setAlpha(alpha);
}
});
@@ -1795,6 +1881,7 @@ public class BubbleStackView extends FrameLayout
private void showNewlySelectedBubble(BubbleViewProvider bubbleToSelect) {
final BubbleViewProvider previouslySelected = mExpandedBubble;
mExpandedBubble = bubbleToSelect;
mExpandedViewAnimationController.setExpandedView(mExpandedBubble.getExpandedView());
if (mIsExpanded) {
hideCurrentInputMethod();
@@ -1848,7 +1935,12 @@ public class BubbleStackView extends FrameLayout
mBubbleController.getSysuiProxy().onStackExpandChanged(shouldExpand);
if (mIsExpanded) {
animateCollapse();
stopMonitoringSwipeUpGesture();
if (HOME_GESTURE_ENABLED) {
animateCollapse();
} else {
animateCollapseWithScale();
}
logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
} else {
animateExpansion();
@@ -1856,10 +1948,37 @@ public class BubbleStackView extends FrameLayout
logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
logBubbleEvent(mExpandedBubble,
FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
if (HOME_GESTURE_ENABLED) {
startMonitoringSwipeUpGesture();
}
}
notifyExpansionChanged(mExpandedBubble, mIsExpanded);
}
private void startMonitoringSwipeUpGesture() {
stopMonitoringSwipeUpGesture();
if (isGestureNavEnabled()) {
mBubblesNavBarGestureTracker = new BubblesNavBarGestureTracker(mContext, mPositioner);
mBubblesNavBarGestureTracker.start(mSwipeUpListener);
setOnTouchListener(mContainerSwipeListener);
}
}
private boolean isGestureNavEnabled() {
return mContext.getResources().getInteger(
com.android.internal.R.integer.config_navBarInteractionMode)
== WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
}
private void stopMonitoringSwipeUpGesture() {
if (mBubblesNavBarGestureTracker != null) {
mBubblesNavBarGestureTracker.stop();
mBubblesNavBarGestureTracker = null;
setOnTouchListener(null);
}
}
/**
* Called when back press occurs while bubbles are expanded.
*/
@@ -2072,11 +2191,12 @@ public class BubbleStackView extends FrameLayout
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
if (mExpandedBubble.getExpandedView() != null) {
mExpandedBubble.getExpandedView().setTaskViewAlpha(0f);
mExpandedBubble.getExpandedView().setContentAlpha(0f);
mExpandedBubble.getExpandedView().setAlpha(0f);
// We'll be starting the alpha animation after a slight delay, so set this flag early
// here.
mExpandedBubble.getExpandedView().setAlphaAnimating(true);
mExpandedBubble.getExpandedView().setAnimating(true);
}
mDelayedAnimation = () -> {
@@ -2114,10 +2234,10 @@ public class BubbleStackView extends FrameLayout
})
.start();
};
mDelayedAnimationExecutor.executeDelayed(mDelayedAnimation, startDelay);
mMainExecutor.executeDelayed(mDelayedAnimation, startDelay);
}
private void animateCollapse() {
private void animateCollapseWithScale() {
cancelDelayedExpandCollapseSwitchAnimations();
if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
@@ -2217,6 +2337,68 @@ public class BubbleStackView extends FrameLayout
.start();
}
private void animateCollapse() {
cancelDelayedExpandCollapseSwitchAnimations();
if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
mManageEduView.hide();
}
mIsExpanded = false;
mIsExpansionAnimating = true;
showScrim(false);
mBubbleContainer.cancelAllAnimations();
// If we were in the middle of swapping, the animating-out surface would have been scaling
// to zero - finish it off.
PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel();
mAnimatingOutSurfaceContainer.setScaleX(0f);
mAnimatingOutSurfaceContainer.setScaleY(0f);
// Let the expanded animation controller know that it shouldn't animate child adds/reorders
// since we're about to animate collapsed.
mExpandedAnimationController.notifyPreparingToCollapse();
final Runnable collapseBackToStack = () -> mExpandedAnimationController.collapseBackToStack(
mStackAnimationController
.getStackPositionAlongNearestHorizontalEdge()
/* collapseTo */,
() -> mBubbleContainer.setActiveController(mStackAnimationController));
final Runnable after = () -> {
final BubbleViewProvider previouslySelected = mExpandedBubble;
// TODO(b/231350255): investigate why this call is needed here
beforeExpandedViewAnimation();
if (mManageEduView != null) {
mManageEduView.hide();
}
if (DEBUG_BUBBLE_STACK_VIEW) {
Log.d(TAG, "animateCollapse");
Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(),
mExpandedBubble));
}
updateOverflowVisibility();
updateZOrder();
updateBadges(true /* setBadgeForCollapsedStack */);
afterExpandedViewAnimation();
if (previouslySelected != null) {
previouslySelected.setTaskViewVisibility(false);
}
mExpandedViewAnimationController.reset();
};
mExpandedViewAnimationController.animateCollapse(collapseBackToStack, after);
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
// When the animation completes, we should no longer be showing the content.
// This won't actually update content visibility immediately, if we are currently
// animating. But updates the internal state for the content to be hidden after
// animation completes.
mExpandedBubble.getExpandedView().setContentVisibility(false);
}
}
private void animateSwitchBubbles() {
// If we're no longer expanded, this is meaningless.
if (!mIsExpanded) {
@@ -2278,7 +2460,7 @@ public class BubbleStackView extends FrameLayout
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
mDelayedAnimationExecutor.executeDelayed(() -> {
mMainExecutor.executeDelayed(() -> {
if (!mIsExpanded) {
mIsBubbleSwitchAnimating = false;
return;
@@ -2309,7 +2491,7 @@ public class BubbleStackView extends FrameLayout
* animating flags for those animations.
*/
private void cancelDelayedExpandCollapseSwitchAnimations() {
mDelayedAnimationExecutor.removeCallbacks(mDelayedAnimation);
mMainExecutor.removeCallbacks(mDelayedAnimation);
mIsExpansionAnimating = false;
mIsBubbleSwitchAnimating = false;
@@ -2333,9 +2515,18 @@ public class BubbleStackView extends FrameLayout
/**
* Updates the stack based for IME changes. When collapsed it'll move the stack if it
* overlaps where they IME would be. When expanded it'll shift the expanded bubbles
* if they might overlap with the IME (this only happens for large screens).
* if they might overlap with the IME (this only happens for large screens)
* and clip the expanded view.
*/
public void animateForIme(boolean visible) {
public void setImeVisible(boolean visible) {
if (HOME_GESTURE_ENABLED) {
setImeVisibleInternal(visible);
} else {
setImeVisibleWithoutClipping(visible);
}
}
private void setImeVisibleWithoutClipping(boolean visible) {
if ((mIsExpansionAnimating || mIsBubbleSwitchAnimating) && mIsExpanded) {
// This will update the animation so the bubbles move to position for the IME
mExpandedAnimationController.expandFromStack(() -> {
@@ -2386,6 +2577,62 @@ public class BubbleStackView extends FrameLayout
}
}
private void setImeVisibleInternal(boolean visible) {
if ((mIsExpansionAnimating || mIsBubbleSwitchAnimating) && mIsExpanded) {
// This will update the animation so the bubbles move to position for the IME
mExpandedAnimationController.expandFromStack(() -> {
updatePointerPosition(false /* forIme */);
afterExpandedViewAnimation();
mExpandedViewAnimationController.animateForImeVisibilityChange(visible);
} /* after */);
return;
}
if (!mIsExpanded && getBubbleCount() > 0) {
final float stackDestinationY =
mStackAnimationController.animateForImeVisibility(visible);
// How far the stack is animating due to IME, we'll just animate the flyout by that
// much too.
final float stackDy =
stackDestinationY - mStackAnimationController.getStackPosition().y;
// If the flyout is visible, translate it along with the bubble stack.
if (mFlyout.getVisibility() == VISIBLE) {
PhysicsAnimator.getInstance(mFlyout)
.spring(DynamicAnimation.TRANSLATION_Y,
mFlyout.getTranslationY() + stackDy,
FLYOUT_IME_ANIMATION_SPRING_CONFIG)
.start();
}
}
if (mIsExpanded) {
mExpandedViewAnimationController.animateForImeVisibilityChange(visible);
if (mPositioner.showBubblesVertically()
&& mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
float selectedY = mPositioner.getExpandedBubbleXY(getState().selectedIndex,
getState()).y;
float newExpandedViewTop = mPositioner.getExpandedViewY(mExpandedBubble, selectedY);
mExpandedBubble.getExpandedView().setImeVisible(visible);
if (!mExpandedBubble.getExpandedView().isUsingMaxHeight()) {
mExpandedViewContainer.animate().translationY(newExpandedViewTop);
}
List<Animator> animList = new ArrayList();
for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
View child = mBubbleContainer.getChildAt(i);
float transY = mPositioner.getExpandedBubbleXY(i, getState()).y;
ObjectAnimator anim = ObjectAnimator.ofFloat(child, TRANSLATION_Y, transY);
animList.add(anim);
}
updatePointerPosition(true /* forIme */);
AnimatorSet set = new AnimatorSet();
set.playTogether(animList);
set.start();
}
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() != MotionEvent.ACTION_DOWN && ev.getActionIndex() != mPointerIndexDown) {
@@ -2821,7 +3068,7 @@ public class BubbleStackView extends FrameLayout
&& mExpandedBubble.getExpandedView() != null) {
BubbleExpandedView bev = mExpandedBubble.getExpandedView();
bev.setContentVisibility(false);
bev.setAlphaAnimating(!mIsExpansionAnimating);
bev.setAnimating(!mIsExpansionAnimating);
mExpandedViewContainerMatrix.setScaleX(0f);
mExpandedViewContainerMatrix.setScaleY(0f);
mExpandedViewContainerMatrix.setTranslate(0f, 0f);

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2022 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.wm.shell.bubbles;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import android.content.Context;
import android.hardware.input.InputManager;
import android.util.Log;
import android.view.Choreographer;
import android.view.InputChannel;
import android.view.InputEventReceiver;
import android.view.InputMonitor;
import androidx.annotation.Nullable;
import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener;
/**
* Set up tracking bubbles gestures that begin in navigation bar
*/
class BubblesNavBarGestureTracker {
private static final String TAG = TAG_WITH_CLASS_NAME ? "BubblesGestureTracker" : TAG_BUBBLES;
private static final String GESTURE_MONITOR = "bubbles-gesture";
private final Context mContext;
private final BubblePositioner mPositioner;
@Nullable
private InputMonitor mInputMonitor;
@Nullable
private InputEventReceiver mInputEventReceiver;
BubblesNavBarGestureTracker(Context context, BubblePositioner positioner) {
mContext = context;
mPositioner = positioner;
}
/**
* Start tracking gestures
*
* @param listener listener that is notified of touch events
*/
void start(MotionEventListener listener) {
if (BubbleDebugConfig.DEBUG_BUBBLE_GESTURE) {
Log.d(TAG, "start monitoring bubbles swipe up gesture");
}
stopInternal();
mInputMonitor = InputManager.getInstance().monitorGestureInput(GESTURE_MONITOR,
mContext.getDisplayId());
InputChannel inputChannel = mInputMonitor.getInputChannel();
BubblesNavBarMotionEventHandler motionEventHandler =
new BubblesNavBarMotionEventHandler(mContext, mPositioner,
this::onInterceptTouch, listener);
mInputEventReceiver = new BubblesNavBarInputEventReceiver(inputChannel,
Choreographer.getInstance(), motionEventHandler);
}
void stop() {
if (BubbleDebugConfig.DEBUG_BUBBLE_GESTURE) {
Log.d(TAG, "stop monitoring bubbles swipe up gesture");
}
stopInternal();
}
private void stopInternal() {
if (mInputEventReceiver != null) {
mInputEventReceiver.dispose();
mInputEventReceiver = null;
}
if (mInputMonitor != null) {
mInputMonitor.dispose();
mInputMonitor = null;
}
}
private void onInterceptTouch() {
if (BubbleDebugConfig.DEBUG_BUBBLE_GESTURE) {
Log.d(TAG, "intercept touch event");
}
if (mInputMonitor != null) {
mInputMonitor.pilferPointers();
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2022 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.wm.shell.bubbles;
import android.os.Looper;
import android.view.BatchedInputEventReceiver;
import android.view.Choreographer;
import android.view.InputChannel;
import android.view.InputEvent;
import android.view.MotionEvent;
/**
* Bubbles {@link BatchedInputEventReceiver} for monitoring touches from navbar gesture area
*/
class BubblesNavBarInputEventReceiver extends BatchedInputEventReceiver {
private final BubblesNavBarMotionEventHandler mMotionEventHandler;
BubblesNavBarInputEventReceiver(InputChannel inputChannel,
Choreographer choreographer, BubblesNavBarMotionEventHandler motionEventHandler) {
super(inputChannel, Looper.myLooper(), choreographer);
mMotionEventHandler = motionEventHandler;
}
@Override
public void onInputEvent(InputEvent event) {
boolean handled = false;
try {
if (!(event instanceof MotionEvent)) {
return;
}
handled = mMotionEventHandler.onMotionEvent((MotionEvent) event);
} finally {
finishInputEvent(event, handled);
}
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright (C) 2022 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.wm.shell.bubbles;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import android.content.Context;
import android.graphics.PointF;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
import androidx.annotation.Nullable;
/**
* Handles {@link MotionEvent}s for bubbles that begin in the nav bar area
*/
class BubblesNavBarMotionEventHandler {
private static final String TAG =
TAG_WITH_CLASS_NAME ? "BubblesNavBarMotionEventHandler" : TAG_BUBBLES;
private static final int VELOCITY_UNITS = 1000;
private final Runnable mOnInterceptTouch;
private final MotionEventListener mMotionEventListener;
private final int mTouchSlop;
private final BubblePositioner mPositioner;
private final PointF mTouchDown = new PointF();
private boolean mTrackingTouches;
private boolean mInterceptingTouches;
@Nullable
private VelocityTracker mVelocityTracker;
BubblesNavBarMotionEventHandler(Context context, BubblePositioner positioner,
Runnable onInterceptTouch, MotionEventListener motionEventListener) {
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mPositioner = positioner;
mOnInterceptTouch = onInterceptTouch;
mMotionEventListener = motionEventListener;
}
/**
* Handle {@link MotionEvent} and forward it to {@code motionEventListener} defined in
* constructor
*
* @return {@code true} if this {@link MotionEvent} is handled (it started in the gesture area)
*/
public boolean onMotionEvent(MotionEvent motionEvent) {
float dx = motionEvent.getX() - mTouchDown.x;
float dy = motionEvent.getY() - mTouchDown.y;
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
if (isInGestureRegion(motionEvent)) {
mTouchDown.set(motionEvent.getX(), motionEvent.getY());
mMotionEventListener.onDown(motionEvent.getX(), motionEvent.getY());
mTrackingTouches = true;
return true;
}
break;
case MotionEvent.ACTION_MOVE:
if (mTrackingTouches) {
if (!mInterceptingTouches && Math.hypot(dx, dy) > mTouchSlop) {
mInterceptingTouches = true;
mOnInterceptTouch.run();
}
if (mInterceptingTouches) {
getVelocityTracker().addMovement(motionEvent);
mMotionEventListener.onMove(dx, dy);
}
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
if (mTrackingTouches) {
mMotionEventListener.onCancel();
finishTracking();
return true;
}
break;
case MotionEvent.ACTION_UP:
if (mTrackingTouches) {
if (mInterceptingTouches) {
getVelocityTracker().computeCurrentVelocity(VELOCITY_UNITS);
mMotionEventListener.onUp(getVelocityTracker().getXVelocity(),
getVelocityTracker().getYVelocity());
}
finishTracking();
return true;
}
break;
}
return false;
}
private boolean isInGestureRegion(MotionEvent ev) {
// Only handles touch events beginning in navigation bar system gesture zone
if (mPositioner.getNavBarGestureZone().contains((int) ev.getX(), (int) ev.getY())) {
if (DEBUG_BUBBLE_GESTURE) {
Log.d(TAG, "handling touch y=" + ev.getY()
+ " navBarGestureZone=" + mPositioner.getNavBarGestureZone());
}
return true;
}
return false;
}
private VelocityTracker getVelocityTracker() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
return mVelocityTracker;
}
private void finishTracking() {
mTouchDown.set(0, 0);
mTrackingTouches = false;
mInterceptingTouches = false;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
/**
* Callback for receiving {@link MotionEvent} updates
*/
interface MotionEventListener {
/**
* Touch down action.
*
* @param x x coordinate
* @param y y coordinate
*/
void onDown(float x, float y);
/**
* Move action.
* Reports distance from point reported in {@link #onDown(float, float)}
*
* @param dx distance moved on x-axis from starting point, in pixels
* @param dy distance moved on y-axis from starting point, in pixels
*/
void onMove(float dx, float dy);
/**
* Touch up action.
*
* @param velX velocity of the move action on x axis
* @param velY velocity of the move actin on y axis
*/
void onUp(float velX, float velY);
/**
* Motion action was cancelled.
*/
void onCancel();
}
}

View File

@@ -17,8 +17,6 @@
package com.android.wm.shell.bubbles
import android.graphics.PointF
import android.os.Handler
import android.os.Looper
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.View
@@ -146,6 +144,12 @@ abstract class RelativeTouchListener : View.OnTouchListener {
velocityTracker.clear()
movedEnough = false
}
MotionEvent.ACTION_CANCEL -> {
v.handler.removeCallbacksAndMessages(null)
velocityTracker.clear()
movedEnough = false
}
}
return true

View File

@@ -17,11 +17,13 @@
package com.android.wm.shell.bubbles.animation;
import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING;
import static com.android.wm.shell.bubbles.BubbleStackView.HOME_GESTURE_ENABLED;
import android.content.res.Resources;
import android.graphics.Path;
import android.graphics.PointF;
import android.view.View;
import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -61,7 +63,10 @@ public class ExpandedAnimationController
private static final float DAMPING_RATIO_MEDIUM_LOW_BOUNCY = 0.65f;
/** Stiffness for the expand/collapse path-following animation. */
private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000;
private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 400;
/** Stiffness for the expand/collapse animation when home gesture handling is off */
private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS_WITHOUT_HOME_GESTURE = 1000;
/**
* Velocity required to dismiss an individual bubble without dragging it into the dismiss
@@ -73,6 +78,11 @@ public class ExpandedAnimationController
new PhysicsAnimator.SpringConfig(
EXPAND_COLLAPSE_ANIM_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY);
private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfigWithoutHomeGesture =
new PhysicsAnimator.SpringConfig(
EXPAND_COLLAPSE_ANIM_STIFFNESS_WITHOUT_HOME_GESTURE,
SpringForce.DAMPING_RATIO_NO_BOUNCY);
/** Horizontal offset between bubbles, which we need to know to re-stack them. */
private float mStackOffsetPx;
/** Size of each bubble. */
@@ -278,11 +288,20 @@ public class ExpandedAnimationController
(firstBubbleLeads && index == 0)
|| (!firstBubbleLeads && index == mLayout.getChildCount() - 1);
Interpolator interpolator;
if (HOME_GESTURE_ENABLED) {
// When home gesture is enabled, we use a different animation timing for collapse
interpolator = expanding
? Interpolators.EMPHASIZED_ACCELERATE : Interpolators.EMPHASIZED_DECELERATE;
} else {
interpolator = Interpolators.LINEAR;
}
animation
.followAnimatedTargetAlongPath(
path,
EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */,
Interpolators.LINEAR /* targetAnimInterpolator */,
interpolator /* targetAnimInterpolator */,
isLeadBubble ? mLeadBubbleEndAction : null /* endAction */,
() -> mLeadBubbleEndAction = null /* endAction */)
.withStartDelay(startDelay)
@@ -525,10 +544,16 @@ public class ExpandedAnimationController
finishRemoval.run();
mOnBubbleAnimatedOutAction.run();
} else {
PhysicsAnimator.SpringConfig springConfig;
if (HOME_GESTURE_ENABLED) {
springConfig = mAnimateOutSpringConfig;
} else {
springConfig = mAnimateOutSpringConfigWithoutHomeGesture;
}
PhysicsAnimator.getInstance(child)
.spring(DynamicAnimation.ALPHA, 0f)
.spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig)
.spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig)
.spring(DynamicAnimation.SCALE_X, 0f, springConfig)
.spring(DynamicAnimation.SCALE_Y, 0f, springConfig)
.withEndActions(finishRemoval, mOnBubbleAnimatedOutAction)
.start();
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (C) 2022 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.wm.shell.bubbles.animation;
import com.android.wm.shell.bubbles.BubbleExpandedView;
/**
* Animation controller for bubble expanded view collapsing
*/
public interface ExpandedViewAnimationController {
/**
* Set expanded view that this controller is working with.
*/
void setExpandedView(BubbleExpandedView expandedView);
/**
* Set current collapse value, in pixels.
*
* @param distance pixels that user dragged the view by
*/
void updateDrag(float distance);
/**
* Set current swipe velocity.
* Velocity is directional:
* <ul>
* <li>velocity < 0 means swipe direction is up</li>
* <li>velocity > 0 means swipe direction is down</li>
* </ul>
*/
void setSwipeVelocity(float velocity);
/**
* Check if view is dragged past collapse threshold or swipe up velocity exceeds min velocity
* required to collapse the view
*/
boolean shouldCollapse();
/**
* Animate view to collapsed state
*
* @param startStackCollapse runnable that is triggered when bubbles can start moving back to
* their collapsed location
* @param after runnable to run after animation is complete
*/
void animateCollapse(Runnable startStackCollapse, Runnable after);
/**
* Animate the view back to fully expanded state.
*/
void animateBackToExpanded();
/**
* Animate view for IME visibility change
*/
void animateForImeVisibilityChange(boolean visible);
/**
* Reset the view to fully expanded state
*/
void reset();
}

View File

@@ -0,0 +1,432 @@
/*
* Copyright (C) 2022 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.wm.shell.bubbles.animation;
import static android.view.View.ALPHA;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_COLLAPSE_ANIMATOR;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_EXPANDED_VIEW_DRAGGING;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.wm.shell.bubbles.BubbleExpandedView.BOTTOM_CLIP_PROPERTY;
import static com.android.wm.shell.bubbles.BubbleExpandedView.CONTENT_ALPHA;
import static com.android.wm.shell.bubbles.BubbleExpandedView.MANAGE_BUTTON_ALPHA;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.ViewConfiguration;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import com.android.wm.shell.animation.FlingAnimationUtils;
import com.android.wm.shell.animation.Interpolators;
import com.android.wm.shell.bubbles.BubbleExpandedView;
import com.android.wm.shell.bubbles.BubblePositioner;
import java.util.ArrayList;
import java.util.List;
/**
* Implementation of {@link ExpandedViewAnimationController} that uses a collapse animation to
* hide the {@link BubbleExpandedView}
*/
public class ExpandedViewAnimationControllerImpl implements ExpandedViewAnimationController {
private static final String TAG = TAG_WITH_CLASS_NAME ? "ExpandedViewAnimCtrl" : TAG_BUBBLES;
private static final float COLLAPSE_THRESHOLD = 0.02f;
private static final int COLLAPSE_DURATION_MS = 250;
private static final int MANAGE_BUTTON_ANIM_DURATION_MS = 78;
private static final int CONTENT_OPACITY_ANIM_DELAY_MS = 93;
private static final int CONTENT_OPACITY_ANIM_DURATION_MS = 78;
private static final int BACKGROUND_OPACITY_ANIM_DELAY_MS = 172;
private static final int BACKGROUND_OPACITY_ANIM_DURATION_MS = 78;
/** Animation fraction threshold for content alpha animation when stack collapse should begin */
private static final float STACK_COLLAPSE_THRESHOLD = 0.5f;
private static final FloatPropertyCompat<ExpandedViewAnimationControllerImpl>
COLLAPSE_HEIGHT_PROPERTY =
new FloatPropertyCompat<ExpandedViewAnimationControllerImpl>("CollapseSpring") {
@Override
public float getValue(ExpandedViewAnimationControllerImpl controller) {
return controller.getCollapsedAmount();
}
@Override
public void setValue(ExpandedViewAnimationControllerImpl controller,
float value) {
controller.setCollapsedAmount(value);
}
};
private final int mMinFlingVelocity;
private float mSwipeUpVelocity;
private float mSwipeDownVelocity;
private final BubblePositioner mPositioner;
private final FlingAnimationUtils mFlingAnimationUtils;
private int mDraggedAmount;
private float mCollapsedAmount;
@Nullable
private BubbleExpandedView mExpandedView;
@Nullable
private AnimatorSet mCollapseAnimation;
private boolean mNotifiedAboutThreshold;
private SpringAnimation mBackToExpandedAnimation;
@Nullable
private ObjectAnimator mBottomClipAnim;
public ExpandedViewAnimationControllerImpl(Context context, BubblePositioner positioner) {
mFlingAnimationUtils = new FlingAnimationUtils(context.getResources().getDisplayMetrics(),
COLLAPSE_DURATION_MS / 1000f);
mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
mPositioner = positioner;
}
private static void adjustAnimatorSetDuration(AnimatorSet animatorSet,
float durationAdjustment) {
for (Animator animator : animatorSet.getChildAnimations()) {
animator.setStartDelay((long) (animator.getStartDelay() * durationAdjustment));
animator.setDuration((long) (animator.getDuration() * durationAdjustment));
}
}
@Override
public void setExpandedView(BubbleExpandedView expandedView) {
if (mExpandedView != null) {
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG, "updating expandedView, resetting previous");
}
if (mCollapseAnimation != null) {
mCollapseAnimation.cancel();
}
if (mBackToExpandedAnimation != null) {
mBackToExpandedAnimation.cancel();
}
reset();
}
mExpandedView = expandedView;
}
@Override
public void updateDrag(float distance) {
if (mExpandedView != null) {
mDraggedAmount = OverScroll.dampedScroll(distance, mExpandedView.getContentHeight());
if (DEBUG_COLLAPSE_ANIMATOR && DEBUG_EXPANDED_VIEW_DRAGGING) {
Log.d(TAG, "updateDrag: distance=" + distance + " dragged=" + mDraggedAmount);
}
setCollapsedAmount(mDraggedAmount);
if (!mNotifiedAboutThreshold && isPastCollapseThreshold()) {
mNotifiedAboutThreshold = true;
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG, "notifying over collapse threshold");
}
vibrateIfEnabled();
}
}
}
@Override
public void setSwipeVelocity(float velocity) {
if (velocity < 0) {
mSwipeUpVelocity = Math.abs(velocity);
mSwipeDownVelocity = 0;
} else {
mSwipeUpVelocity = 0;
mSwipeDownVelocity = velocity;
}
}
@Override
public boolean shouldCollapse() {
if (mSwipeDownVelocity > mMinFlingVelocity) {
// Swipe velocity is positive and over fling velocity.
// This is a swipe down, always reset to expanded state, regardless of dragged amount.
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG,
"not collapsing expanded view, swipe down velocity: " + mSwipeDownVelocity
+ " minV: " + mMinFlingVelocity);
}
return false;
}
if (mSwipeUpVelocity > mMinFlingVelocity) {
// Swiping up and over fling velocity, collapse the view.
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG,
"collapse expanded view, swipe up velocity: " + mSwipeUpVelocity + " minV: "
+ mMinFlingVelocity);
}
return true;
}
if (isPastCollapseThreshold()) {
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG, "collapse expanded view, past threshold, dragged: " + mDraggedAmount);
}
return true;
}
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG, "not collapsing expanded view");
}
return false;
}
@Override
public void animateCollapse(Runnable startStackCollapse, Runnable after) {
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG,
"expandedView animate collapse swipeVel=" + mSwipeUpVelocity + " minFlingVel="
+ mMinFlingVelocity);
}
if (mExpandedView != null) {
// Mark it as animating immediately to avoid updates to the view before animation starts
mExpandedView.setAnimating(true);
if (mCollapseAnimation != null) {
mCollapseAnimation.cancel();
}
mCollapseAnimation = createCollapseAnimation(mExpandedView, startStackCollapse, after);
if (mSwipeUpVelocity >= mMinFlingVelocity) {
int contentHeight = mExpandedView.getContentHeight();
// Use a temp animator to get adjusted duration value for swipe.
// This new value will be used to adjust animation times proportionally in the
// animator set. If we adjust animator set duration directly, all child animations
// will get the same animation time.
ValueAnimator tempAnimator = new ValueAnimator();
mFlingAnimationUtils.applyDismissing(tempAnimator, mCollapsedAmount, contentHeight,
mSwipeUpVelocity, (contentHeight - mCollapsedAmount));
float durationAdjustment =
(float) tempAnimator.getDuration() / COLLAPSE_DURATION_MS;
adjustAnimatorSetDuration(mCollapseAnimation, durationAdjustment);
mCollapseAnimation.setInterpolator(tempAnimator.getInterpolator());
}
mCollapseAnimation.start();
}
}
@Override
public void animateBackToExpanded() {
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG, "expandedView animate back to expanded");
}
BubbleExpandedView expandedView = mExpandedView;
if (expandedView == null) {
return;
}
expandedView.setAnimating(true);
mBackToExpandedAnimation = new SpringAnimation(this, COLLAPSE_HEIGHT_PROPERTY);
mBackToExpandedAnimation.setSpring(new SpringForce()
.setStiffness(SpringForce.STIFFNESS_LOW)
.setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
);
mBackToExpandedAnimation.addEndListener(new OneTimeEndListener() {
@Override
public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
float velocity) {
super.onAnimationEnd(animation, canceled, value, velocity);
mNotifiedAboutThreshold = false;
mBackToExpandedAnimation = null;
expandedView.setAnimating(false);
}
});
mBackToExpandedAnimation.setStartValue(mCollapsedAmount);
mBackToExpandedAnimation.animateToFinalPosition(0);
}
@Override
public void animateForImeVisibilityChange(boolean visible) {
if (mExpandedView != null) {
if (mBottomClipAnim != null) {
mBottomClipAnim.cancel();
}
int clip = 0;
if (visible) {
// Clip the expanded view at the top of the IME view
clip = mExpandedView.getContentBottomOnScreen() - mPositioner.getImeTop();
// Don't allow negative clip value
clip = Math.max(clip, 0);
}
mBottomClipAnim = ObjectAnimator.ofInt(mExpandedView, BOTTOM_CLIP_PROPERTY, clip);
mBottomClipAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mBottomClipAnim = null;
}
});
mBottomClipAnim.start();
}
}
@Override
public void reset() {
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG, "reset expandedView collapsed state");
}
if (mExpandedView == null) {
return;
}
mExpandedView.setAnimating(false);
if (mCollapseAnimation != null) {
mCollapseAnimation.cancel();
}
if (mBackToExpandedAnimation != null) {
mBackToExpandedAnimation.cancel();
}
mExpandedView.setContentAlpha(1);
mExpandedView.setAlpha(1);
mExpandedView.setManageButtonAlpha(1);
setCollapsedAmount(0);
mExpandedView.setBottomClip(0);
mExpandedView.movePointerBy(0, 0);
mCollapsedAmount = 0;
mDraggedAmount = 0;
mSwipeUpVelocity = 0;
mSwipeDownVelocity = 0;
mNotifiedAboutThreshold = false;
}
private float getCollapsedAmount() {
return mCollapsedAmount;
}
private void setCollapsedAmount(float collapsed) {
if (mCollapsedAmount != collapsed) {
float previous = mCollapsedAmount;
mCollapsedAmount = collapsed;
if (mExpandedView != null) {
if (previous == 0) {
// View was not collapsed before. Apply z order change
mExpandedView.setSurfaceZOrderedOnTop(true);
mExpandedView.setAnimating(true);
}
mExpandedView.setTopClip((int) mCollapsedAmount);
// Move up with translationY. Use negative collapsed value
mExpandedView.setContentTranslationY(-mCollapsedAmount);
mExpandedView.setManageButtonTranslationY(-mCollapsedAmount);
if (mCollapsedAmount == 0) {
// View is no longer collapsed. Revert z order change
mExpandedView.setSurfaceZOrderedOnTop(false);
mExpandedView.setAnimating(false);
}
}
}
}
private boolean isPastCollapseThreshold() {
if (mExpandedView != null) {
return mDraggedAmount > mExpandedView.getContentHeight() * COLLAPSE_THRESHOLD;
}
return false;
}
private AnimatorSet createCollapseAnimation(BubbleExpandedView expandedView,
Runnable startStackCollapse, Runnable after) {
List<Animator> animatorList = new ArrayList<>();
animatorList.add(createHeightAnimation(expandedView));
animatorList.add(createManageButtonAnimation());
ObjectAnimator contentAlphaAnimation = createContentAlphaAnimation();
final boolean[] notified = {false};
contentAlphaAnimation.addUpdateListener(animation -> {
if (!notified[0] && animation.getAnimatedFraction() > STACK_COLLAPSE_THRESHOLD) {
notified[0] = true;
// Notify bubbles that they can start moving back to the collapsed position
startStackCollapse.run();
}
});
animatorList.add(contentAlphaAnimation);
animatorList.add(createBackgroundAlphaAnimation());
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
after.run();
}
});
animatorSet.playTogether(animatorList);
return animatorSet;
}
private ValueAnimator createHeightAnimation(BubbleExpandedView expandedView) {
ValueAnimator animator = ValueAnimator.ofInt((int) mCollapsedAmount,
expandedView.getContentHeight());
animator.setInterpolator(Interpolators.EMPHASIZED_ACCELERATE);
animator.setDuration(COLLAPSE_DURATION_MS);
animator.addUpdateListener(anim -> setCollapsedAmount((int) anim.getAnimatedValue()));
return animator;
}
private ObjectAnimator createManageButtonAnimation() {
ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandedView, MANAGE_BUTTON_ALPHA, 0f);
animator.setDuration(MANAGE_BUTTON_ANIM_DURATION_MS);
animator.setInterpolator(Interpolators.LINEAR);
return animator;
}
private ObjectAnimator createContentAlphaAnimation() {
ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandedView, CONTENT_ALPHA, 0f);
animator.setDuration(CONTENT_OPACITY_ANIM_DURATION_MS);
animator.setInterpolator(Interpolators.LINEAR);
animator.setStartDelay(CONTENT_OPACITY_ANIM_DELAY_MS);
return animator;
}
private ObjectAnimator createBackgroundAlphaAnimation() {
ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandedView, ALPHA, 0f);
animator.setDuration(BACKGROUND_OPACITY_ANIM_DURATION_MS);
animator.setInterpolator(Interpolators.LINEAR);
animator.setStartDelay(BACKGROUND_OPACITY_ANIM_DELAY_MS);
return animator;
}
@SuppressLint("MissingPermission")
private void vibrateIfEnabled() {
if (mExpandedView != null) {
mExpandedView.performHapticFeedback(HapticFeedbackConstants.DRAG_CROSSING);
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2022 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.wm.shell.bubbles.animation;
import com.android.wm.shell.bubbles.BubbleExpandedView;
/**
* Stub implementation {@link ExpandedViewAnimationController} that does not animate the
* {@link BubbleExpandedView}
*/
public class ExpandedViewAnimationControllerStub implements ExpandedViewAnimationController {
@Override
public void setExpandedView(BubbleExpandedView expandedView) {
}
@Override
public void updateDrag(float distance) {
}
@Override
public void setSwipeVelocity(float velocity) {
}
@Override
public boolean shouldCollapse() {
return false;
}
@Override
public void animateCollapse(Runnable startStackCollapse, Runnable after) {
}
@Override
public void animateBackToExpanded() {
}
@Override
public void animateForImeVisibilityChange(boolean visible) {
}
@Override
public void reset() {
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2022 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.wm.shell.bubbles.animation;
/**
* Utility methods for overscroll damping and related effect.
*
* Copied from packages/apps/Launcher3/src/com/android/launcher3/touch/OverScroll.java
*/
public class OverScroll {
private static final float OVERSCROLL_DAMP_FACTOR = 0.07f;
/**
* This curve determines how the effect of scrolling over the limits of the page diminishes
* as the user pulls further and further from the bounds
*
* @param f The percentage of how much the user has overscrolled.
* @return A transformed percentage based on the influence curve.
*/
private static float overScrollInfluenceCurve(float f) {
f -= 1.0f;
return f * f * f + 1.0f;
}
/**
* @param amount The original amount overscrolled.
* @param max The maximum amount that the View can overscroll.
* @return The dampened overscroll amount.
*/
public static int dampedScroll(float amount, int max) {
if (Float.compare(amount, 0) == 0) return 0;
float f = amount / max;
f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f)));
// Clamp this factor, f, to -1 < f < 1
if (Math.abs(f) >= 1) {
f /= Math.abs(f);
}
return Math.round(OVERSCROLL_DAMP_FACTOR * f * max);
}
}

View File

@@ -0,0 +1,142 @@
/*
* Copyright (C) 2022 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.wm.shell.bubbles;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_UP;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.floatThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import android.os.SystemClock;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.MotionEvent;
import android.view.WindowManager;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* Test {@link MotionEvent} handling in {@link BubblesNavBarMotionEventHandler}.
* Verifies that swipe events
*/
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidTestingRunner.class)
public class BubblesNavBarMotionEventHandlerTest extends ShellTestCase {
private BubblesNavBarMotionEventHandler mMotionEventHandler;
@Mock
private WindowManager mWindowManager;
@Mock
private Runnable mInterceptTouchRunnable;
@Mock
private MotionEventListener mMotionEventListener;
private long mMotionEventTime;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
TestableBubblePositioner positioner = new TestableBubblePositioner(getContext(),
mWindowManager);
mMotionEventHandler = new BubblesNavBarMotionEventHandler(getContext(), positioner,
mInterceptTouchRunnable, mMotionEventListener);
mMotionEventTime = SystemClock.uptimeMillis();
}
@Test
public void testMotionEvent_swipeUpInGestureZone_handled() {
mMotionEventHandler.onMotionEvent(newEvent(ACTION_DOWN, 0, 990));
mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 0, 690));
mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 0, 490));
mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 0, 390));
mMotionEventHandler.onMotionEvent(newEvent(ACTION_UP, 0, 390));
verify(mMotionEventListener).onDown(0, 990);
verify(mMotionEventListener).onMove(0, -300);
verify(mMotionEventListener).onMove(0, -500);
verify(mMotionEventListener).onMove(0, -600);
// Check that velocity up is about 5000
verify(mMotionEventListener).onUp(eq(0f), floatThat(f -> Math.round(f) == -5000));
verifyZeroInteractions(mMotionEventListener);
verify(mInterceptTouchRunnable).run();
}
@Test
public void testMotionEvent_swipeUpOutsideGestureZone_ignored() {
mMotionEventHandler.onMotionEvent(newEvent(ACTION_DOWN, 0, 500));
mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 0, 100));
mMotionEventHandler.onMotionEvent(newEvent(ACTION_UP, 0, 100));
verifyZeroInteractions(mMotionEventListener);
verifyZeroInteractions(mInterceptTouchRunnable);
}
@Test
public void testMotionEvent_horizontalMoveMoreThanTouchSlop_handled() {
mMotionEventHandler.onMotionEvent(newEvent(ACTION_DOWN, 0, 990));
mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 100, 990));
mMotionEventHandler.onMotionEvent(newEvent(ACTION_UP, 100, 990));
verify(mMotionEventListener).onDown(0, 990);
verify(mMotionEventListener).onMove(100, 0);
verify(mMotionEventListener).onUp(0, 0);
verifyZeroInteractions(mMotionEventListener);
verify(mInterceptTouchRunnable).run();
}
@Test
public void testMotionEvent_moveLessThanTouchSlop_ignored() {
mMotionEventHandler.onMotionEvent(newEvent(ACTION_DOWN, 0, 990));
mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 0, 989));
mMotionEventHandler.onMotionEvent(newEvent(ACTION_UP, 0, 989));
verify(mMotionEventListener).onDown(0, 990);
verifyNoMoreInteractions(mMotionEventListener);
verifyZeroInteractions(mInterceptTouchRunnable);
}
@Test
public void testMotionEvent_actionCancel_listenerNotified() {
mMotionEventHandler.onMotionEvent(newEvent(ACTION_DOWN, 0, 990));
mMotionEventHandler.onMotionEvent(newEvent(ACTION_CANCEL, 0, 990));
verify(mMotionEventListener).onDown(0, 990);
verify(mMotionEventListener).onCancel();
verifyNoMoreInteractions(mMotionEventListener);
verifyZeroInteractions(mInterceptTouchRunnable);
}
private MotionEvent newEvent(int actionDown, float x, float y) {
MotionEvent event = MotionEvent.obtain(0L, mMotionEventTime, actionDown, x, y, 0);
mMotionEventTime += 10;
return event;
}
}

View File

@@ -0,0 +1,181 @@
/*
* Copyright (C) 2022 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.wm.shell.bubbles.animation;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.bubbles.BubbleExpandedView;
import com.android.wm.shell.bubbles.TestableBubblePositioner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class ExpandedViewAnimationControllerTest extends ShellTestCase {
private ExpandedViewAnimationController mController;
@Mock
private WindowManager mWindowManager;
@Mock
private BubbleExpandedView mMockExpandedView;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
TestableBubblePositioner positioner = new TestableBubblePositioner(getContext(),
mWindowManager);
mController = new ExpandedViewAnimationControllerImpl(getContext(), positioner);
mController.setExpandedView(mMockExpandedView);
when(mMockExpandedView.getContentHeight()).thenReturn(1000);
}
@Test
public void testUpdateDrag_expandedViewMovesUpAndClipped() {
// Drag by 50 pixels which corresponds to 10 pixels with overscroll
int dragDistance = 50;
int dampenedDistance = 10;
mController.updateDrag(dragDistance);
verify(mMockExpandedView).setTopClip(dampenedDistance);
verify(mMockExpandedView).setContentTranslationY(-dampenedDistance);
verify(mMockExpandedView).setManageButtonTranslationY(-dampenedDistance);
}
@Test
public void testUpdateDrag_zOrderUpdates() {
mController.updateDrag(10);
mController.updateDrag(20);
verify(mMockExpandedView, times(1)).setSurfaceZOrderedOnTop(true);
verify(mMockExpandedView, times(1)).setAnimating(true);
}
@Test
public void testUpdateDrag_moveBackToZero_zOrderRestored() {
mController.updateDrag(50);
reset(mMockExpandedView);
mController.updateDrag(0);
mController.updateDrag(0);
verify(mMockExpandedView, times(1)).setSurfaceZOrderedOnTop(false);
verify(mMockExpandedView, times(1)).setAnimating(false);
}
@Test
public void testUpdateDrag_hapticFeedbackOnlyOnce() {
// Drag by 10 which is below the collapse threshold - no feedback
mController.updateDrag(10);
verify(mMockExpandedView, times(0)).performHapticFeedback(anyInt());
// 150 takes it over the threshold - perform feedback
mController.updateDrag(150);
verify(mMockExpandedView, times(1)).performHapticFeedback(anyInt());
// Continue dragging, no more feedback
mController.updateDrag(200);
verify(mMockExpandedView, times(1)).performHapticFeedback(anyInt());
// Drag below threshold and over again - no more feedback
mController.updateDrag(10);
mController.updateDrag(150);
verify(mMockExpandedView, times(1)).performHapticFeedback(anyInt());
}
@Test
public void testShouldCollapse_doNotCollapseIfNotDragged() {
assertThat(mController.shouldCollapse()).isFalse();
}
@Test
public void testShouldCollapse_doNotCollapseIfVelocityDown() {
assumeTrue("Min fling velocity should be > 1 for this test", getMinFlingVelocity() > 1);
mController.setSwipeVelocity(getVelocityAboveMinFling());
assertThat(mController.shouldCollapse()).isFalse();
}
@Test
public void tesShouldCollapse_doNotCollapseIfVelocityUpIsSmall() {
assumeTrue("Min fling velocity should be > 1 for this test", getMinFlingVelocity() > 1);
mController.setSwipeVelocity(-getVelocityBelowMinFling());
assertThat(mController.shouldCollapse()).isFalse();
}
@Test
public void testShouldCollapse_collapseIfVelocityUpIsLarge() {
assumeTrue("Min fling velocity should be > 1 for this test", getMinFlingVelocity() > 1);
mController.setSwipeVelocity(-getVelocityAboveMinFling());
assertThat(mController.shouldCollapse()).isTrue();
}
@Test
public void testShouldCollapse_collapseIfPastThreshold() {
mController.updateDrag(500);
assertThat(mController.shouldCollapse()).isTrue();
}
@Test
public void testReset() {
mController.updateDrag(100);
reset(mMockExpandedView);
mController.reset();
verify(mMockExpandedView, atLeastOnce()).setAnimating(false);
verify(mMockExpandedView).setContentAlpha(1);
verify(mMockExpandedView).setAlpha(1);
verify(mMockExpandedView).setManageButtonAlpha(1);
verify(mMockExpandedView).setManageButtonAlpha(1);
verify(mMockExpandedView).setTopClip(0);
verify(mMockExpandedView).setContentTranslationY(-0f);
verify(mMockExpandedView).setManageButtonTranslationY(-0f);
verify(mMockExpandedView).setBottomClip(0);
verify(mMockExpandedView).movePointerBy(0, 0);
assertThat(mController.shouldCollapse()).isFalse();
}
private int getVelocityBelowMinFling() {
return getMinFlingVelocity() - 1;
}
private int getVelocityAboveMinFling() {
return getMinFlingVelocity() + 1;
}
private int getMinFlingVelocity() {
return ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity();
}
}

View File

@@ -166,6 +166,13 @@ public class Flags {
public static final SysPropBooleanFlag WM_ENABLE_SHELL_TRANSITIONS =
new SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false);
/**
* b/170163464: animate bubbles expanded view collapse with home gesture
*/
@Keep
public static final SysPropBooleanFlag BUBBLES_HOME_GESTURE =
new SysPropBooleanFlag(1101, "persist.wm.debug.bubbles_home_gesture", false);
// 1200 - predictive back
@Keep
public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag(