Merge "Improved the animation logic of the stack scroller." into lmp-preview-dev

This commit is contained in:
Selim Cinek
2014-05-23 12:50:29 +00:00
committed by Android (Google) Code Review
13 changed files with 633 additions and 110 deletions

View File

@@ -21,15 +21,25 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.RectF;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.PathInterpolator;
import com.android.systemui.R;
import com.android.systemui.statusbar.stack.StackStateAnimator;
/**
* Base class for both {@link ExpandableNotificationRow} and {@link NotificationOverflowContainer}
@@ -41,6 +51,36 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220;
private static final int ACTIVATE_ANIMATION_LENGTH = 220;
/**
* The amount of width, which is kept in the end when performing a disappear animation (also
* the amount from which the horizontal appearing begins)
*/
private static final float HORIZONTAL_COLLAPSED_REST_PARTIAL = 0.05f;
/**
* At which point from [0,1] does the horizontal collapse animation end (or start when
* expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated.
*/
private static final float HORIZONTAL_ANIMATION_END = 0.2f;
/**
* At which point from [0,1] does the alpha animation end (or start when
* expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated.
*/
private static final float ALPHA_ANIMATION_END = 0.0f;
/**
* At which point from [0,1] does the horizontal collapse animation start (or start when
* expanding)? 1.0 meaning that it starts immediately and 0.0 that it is animated at all.
*/
private static final float HORIZONTAL_ANIMATION_START = 1.0f;
/**
* At which point from [0,1] does the vertical collapse animation start (or end when
* expanding) 1.0 meaning that it starts immediately and 0.0 that it is animated at all.
*/
private static final float VERTICAL_ANIMATION_START = 1.0f;
private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR
= new PathInterpolator(0.6f, 0, 0.5f, 1);
private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR
@@ -53,6 +93,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
private int mBgTint = 0;
private int mDimmedBgTint = 0;
private final int mRoundedRectCornerRadius;
/**
* Flag to indicate that the notification has been touched once and the second touch will
@@ -66,22 +107,41 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
private OnActivatedListener mOnActivatedListener;
private Interpolator mLinearOutSlowInInterpolator;
private Interpolator mFastOutSlowInInterpolator;
private final Interpolator mLinearOutSlowInInterpolator;
private final Interpolator mFastOutSlowInInterpolator;
private final Interpolator mSlowOutFastInInterpolator;
private final Interpolator mSlowOutLinearInInterpolator;
private final Interpolator mLinearInterpolator;
private Interpolator mCurrentAppearInterpolator;
private Interpolator mCurrentAlphaInterpolator;
private NotificationBackgroundView mBackgroundNormal;
private NotificationBackgroundView mBackgroundDimmed;
private ObjectAnimator mBackgroundAnimator;
private RectF mAppearAnimationRect = new RectF();
private PorterDuffColorFilter mAppearAnimationFilter;
private float mAnimationTranslationY;
private boolean mDrawingAppearAnimation;
private Paint mAppearPaint = new Paint();
private ValueAnimator mAppearAnimator;
private float mAppearAnimationFraction = -1.0f;
private float mAppearAnimationTranslation;
public ActivatableNotificationView(Context context, AttributeSet attrs) {
super(context, attrs);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mFastOutSlowInInterpolator =
AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f);
mLinearOutSlowInInterpolator =
AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
mSlowOutLinearInInterpolator = new PathInterpolator(0.8f, 0.0f, 1.0f, 1.0f);
mLinearInterpolator = new LinearInterpolator();
setClipChildren(false);
setClipToPadding(false);
mAppearAnimationFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP);
mRoundedRectCornerRadius = getResources().getDimensionPixelSize(
com.android.internal.R.dimen.notification_quantum_rounded_rect_radius);
}
@Override
@@ -316,6 +376,202 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
mBackgroundDimmed.setClipTopAmount(clipTopAmount);
}
@Override
public void performRemoveAnimation(float translationDirection, Runnable onFinishedRunnable) {
enableAppearDrawing(true);
if (mDrawingAppearAnimation) {
startAppearAnimation(false /* isAppearing */, translationDirection,
0, onFinishedRunnable);
}
}
@Override
public void performAddAnimation(long delay) {
enableAppearDrawing(true);
if (mDrawingAppearAnimation) {
startAppearAnimation(true /* isAppearing */, -1.0f, delay, null);
}
}
private void startAppearAnimation(boolean isAppearing,
float translationDirection, long delay, final Runnable onFinishedRunnable) {
if (mAppearAnimator != null) {
mAppearAnimator.cancel();
}
mAnimationTranslationY = translationDirection * mActualHeight;
if (mAppearAnimationFraction == -1.0f) {
// not initialized yet, we start anew
if (isAppearing) {
mAppearAnimationFraction = 0.0f;
mAppearAnimationTranslation = mAnimationTranslationY;
} else {
mAppearAnimationFraction = 1.0f;
mAppearAnimationTranslation = 0;
}
}
float targetValue;
if (isAppearing) {
mCurrentAppearInterpolator = mSlowOutFastInInterpolator;
mCurrentAlphaInterpolator = mLinearOutSlowInInterpolator;
targetValue = 1.0f;
} else {
mCurrentAppearInterpolator = mFastOutSlowInInterpolator;
mCurrentAlphaInterpolator = mSlowOutLinearInInterpolator;
targetValue = 0.0f;
}
mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction,
targetValue);
mAppearAnimator.setInterpolator(mLinearInterpolator);
mAppearAnimator.setDuration(
(long) (StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR
* Math.abs(mAppearAnimationFraction - targetValue)));
mAppearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAppearAnimationFraction = (float) animation.getAnimatedValue();
updateAppearAnimationAlpha();
updateAppearRect();
invalidate();
}
});
if (delay > 0) {
// we need to apply the initial state already to avoid drawn frames in the wrong state
updateAppearAnimationAlpha();
updateAppearRect();
mAppearAnimator.setStartDelay(delay);
}
mAppearAnimator.addListener(new AnimatorListenerAdapter() {
private boolean mWasCancelled;
@Override
public void onAnimationEnd(Animator animation) {
if (onFinishedRunnable != null) {
onFinishedRunnable.run();
}
if (!mWasCancelled) {
mAppearAnimationFraction = -1;
setOutlineRect(null);
enableAppearDrawing(false);
}
}
@Override
public void onAnimationStart(Animator animation) {
mWasCancelled = false;
}
@Override
public void onAnimationCancel(Animator animation) {
mWasCancelled = true;
}
});
mAppearAnimator.start();
}
private void updateAppearRect() {
float inverseFraction = (1.0f - mAppearAnimationFraction);
float translationFraction = mCurrentAppearInterpolator.getInterpolation(inverseFraction);
float translateYTotalAmount = translationFraction * mAnimationTranslationY;
mAppearAnimationTranslation = translateYTotalAmount;
// handle width animation
float widthFraction = (inverseFraction - (1.0f - HORIZONTAL_ANIMATION_START))
/ (HORIZONTAL_ANIMATION_START - HORIZONTAL_ANIMATION_END);
widthFraction = Math.min(1.0f, Math.max(0.0f, widthFraction));
widthFraction = mCurrentAppearInterpolator.getInterpolation(widthFraction);
float left = (getWidth() * (0.5f - HORIZONTAL_COLLAPSED_REST_PARTIAL / 2.0f) *
widthFraction);
float right = getWidth() - left;
// handle top animation
float heightFraction = (inverseFraction - (1.0f - VERTICAL_ANIMATION_START)) /
VERTICAL_ANIMATION_START;
heightFraction = Math.max(0.0f, heightFraction);
heightFraction = mCurrentAppearInterpolator.getInterpolation(heightFraction);
float top;
float bottom;
if (mAnimationTranslationY > 0.0f) {
bottom = mActualHeight - heightFraction * mAnimationTranslationY * 0.1f
- translateYTotalAmount;
top = bottom * heightFraction;
} else {
top = heightFraction * (mActualHeight + mAnimationTranslationY) * 0.1f -
translateYTotalAmount;
bottom = mActualHeight * (1 - heightFraction) + top * heightFraction;
}
mAppearAnimationRect.set(left, top, right, bottom);
setOutlineRect(left, top + mAppearAnimationTranslation, right,
bottom + mAppearAnimationTranslation);
}
private void updateAppearAnimationAlpha() {
int backgroundColor = getBackgroundColor();
if (backgroundColor != -1) {
float contentAlphaProgress = mAppearAnimationFraction;
contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END);
contentAlphaProgress = Math.min(1.0f, contentAlphaProgress);
contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress);
int sourceColor = Color.argb((int) (255 * (1.0f - contentAlphaProgress)),
Color.red(backgroundColor), Color.green(backgroundColor),
Color.blue(backgroundColor));
mAppearAnimationFilter.setColor(sourceColor);
mAppearPaint.setColorFilter(mAppearAnimationFilter);
}
}
private int getBackgroundColor() {
// TODO: get real color
return 0xfffafafa;
}
/**
* When we draw the appear animation, we render the view in a bitmap and render this bitmap
* as a shader of a rect. This call creates the Bitmap and switches the drawing mode,
* such that the normal drawing of the views does not happen anymore.
*
* @param enable Should it be enabled.
*/
private void enableAppearDrawing(boolean enable) {
if (enable != mDrawingAppearAnimation) {
if (enable) {
if (getWidth() == 0 || getActualHeight() == 0) {
// TODO: This should not happen, but it can during expansion. Needs
// investigation
return;
}
Bitmap bitmap = Bitmap.createBitmap(getWidth(), getActualHeight(),
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
draw(canvas);
mAppearPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP,
Shader.TileMode.CLAMP));
} else {
mAppearPaint.setShader(null);
}
mDrawingAppearAnimation = enable;
invalidate();
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (!mDrawingAppearAnimation) {
super.dispatchDraw(canvas);
} else {
drawAppearRect(canvas);
}
}
private void drawAppearRect(Canvas canvas) {
canvas.save();
canvas.translate(0, mAppearAnimationTranslation);
canvas.drawRoundRect(mAppearAnimationRect, mRoundedRectCornerRadius,
mRoundedRectCornerRadius, mAppearPaint);
canvas.restore();
}
public void setOnActivatedListener(OnActivatedListener onActivatedListener) {
mOnActivatedListener = onActivatedListener;
}

