OSDN Git Service

Introduced basic animations for the new notifications.
authorSelim Cinek <cinek@google.com>
Fri, 25 Apr 2014 14:43:27 +0000 (16:43 +0200)
committerSelim Cinek <cinek@google.com>
Tue, 29 Apr 2014 22:01:06 +0000 (00:01 +0200)
Animations between two different states of the notification stack scroller
are now possible.

Bug: 14081264
Change-Id: I2b8e964095f71766feac5a76c4e3b85d22648d35

packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java
packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java [new file with mode: 0644]

index 90b63f4..3ef64bb 100644 (file)
@@ -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++;
             }
index d6d90a6..2e30594 100644 (file)
@@ -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;
+        mCurrentStackScrollState.setScrollY(mOwnScrollY);
+        mStackScrollAlgorithm.getStackScrollState(mCurrentStackScrollState);
+        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;
+        }
+    }
+
 }
index 9215110..8c75adc 100644 (file)
@@ -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);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
new file mode 100644 (file)
index 0000000..9c6238f
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * 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);
+            }
+        }
+    }
+}