From d8aa1c311970f488b5353737f65d3dbdb8bfb4cd Mon Sep 17 00:00:00 2001 From: Ats Jenk Date: Wed, 16 Mar 2022 10:10:27 -0700 Subject: [PATCH] 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 --- .../wm/shell/animation/Interpolators.java | 13 + .../wm/shell/bubbles/BubbleController.java | 2 +- .../wm/shell/bubbles/BubbleDebugConfig.java | 3 + .../wm/shell/bubbles/BubbleExpandedView.java | 276 +++++++++-- .../wm/shell/bubbles/BubblePositioner.java | 25 + .../wm/shell/bubbles/BubbleStackView.java | 279 ++++++++++- .../bubbles/BubblesNavBarGestureTracker.java | 104 +++++ .../BubblesNavBarInputEventReceiver.java | 51 +++ .../BubblesNavBarMotionEventHandler.java | 175 +++++++ .../wm/shell/bubbles/RelativeTouchListener.kt | 8 +- .../ExpandedAnimationController.java | 33 +- .../ExpandedViewAnimationController.java | 75 +++ .../ExpandedViewAnimationControllerImpl.java | 432 ++++++++++++++++++ .../ExpandedViewAnimationControllerStub.java | 57 +++ .../shell/bubbles/animation/OverScroll.java | 57 +++ .../BubblesNavBarMotionEventHandlerTest.java | 142 ++++++ .../ExpandedViewAnimationControllerTest.java | 181 ++++++++ .../src/com/android/systemui/flags/Flags.java | 7 + 18 files changed, 1869 insertions(+), 51 deletions(-) create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarInputEventReceiver.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerStub.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/OverScroll.java create mode 100644 libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandlerTest.java create mode 100644 libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerTest.java diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java index 2aead9392e595..a0dde6ad168d0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java @@ -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. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 8cfeefe71b15d..c4a497af79673 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -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); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java index dc2ace949f0ce..dce6b56261ff6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java @@ -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 = diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index a089585a5a004..1eb6f731d1737 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -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 BOTTOM_CLIP_PROPERTY = + new IntProperty("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 CONTENT_ALPHA = + new FloatProperty("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 MANAGE_BUTTON_ALPHA = + new FloatProperty("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. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index 7cfacbcc92f8b..7d60114fe927b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -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); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index b7c5eb06fbfa5..941bcab9b81da 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -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 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); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java new file mode 100644 index 0000000000000..e7beeeb065349 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java @@ -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(); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarInputEventReceiver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarInputEventReceiver.java new file mode 100644 index 0000000000000..45037b87830f5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarInputEventReceiver.java @@ -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); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java new file mode 100644 index 0000000000000..844526ca0f35e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java @@ -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(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt index cf0cefec401aa..ea9d065d5f537 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt @@ -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 diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java index 573f42468512b..c24efb9f55348 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java @@ -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(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java new file mode 100644 index 0000000000000..8a33780bc8d53 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java @@ -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: + *
    + *
  • velocity < 0 means swipe direction is up
  • + *
  • velocity > 0 means swipe direction is down
  • + *
+ */ + 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(); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java new file mode 100644 index 0000000000000..ca54232c95b56 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java @@ -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 + COLLAPSE_HEIGHT_PROPERTY = + new FloatPropertyCompat("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 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); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerStub.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerStub.java new file mode 100644 index 0000000000000..bb8a3aaaf551a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerStub.java @@ -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() { + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/OverScroll.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/OverScroll.java new file mode 100644 index 0000000000000..d4e76ed0282e3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/OverScroll.java @@ -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); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandlerTest.java new file mode 100644 index 0000000000000..44ff35466ae23 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandlerTest.java @@ -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; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerTest.java new file mode 100644 index 0000000000000..21887c03833a1 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerTest.java @@ -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(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index 94418f498d3b0..55bbcb654cf5e 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -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(