Merge "Animation for bubbles home gesture" into tm-qpr-dev
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user