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

Commit 572bbd42 authored by Selim Cinek's avatar Selim Cinek
Browse files

Introduced basic animations for the new notifications.

Animations between two different states of the notification stack scroller
are now possible.

Bug: 14081264
Change-Id: I2b8e964095f71766feac5a76c4e3b85d22648d35
parent 707eb8ba
Loading
Loading
Loading
Loading
+4 −1
Original line number Diff line number Diff line
@@ -71,7 +71,6 @@ import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.statusbar.StatusBarIconList;
import com.android.internal.util.LegacyNotificationUtil;
import com.android.internal.widget.SizeAdaptiveLayout;
import com.android.systemui.R;
import com.android.systemui.RecentsComponent;
import com.android.systemui.SearchPanelView;
@@ -1078,6 +1077,10 @@ public abstract class BaseStatusBar extends SystemUI implements
                    mKeyguardIconOverflowContainer.getIconsView().addNotification(entry);
                }
            } else {
                if (entry.row.getVisibility() == View.GONE) {
                    // notify the scroller of a child addition
                    mStackScroller.generateAddAnimation(entry.row);
                }
                entry.row.setVisibility(View.VISIBLE);
                visibleNotifications++;
            }
+162 −20
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.AnimationUtils;
import android.widget.OverScroller;

import com.android.systemui.ExpandHelper;
@@ -41,6 +42,8 @@ import com.android.systemui.statusbar.ExpandableView;
import com.android.systemui.statusbar.stack.StackScrollState.ViewState;
import com.android.systemui.statusbar.policy.ScrollAdapter;

import java.util.ArrayList;

/**
 * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack.
 */
@@ -90,10 +93,28 @@ public class NotificationStackScrollLayout extends ViewGroup
    /**
     * The current State this Layout is in
     */
    private final StackScrollState mCurrentStackScrollState = new StackScrollState(this);
    private StackScrollState mCurrentStackScrollState = new StackScrollState(this);
    private ArrayList<View> mChildrenToAddAnimated = new ArrayList<View>();
    private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<View>();
    private ArrayList<ChildHierarchyChangeEvent> mAnimationEvents
            = new ArrayList<ChildHierarchyChangeEvent>();
    private ArrayList<View> mSwipedOutViews = new ArrayList<View>();
    private final StackStateAnimator mStateAnimator = new StackStateAnimator(this);

    private OnChildLocationsChangedListener mListener;
    private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
    private boolean mChildHierarchyDirty;
    private boolean mIsExpanded = true;
    private ViewTreeObserver.OnPreDrawListener mAfterLayoutPreDrawListener
            = new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            updateScrollPositionIfNecessary();
            updateChildren();
            getViewTreeObserver().removeOnPreDrawListener(this);
            return true;
        }
    };

    public NotificationStackScrollLayout(Context context) {
        this(context, null);
@@ -184,16 +205,7 @@ public class NotificationStackScrollLayout extends ViewGroup
        }
        setMaxLayoutHeight(getHeight() - mEmptyMarginBottom);
        updateContentHeight();
        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                updateScrollPositionIfNecessary();
                updateChildren();
                getViewTreeObserver().removeOnPreDrawListener(this);
                return true;
            }
        });

        getViewTreeObserver().addOnPreDrawListener(mAfterLayoutPreDrawListener);
    }

    public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) {
@@ -228,22 +240,20 @@ public class NotificationStackScrollLayout extends ViewGroup
     * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout.
     */
    private void updateChildren() {
        if (!isCurrentlyAnimating()) {
        mCurrentStackScrollState.setScrollY(mOwnScrollY);
        mStackScrollAlgorithm.getStackScrollState(mCurrentStackScrollState);
            mListenForHeightChanges = false;
            mCurrentStackScrollState.apply();
            mListenForHeightChanges = true;
        if (!isCurrentlyAnimating() && !mChildHierarchyDirty) {
            applyCurrentState();
            if (mListener != null) {
                mListener.onChildLocationsChanged(this);
            }
        } else {
            // TODO: handle animation
            startAnimationToState(mCurrentStackScrollState);
        }
    }

    private boolean isCurrentlyAnimating() {
        return false;
        return mStateAnimator.isRunning();
    }

    private void updateScrollPositionIfNecessary() {
@@ -288,6 +298,7 @@ public class NotificationStackScrollLayout extends ViewGroup
            veto.performClick();
        }
        setSwipingInProgress(false);
        mSwipedOutViews.add(v);
    }

    public void onBeginDrag(View v) {
@@ -734,6 +745,50 @@ public class NotificationStackScrollLayout extends ViewGroup
        ((ExpandableView) child).setOnHeightChangedListener(null);
        mCurrentStackScrollState.removeViewStateForView(child);
        mStackScrollAlgorithm.notifyChildrenChanged(this);
        updateScrollStateForRemovedChild(child);
        if (mIsExpanded) {

            // Generate Animations
            mChildrenToRemoveAnimated.add(child);
            mChildHierarchyDirty = true;
        }
    }

    /**
     * Updates the scroll position when a child was removed
     *
     * @param removedChild the removed child
     */
    private void updateScrollStateForRemovedChild(View removedChild) {
        int startingPosition = getPositionInLinearLayout(removedChild);
        int childHeight = removedChild.getHeight() + mPaddingBetweenElements;
        int endPosition = startingPosition + childHeight;
        if (endPosition <= mOwnScrollY) {
            // This child is fully scrolled of the top, so we have to deduct its height from the
            // scrollPosition
            mOwnScrollY -= childHeight;
        } else if (startingPosition < mOwnScrollY) {
            // This child is currently being scrolled into, set the scroll position to the start of
            // this child
            mOwnScrollY = startingPosition;
        }
    }

    private int getPositionInLinearLayout(View requestedChild) {
        int position = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child == requestedChild) {
                return position;
            }
            if (child.getVisibility() != View.GONE) {
                position += child.getHeight();
                if (i < getChildCount()-1) {
                    position += mPaddingBetweenElements;
                }
            }
        }
        return 0;
    }

    @Override
