Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e1193672 authored by Mady Mellor's avatar Mady Mellor Committed by Android (Google) Code Review
Browse files

Merge "Improve interaction around showing / hiding the gear behind notifications" into nyc-dev

parents 18fafa4b 273ed107
Loading
Loading
Loading
Loading
+48 −27
Original line number Diff line number Diff line
@@ -517,35 +517,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);

                    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;
                if (mCurrView == null) {
                    break;
                }
                mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, getMaxVelocity());
                float velocity = getVelocity(mVelocityTracker);

                    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);
@@ -562,6 +543,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);

+62 −22
Original line number Diff line number Diff line
@@ -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() {
@@ -164,40 +191,53 @@ 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;
            public void onAnimationStart(Animator animation) {
                mAnimating = true;
            }

            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                mAnimating = true;
            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) {
+148 −43
Original line number Diff line number Diff line
@@ -371,6 +371,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);
@@ -717,12 +722,16 @@ public class NotificationStackScrollLayout extends ViewGroup
            mDragAnimPendingChildren.remove(animView);
        }

        if (targetLeft == 0 && mCurrIconRow != null) {
        if (mCurrIconRow != null) {
            if (targetLeft == 0) {
                mCurrIconRow.resetState();
                mCurrIconRow = null;
                if (mGearExposedView != null && mGearExposedView == mTranslatingParentView) {
                    mGearExposedView = null;
                }
            } else {
                mSwipeHelper.setSnappedToGear(true);
            }
        }
    }

@@ -3379,15 +3388,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);
@@ -3400,6 +3405,10 @@ public class NotificationStackScrollLayout extends ViewGroup
            mTranslatingParentView = currView;

            // Reset check for drag gesture
            cancelCheckForDrag();
            if (mCurrIconRow != null) {
                mCurrIconRow.setSnapping(false);
            }
            mCheckForDrag = null;
            mCurrIconRow = null;

@@ -3411,17 +3420,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.
            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();
@@ -3434,35 +3458,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) {
            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;
            super.snapChild(animView, targetLeft, velocity);
            if (targetLeft == 0) {
                cancelCheckForDrag();
                setSnappedToGear(false);
            }
        }

            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;
        @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) {
            if (mGearDisplayedListener != null
                    && (animView instanceof ExpandableNotificationRow)) {
                mGearDisplayedListener.onGearDisplayed((ExpandableNotificationRow) animView);
            }
            if (mCurrIconRow != null) {
                mCurrIconRow.setSnapping(true);
                setSnappedToGear(true);
            }
            super.snapChild(animView, target, velocity);
            } else {
                super.snapChild(animView, 0, 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;

            // 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
@@ -3498,6 +3586,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.
@@ -3510,7 +3617,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);
            }
@@ -3521,7 +3628,6 @@ public class NotificationStackScrollLayout extends ViewGroup
                mCurrIconRow.cancelFadeAnimator();
            }
            mHandler.removeCallbacks(mCheckForDrag);
            mCheckForDrag = null;
        }

        private final class CheckForDrag implements Runnable {
@@ -3531,14 +3637,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;
                }
            }
        }
@@ -3551,7 +3656,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) {