Animation for bubbles home gesture

Move bubbles expanded view up when user is performing the home gesture.
This gives immediate visual feedback to the user when swiping up to
collapse the expanded bubble.
Updating the expanded view collapse animation so that it works well
together with swipe up gesture.

Bug: 170163464
Test: atest PlatformScenarioTests:android.platform.test.scenario.sysui.bubble
Test: atest frameworks/base/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles
Change-Id: I834ef8de73dc4f9682583fa727ef42db21c19a7f
This commit is contained in:
Ats Jenk
2022-03-16 10:10:27 -07:00
parent bb30e29697
commit d8aa1c3119
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(