View File

@@ -1092,6 +1092,7 @@ public abstract class BaseStatusBar extends SystemUI implements
if (rowParent != null) rowParent.removeView(entry.row);
updateRowStates();
updateNotificationIcons();
updateSpeedBump();
return entry.notification;
}
@@ -1135,8 +1136,22 @@ public abstract class BaseStatusBar extends SystemUI implements
if (DEBUG) {
Log.d(TAG, "addNotificationViews: added at " + pos);
}
updateNotificationIcons();
updateRowStates();
updateNotificationIcons();
updateSpeedBump();
}
protected void updateSpeedBump() {
int n = mNotificationData.size();
int speedBumpIndex = -1;
for (int i = n-1; i >= 0; i--) {
NotificationData.Entry entry = mNotificationData.get(i);
if (entry.row.getVisibility() != View.GONE && speedBumpIndex == -1
&& entry.row.isBelowSpeedBump() ) {
speedBumpIndex = n - 1 - i;
}
}
mStackScroller.updateSpeedBumpIndex(speedBumpIndex);
}
private void addNotificationViews(StatusBarNotification notification) {
@@ -1156,7 +1171,6 @@ public abstract class BaseStatusBar extends SystemUI implements
mKeyguardIconOverflowContainer.getIconsView().removeAllViews();
int n = mNotificationData.size();
int visibleNotifications = 0;
int speedBumpIndex = -1;
boolean onKeyguard = mState == StatusBarState.KEYGUARD;
for (int i = n-1; i >= 0; i--) {
NotificationData.Entry entry = mNotificationData.get(i);
@@ -1177,17 +1191,14 @@ public abstract class BaseStatusBar extends SystemUI implements
mKeyguardIconOverflowContainer.getIconsView().addNotification(entry);
}
} else {
if (entry.row.getVisibility() == View.GONE) {
boolean wasGone = entry.row.getVisibility() == View.GONE;
entry.row.setVisibility(View.VISIBLE);
if (wasGone) {
// notify the scroller of a child addition
mStackScroller.generateAddAnimation(entry.row);
}
entry.row.setVisibility(View.VISIBLE);
visibleNotifications++;
}
if (entry.row.getVisibility() != View.GONE && speedBumpIndex == -1
&& entry.row.isBelowSpeedBump() ) {
speedBumpIndex = n - 1 - i;
}
}
if (onKeyguard && mKeyguardIconOverflowContainer.getIconsView().getChildCount() > 0) {
@@ -1195,8 +1206,6 @@ public abstract class BaseStatusBar extends SystemUI implements
} else {
mKeyguardIconOverflowContainer.setVisibility(View.GONE);
}
mStackScroller.updateSpeedBumpIndex(speedBumpIndex);
}
private boolean shouldShowOnKeyguard(StatusBarNotification sbn) {
@@ -1357,6 +1366,7 @@ public abstract class BaseStatusBar extends SystemUI implements
return;
}
updateRowStates();
updateSpeedBump();
}
catch (RuntimeException e) {
// It failed to add cleanly. Log, and remove the view from the panel.

View File

@@ -18,8 +18,8 @@ package com.android.systemui.statusbar;
import android.content.Context;
import android.graphics.Outline;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.widget.FrameLayout;
/**
* Like {@link ExpandableView}, but setting an outline for the height and clipping.
@@ -27,9 +27,12 @@ import android.widget.FrameLayout;
public abstract class ExpandableOutlineView extends ExpandableView {
private final Outline mOutline = new Outline();
private boolean mCustomOutline;
private float mDensity;
public ExpandableOutlineView(Context context, AttributeSet attrs) {
super(context, attrs);
mDensity = getResources().getDisplayMetrics().density;
}
@Override
@@ -50,11 +53,37 @@ public abstract class ExpandableOutlineView extends ExpandableView {
updateOutline();
}
private void updateOutline() {
mOutline.setRect(0,
mClipTopAmount,
getWidth(),
Math.max(mActualHeight, mClipTopAmount));
protected void setOutlineRect(RectF rect) {
if (rect != null) {
setOutlineRect(rect.left, rect.top, rect.right, rect.bottom);
} else {
mCustomOutline = false;
updateOutline();
}
}
protected void setOutlineRect(float left, float top, float right, float bottom) {
mCustomOutline = true;
int rectLeft = (int) left;
int rectTop = (int) top;
int rectRight = (int) right;
int rectBottom = (int) bottom;
// Outlines need to be at least 1 dp
rectBottom = (int) Math.max(top + mDensity, rectBottom);
rectRight = (int) Math.max(left + mDensity, rectRight);
mOutline.setRect(rectLeft, rectTop, rectRight, rectBottom);
setOutline(mOutline);
}
private void updateOutline() {
if (!mCustomOutline) {
mOutline.setRect(0,
mClipTopAmount,
getWidth(),
Math.max(mActualHeight, mClipTopAmount));
setOutline(mOutline);
}
}
}

View File

@@ -204,6 +204,21 @@ public abstract class ExpandableView extends FrameLayout {
return false;
}
/**
* Perform a remove animation on this view.
*
* @param translationDirection The direction value from [-1 ... 1] indicating in which the
* animation should be performed. A value of -1 means that The
* remove animation should be performed upwards,
* such that the child appears to be going away to the top. 1
* Should mean the opposite.
* @param onFinishedRunnable A runnable which should be run when the animation is finished.
*/
public abstract void performRemoveAnimation(float translationDirection,
Runnable onFinishedRunnable);
public abstract void performAddAnimation(long delay);
/**
* A listener notifying when {@link #getActualHeight} changes.
*/

View File

@@ -34,7 +34,6 @@ public class NotificationBackgroundView extends View {
public NotificationBackgroundView(Context context, AttributeSet attrs) {
super(context, attrs);
setWillNotDraw(false);
}
@Override

View File

@@ -184,7 +184,7 @@ public class SpeedBumpView extends ExpandableView implements View.OnClickListene
}
public void performVisibilityAnimation(boolean nowVisible) {
animateDivider(nowVisible);
animateDivider(nowVisible, null /* onFinishedRunnable */);
// Animate explanation Text
if (mIsExpanded) {
@@ -192,7 +192,14 @@ public class SpeedBumpView extends ExpandableView implements View.OnClickListene
}
}
public void animateDivider(boolean nowVisible) {
/**
* Animate the divider to a new visibility.
*
* @param nowVisible should it now be visible
* @param onFinishedRunnable A runnable which should be run when the animation is
* finished.
*/
public void animateDivider(boolean nowVisible, Runnable onFinishedRunnable) {
if (nowVisible != mDividerVisible) {
// Animate dividers
float endValue = nowVisible ? 1.0f : 0.0f;
@@ -204,7 +211,8 @@ public class SpeedBumpView extends ExpandableView implements View.OnClickListene
.scaleX(endValue)
.scaleY(endValue)
.translationX(endTranslationXLeft)
.setInterpolator(mFastOutSlowInInterpolator);
.setInterpolator(mFastOutSlowInInterpolator)
.withEndAction(onFinishedRunnable);
mLineRight.animate()
.alpha(endValue)
.withLayer()
@@ -216,6 +224,10 @@ public class SpeedBumpView extends ExpandableView implements View.OnClickListene
// Animate dots
mDots.performVisibilityAnimation(nowVisible);
mDividerVisible = nowVisible;
} else {
if (onFinishedRunnable != null) {
onFinishedRunnable.run();
}
}
}
@@ -250,6 +262,16 @@ public class SpeedBumpView extends ExpandableView implements View.OnClickListene
}
}
@Override
public void performRemoveAnimation(float translationDirection, Runnable onFinishedRunnable) {
performVisibilityAnimation(false);
}
@Override
public void performAddAnimation(long delay) {
performVisibilityAnimation(true);
}
private void resetExplanationText() {
mExplanationText.setTranslationY(0);
mExplanationText.setVisibility(INVISIBLE);

View File

@@ -214,7 +214,7 @@ public class NotificationPanelView extends PanelView implements
mClockAnimationTarget = -1;
}
});
StackStateAnimator.startInstantly(mClockAnimator);
mClockAnimator.start();
return true;
}
});