@@ -741,6 +796,64 @@ public class NotificationStackScrollLayout extends ViewGroup
        super.onViewAdded(child);
        mStackScrollAlgorithm.notifyChildrenChanged(this);
        ((ExpandableView) child).setOnHeightChangedListener(this);
        if (child.getVisibility() != View.GONE) {
            generateAddAnimation(child);
        }
    }

    public void generateAddAnimation(View child) {
        if (mIsExpanded) {

            // Generate Animations
            mChildrenToAddAnimated.add(child);
            mChildHierarchyDirty = true;
        }
    }

    /**
     * Change the position of child to a new location
     *
     * @param child the view to change the position for
     * @param newIndex the new index
     */
    public void changeViewPosition(View child, int newIndex) {
        if (child != null && child.getParent() == this) {
            // TODO: handle this
        }
    }

    private void startAnimationToState(StackScrollState finalState) {
        if (mChildHierarchyDirty) {
            generateChildHierarchyEvents();
            mChildHierarchyDirty = false;
        }
        mStateAnimator.startAnimationForEvents(mAnimationEvents, finalState);
    }

    private void generateChildHierarchyEvents() {
        generateChildAdditionEvents();
        generateChildRemovalEvents();
        mChildHierarchyDirty = false;
    }

    private void generateChildRemovalEvents() {
        for (View  child : mChildrenToRemoveAnimated) {
            boolean childWasSwipedOut = mSwipedOutViews.contains(child);
            int animationType = childWasSwipedOut
                    ? ChildHierarchyChangeEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT
                    : ChildHierarchyChangeEvent.ANIMATION_TYPE_REMOVE;
            mAnimationEvents.add(new ChildHierarchyChangeEvent(child, animationType));
        }
        mSwipedOutViews.clear();
        mChildrenToRemoveAnimated.clear();
    }

    private void generateChildAdditionEvents() {
        for (View  child : mChildrenToAddAnimated) {
            mAnimationEvents.add(new ChildHierarchyChangeEvent(child,
                    ChildHierarchyChangeEvent.ANIMATION_TYPE_ADD));
        }
        mChildrenToAddAnimated.clear();
    }

    private boolean onInterceptTouchEventScroll(MotionEvent ev) {
@@ -895,6 +1008,7 @@ public class NotificationStackScrollLayout extends ViewGroup
    }

    public void setIsExpanded(boolean isExpanded) {
        mIsExpanded = isExpanded;
        mStackScrollAlgorithm.setIsExpanded(isExpanded);
        if (!isExpanded) {
            mOwnScrollY = 0;
@@ -903,7 +1017,7 @@ public class NotificationStackScrollLayout extends ViewGroup

    @Override
    public void onHeightChanged(ExpandableView view) {
        if (mListenForHeightChanges) {
        if (mListenForHeightChanges && !isCurrentlyAnimating()) {
            updateContentHeight();
            updateScrollPositionIfNecessary();
            if (mOnHeightChangedListener != null) {
@@ -918,10 +1032,38 @@ public class NotificationStackScrollLayout extends ViewGroup
        this.mOnHeightChangedListener = mOnHeightChangedListener;
    }

    public void onChildAnimationFinished() {
        applyCurrentState();
        mAnimationEvents.clear();
    }

    private void applyCurrentState() {
        mListenForHeightChanges = false;
        mCurrentStackScrollState.apply();
        mListenForHeightChanges = true;
    }

    /**
     * A listener that is notified when some child locations might have changed.
     */
    public interface OnChildLocationsChangedListener {
        public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout);
    }

    static class ChildHierarchyChangeEvent {

        static int ANIMATION_TYPE_ADD = 1;
        static int ANIMATION_TYPE_REMOVE = 2;
        static int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 3;
        final long eventStartTime;
        final View changingView;
        final int animationType;

        ChildHierarchyChangeEvent(View view, int type) {
            eventStartTime = AnimationUtils.currentAnimationTimeMillis();
            changingView = view;
            animationType = type;
        }
    }

}
+1 −2
Original line number Diff line number Diff line
@@ -72,12 +72,11 @@ public class StackScrollState {
            }
            // initialize with the default values of the view
            viewState.height = child.getActualHeight();
            viewState.alpha = 1;
            viewState.gone = child.getVisibility() == View.GONE;
            viewState.alpha = 1;
        }
    }


    public ViewState getViewStateForView(View requestedView) {
        return mStateMap.get(requestedView);
    }
+148 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2014 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.systemui.statusbar.stack;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.view.View;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import com.android.systemui.statusbar.ExpandableView;

import java.util.ArrayList;

/**
 * An stack state animator which handles animations to new StackScrollStates
 */
public class StackStateAnimator {

    private static final int ANIMATION_DURATION = 360;

    private final Interpolator mFastOutSlowInInterpolator;
    public NotificationStackScrollLayout mHostLayout;
    private boolean mAnimationIsRunning;
    private ArrayList<NotificationStackScrollLayout.ChildHierarchyChangeEvent> mHandledEvents =
            new ArrayList<>();

    public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
        mHostLayout = hostLayout;
        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(hostLayout.getContext(),
                        android.R.interpolator.fast_out_slow_in);
    }

    public boolean isRunning() {
        return mAnimationIsRunning;
    }

    public void startAnimationForEvents(
            ArrayList<NotificationStackScrollLayout.ChildHierarchyChangeEvent> mAnimationEvents,
            StackScrollState finalState) {
        int numEvents = mAnimationEvents.size();
        if (numEvents == 0) {
            // No events, so we don't perform any animation
            return;
        }
        long lastEventStartTime = mAnimationEvents.get(numEvents - 1).eventStartTime;
        long eventEnd = lastEventStartTime + ANIMATION_DURATION;
        long currentTime = AnimationUtils.currentAnimationTimeMillis();
        long newDuration = eventEnd - currentTime;
        if (newDuration <= 0) {
            // last event is long before this, so we don't do anything
            return;
        }
        initializeAddedViewStates(mAnimationEvents, finalState);
        int childCount = mHostLayout.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final boolean isFirstView = i == 0;
            final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
            StackScrollState.ViewState viewState = finalState.getViewStateForView(child);
            if (viewState == null) {
                continue;
            }
            int childVisibility = child.getVisibility();
            boolean wasVisible = childVisibility == View.VISIBLE;
            final float alpha = viewState.alpha;
            if (!wasVisible && alpha != 0 && !viewState.gone) {
                child.setVisibility(View.VISIBLE);
            }

            startPropertyAnimation(newDuration, isFirstView, child, viewState, alpha);

            // TODO: animate clipBounds
            child.setClipBounds(null);
            int currentHeigth = child.getActualHeight();
            if (viewState.height != currentHeigth) {
                startHeightAnimation(newDuration, child, viewState, currentHeigth);
            }
        }
        mAnimationIsRunning = true;
    }

    private void startPropertyAnimation(long newDuration, final boolean isFirstView,
            final ExpandableView child, StackScrollState.ViewState viewState, final float alpha) {
        child.animate().setInterpolator(mFastOutSlowInInterpolator)
                .alpha(alpha)
                .translationY(viewState.yTranslation)
                .translationZ(viewState.zTranslation)
                .setDuration(newDuration)
                .withEndAction(new Runnable() {
                    @Override
                    public void run() {
                        mAnimationIsRunning = false;
                        if (isFirstView) {
                            mHandledEvents.clear();
                            mHostLayout.onChildAnimationFinished();
                        }
                        if (alpha == 0) {
                            child.setVisibility(View.INVISIBLE);
                        }
                    }
                });
    }

    private void startHeightAnimation(long newDuration, final ExpandableView child,
            StackScrollState.ViewState viewState, int currentHeigth) {
        ValueAnimator heightAnimator = ValueAnimator.ofInt(currentHeigth, viewState.height);
        heightAnimator.setInterpolator(mFastOutSlowInInterpolator);
        heightAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                child.setActualHeight((int) animation.getAnimatedValue());
            }
        });
        heightAnimator.setDuration(newDuration);
        heightAnimator.start();
    }

    private void initializeAddedViewStates(
            ArrayList<NotificationStackScrollLayout.ChildHierarchyChangeEvent> mAnimationEvents,
            StackScrollState finalState) {
        for (NotificationStackScrollLayout.ChildHierarchyChangeEvent event: mAnimationEvents) {
            View changingView = event.changingView;
            if (event.animationType == NotificationStackScrollLayout.ChildHierarchyChangeEvent
                    .ANIMATION_TYPE_ADD && !mHandledEvents.contains(event)) {

                // This item is added, initialize it's properties.
                StackScrollState.ViewState viewState = finalState.getViewStateForView(changingView);
                changingView.setAlpha(0);
                changingView.setTranslationY(viewState.yTranslation);
                changingView.setTranslationZ(viewState.zTranslation);
                mHandledEvents.add(event);
            }
        }
    }
}