From 273ed107f1d414571a01bf8ebd9c9ffa942d9e11 Mon Sep 17 00:00:00 2001 From: Mady Mellor Date: Fri, 4 Mar 2016 08:52:39 -0800 Subject: [PATCH] Improve interaction around showing / hiding the gear behind notifications If the gesture on the notification was not considered a dismiss gesture the code always check if the notification had moved enough to display the gear. This causes some undesirable behavior: - For unclearable notifications the gear would *always* be shown - Even if the velocity of the gesture was enough to be a dismiss it may have failed other requirements and then the gear would be shown This CL alters NotificationSwipeHelper to take into account the velocity of the gesture, and if it was great enough to be a dismiss but wasn't (unclearable, didn't meet other requirements) it'll just snap the notification back into place. Additionally this CL alters the behavior so that if the gear is visible and the notification is swiped in the direction of the gear it *won't* dismiss the notification but rather cover the gear. If a dismiss gesture is made in the opposite direction of the gear, the notification will still be dismissed. Bug: 27378399 Bug: 27319053 Bug: 27335353 Change-Id: I0849eecc71f2c6722e811d284534c2ea29b1b8aa --- .../src/com/android/systemui/SwipeHelper.java | 75 ++++--- .../NotificationSettingsIconRow.java | 86 +++++--- .../stack/NotificationStackScrollLayout.java | 191 ++++++++++++++---- 3 files changed, 259 insertions(+), 93 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java index f6dcc115401d0..427273ddaca69 100644 --- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java @@ -511,35 +511,16 @@ public class SwipeHelper implements Gefingerpoken { break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: - if (mCurrView != null) { - float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; - mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); - float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; - float velocity = getVelocity(mVelocityTracker); - float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker); + if (mCurrView == null) { + break; + } + mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, getMaxVelocity()); + float velocity = getVelocity(mVelocityTracker); - float translation = getTranslation(mCurrView); - // Decide whether to dismiss the current view - boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH && - Math.abs(translation) > 0.4 * getSize(mCurrView); - boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) && - (Math.abs(velocity) > Math.abs(perpendicularVelocity)) && - (velocity > 0) == (translation > 0); - boolean falsingDetected = mCallback.isAntiFalsingNeeded(); - - if (mFalsingManager.isClassiferEnabled()) { - falsingDetected = falsingDetected && mFalsingManager.isFalseTouch(); - } else { - falsingDetected = falsingDetected && !mTouchAboveFalsingThreshold; - } - - boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) - && !falsingDetected && (childSwipedFastEnough || childSwipedFarEnough) - && ev.getActionMasked() == MotionEvent.ACTION_UP; - - if (dismissChild) { + if (!handleUpEvent(ev, mCurrView, velocity, getTranslation(mCurrView))) { + if (isDismissGesture(ev)) { // flingadingy - dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); + dismissChild(mCurrView, swipedFastEnough() ? velocity : 0f); } else { // snappity mCallback.onDragCancelled(mCurrView); @@ -556,6 +537,46 @@ public class SwipeHelper implements Gefingerpoken { return (int) (mFalsingThreshold * factor); } + private float getMaxVelocity() { + return MAX_DISMISS_VELOCITY * mDensityScale; + } + + protected float getEscapeVelocity() { + return SWIPE_ESCAPE_VELOCITY * mDensityScale; + } + + protected boolean swipedFarEnough() { + float translation = getTranslation(mCurrView); + return DISMISS_IF_SWIPED_FAR_ENOUGH && Math.abs(translation) > 0.4 * getSize(mCurrView); + } + + protected boolean isDismissGesture(MotionEvent ev) { + boolean falsingDetected = mCallback.isAntiFalsingNeeded(); + if (mFalsingManager.isClassiferEnabled()) { + falsingDetected = falsingDetected && mFalsingManager.isFalseTouch(); + } else { + falsingDetected = falsingDetected && !mTouchAboveFalsingThreshold; + } + return !falsingDetected && (swipedFastEnough() || swipedFarEnough()) + && ev.getActionMasked() == MotionEvent.ACTION_UP + && mCallback.canChildBeDismissed(mCurrView); + } + + protected boolean swipedFastEnough() { + float velocity = getVelocity(mVelocityTracker); + float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker); + float translation = getTranslation(mCurrView); + boolean ret = (Math.abs(velocity) > getEscapeVelocity()) && + (Math.abs(velocity) > Math.abs(perpendicularVelocity)) && + (velocity > 0) == (translation > 0); + return ret; + } + + protected boolean handleUpEvent(MotionEvent ev, View animView, float velocity, + float translation) { + return false; + } + public interface Callback { View getChildAtPosition(MotionEvent ev); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationSettingsIconRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationSettingsIconRow.java index 375459f0f30ce..fcc48bf8d5103 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationSettingsIconRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationSettingsIconRow.java @@ -29,11 +29,18 @@ import com.android.systemui.R; public class NotificationSettingsIconRow extends FrameLayout implements View.OnClickListener { + private static final int GEAR_ALPHA_ANIM_DURATION = 200; + public interface SettingsIconRowListener { /** * Called when the gear behind a notification is touched. */ public void onGearTouched(ExpandableNotificationRow row, int x, int y); + + /** + * Called when a notification is slid back over the gear. + */ + public void onSettingsIconRowReset(NotificationSettingsIconRow row); } private ExpandableNotificationRow mParent; @@ -45,6 +52,8 @@ public class NotificationSettingsIconRow extends FrameLayout implements View.OnC private boolean mSettingsFadedIn = false; private boolean mAnimating = false; private boolean mOnLeft = true; + private boolean mDismissing = false; + private boolean mSnapping = false; private int[] mGearLocation = new int[2]; private int[] mParentLocation = new int[2]; @@ -78,8 +87,14 @@ public class NotificationSettingsIconRow extends FrameLayout implements View.OnC public void resetState() { setGearAlpha(0f); + mSettingsFadedIn = false; mAnimating = false; + mSnapping = false; + mDismissing = false; setIconLocation(true /* on left */); + if (mListener != null) { + mListener.onSettingsIconRowReset(this); + } } public void setGearListener(SettingsIconRowListener listener) { @@ -94,19 +109,23 @@ public class NotificationSettingsIconRow extends FrameLayout implements View.OnC return mParent; } - private void setGearAlpha(float alpha) { + public void setGearAlpha(float alpha) { if (alpha == 0) { mSettingsFadedIn = false; // Can fade in again once it's gone. setVisibility(View.INVISIBLE); } else { - if (alpha == 1) { - mSettingsFadedIn = true; - } setVisibility(View.VISIBLE); } mGearIcon.setAlpha(alpha); } + /** + * Returns whether the icon is on the left side of the view or not. + */ + public boolean isIconOnLeft() { + return mOnLeft; + } + /** * Returns the horizontal space in pixels required to display the gear behind a notification. */ @@ -119,7 +138,7 @@ public class NotificationSettingsIconRow extends FrameLayout implements View.OnC * if entire view is visible. */ public boolean isVisible() { - return mSettingsFadedIn; + return mGearIcon.getAlpha() > 0; } public void cancelFadeAnimator() { @@ -129,16 +148,18 @@ public class NotificationSettingsIconRow extends FrameLayout implements View.OnC } public void updateSettingsIcons(final float transX, final float size) { - if (mAnimating || (mGearIcon.getAlpha() == 0)) { - // Don't adjust when animating or settings aren't visible + if (mAnimating || !mSettingsFadedIn) { + // Don't adjust when animating, or if the gear hasn't been shown yet. return; } - setIconLocation(transX > 0 /* fromLeft */); + final float fadeThreshold = size * 0.3f; final float absTrans = Math.abs(transX); float desiredAlpha = 0; - if (absTrans <= fadeThreshold) { + if (absTrans == 0) { + desiredAlpha = 0; + } else if (absTrans <= fadeThreshold) { desiredAlpha = 1; } else { desiredAlpha = 1 - ((absTrans - fadeThreshold) / (size - fadeThreshold)); @@ -148,6 +169,12 @@ public class NotificationSettingsIconRow extends FrameLayout implements View.OnC public void fadeInSettings(final boolean fromLeft, final float transX, final float notiThreshold) { + if (mDismissing || mAnimating) { + return; + } + if (isIconLocationChange(transX)) { + setGearAlpha(0f); + } setIconLocation(transX > 0 /* fromLeft */); mFadeAnimator = ValueAnimator.ofFloat(mGearIcon.getAlpha(), 1); mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @@ -163,41 +190,54 @@ public class NotificationSettingsIconRow extends FrameLayout implements View.OnC } }); mFadeAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(Animator animation) { - super.onAnimationCancel(animation); - mAnimating = false; - mSettingsFadedIn = false; - } - @Override public void onAnimationStart(Animator animation) { - super.onAnimationStart(animation); mAnimating = true; } + @Override + public void onAnimationCancel(Animator animation) { + // TODO should animate back to 0f from current alpha + mGearIcon.setAlpha(0f); + } + @Override public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); mAnimating = false; - mSettingsFadedIn = true; + mSettingsFadedIn = mGearIcon.getAlpha() == 1; } }); mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN); - mFadeAnimator.setDuration(200); + mFadeAnimator.setDuration(GEAR_ALPHA_ANIM_DURATION); mFadeAnimator.start(); } - private void setIconLocation(boolean onLeft) { - if (onLeft == mOnLeft) { + public void setIconLocation(boolean onLeft) { + if (onLeft == mOnLeft || mSnapping) { // Same side? Do nothing. return; } - setTranslationX(onLeft ? 0 : (mParent.getWidth() - mHorizSpaceForGear)); mOnLeft = onLeft; } + public boolean isIconLocationChange(float translation) { + boolean onLeft = translation > mGearIcon.getPaddingStart(); + boolean onRight = translation < -mGearIcon.getPaddingStart(); + if ((mOnLeft && onRight) || (!mOnLeft && onLeft)) { + return true; + } + return false; + } + + public void setDismissing() { + mDismissing = true; + } + + public void setSnapping(boolean snapping) { + mSnapping = snapping; + } + @Override public void onClick(View v) { if (v.getId() == R.id.gear_icon) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java index cca374602b63c..1b7161f808968 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java @@ -370,6 +370,11 @@ public class NotificationStackScrollLayout extends ViewGroup } } + @Override + public void onSettingsIconRowReset(NotificationSettingsIconRow row) { + mSwipeHelper.setSnappedToGear(false); + } + @Override protected void onDraw(Canvas canvas) { canvas.drawRect(0, mCurrentBounds.top, getWidth(), mCurrentBounds.bottom, mBackgroundPaint); @@ -716,11 +721,15 @@ public class NotificationStackScrollLayout extends ViewGroup mDragAnimPendingChildren.remove(animView); } - if (targetLeft == 0 && mCurrIconRow != null) { - mCurrIconRow.resetState(); - mCurrIconRow = null; - if (mGearExposedView != null && mGearExposedView == mTranslatingParentView) { - mGearExposedView = null; + if (mCurrIconRow != null) { + if (targetLeft == 0) { + mCurrIconRow.resetState(); + mCurrIconRow = null; + if (mGearExposedView != null && mGearExposedView == mTranslatingParentView) { + mGearExposedView = null; + } + } else { + mSwipeHelper.setSnappedToGear(true); } } } @@ -3369,15 +3378,11 @@ public class NotificationStackScrollLayout extends ViewGroup } private class NotificationSwipeHelper extends SwipeHelper { - private static final int MOVE_STATE_LEFT = -1; - private static final int MOVE_STATE_UNDEFINED = 0; - private static final int MOVE_STATE_RIGHT = 1; - private static final long GEAR_SHOW_DELAY = 60; - private CheckForDrag mCheckForDrag; private Handler mHandler; - private int mMoveState = MOVE_STATE_UNDEFINED; + private boolean mGearSnappedTo; + private boolean mGearSnappedOnLeft; public NotificationSwipeHelper(int swipeDirection, Callback callback, Context context) { super(swipeDirection, callback, context); @@ -3390,6 +3395,10 @@ public class NotificationStackScrollLayout extends ViewGroup mTranslatingParentView = currView; // Reset check for drag gesture + cancelCheckForDrag(); + if (mCurrIconRow != null) { + mCurrIconRow.setSnapping(false); + } mCheckForDrag = null; mCurrIconRow = null; @@ -3401,17 +3410,32 @@ public class NotificationStackScrollLayout extends ViewGroup mCurrIconRow = ((ExpandableNotificationRow) currView).getSettingsRow(); mCurrIconRow.setGearListener(NotificationStackScrollLayout.this); } - mMoveState = MOVE_STATE_UNDEFINED; } @Override public void onMoveUpdate(View view, float translation, float delta) { - final int newMoveState = (delta < 0) ? MOVE_STATE_RIGHT : MOVE_STATE_LEFT; - if (mMoveState != MOVE_STATE_UNDEFINED && mMoveState != newMoveState) { - // Changed directions, make sure we check for drag again. - mCheckForDrag = null; + if (mCurrIconRow != null) { + mCurrIconRow.setSnapping(false); // If we're moving, we're not snapping. + + // If the gear is visible and the movement is towards it it's not a location change. + boolean onLeft = mGearSnappedTo ? mGearSnappedOnLeft : mCurrIconRow.isIconOnLeft(); + boolean locationChange = isTowardsGear(translation, onLeft) + ? false : mCurrIconRow.isIconLocationChange(translation); + if (locationChange) { + // Don't consider it "snapped" if location has changed. + setSnappedToGear(false); + + // Changed directions, make sure we check to fade in icon again. + if (!mHandler.hasCallbacks(mCheckForDrag)) { + // No check scheduled, set null to schedule a new one. + mCheckForDrag = null; + } else { + // Check scheduled, reset alpha and update location; check will fade it in + mCurrIconRow.setGearAlpha(0f); + mCurrIconRow.setIconLocation(translation > 0 /* onLeft */); + } + } } - mMoveState = newMoveState; final boolean gutsExposed = (view instanceof ExpandableNotificationRow) && ((ExpandableNotificationRow) view).areGutsExposed(); @@ -3424,35 +3448,99 @@ public class NotificationStackScrollLayout extends ViewGroup @Override public void dismissChild(final View view, float velocity) { - cancelCheckForDrag(); super.dismissChild(view, velocity); + cancelCheckForDrag(); + setSnappedToGear(false); } @Override public void snapChild(final View animView, final float targetLeft, float velocity) { + super.snapChild(animView, targetLeft, velocity); + if (targetLeft == 0) { + cancelCheckForDrag(); + setSnappedToGear(false); + } + } + + + @Override + public boolean handleUpEvent(MotionEvent ev, View animView, float velocity, + float translation) { + if (mCurrIconRow == null) { + cancelCheckForDrag(); + return false; // Let SwipeHelper handle it. + } + + boolean gestureTowardsGear = isTowardsGear(velocity, mCurrIconRow.isIconOnLeft()); + boolean gestureFastEnough = Math.abs(velocity) > getEscapeVelocity(); + + if (mGearSnappedTo && mCurrIconRow.isVisible()) { + if (mGearSnappedOnLeft == mCurrIconRow.isIconOnLeft()) { + boolean coveringGear = + Math.abs(getTranslation(animView)) <= getSpaceForGear(animView) * 0.6f; + if (gestureTowardsGear || coveringGear) { + // Gesture is towards or covering the gear + snapChild(animView, 0 /* leftTarget */, velocity); + } else if (isDismissGesture(ev)) { + // Gesture is a dismiss that's not towards the gear + dismissChild(animView, swipedFastEnough() ? velocity : 0f); + } else { + // Didn't move enough to dismiss or cover, snap to the gear + snapToGear(animView, velocity); + } + } else if ((!gestureFastEnough && swipedEnoughToShowGear(animView)) + || (gestureTowardsGear && !swipedFarEnough())) { + // The gear has been snapped to previously, however, the gear is now on the + // other side. If gesture is towards gear and not too far snap to the gear. + snapToGear(animView, velocity); + } else { + dismissOrSnapBack(animView, velocity, ev); + } + } else if ((!gestureFastEnough && swipedEnoughToShowGear(animView)) + || gestureTowardsGear) { + // Gear has not been snapped to previously and this is gear revealing gesture + snapToGear(animView, velocity); + } else { + dismissOrSnapBack(animView, velocity, ev); + } + return true; + } + + private void dismissOrSnapBack(View animView, float velocity, MotionEvent ev) { + if (isDismissGesture(ev)) { + dismissChild(animView, swipedFastEnough() ? velocity : 0f); + } else { + snapChild(animView, 0 /* leftTarget */, velocity); + } + } + + private void snapToGear(View animView, float velocity) { + final float snapBackThreshold = getSpaceForGear(animView); + final float target = mCurrIconRow.isIconOnLeft() ? snapBackThreshold + : -snapBackThreshold; + mGearExposedView = mTranslatingParentView; + if (mGearDisplayedListener != null + && (animView instanceof ExpandableNotificationRow)) { + mGearDisplayedListener.onGearDisplayed((ExpandableNotificationRow) animView); + } + if (mCurrIconRow != null) { + mCurrIconRow.setSnapping(true); + setSnappedToGear(true); + } + super.snapChild(animView, target, velocity); + } + + private boolean swipedEnoughToShowGear(View animView) { final float snapBackThreshold = getSpaceForGear(animView); final float translation = getTranslation(animView); final boolean fromLeft = translation > 0; final float absTrans = Math.abs(translation); final float notiThreshold = getSize(mTranslatingParentView) * 0.4f; - boolean pastGear = (fromLeft && translation >= snapBackThreshold * 0.4f - && translation <= notiThreshold) || - (!fromLeft && absTrans >= snapBackThreshold * 0.4f - && absTrans <= notiThreshold); - - if (pastGear && !isPinnedHeadsUp(animView) - && (animView instanceof ExpandableNotificationRow)) { - // bouncity - final float target = fromLeft ? snapBackThreshold : -snapBackThreshold; - mGearExposedView = mTranslatingParentView; - if (mGearDisplayedListener != null) { - mGearDisplayedListener.onGearDisplayed((ExpandableNotificationRow) animView); - } - super.snapChild(animView, target, velocity); - } else { - super.snapChild(animView, 0, velocity); - } + // If the notification can't be dismissed then how far it can move is + // restricted -- reduce the distance it needs to move in this case. + final float multiplier = canChildBeDismissed(animView) ? 0.4f : 0.2f; + return absTrans >= snapBackThreshold * 0.4f && absTrans <= notiThreshold; } @Override @@ -3488,6 +3576,25 @@ public class NotificationStackScrollLayout extends ViewGroup } } + /** + * Returns whether the gesture is towards the gear location or not. + */ + private boolean isTowardsGear(float velocity, boolean onLeft) { + if (mCurrIconRow == null) { + return false; + } + return mCurrIconRow.isVisible() + && ((onLeft && velocity <= 0) || (!onLeft && velocity >= 0)); + } + + /** + * Indicates the the gear has been snapped to. + */ + private void setSnappedToGear(boolean snapped) { + mGearSnappedOnLeft = (mCurrIconRow != null) ? mCurrIconRow.isIconOnLeft() : false; + mGearSnappedTo = snapped && mCurrIconRow != null; + } + /** * Returns the horizontal space in pixels required to display the gear behind a * notification. @@ -3500,7 +3607,7 @@ public class NotificationStackScrollLayout extends ViewGroup } private void checkForDrag() { - if (mCheckForDrag == null) { + if (mCheckForDrag == null || !mHandler.hasCallbacks(mCheckForDrag)) { mCheckForDrag = new CheckForDrag(); mHandler.postDelayed(mCheckForDrag, GEAR_SHOW_DELAY); } @@ -3511,7 +3618,6 @@ public class NotificationStackScrollLayout extends ViewGroup mCurrIconRow.cancelFadeAnimator(); } mHandler.removeCallbacks(mCheckForDrag); - mCheckForDrag = null; } private final class CheckForDrag implements Runnable { @@ -3521,14 +3627,13 @@ public class NotificationStackScrollLayout extends ViewGroup final float absTransX = Math.abs(translation); final float bounceBackToGearWidth = getSpaceForGear(mTranslatingParentView); final float notiThreshold = getSize(mTranslatingParentView) * 0.4f; - if (mCurrIconRow != null && absTransX >= bounceBackToGearWidth * 0.4 + if ((mCurrIconRow != null && (!mCurrIconRow.isVisible() + || mCurrIconRow.isIconLocationChange(translation))) + && absTransX >= bounceBackToGearWidth * 0.4 && absTransX < notiThreshold) { - // Show icon + // Fade in the gear mCurrIconRow.fadeInSettings(translation > 0 /* fromLeft */, translation, notiThreshold); - } else { - // Allow more to be posted if this wasn't a drag. - mCheckForDrag = null; } } } @@ -3541,7 +3646,7 @@ public class NotificationStackScrollLayout extends ViewGroup final View prevGearExposedView = mGearExposedView; mGearExposedView = null; - + mGearSnappedTo = false; Animator anim = getViewTranslationAnimator(prevGearExposedView, 0 /* leftTarget */, null /* updateListener */); if (anim != null) {