diff options
author | Selim Cinek <cinek@google.com> | 2014-04-25 16:43:27 +0200 |
---|---|---|
committer | Selim Cinek <cinek@google.com> | 2014-04-30 00:01:06 +0200 |
commit | 572bbd42a473980c2d59af80d378f6270ba6860a (patch) | |
tree | ea2aa8a5b237983d1cbac10ca14cb4cd5d23de6c | |
parent | 707eb8bab9a844812e418750a79b25938f9f46ba (diff) | |
download | frameworks_base-572bbd42a473980c2d59af80d378f6270ba6860a.zip frameworks_base-572bbd42a473980c2d59af80d378f6270ba6860a.tar.gz frameworks_base-572bbd42a473980c2d59af80d378f6270ba6860a.tar.bz2 |
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
4 files changed, 315 insertions, 23 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java index 90b63f4..3ef64bb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java @@ -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++; } 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 d6d90a6..2e30594 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java @@ -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; + } + } + } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java index 9215110..8c75adc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java @@ -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 index 0000000..9c6238f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java @@ -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); + } + } + } +} |