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

Commit 56e0d6c3 authored by Lyn Han's avatar Lyn Han
Browse files

Update clear all animation

- Fade out clear all button on tap
- Fade out silent label once there are no silent notifs
- Fade out footer once there are no notifs
- Round corners of dismissing notifs
- Pause to show "no notifications" before closing shade

Timing
- Remove wait before clear all swipe animations start
- Wait less between individual swipe animations
- Faster swipe animation

Bug: 172065269
Test: clear all with scroll, silent notifs, no ongoing notifs
	=> clear button fades out on tap
	=> clearable notifs swipe out with new delay, timing
	=> silent header fades
	=> no scroll change
	=> show "no notifications" at top of shade
	=> close shade
Test: clear all with no scroll, ongoing silent notifs
        => clearable silent notifs swipe out
	=> ongoing silent notifs stay
	=> silent header "x" fades
	=> show "no notifications" at top of shade
	=> close shade
Test: clear a lot of notifications
	=> pause same amount of time after swipe animations
	before closing shade
Test: press "x" to clear silent notifications
	=> silent notifications animate away
	=> shade stays open if there are other clearable notifs

Change-Id: I4db85068f46b95a34e71abb6428d3c2ae757cf0a
parent 6c705ff3
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -451,6 +451,12 @@ public class SwipeHelper implements Gefingerpoken {
        anim.addListener(new AnimatorListenerAdapter() {
            private boolean mCancelled;

            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                mCallback.onBeginDrag(animView);
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                mCancelled = true;
+22 −15
Original line number Diff line number Diff line
@@ -85,23 +85,26 @@ public abstract class StackScrollerDecorView extends ExpandableView {
    }

    /**
     * Set the content of this view to be visible in an animated way.
     *
     * @param contentVisible True if the content should be visible or false if it should be hidden.
     * @param visible True if we should animate contents visible
     */
    public void setContentVisible(boolean contentVisible) {
        setContentVisible(contentVisible, true /* animate */);
    public void setContentVisible(boolean visible) {
        setContentVisible(visible, true /* animate */, null /* runAfter */);
    }

    /**
     * Set the content of this view to be visible.
     * @param contentVisible True if the content should be visible or false if it should be hidden.
     * @param animate Should an animation be performed.
     * @param visible True if the contents should be visible
     * @param animate True if we should fade to new visibility
     * @param runAfter Runnable to run after visibility updates
     */
    private void setContentVisible(boolean contentVisible, boolean animate) {
        if (mContentVisible != contentVisible) {
    public void setContentVisible(boolean visible, boolean animate, Runnable runAfter) {
        if (mContentVisible != visible) {
            mContentAnimating = animate;
            mContentVisible = contentVisible;
            setViewVisible(mContent, contentVisible, animate, mContentVisibilityEndRunnable);
            mContentVisible = visible;
            Runnable endRunnable = runAfter == null ? mContentVisibilityEndRunnable : () -> {
                mContentVisibilityEndRunnable.run();
                runAfter.run();
            };
            setViewVisible(mContent, visible, animate, endRunnable);
        }

        if (!mContentAnimating) {
@@ -113,6 +116,10 @@ public abstract class StackScrollerDecorView extends ExpandableView {
        return mContentVisible;
    }

    public void setVisible(boolean nowVisible, boolean animate) {
        setVisible(nowVisible, animate, null);
    }

    /**
     * Make this view visible. If {@code false} is passed, the view will fade out it's content
     * and set the view Visibility to GONE. If only the content should be changed
@@ -121,7 +128,7 @@ public abstract class StackScrollerDecorView extends ExpandableView {
     * @param nowVisible should the view be visible
     * @param animate should the change be animated.
     */
    public void setVisible(boolean nowVisible, boolean animate) {
    public void setVisible(boolean nowVisible, boolean animate, Runnable runAfter) {
        if (mIsVisible != nowVisible) {
            mIsVisible = nowVisible;
            if (animate) {
@@ -132,10 +139,10 @@ public abstract class StackScrollerDecorView extends ExpandableView {
                } else {
                    setWillBeGone(true);
                }
                setContentVisible(nowVisible, true /* animate */);
                setContentVisible(nowVisible, true /* animate */, runAfter);
            } else {
                setVisibility(nowVisible ? VISIBLE : GONE);
                setContentVisible(nowVisible, false /* animate */);
                setContentVisible(nowVisible, false /* animate */, runAfter);
                setWillBeGone(false);
                notifyHeightChanged(false /* needsAnimation */);
            }
+10 −0
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ public class NotificationRoundnessManager {
    private ExpandableNotificationRow mTrackedHeadsUp;
    private float mAppearFraction;
    private boolean mRoundForPulsingViews;
    private boolean mIsDismissAllInProgress;

    private ExpandableView mSwipedView = null;
    private ExpandableView mViewBeforeSwipedView = null;
@@ -158,6 +159,10 @@ public class NotificationRoundnessManager {
        }
    }

    void setDismissAllInProgress(boolean isClearingAll) {
        mIsDismissAllInProgress = isClearingAll;
    }

    private float getRoundnessFraction(ExpandableView view, boolean top) {
        if (view == null) {
            return 0f;
@@ -167,6 +172,11 @@ public class NotificationRoundnessManager {
                || view == mViewAfterSwipedView) {
            return 1f;
        }
        if (view instanceof ExpandableNotificationRow
                && ((ExpandableNotificationRow) view).canViewBeDismissed()
                && mIsDismissAllInProgress) {
            return 1.0f;
        }
        if ((view.isPinned()
                || (view.isHeadsUpAnimatingAway()) && !mExpanded)) {
            return 1.0f;
+124 −94
Original line number Diff line number Diff line
@@ -139,6 +139,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
    private static final boolean DEBUG_REMOVE_ANIMATION = SystemProperties.getBoolean(
            "persist.debug.nssl.dismiss", false /* default */);

    // Delay in milli-seconds before shade closes for clear all.
    private final int DELAY_BEFORE_SHADE_CLOSE = 200;
    private boolean mShadeNeedsToClose = false;

    private static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f;
    private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f;
    private static final float RUBBER_BAND_FACTOR_ON_PANEL_EXPAND = 0.21f;
@@ -253,7 +257,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
    protected FooterView mFooterView;
    protected EmptyShadeView mEmptyShadeView;
    private boolean mDismissAllInProgress;
    private boolean mFadeNotificationsOnDismiss;
    private FooterDismissListener mFooterDismissListener;
    private boolean mFlingAfterUpEvent;

@@ -1205,7 +1208,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
    @ShadeViewRefactor(RefactorComponent.COORDINATOR)
    private void clampScrollPosition() {
        int scrollRange = getScrollRange();
        if (scrollRange < mOwnScrollY) {
        if (scrollRange < mOwnScrollY && !mAmbientState.isDismissAllInProgress()) {
            boolean animateStackY = false;
            if (scrollRange < getScrollAmountToScrollBoundary()
                    && mAnimateStackYForContentHeightChange) {
@@ -1721,6 +1724,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable

    @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
    public void dismissViewAnimated(View child, Runnable endRunnable, int delay, long duration) {
        if (child instanceof SectionHeaderView) {
             ((StackScrollerDecorView) child).setContentVisible(
                     false /* visible */, true /* animate */, endRunnable);
             return;
        }
        mSwipeHelper.dismissChild(child, 0, endRunnable, delay, true, duration,
                true /* isDismissAll */);
    }
@@ -4062,6 +4070,18 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
        runAnimationFinishedRunnables();
        clearTransient();
        clearHeadsUpDisappearRunning();

        if (mAmbientState.isDismissAllInProgress()) {
            setDismissAllInProgress(false);
            if (mShadeNeedsToClose) {
                mShadeNeedsToClose = false;
                postDelayed(
                        () -> {
                            mShadeController.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE);
                        },
                        DELAY_BEFORE_SHADE_CLOSE /* delayMillis */);
            }
        }
    }

    @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
@@ -4430,6 +4450,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
    public void setDismissAllInProgress(boolean dismissAllInProgress) {
        mDismissAllInProgress = dismissAllInProgress;
        mAmbientState.setDismissAllInProgress(dismissAllInProgress);
        mController.getNoticationRoundessManager().setDismissAllInProgress(dismissAllInProgress);
        handleDismissAllClipping();
    }

@@ -4973,129 +4994,137 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
        mHeadsUpAppearanceController = headsUpAppearanceController;
    }

    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
    @VisibleForTesting
    void clearNotifications(@SelectedRows int selection, boolean closeShade) {
        // animate-swipe all dismissable notifications, then animate the shade closed
        int numChildren = getChildCount();

        final ArrayList<View> viewsToHide = new ArrayList<>(numChildren);
        final ArrayList<ExpandableNotificationRow> viewsToRemove = new ArrayList<>(numChildren);
        for (int i = 0; i < numChildren; i++) {
            final View child = getChildAt(i);
            if (child instanceof ExpandableNotificationRow) {
                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
                boolean parentVisible = false;
    private boolean isVisible(View child) {
        boolean hasClipBounds = child.getClipBounds(mTmpRect);
                if (includeChildInDismissAll(row, selection)) {
                    viewsToRemove.add(row);
                    if (child.getVisibility() == View.VISIBLE
                            && (!hasClipBounds || mTmpRect.height() > 0)) {
                        viewsToHide.add(child);
                        parentVisible = true;
        return child.getVisibility() == View.VISIBLE
                && (!hasClipBounds || mTmpRect.height() > 0);
    }
                } else if (child.getVisibility() == View.VISIBLE
                        && (!hasClipBounds || mTmpRect.height() > 0)) {
                    parentVisible = true;

    private boolean shouldHideParent(View view, @SelectedRows int selection) {
        final boolean silentSectionWillBeGone =
                !mController.hasNotifications(ROWS_GENTLE, false /* clearable */);

        // The only SectionHeaderView we have is the silent section header.
        if (view instanceof SectionHeaderView && silentSectionWillBeGone) {
            return true;
        }
                List<ExpandableNotificationRow> children = row.getAttachedChildren();
                if (children != null) {
                    for (ExpandableNotificationRow childRow : children) {
                        if (includeChildInDismissAll(row, selection)) {
                            viewsToRemove.add(childRow);
                            if (parentVisible && row.areChildrenExpanded()) {
                                hasClipBounds = childRow.getClipBounds(mTmpRect);
                                if (childRow.getVisibility() == View.VISIBLE
                                        && (!hasClipBounds || mTmpRect.height() > 0)) {
                                    viewsToHide.add(childRow);
        if (view instanceof ExpandableNotificationRow) {
            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
            if (isVisible(row) && includeChildInDismissAll(row, selection)) {
                return true;
            }
        }
        return false;
    }

    private boolean isChildrenVisible(ExpandableNotificationRow parent) {
        List<ExpandableNotificationRow> children = parent.getAttachedChildren();
        return isVisible(parent)
                && children != null
                && parent.areChildrenExpanded();
    }

    // Similar to #getRowsToDismissInBackend, but filters for visible views.
    private ArrayList<View> getVisibleViewsToAnimateAway(@SelectedRows int selection) {
        final int viewCount = getChildCount();
        final ArrayList<View> viewsToHide = new ArrayList<>(viewCount);

        for (int i = 0; i < viewCount; i++) {
            final View view = getChildAt(i);

            if (shouldHideParent(view, selection)) {
                viewsToHide.add(view);
            }
            if (view instanceof ExpandableNotificationRow) {
                ExpandableNotificationRow parent = (ExpandableNotificationRow) view;

                if (isChildrenVisible(parent)) {
                    for (ExpandableNotificationRow child : parent.getAttachedChildren()) {
                        if (isVisible(child) && includeChildInDismissAll(child, selection)) {
                            viewsToHide.add(child);
                        }
                    }

        if (mDismissListener != null) {
            mDismissListener.onDismiss(selection);
                }

        if (viewsToRemove.isEmpty()) {
            if (closeShade && mShadeController != null) {
                mShadeController.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE);
            }
            return;
        }

        performDismissAllAnimations(
                viewsToHide,
                closeShade,
                () -> onDismissAllAnimationsEnd(viewsToRemove, selection));
        return viewsToHide;
    }

    private boolean includeChildInDismissAll(
            ExpandableNotificationRow row,
    private ArrayList<ExpandableNotificationRow> getRowsToDismissInBackend(
            @SelectedRows int selection) {
        return canChildBeDismissed(row) && matchesSelection(row, selection);
        final int childCount = getChildCount();
        final ArrayList<ExpandableNotificationRow> viewsToRemove = new ArrayList<>(childCount);

        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            if (!(view instanceof ExpandableNotificationRow)) {
                continue;
            }
            ExpandableNotificationRow parent = (ExpandableNotificationRow) view;
            if (includeChildInDismissAll(parent, selection)) {
                viewsToRemove.add(parent);
            }
            List<ExpandableNotificationRow> children = parent.getAttachedChildren();
            if (isVisible(parent) && children != null) {
                for (ExpandableNotificationRow child : children) {
                    if (includeChildInDismissAll(parent, selection)) {
                        viewsToRemove.add(child);
                    }
                }
            }
        }
        return viewsToRemove;
    }

    /**
     * Given a list of rows, animates them away in a staggered fashion as if they were dismissed.
     * Doesn't actually dismiss them, though -- that must be done in the onAnimationComplete
     * handler.
     *
     * @param hideAnimatedList List of rows to animated away. Should only be views that are
     *                         currently visible, or else the stagger will look funky.
     * @param closeShade Whether to close the shade after the stagger animation completes.
     * @param onAnimationComplete Called after the entire animation completes (including the shade
     *                            closing if appropriate). The rows must be dismissed for real here.
     * Collects a list of visible rows, and animates them away in a staggered fashion as if they
     * were dismissed. Notifications are dismissed in the backend via onDismissAllAnimationsEnd.
     */
    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
    private void performDismissAllAnimations(
            final ArrayList<View> hideAnimatedList,
            final boolean closeShade,
            final Runnable onAnimationComplete) {

        final Runnable onSlideAwayAnimationComplete = () -> {
            if (closeShade) {
                mShadeController.addPostCollapseAction(() -> {
                    setDismissAllInProgress(false);
                    onAnimationComplete.run();
                });
                mShadeController.animateCollapsePanels(
                        CommandQueue.FLAG_EXCLUDE_NONE);
            } else {
                setDismissAllInProgress(false);
                onAnimationComplete.run();
    @VisibleForTesting
    void clearNotifications(@SelectedRows int selection, boolean closeShade) {
        // Animate-swipe all dismissable notifications, then animate the shade closed
        final ArrayList<View> viewsToAnimateAway = getVisibleViewsToAnimateAway(selection);
        final ArrayList<ExpandableNotificationRow> rowsToDismissInBackend =
                getRowsToDismissInBackend(selection);
        if (mDismissListener != null) {
            mDismissListener.onDismiss(selection);
        }
        final Runnable dismissInBackend = () -> {
            onDismissAllAnimationsEnd(rowsToDismissInBackend, selection);
        };

        if (hideAnimatedList.isEmpty()) {
            onSlideAwayAnimationComplete.run();
        if (viewsToAnimateAway.isEmpty()) {
            dismissInBackend.run();
            return;
        }

        // let's disable our normal animations
        // Disable normal animations
        setDismissAllInProgress(true);
        mShadeNeedsToClose = closeShade;

        // Decrease the delay for every row we animate to give the sense of
        // accelerating the swipes
        int rowDelayDecrement = 10;
        int currentDelay = 140;
        int totalDelay = 180;
        int numItems = hideAnimatedList.size();
        final int rowDelayDecrement = 5;
        int currentDelay = 60;
        int totalDelay = 0;
        final int numItems = viewsToAnimateAway.size();
        for (int i = numItems - 1; i >= 0; i--) {
            View view = hideAnimatedList.get(i);
            View view = viewsToAnimateAway.get(i);
            Runnable endRunnable = null;
            if (i == 0) {
                endRunnable = onSlideAwayAnimationComplete;
                endRunnable = dismissInBackend;
            }
            dismissViewAnimated(view, endRunnable, totalDelay, ANIMATION_DURATION_SWIPE);
            currentDelay = Math.max(50, currentDelay - rowDelayDecrement);
            currentDelay = Math.max(30, currentDelay - rowDelayDecrement);
            totalDelay += currentDelay;
        }
    }

    private boolean includeChildInDismissAll(
            ExpandableNotificationRow row,
            @SelectedRows int selection) {
        return canChildBeDismissed(row) && matchesSelection(row, selection);
    }

    /** Register a {@link View.OnClickListener} to be invoked when the Manage button is clicked. */
    public void setManageButtonClickListener(@Nullable OnClickListener listener) {
        mManageButtonClickListener = listener;
@@ -5114,6 +5143,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
                mFooterDismissListener.onDismiss();
            }
            clearNotifications(ROWS_ALL, true /* closeShade */);
            footerView.setSecondaryVisible(false /* visible */, true /* animate */);
        });
        setFooterView(footerView);
    }
+13 −2
Original line number Diff line number Diff line
@@ -1157,6 +1157,10 @@ public class NotificationStackScrollLayoutController {
                mZenModeController.areNotificationsHiddenInShade());
    }

    public boolean areNotificationsHiddenInShade() {
        return mZenModeController.areNotificationsHiddenInShade();
    }

    public boolean isShowingEmptyShadeView() {
        return mShowEmptyShadeView;
    }
@@ -1205,6 +1209,10 @@ public class NotificationStackScrollLayoutController {
     * Return whether there are any clearable notifications
     */
    public boolean hasActiveClearableNotifications(@SelectedRows int selection) {
        return hasNotifications(selection, true /* clearable */);
    }

    public boolean hasNotifications(@SelectedRows int selection, boolean isClearable) {
        if (mDynamicPrivacyController.isInLockedDownShade()) {
            return false;
        }
@@ -1215,8 +1223,11 @@ public class NotificationStackScrollLayoutController {
                continue;
            }
            final ExpandableNotificationRow row = (ExpandableNotificationRow) child;
            if (row.canViewBeDismissed() &&
                    NotificationStackScrollLayout.matchesSelection(row, selection)) {
            final boolean matchClearable =
                    isClearable ? row.canViewBeDismissed() : !row.canViewBeDismissed();
            final boolean inSection =
                    NotificationStackScrollLayout.matchesSelection(row, selection);
            if (matchClearable && inSection) {
                return true;
            }
        }
Loading