summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSelim Cinek <cinek@google.com>2014-04-25 16:43:27 +0200
committerSelim Cinek <cinek@google.com>2014-04-30 00:01:06 +0200
commit572bbd42a473980c2d59af80d378f6270ba6860a (patch)
treeea2aa8a5b237983d1cbac10ca14cb4cd5d23de6c
parent707eb8bab9a844812e418750a79b25938f9f46ba (diff)
downloadframeworks_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
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java182
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java148
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);
+ }
+ }
+ }
+}