View File

@@ -2768,6 +2768,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
updateStackScrollerState();
updatePublicMode();
updateRowStates();
updateSpeedBump();
checkBarModes();
updateNotificationIcons();
updateCarrierLabelVisibility(false);

View File

@@ -28,6 +28,7 @@ public class AnimationFilter {
boolean animateScale;
boolean animateHeight;
boolean animateDimmed;
boolean hasDelays;
public AnimationFilter animateAlpha() {
animateAlpha = true;
@@ -39,6 +40,11 @@ public class AnimationFilter {
return this;
}
public AnimationFilter hasDelays() {
hasDelays = true;
return this;
}
public AnimationFilter animateZ() {
animateZ = true;
return this;
@@ -79,6 +85,7 @@ public class AnimationFilter {
animateScale |= filter.animateScale;
animateHeight |= filter.animateHeight;
animateDimmed |= filter.animateDimmed;
hasDelays |= filter.hasDelays;
}
private void reset() {
@@ -88,5 +95,6 @@ public class AnimationFilter {
animateScale = false;
animateHeight = false;
animateDimmed = false;
hasDelays = false;
}
}

View File

@@ -103,6 +103,7 @@ public class NotificationStackScrollLayout extends ViewGroup
private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<View>();
private ArrayList<View> mSnappedBackChildren = new ArrayList<View>();
private ArrayList<View> mDragAnimPendingChildren = new ArrayList<View>();
private ArrayList<View> mChildrenChangingPositions = new ArrayList<View>();
private ArrayList<AnimationEvent> mAnimationEvents
= new ArrayList<AnimationEvent>();
private ArrayList<View> mSwipedOutViews = new ArrayList<View>();
@@ -968,10 +969,25 @@ public class NotificationStackScrollLayout extends ViewGroup
return null;
}
/**
* @return The first child which has visibility unequal to GONE which is currently below the
* given translationY or equal to it.
*/
private View getFirstChildBelowTranlsationY(float translationY) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != View.GONE && child.getTranslationY() >= translationY) {
return child;
}
}
return null;
}
/**
* @return the last child which has visibility unequal to GONE
*/
private View getLastChildNotGone() {
public View getLastChildNotGone() {
int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
View child = getChildAt(i);
@@ -1094,23 +1110,41 @@ public class NotificationStackScrollLayout extends ViewGroup
@Override
protected void onViewRemoved(View child) {
super.onViewRemoved(child);
mStackScrollAlgorithm.notifyChildrenChanged(this);
if (mChildrenChangingPositions.contains(child)) {
// This is only a position change, don't do anything special
return;
}
((ExpandableView) child).setOnHeightChangedListener(null);
mCurrentStackScrollState.removeViewStateForView(child);
mStackScrollAlgorithm.notifyChildrenChanged(this);
updateScrollStateForRemovedChild(child);
generateRemoveAnimation(child);
boolean animationGenerated = generateRemoveAnimation(child);
if (animationGenerated && !mSwipedOutViews.contains(child)) {
// Add this view to an overlay in order to ensure that it will still be temporary
// drawn when removed
getOverlay().add(child);
}
}
private void generateRemoveAnimation(View child) {
/**
* Generate a remove animation for a child view.
*
* @param child The view to generate the remove animation for.
* @return Whether an animation was generated.
*/
private boolean generateRemoveAnimation(View child) {
if (mIsExpanded && mAnimationsEnabled) {
if (!mChildrenToAddAnimated.contains(child)) {
// Generate Animations
mChildrenToRemoveAnimated.add(child);
mNeedsAnimation = true;
return true;
} else {
mChildrenToAddAnimated.remove(child);
return false;
}
}
return false;
}
/**
@@ -1155,9 +1189,7 @@ public class NotificationStackScrollLayout extends ViewGroup
super.onViewAdded(child);
mStackScrollAlgorithm.notifyChildrenChanged(this);
((ExpandableView) child).setOnHeightChangedListener(this);
if (child.getVisibility() != View.GONE) {
generateAddAnimation(child);
}
generateAddAnimation(child);
}
public void setAnimationsEnabled(boolean animationsEnabled) {
@@ -1168,10 +1200,13 @@ public class NotificationStackScrollLayout extends ViewGroup
return mNeedsAnimation
&& (!mChildrenToAddAnimated.isEmpty() || !mChildrenToRemoveAnimated.isEmpty());
}
/**
* Generate an animation for an added child view.
*
* @param child The view to be added.
*/
public void generateAddAnimation(View child) {
if (mIsExpanded && mAnimationsEnabled) {
if (mIsExpanded && mAnimationsEnabled && !mChildrenChangingPositions.contains(child)) {
// Generate Animations
mChildrenToAddAnimated.add(child);
mNeedsAnimation = true;
@@ -1186,9 +1221,10 @@ public class NotificationStackScrollLayout extends ViewGroup
*/
public void changeViewPosition(View child, int newIndex) {
if (child != null && child.getParent() == this) {
mChildrenChangingPositions.add(child);
removeView(child);
addView(child, newIndex);
// TODO: handle events
mNeedsAnimation = true;
}
}
@@ -1197,16 +1233,18 @@ public class NotificationStackScrollLayout extends ViewGroup
generateChildHierarchyEvents();
mNeedsAnimation = false;
}
if (!mAnimationEvents.isEmpty()) {
if (!mAnimationEvents.isEmpty() || isCurrentlyAnimating()) {
mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState);
mAnimationEvents.clear();
} else {
applyCurrentState();
}
}
private void generateChildHierarchyEvents() {
generateChildAdditionEvents();
generateChildRemovalEvents();
generateChildAdditionEvents();
generatePositionChangeEvents();
generateSnapBackEvents();
generateDragEvents();
generateTopPaddingEvent();
@@ -1237,12 +1275,24 @@ public class NotificationStackScrollLayout extends ViewGroup
int animationType = childWasSwipedOut
? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT
: AnimationEvent.ANIMATION_TYPE_REMOVE;
mAnimationEvents.add(new AnimationEvent(child, animationType));
AnimationEvent event = new AnimationEvent(child, animationType);
// we need to know the view after this one
event.viewAfterChangingView = getFirstChildBelowTranlsationY(child.getTranslationY());
mAnimationEvents.add(event);
}
mSwipedOutViews.clear();
mChildrenToRemoveAnimated.clear();
}
private void generatePositionChangeEvents() {
for (View child : mChildrenChangingPositions) {
mAnimationEvents.add(new AnimationEvent(child,
AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION));
}
mChildrenChangingPositions.clear();
}
private void generateChildAdditionEvents() {
for (View child : mChildrenToAddAnimated) {
mAnimationEvents.add(new AnimationEvent(child,
@@ -1467,7 +1517,6 @@ public class NotificationStackScrollLayout extends ViewGroup
public void onChildAnimationFinished() {
requestChildrenUpdate();
mAnimationEvents.clear();
}
/**
@@ -1513,9 +1562,9 @@ public class NotificationStackScrollLayout extends ViewGroup
}
private void updateSpeedBump(boolean visible) {
int newVisibility = visible ? VISIBLE : GONE;
int oldVisibility = mSpeedBumpView.getVisibility();
if (newVisibility != oldVisibility) {
boolean notGoneBefore = mSpeedBumpView.getVisibility() != GONE;
if (visible != notGoneBefore) {
int newVisibility = visible ? VISIBLE : GONE;
mSpeedBumpView.setVisibility(newVisibility);
if (visible) {
mSpeedBumpView.collapse();
@@ -1551,21 +1600,24 @@ public class NotificationStackScrollLayout extends ViewGroup
.animateAlpha()
.animateHeight()
.animateY()
.animateZ(),
.animateZ()
.hasDelays(),
// ANIMATION_TYPE_REMOVE
new AnimationFilter()
.animateAlpha()
.animateHeight()
.animateY()
.animateZ(),
.animateZ()
.hasDelays(),
// ANIMATION_TYPE_REMOVE_SWIPED_OUT
new AnimationFilter()
.animateAlpha()
.animateHeight()
.animateY()
.animateZ(),
.animateZ()
.hasDelays(),
// ANIMATION_TYPE_TOP_PADDING_CHANGED
new AnimationFilter()
@@ -1593,16 +1645,23 @@ public class NotificationStackScrollLayout extends ViewGroup
new AnimationFilter()
.animateY()
.animateScale()
.animateDimmed()
.animateDimmed(),
// ANIMATION_TYPE_CHANGE_POSITION
new AnimationFilter()
.animateAlpha()
.animateHeight()
.animateY()
.animateZ()
};
static int[] LENGTHS = new int[] {
// ANIMATION_TYPE_ADD
StackStateAnimator.ANIMATION_DURATION_STANDARD,
StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
// ANIMATION_TYPE_REMOVE
StackStateAnimator.ANIMATION_DURATION_STANDARD,
StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
// ANIMATION_TYPE_REMOVE_SWIPED_OUT
StackStateAnimator.ANIMATION_DURATION_STANDARD,
@@ -1621,22 +1680,27 @@ public class NotificationStackScrollLayout extends ViewGroup
// ANIMATION_TYPE_DIMMED
StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
// ANIMATION_TYPE_CHANGE_POSITION
StackStateAnimator.ANIMATION_DURATION_STANDARD,
};
static int ANIMATION_TYPE_ADD = 0;
static int ANIMATION_TYPE_REMOVE = 1;
static int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2;
static int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3;
static int ANIMATION_TYPE_START_DRAG = 4;
static int ANIMATION_TYPE_SNAP_BACK = 5;
static int ANIMATION_TYPE_ACTIVATED_CHILD = 6;
static int ANIMATION_TYPE_DIMMED = 7;
static final int ANIMATION_TYPE_ADD = 0;
static final int ANIMATION_TYPE_REMOVE = 1;
static final int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2;
static final int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3;
static final int ANIMATION_TYPE_START_DRAG = 4;
static final int ANIMATION_TYPE_SNAP_BACK = 5;
static final int ANIMATION_TYPE_ACTIVATED_CHILD = 6;
static final int ANIMATION_TYPE_DIMMED = 7;
static final int ANIMATION_TYPE_CHANGE_POSITION = 8;
final long eventStartTime;
final View changingView;
final int animationType;
final AnimationFilter filter;
final long length;
View viewAfterChangingView;
AnimationEvent(View view, int type) {
eventStartTime = AnimationUtils.currentAnimationTimeMillis();

View File

@@ -208,6 +208,8 @@ public class StackScrollAlgorithm {
for (int i = 0; i < childCount; i++) {
ExpandableView v = (ExpandableView) hostView.getChildAt(i);
if (v.getVisibility() != View.GONE) {
StackScrollState.ViewState viewState = resultState.getViewStateForView(v);
viewState.notGoneIndex = state.visibleChildren.size();
state.visibleChildren.add(v);
}
}

View File

@@ -39,14 +39,10 @@ public class StackScrollState {
private final ViewGroup mHostView;
private Map<ExpandableView, ViewState> mStateMap;
private final Rect mClipRect = new Rect();
private int mBackgroundRoundedRectCornerRadius;
private final Outline mChildOutline = new Outline();
public StackScrollState(ViewGroup hostView) {
mHostView = hostView;
mStateMap = new HashMap<ExpandableView, ViewState>();
mBackgroundRoundedRectCornerRadius = hostView.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.notification_quantum_rounded_rect_radius);
}
public ViewGroup getHostView() {
@@ -66,6 +62,7 @@ public class StackScrollState {
viewState.height = child.getIntrinsicHeight();
viewState.gone = child.getVisibility() == View.GONE;
viewState.alpha = 1;
viewState.notGoneIndex = -1;
}
}
@@ -190,7 +187,7 @@ public class StackScrollState {
if (nextChild != null) {
ViewState nextState = getViewStateForView(nextChild);
boolean startIsAboveNext = nextState.yTranslation > speedBumpStart;
speedBump.animateDivider(startIsAboveNext);
speedBump.animateDivider(startIsAboveNext, null /* onFinishedRunnable */);
// handle expanded case
if (speedBump.isExpanded()) {
@@ -271,6 +268,11 @@ public class StackScrollState {
float scale;
boolean dimmed;
/**
* The index of the view, only accounting for views not equal to GONE
*/
int notGoneIndex;
/**
* The location this view is currently rendered at.
*

View File

@@ -39,7 +39,11 @@ import java.util.Stack;
public class StackStateAnimator {
public static final int ANIMATION_DURATION_STANDARD = 360;
public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
private static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
private static final int TAG_ANIMATOR_TRANSLATION_Y = R.id.translation_y_animator_tag;
private static final int TAG_ANIMATOR_TRANSLATION_Z = R.id.translation_z_animator_tag;
@@ -62,10 +66,9 @@ public class StackStateAnimator {
private final Interpolator mFastOutSlowInInterpolator;
public NotificationStackScrollLayout mHostLayout;
private ArrayList<NotificationStackScrollLayout.AnimationEvent> mHandledEvents =
new ArrayList<>();
private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents =
new ArrayList<>();
private ArrayList<View> mNewAddChildren = new ArrayList<>();
private Set<Animator> mAnimatorSet = new HashSet<Animator>();
private Stack<AnimatorListenerAdapter> mAnimationListenerPool
= new Stack<AnimatorListenerAdapter>();
@@ -96,57 +99,130 @@ public class StackStateAnimator {
mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents);
for (int i = 0; i < childCount; i++) {
final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
StackScrollState.ViewState viewState = finalState.getViewStateForView(child);
if (viewState == null) {
if (viewState == null || child.getVisibility() == View.GONE) {
continue;
}
startAnimations(child, viewState);
child.setClipBounds(null);
startAnimations(child, viewState, finalState);
}
if (!isRunning()) {
// no child has preformed any animation, lets finish
onAnimationFinished();
}
mNewEvents.clear();
mNewAddChildren.clear();
}
/**
* Start an animation to the given viewState
*/
private void startAnimations(final ExpandableView child, StackScrollState.ViewState viewState) {
private void startAnimations(final ExpandableView child, StackScrollState.ViewState viewState,
StackScrollState finalState) {
int childVisibility = child.getVisibility();
boolean wasVisible = childVisibility == View.VISIBLE;
final float alpha = viewState.alpha;
if (!wasVisible && alpha != 0 && !viewState.gone) {
child.setVisibility(View.VISIBLE);
}
boolean yTranslationChanging = child.getTranslationY() != viewState.yTranslation;
boolean zTranslationChanging = child.getTranslationZ() != viewState.zTranslation;
boolean scaleChanging = child.getScaleX() != viewState.scale;
boolean alphaChanging = alpha != child.getAlpha();
boolean heightChanging = viewState.height != child.getActualHeight();
boolean wasAdded = mNewAddChildren.contains(child);
boolean hasDelays = mAnimationFilter.hasDelays;
boolean isDelayRelevant = yTranslationChanging || zTranslationChanging || scaleChanging ||
alphaChanging || heightChanging;
long delay = 0;
if (hasDelays && isDelayRelevant || wasAdded) {
delay = calculateChildAnimationDelay(viewState, finalState);
}
// start translationY animation
if (child.getTranslationY() != viewState.yTranslation) {
startYTranslationAnimation(child, viewState);
if (yTranslationChanging) {
startYTranslationAnimation(child, viewState, delay);
}
// start translationZ animation
if (child.getTranslationZ() != viewState.zTranslation) {
startZTranslationAnimation(child, viewState);
if (zTranslationChanging) {
startZTranslationAnimation(child, viewState, delay);
}
// start scale animation
if (child.getScaleX() != viewState.scale) {
if (scaleChanging) {
startScaleAnimation(child, viewState);
}
// start alpha animation
if (alpha != child.getAlpha()) {
startAlphaAnimation(child, viewState);
if (alphaChanging) {
startAlphaAnimation(child, viewState, delay);
}
// start height animation
if (viewState.height != child.getActualHeight()) {
startHeightAnimation(child, viewState);
if (heightChanging) {
startHeightAnimation(child, viewState, delay);
}
// start dimmed animation
child.setDimmed(viewState.dimmed, mAnimationFilter.animateDimmed);
if (wasAdded) {
child.performAddAnimation(delay);
}
}
private long calculateChildAnimationDelay(StackScrollState.ViewState viewState,
StackScrollState finalState) {
long minDelay = 0;
for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) {
long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING;
switch (event.animationType) {
case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: {
int ownIndex = viewState.notGoneIndex;
int changingIndex = finalState
.getViewStateForView(event.changingView).notGoneIndex;
int difference = Math.abs(ownIndex - changingIndex);
difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
difference - 1));
long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement;
minDelay = Math.max(delay, minDelay);
break;
}
case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT:
delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL;
case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: {
int ownIndex = viewState.notGoneIndex;
boolean noNextView = event.viewAfterChangingView == null;
View viewAfterChangingView = noNextView
? mHostLayout.getLastChildNotGone()
: event.viewAfterChangingView;
int nextIndex = finalState
.getViewStateForView(viewAfterChangingView).notGoneIndex;
if (ownIndex >= nextIndex) {
// we only have the view afterwards
ownIndex++;
}
int difference = Math.abs(ownIndex - nextIndex);
difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
difference - 1));
long delay = difference * delayPerElement;
minDelay = Math.max(delay, minDelay);
break;
}
default:
break;
}
}
return minDelay;
}
private void startHeightAnimation(final ExpandableView child,
StackScrollState.ViewState viewState) {
StackScrollState.ViewState viewState, long delay) {
Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT);
Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT);
int newEndValue = viewState.height;
@@ -185,6 +261,9 @@ public class StackStateAnimator {
animator.setInterpolator(mFastOutSlowInInterpolator);
long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
animator.setDuration(newDuration);
if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
animator.setStartDelay(delay);
}
animator.addListener(getGlobalAnimationFinishedListener());
// remove the tag when the animation is finished
animator.addListener(new AnimatorListenerAdapter() {
@@ -195,14 +274,14 @@ public class StackStateAnimator {
child.setTag(TAG_END_HEIGHT, null);
}
});
startInstantly(animator);
startAnimator(animator);
child.setTag(TAG_ANIMATOR_HEIGHT, animator);
child.setTag(TAG_START_HEIGHT, child.getActualHeight());
child.setTag(TAG_END_HEIGHT, newEndValue);
}
private void startAlphaAnimation(final ExpandableView child,
final StackScrollState.ViewState viewState) {
final StackScrollState.ViewState viewState, long delay) {
Float previousStartValue = getChildTag(child,TAG_START_ALPHA);
Float previousEndValue = getChildTag(child,TAG_END_ALPHA);
final float newEndValue = viewState.alpha;
@@ -264,6 +343,9 @@ public class StackStateAnimator {
});
long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
animator.setDuration(newDuration);
if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
animator.setStartDelay(delay);
}
animator.addListener(getGlobalAnimationFinishedListener());
// remove the tag when the animation is finished
animator.addListener(new AnimatorListenerAdapter() {
@@ -272,14 +354,14 @@ public class StackStateAnimator {
}
});
startInstantly(animator);
startAnimator(animator);
child.setTag(TAG_ANIMATOR_ALPHA, animator);
child.setTag(TAG_START_ALPHA, child.getAlpha());
child.setTag(TAG_END_ALPHA, newEndValue);
}
private void startZTranslationAnimation(final ExpandableView child,
final StackScrollState.ViewState viewState) {
final StackScrollState.ViewState viewState, long delay) {
Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Z);
Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Z);
float newEndValue = viewState.zTranslation;
@@ -311,6 +393,9 @@ public class StackStateAnimator {
animator.setInterpolator(mFastOutSlowInInterpolator);
long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
animator.setDuration(newDuration);
if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
animator.setStartDelay(delay);
}
animator.addListener(getGlobalAnimationFinishedListener());
// remove the tag when the animation is finished
animator.addListener(new AnimatorListenerAdapter() {
@@ -321,14 +406,14 @@ public class StackStateAnimator {
child.setTag(TAG_END_TRANSLATION_Z, null);
}
});
startInstantly(animator);
startAnimator(animator);
child.setTag(TAG_ANIMATOR_TRANSLATION_Z, animator);
child.setTag(TAG_START_TRANSLATION_Z, child.getTranslationZ());
child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
}
private void startYTranslationAnimation(final ExpandableView child,
StackScrollState.ViewState viewState) {
StackScrollState.ViewState viewState, long delay) {
Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Y);
Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Y);
float newEndValue = viewState.yTranslation;
@@ -361,6 +446,9 @@ public class StackStateAnimator {
animator.setInterpolator(mFastOutSlowInInterpolator);
long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
animator.setDuration(newDuration);
if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
animator.setStartDelay(delay);
}
animator.addListener(getGlobalAnimationFinishedListener());
// remove the tag when the animation is finished
animator.addListener(new AnimatorListenerAdapter() {
@@ -371,7 +459,7 @@ public class StackStateAnimator {
child.setTag(TAG_END_TRANSLATION_Y, null);
}
});
startInstantly(animator);
startAnimator(animator);
child.setTag(TAG_ANIMATOR_TRANSLATION_Y, animator);
child.setTag(TAG_START_TRANSLATION_Y, child.getTranslationY());
child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
@@ -425,18 +513,15 @@ public class StackStateAnimator {
child.setTag(TAG_END_SCALE, null);
}
});
startInstantly(animator);
startAnimator(animator);
child.setTag(TAG_ANIMATOR_SCALE, animator);
child.setTag(TAG_START_SCALE, child.getScaleX());
child.setTag(TAG_END_SCALE, newEndValue);
}
/**
* Start an animator instantly instead of waiting on the next synchronization frame
*/
public static void startInstantly(ValueAnimator animator) {
private void startAnimator(ValueAnimator animator) {
mAnimatorSet.add(animator);
animator.start();
animator.setCurrentPlayTime(0);
}
/**
@@ -468,7 +553,6 @@ public class StackStateAnimator {
@Override
public void onAnimationStart(Animator animation) {
mAnimatorSet.add(animation);
mWasCancelled = false;
}
};
@@ -497,8 +581,6 @@ public class StackStateAnimator {
}
private void onAnimationFinished() {
mHandledEvents.clear();
mNewEvents.clear();
mHostLayout.onChildAnimationFinished();
}
@@ -511,27 +593,60 @@ public class StackStateAnimator {
private void processAnimationEvents(
ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents,
StackScrollState finalState) {
mNewEvents.clear();
for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) {
View changingView = event.changingView;
if (!mHandledEvents.contains(event)) {
if (event.animationType == NotificationStackScrollLayout.AnimationEvent
.ANIMATION_TYPE_ADD) {
final ExpandableView changingView = (ExpandableView) event.changingView;
if (event.animationType ==
NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) {
// This item is added, initialize it's properties.
StackScrollState.ViewState viewState = finalState
.getViewStateForView(changingView);
if (viewState == null) {
// The position for this child was never generated, let's continue.
continue;
}
changingView.setAlpha(0);
changingView.setTranslationY(viewState.yTranslation);
changingView.setTranslationZ(viewState.zTranslation);
// This item is added, initialize it's properties.
StackScrollState.ViewState viewState = finalState
.getViewStateForView(changingView);
if (viewState == null) {
// The position for this child was never generated, let's continue.
continue;
}
mHandledEvents.add(event);
mNewEvents.add(event);
if (changingView.getVisibility() == View.GONE) {
// The view was set to gone but the state never removed
finalState.removeViewStateForView(changingView);
continue;
}
changingView.setAlpha(viewState.alpha);
changingView.setTranslationY(viewState.yTranslation);
changingView.setTranslationZ(viewState.zTranslation);
changingView.setActualHeight(viewState.height, false);
mNewAddChildren.add(changingView);
} else if (event.animationType ==
NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) {
if (changingView.getVisibility() == View.GONE) {
continue;
}
// Find the amount to translate up. This is needed in order to understand the
// direction of the remove animation (either downwards or upwards)
StackScrollState.ViewState viewState = finalState
.getViewStateForView(event.viewAfterChangingView);
int actualHeight = changingView.getActualHeight();
// upwards by default
float translationDirection = -1.0f;
if (viewState != null) {
// there was a view after this one, Approximate the distance the next child
// travelled
translationDirection = ((viewState.yTranslation
- (changingView.getTranslationY() + actualHeight / 2.0f)) * 2 /
actualHeight);
translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f);
}
changingView.performRemoveAnimation(translationDirection, new Runnable() {
@Override
public void run() {
// remove the temporary overlay
mHostLayout.getOverlay().remove(changingView);
}
});
}
mNewEvents.add(event);
}
}