2 * Copyright (C) 2014 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
17 package com.android.systemui.statusbar.stack;
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ObjectAnimator;
22 import android.animation.PropertyValuesHolder;
23 import android.animation.ValueAnimator;
24 import android.view.View;
25 import android.view.animation.AnimationUtils;
26 import android.view.animation.Interpolator;
28 import com.android.systemui.R;
29 import com.android.systemui.statusbar.ExpandableNotificationRow;
30 import com.android.systemui.statusbar.ExpandableView;
31 import com.android.systemui.statusbar.SpeedBumpView;
33 import java.util.ArrayList;
34 import java.util.HashSet;
36 import java.util.Stack;
39 * An stack state animator which handles animations to new StackScrollStates
41 public class StackStateAnimator {
43 public static final int ANIMATION_DURATION_STANDARD = 360;
44 public static final int ANIMATION_DURATION_GO_TO_FULL_SHADE = 448;
45 public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
46 public static final int ANIMATION_DURATION_EXPAND_CLICKED = 360;
47 public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
48 public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 280;
49 public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 220;
50 public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
51 public static final int ANIMATION_DELAY_PER_ELEMENT_EXPAND_CHILDREN = 54;
52 public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
53 public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48;
54 public static final int ANIMATION_DELAY_PER_ELEMENT_DARK = 24;
55 public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
56 public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE_CHILDREN = 3;
58 private static final int TAG_ANIMATOR_TRANSLATION_Y = R.id.translation_y_animator_tag;
59 private static final int TAG_ANIMATOR_TRANSLATION_Z = R.id.translation_z_animator_tag;
60 private static final int TAG_ANIMATOR_SCALE = R.id.scale_animator_tag;
61 private static final int TAG_ANIMATOR_ALPHA = R.id.alpha_animator_tag;
62 private static final int TAG_ANIMATOR_HEIGHT = R.id.height_animator_tag;
63 private static final int TAG_ANIMATOR_TOP_INSET = R.id.top_inset_animator_tag;
64 private static final int TAG_END_TRANSLATION_Y = R.id.translation_y_animator_end_value_tag;
65 private static final int TAG_END_TRANSLATION_Z = R.id.translation_z_animator_end_value_tag;
66 private static final int TAG_END_SCALE = R.id.scale_animator_end_value_tag;
67 private static final int TAG_END_ALPHA = R.id.alpha_animator_end_value_tag;
68 private static final int TAG_END_HEIGHT = R.id.height_animator_end_value_tag;
69 private static final int TAG_END_TOP_INSET = R.id.top_inset_animator_end_value_tag;
70 private static final int TAG_START_TRANSLATION_Y = R.id.translation_y_animator_start_value_tag;
71 private static final int TAG_START_TRANSLATION_Z = R.id.translation_z_animator_start_value_tag;
72 private static final int TAG_START_SCALE = R.id.scale_animator_start_value_tag;
73 private static final int TAG_START_ALPHA = R.id.alpha_animator_start_value_tag;
74 private static final int TAG_START_HEIGHT = R.id.height_animator_start_value_tag;
75 private static final int TAG_START_TOP_INSET = R.id.top_inset_animator_start_value_tag;
77 private final Interpolator mFastOutSlowInInterpolator;
78 private final int mGoToFullShadeAppearingTranslation;
79 public NotificationStackScrollLayout mHostLayout;
80 private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents =
82 private ArrayList<View> mNewAddChildren = new ArrayList<>();
83 private Set<Animator> mAnimatorSet = new HashSet<>();
84 private Stack<AnimatorListenerAdapter> mAnimationListenerPool = new Stack<>();
85 private AnimationFilter mAnimationFilter = new AnimationFilter();
86 private long mCurrentLength;
87 private long mCurrentAdditionalDelay;
89 /** The current index for the last child which was not added in this event set. */
90 private int mCurrentLastNotAddedIndex;
92 private ValueAnimator mTopOverScrollAnimator;
93 private ValueAnimator mBottomOverScrollAnimator;
94 private ExpandableNotificationRow mChildExpandingView;
95 private StackViewState mTmpState = new StackViewState();
97 public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
98 mHostLayout = hostLayout;
99 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(hostLayout.getContext(),
100 android.R.interpolator.fast_out_slow_in);
101 mGoToFullShadeAppearingTranslation =
102 hostLayout.getContext().getResources().getDimensionPixelSize(
103 R.dimen.go_to_full_shade_appearing_translation);
106 public boolean isRunning() {
107 return !mAnimatorSet.isEmpty();
110 public void startAnimationForEvents(
111 ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents,
112 StackScrollState finalState, long additionalDelay) {
114 processAnimationEvents(mAnimationEvents, finalState);
116 int childCount = mHostLayout.getChildCount();
117 mAnimationFilter.applyCombination(mNewEvents);
118 mCurrentAdditionalDelay = additionalDelay;
119 mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents);
120 mCurrentLastNotAddedIndex = findLastNotAddedIndex(finalState);
121 for (int i = 0; i < childCount; i++) {
122 final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
124 StackViewState viewState = finalState.getViewStateForView(child);
125 if (viewState == null || child.getVisibility() == View.GONE) {
129 child.setClipTopOptimization(0);
130 startStackAnimations(child, viewState, finalState, i, -1 /* fixedDelay */);
133 // no child has preformed any animation, lets finish
134 onAnimationFinished();
137 mNewAddChildren.clear();
138 mChildExpandingView = null;
141 private int findLastNotAddedIndex(StackScrollState finalState) {
142 int childCount = mHostLayout.getChildCount();
143 for (int i = childCount - 1; i >= 0; i--) {
144 final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
146 StackViewState viewState = finalState.getViewStateForView(child);
147 if (viewState == null || child.getVisibility() == View.GONE) {
150 if (!mNewAddChildren.contains(child)) {
151 return viewState.notGoneIndex;
159 * Start an animation to the given {@link StackViewState}.
161 * @param child the child to start the animation on
162 * @param viewState the {@link StackViewState} of the view to animate to
163 * @param finalState the final state after the animation
164 * @param i the index of the view; only relevant if the view is the speed bump and is
166 * @param fixedDelay a fixed delay if desired or -1 if the delay should be calculated
168 public void startStackAnimations(final ExpandableView child, StackViewState viewState,
169 StackScrollState finalState, int i, long fixedDelay) {
170 final float alpha = viewState.alpha;
171 boolean wasAdded = mNewAddChildren.contains(child);
172 long duration = mCurrentLength;
173 if (wasAdded && mAnimationFilter.hasGoToFullShadeEvent) {
174 child.setTranslationY(child.getTranslationY() + mGoToFullShadeAppearingTranslation);
175 float longerDurationFactor = viewState.notGoneIndex - mCurrentLastNotAddedIndex;
176 longerDurationFactor = (float) Math.pow(longerDurationFactor, 0.7f);
177 duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50 +
178 (long) (100 * longerDurationFactor);
180 boolean yTranslationChanging = child.getTranslationY() != viewState.yTranslation;
181 boolean zTranslationChanging = child.getTranslationZ() != viewState.zTranslation;
182 boolean scaleChanging = child.getScaleX() != viewState.scale;
183 boolean alphaChanging = alpha != child.getAlpha();
184 boolean heightChanging = viewState.height != child.getActualHeight();
185 boolean darkChanging = viewState.dark != child.isDark();
186 boolean topInsetChanging = viewState.clipTopAmount != child.getClipTopAmount();
187 boolean hasDelays = mAnimationFilter.hasDelays;
188 boolean isDelayRelevant = yTranslationChanging || zTranslationChanging || scaleChanging ||
189 alphaChanging || heightChanging || topInsetChanging || darkChanging;
191 if (fixedDelay != -1) {
193 } else if (hasDelays && isDelayRelevant || wasAdded) {
194 delay = mCurrentAdditionalDelay + calculateChildAnimationDelay(viewState, finalState);
197 startViewAnimations(child, viewState, delay, duration);
199 // start height animation
200 if (heightChanging && child.getActualHeight() != 0) {
201 startHeightAnimation(child, viewState, duration, delay);
204 // start top inset animation
205 if (topInsetChanging) {
206 startInsetAnimation(child, viewState, duration, delay);
209 // start dimmed animation
210 child.setDimmed(viewState.dimmed, mAnimationFilter.animateDimmed);
212 // start dark animation
213 child.setDark(viewState.dark, mAnimationFilter.animateDark, delay);
215 // apply speed bump state
216 child.setBelowSpeedBump(viewState.belowSpeedBump);
218 // start hiding sensitive animation
219 child.setHideSensitive(viewState.hideSensitive, mAnimationFilter.animateHideSensitive,
223 child.performAddAnimation(delay, mCurrentLength);
225 if (child instanceof SpeedBumpView) {
226 finalState.performSpeedBumpAnimation(i, (SpeedBumpView) child, viewState,
228 } else if (child instanceof ExpandableNotificationRow) {
229 ExpandableNotificationRow row = (ExpandableNotificationRow) child;
230 row.startChildAnimation(finalState, this, child == mChildExpandingView, delay,
236 * Start an animation to a new {@link ViewState}.
238 * @param child the child to start the animation on
239 * @param viewState the {@link StackViewState} of the view to animate to
240 * @param delay a fixed delay
241 * @param duration the duration of the animation
243 public void startViewAnimations(View child, ViewState viewState, long delay, long duration) {
244 boolean wasVisible = child.getVisibility() == View.VISIBLE;
245 final float alpha = viewState.alpha;
246 if (!wasVisible && alpha != 0 && !viewState.gone) {
247 child.setVisibility(View.VISIBLE);
249 boolean yTranslationChanging = child.getTranslationY() != viewState.yTranslation;
250 boolean zTranslationChanging = child.getTranslationZ() != viewState.zTranslation;
251 boolean scaleChanging = child.getScaleX() != viewState.scale;
252 float childAlpha = child.getVisibility() == View.INVISIBLE ? 0.0f : child.getAlpha();
253 boolean alphaChanging = viewState.alpha != childAlpha;
255 // start translationY animation
256 if (yTranslationChanging) {
257 startYTranslationAnimation(child, viewState, duration, delay);
260 // start translationZ animation
261 if (zTranslationChanging) {
262 startZTranslationAnimation(child, viewState, duration, delay);
265 // start scale animation
267 startScaleAnimation(child, viewState, duration);
270 // start alpha animation
271 if (alphaChanging && child.getTranslationX() == 0) {
272 startAlphaAnimation(child, viewState, duration, delay);
276 private long calculateChildAnimationDelay(StackViewState viewState,
277 StackScrollState finalState) {
278 if (mAnimationFilter.hasDarkEvent) {
279 return calculateDelayDark(viewState);
281 if (mAnimationFilter.hasGoToFullShadeEvent) {
282 return calculateDelayGoToFullShade(viewState);
285 for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) {
286 long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING;
287 switch (event.animationType) {
288 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: {
289 int ownIndex = viewState.notGoneIndex;
290 int changingIndex = finalState
291 .getViewStateForView(event.changingView).notGoneIndex;
292 int difference = Math.abs(ownIndex - changingIndex);
293 difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
295 long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement;
296 minDelay = Math.max(delay, minDelay);
299 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT:
300 delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL;
301 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: {
302 int ownIndex = viewState.notGoneIndex;
303 boolean noNextView = event.viewAfterChangingView == null;
304 View viewAfterChangingView = noNextView
305 ? mHostLayout.getLastChildNotGone()
306 : event.viewAfterChangingView;
308 int nextIndex = finalState
309 .getViewStateForView(viewAfterChangingView).notGoneIndex;
310 if (ownIndex >= nextIndex) {
311 // we only have the view afterwards
314 int difference = Math.abs(ownIndex - nextIndex);
315 difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
317 long delay = difference * delayPerElement;
318 minDelay = Math.max(delay, minDelay);
328 private long calculateDelayDark(StackViewState viewState) {
330 if (mAnimationFilter.darkAnimationOriginIndex ==
331 NotificationStackScrollLayout.AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_ABOVE) {
333 } else if (mAnimationFilter.darkAnimationOriginIndex ==
334 NotificationStackScrollLayout.AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_BELOW) {
335 referenceIndex = mHostLayout.getNotGoneChildCount() - 1;
337 referenceIndex = mAnimationFilter.darkAnimationOriginIndex;
339 return Math.abs(referenceIndex - viewState.notGoneIndex) * ANIMATION_DELAY_PER_ELEMENT_DARK;
342 private long calculateDelayGoToFullShade(StackViewState viewState) {
343 float index = viewState.notGoneIndex;
344 index = (float) Math.pow(index, 0.7f);
345 return (long) (index * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE);
348 private void startHeightAnimation(final ExpandableView child,
349 StackViewState viewState, long duration, long delay) {
350 Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT);
351 Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT);
352 int newEndValue = viewState.height;
353 if (previousEndValue != null && previousEndValue == newEndValue) {
356 ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT);
357 if (!mAnimationFilter.animateHeight) {
358 // just a local update was performed
359 if (previousAnimator != null) {
360 // we need to increase all animation keyframes of the previous animator by the
361 // relative change to the end value
362 PropertyValuesHolder[] values = previousAnimator.getValues();
363 int relativeDiff = newEndValue - previousEndValue;
364 int newStartValue = previousStartValue + relativeDiff;
365 values[0].setIntValues(newStartValue, newEndValue);
366 child.setTag(TAG_START_HEIGHT, newStartValue);
367 child.setTag(TAG_END_HEIGHT, newEndValue);
368 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
371 // no new animation needed, let's just apply the value
372 child.setActualHeight(newEndValue, false);
377 ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), newEndValue);
378 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
380 public void onAnimationUpdate(ValueAnimator animation) {
381 child.setActualHeight((int) animation.getAnimatedValue(),
382 false /* notifyListeners */);
385 animator.setInterpolator(mFastOutSlowInInterpolator);
386 long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
387 animator.setDuration(newDuration);
388 if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
389 animator.setStartDelay(delay);
391 animator.addListener(getGlobalAnimationFinishedListener());
392 // remove the tag when the animation is finished
393 animator.addListener(new AnimatorListenerAdapter() {
395 public void onAnimationEnd(Animator animation) {
396 child.setTag(TAG_ANIMATOR_HEIGHT, null);
397 child.setTag(TAG_START_HEIGHT, null);
398 child.setTag(TAG_END_HEIGHT, null);
401 startAnimator(animator);
402 child.setTag(TAG_ANIMATOR_HEIGHT, animator);
403 child.setTag(TAG_START_HEIGHT, child.getActualHeight());
404 child.setTag(TAG_END_HEIGHT, newEndValue);
407 private void startInsetAnimation(final ExpandableView child,
408 StackViewState viewState, long duration, long delay) {
409 Integer previousStartValue = getChildTag(child, TAG_START_TOP_INSET);
410 Integer previousEndValue = getChildTag(child, TAG_END_TOP_INSET);
411 int newEndValue = viewState.clipTopAmount;
412 if (previousEndValue != null && previousEndValue == newEndValue) {
415 ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TOP_INSET);
416 if (!mAnimationFilter.animateTopInset) {
417 // just a local update was performed
418 if (previousAnimator != null) {
419 // we need to increase all animation keyframes of the previous animator by the
420 // relative change to the end value
421 PropertyValuesHolder[] values = previousAnimator.getValues();
422 int relativeDiff = newEndValue - previousEndValue;
423 int newStartValue = previousStartValue + relativeDiff;
424 values[0].setIntValues(newStartValue, newEndValue);
425 child.setTag(TAG_START_TOP_INSET, newStartValue);
426 child.setTag(TAG_END_TOP_INSET, newEndValue);
427 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
430 // no new animation needed, let's just apply the value
431 child.setClipTopAmount(newEndValue);
436 ValueAnimator animator = ValueAnimator.ofInt(child.getClipTopAmount(), newEndValue);
437 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
439 public void onAnimationUpdate(ValueAnimator animation) {
440 child.setClipTopAmount((int) animation.getAnimatedValue());
443 animator.setInterpolator(mFastOutSlowInInterpolator);
444 long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
445 animator.setDuration(newDuration);
446 if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
447 animator.setStartDelay(delay);
449 animator.addListener(getGlobalAnimationFinishedListener());
450 // remove the tag when the animation is finished
451 animator.addListener(new AnimatorListenerAdapter() {
453 public void onAnimationEnd(Animator animation) {
454 child.setTag(TAG_ANIMATOR_TOP_INSET, null);
455 child.setTag(TAG_START_TOP_INSET, null);
456 child.setTag(TAG_END_TOP_INSET, null);
459 startAnimator(animator);
460 child.setTag(TAG_ANIMATOR_TOP_INSET, animator);
461 child.setTag(TAG_START_TOP_INSET, child.getClipTopAmount());
462 child.setTag(TAG_END_TOP_INSET, newEndValue);
465 private void startAlphaAnimation(final View child,
466 final ViewState viewState, long duration, long delay) {
467 Float previousStartValue = getChildTag(child,TAG_START_ALPHA);
468 Float previousEndValue = getChildTag(child,TAG_END_ALPHA);
469 final float newEndValue = viewState.alpha;
470 if (previousEndValue != null && previousEndValue == newEndValue) {
473 ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_ALPHA);
474 if (!mAnimationFilter.animateAlpha) {
475 // just a local update was performed
476 if (previousAnimator != null) {
477 // we need to increase all animation keyframes of the previous animator by the
478 // relative change to the end value
479 PropertyValuesHolder[] values = previousAnimator.getValues();
480 float relativeDiff = newEndValue - previousEndValue;
481 float newStartValue = previousStartValue + relativeDiff;
482 values[0].setFloatValues(newStartValue, newEndValue);
483 child.setTag(TAG_START_ALPHA, newStartValue);
484 child.setTag(TAG_END_ALPHA, newEndValue);
485 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
488 // no new animation needed, let's just apply the value
489 child.setAlpha(newEndValue);
490 if (newEndValue == 0) {
491 child.setVisibility(View.INVISIBLE);
496 ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.ALPHA,
497 child.getAlpha(), newEndValue);
498 animator.setInterpolator(mFastOutSlowInInterpolator);
500 child.setLayerType(View.LAYER_TYPE_HARDWARE, null);
501 animator.addListener(new AnimatorListenerAdapter() {
502 public boolean mWasCancelled;
505 public void onAnimationEnd(Animator animation) {
506 child.setLayerType(View.LAYER_TYPE_NONE, null);
507 if (newEndValue == 0 && !mWasCancelled) {
508 child.setVisibility(View.INVISIBLE);
510 // remove the tag when the animation is finished
511 child.setTag(TAG_ANIMATOR_ALPHA, null);
512 child.setTag(TAG_START_ALPHA, null);
513 child.setTag(TAG_END_ALPHA, null);
517 public void onAnimationCancel(Animator animation) {
518 mWasCancelled = true;
522 public void onAnimationStart(Animator animation) {
523 mWasCancelled = false;
526 long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
527 animator.setDuration(newDuration);
528 if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
529 animator.setStartDelay(delay);
531 animator.addListener(getGlobalAnimationFinishedListener());
533 startAnimator(animator);
534 child.setTag(TAG_ANIMATOR_ALPHA, animator);
535 child.setTag(TAG_START_ALPHA, child.getAlpha());
536 child.setTag(TAG_END_ALPHA, newEndValue);
539 private void startZTranslationAnimation(final View child,
540 final ViewState viewState, long duration, long delay) {
541 Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Z);
542 Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Z);
543 float newEndValue = viewState.zTranslation;
544 if (previousEndValue != null && previousEndValue == newEndValue) {
547 ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Z);
548 if (!mAnimationFilter.animateZ) {
549 // just a local update was performed
550 if (previousAnimator != null) {
551 // we need to increase all animation keyframes of the previous animator by the
552 // relative change to the end value
553 PropertyValuesHolder[] values = previousAnimator.getValues();
554 float relativeDiff = newEndValue - previousEndValue;
555 float newStartValue = previousStartValue + relativeDiff;
556 values[0].setFloatValues(newStartValue, newEndValue);
557 child.setTag(TAG_START_TRANSLATION_Z, newStartValue);
558 child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
559 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
562 // no new animation needed, let's just apply the value
563 child.setTranslationZ(newEndValue);
567 ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Z,
568 child.getTranslationZ(), newEndValue);
569 animator.setInterpolator(mFastOutSlowInInterpolator);
570 long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
571 animator.setDuration(newDuration);
572 if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
573 animator.setStartDelay(delay);
575 animator.addListener(getGlobalAnimationFinishedListener());
576 // remove the tag when the animation is finished
577 animator.addListener(new AnimatorListenerAdapter() {
579 public void onAnimationEnd(Animator animation) {
580 child.setTag(TAG_ANIMATOR_TRANSLATION_Z, null);
581 child.setTag(TAG_START_TRANSLATION_Z, null);
582 child.setTag(TAG_END_TRANSLATION_Z, null);
585 startAnimator(animator);
586 child.setTag(TAG_ANIMATOR_TRANSLATION_Z, animator);
587 child.setTag(TAG_START_TRANSLATION_Z, child.getTranslationZ());
588 child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
591 private void startYTranslationAnimation(final View child,
592 ViewState viewState, long duration, long delay) {
593 Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Y);
594 Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Y);
595 float newEndValue = viewState.yTranslation;
596 if (previousEndValue != null && previousEndValue == newEndValue) {
599 ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y);
600 if (!mAnimationFilter.animateY) {
601 // just a local update was performed
602 if (previousAnimator != null) {
603 // we need to increase all animation keyframes of the previous animator by the
604 // relative change to the end value
605 PropertyValuesHolder[] values = previousAnimator.getValues();
606 float relativeDiff = newEndValue - previousEndValue;
607 float newStartValue = previousStartValue + relativeDiff;
608 values[0].setFloatValues(newStartValue, newEndValue);
609 child.setTag(TAG_START_TRANSLATION_Y, newStartValue);
610 child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
611 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
614 // no new animation needed, let's just apply the value
615 child.setTranslationY(newEndValue);
620 ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Y,
621 child.getTranslationY(), newEndValue);
622 animator.setInterpolator(mFastOutSlowInInterpolator);
623 long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
624 animator.setDuration(newDuration);
625 if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
626 animator.setStartDelay(delay);
628 animator.addListener(getGlobalAnimationFinishedListener());
629 // remove the tag when the animation is finished
630 animator.addListener(new AnimatorListenerAdapter() {
632 public void onAnimationEnd(Animator animation) {
633 child.setTag(TAG_ANIMATOR_TRANSLATION_Y, null);
634 child.setTag(TAG_START_TRANSLATION_Y, null);
635 child.setTag(TAG_END_TRANSLATION_Y, null);
638 startAnimator(animator);
639 child.setTag(TAG_ANIMATOR_TRANSLATION_Y, animator);
640 child.setTag(TAG_START_TRANSLATION_Y, child.getTranslationY());
641 child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
644 private void startScaleAnimation(final View child,
645 ViewState viewState, long duration) {
646 Float previousStartValue = getChildTag(child, TAG_START_SCALE);
647 Float previousEndValue = getChildTag(child, TAG_END_SCALE);
648 float newEndValue = viewState.scale;
649 if (previousEndValue != null && previousEndValue == newEndValue) {
652 ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_SCALE);
653 if (!mAnimationFilter.animateScale) {
654 // just a local update was performed
655 if (previousAnimator != null) {
656 // we need to increase all animation keyframes of the previous animator by the
657 // relative change to the end value
658 PropertyValuesHolder[] values = previousAnimator.getValues();
659 float relativeDiff = newEndValue - previousEndValue;
660 float newStartValue = previousStartValue + relativeDiff;
661 values[0].setFloatValues(newStartValue, newEndValue);
662 values[1].setFloatValues(newStartValue, newEndValue);
663 child.setTag(TAG_START_SCALE, newStartValue);
664 child.setTag(TAG_END_SCALE, newEndValue);
665 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
668 // no new animation needed, let's just apply the value
669 child.setScaleX(newEndValue);
670 child.setScaleY(newEndValue);
674 PropertyValuesHolder holderX =
675 PropertyValuesHolder.ofFloat(View.SCALE_X, child.getScaleX(), newEndValue);
676 PropertyValuesHolder holderY =
677 PropertyValuesHolder.ofFloat(View.SCALE_Y, child.getScaleY(), newEndValue);
678 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(child, holderX, holderY);
679 animator.setInterpolator(mFastOutSlowInInterpolator);
680 long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
681 animator.setDuration(newDuration);
682 animator.addListener(getGlobalAnimationFinishedListener());
683 // remove the tag when the animation is finished
684 animator.addListener(new AnimatorListenerAdapter() {
686 public void onAnimationEnd(Animator animation) {
687 child.setTag(TAG_ANIMATOR_SCALE, null);
688 child.setTag(TAG_START_SCALE, null);
689 child.setTag(TAG_END_SCALE, null);
692 startAnimator(animator);
693 child.setTag(TAG_ANIMATOR_SCALE, animator);
694 child.setTag(TAG_START_SCALE, child.getScaleX());
695 child.setTag(TAG_END_SCALE, newEndValue);
698 private void startAnimator(ValueAnimator animator) {
699 mAnimatorSet.add(animator);
704 * @return an adapter which ensures that onAnimationFinished is called once no animation is
707 private AnimatorListenerAdapter getGlobalAnimationFinishedListener() {
708 if (!mAnimationListenerPool.empty()) {
709 return mAnimationListenerPool.pop();
712 // We need to create a new one, no reusable ones found
713 return new AnimatorListenerAdapter() {
714 private boolean mWasCancelled;
717 public void onAnimationEnd(Animator animation) {
718 mAnimatorSet.remove(animation);
719 if (mAnimatorSet.isEmpty() && !mWasCancelled) {
720 onAnimationFinished();
722 mAnimationListenerPool.push(this);
726 public void onAnimationCancel(Animator animation) {
727 mWasCancelled = true;
731 public void onAnimationStart(Animator animation) {
732 mWasCancelled = false;
737 private static <T> T getChildTag(View child, int tag) {
738 return (T) child.getTag(tag);
742 * Cancel the previous animator and get the duration of the new animation.
744 * @param duration the new duration
745 * @param previousAnimator the animator which was running before
746 * @return the new duration
748 private long cancelAnimatorAndGetNewDuration(long duration, ValueAnimator previousAnimator) {
749 long newDuration = duration;
750 if (previousAnimator != null) {
751 // We take either the desired length of the new animation or the remaining time of
752 // the previous animator, whichever is longer.
753 newDuration = Math.max(previousAnimator.getDuration()
754 - previousAnimator.getCurrentPlayTime(), newDuration);
755 previousAnimator.cancel();
760 private void onAnimationFinished() {
761 mHostLayout.onChildAnimationFinished();
765 * Process the animationEvents for a new animation
767 * @param animationEvents the animation events for the animation to perform
768 * @param finalState the final state to animate to
770 private void processAnimationEvents(
771 ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents,
772 StackScrollState finalState) {
773 for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) {
774 final ExpandableView changingView = (ExpandableView) event.changingView;
775 if (event.animationType ==
776 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) {
778 // This item is added, initialize it's properties.
779 StackViewState viewState = finalState
780 .getViewStateForView(changingView);
781 if (viewState == null) {
782 // The position for this child was never generated, let's continue.
785 if (changingView.getVisibility() == View.GONE) {
786 // The view was set to gone but the state never removed
787 finalState.removeViewStateForView(changingView);
790 finalState.applyState(changingView, viewState);
791 mNewAddChildren.add(changingView);
793 } else if (event.animationType ==
794 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) {
795 if (changingView.getVisibility() == View.GONE) {
796 mHostLayout.getOverlay().remove(changingView);
800 // Find the amount to translate up. This is needed in order to understand the
801 // direction of the remove animation (either downwards or upwards)
802 StackViewState viewState = finalState
803 .getViewStateForView(event.viewAfterChangingView);
804 int actualHeight = changingView.getActualHeight();
805 // upwards by default
806 float translationDirection = -1.0f;
807 if (viewState != null) {
808 // there was a view after this one, Approximate the distance the next child
810 translationDirection = ((viewState.yTranslation
811 - (changingView.getTranslationY() + actualHeight / 2.0f)) * 2 /
813 translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f);
816 changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR,
817 translationDirection, new Runnable() {
820 // remove the temporary overlay
821 mHostLayout.getOverlay().remove(changingView);
824 } else if (event.animationType ==
825 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) {
826 // A race condition can trigger the view to be added to the overlay even though
827 // it is swiped out. So let's remove it
828 mHostLayout.getOverlay().remove(changingView);
829 } else if (event.animationType == NotificationStackScrollLayout
830 .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) {
831 ExpandableNotificationRow row = (ExpandableNotificationRow) event.changingView;
832 row.prepareExpansionChanged(finalState);
833 mChildExpandingView = row;
834 } else if (event.animationType == NotificationStackScrollLayout
835 .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) {
836 // This item is added, initialize it's properties.
837 StackViewState viewState = finalState.getViewStateForView(changingView);
838 mTmpState.copyFrom(viewState);
839 mTmpState.yTranslation = -mTmpState.height;
840 finalState.applyState(changingView, mTmpState);
842 mNewEvents.add(event);
846 public void animateOverScrollToAmount(float targetAmount, final boolean onTop,
847 final boolean isRubberbanded) {
848 final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
849 if (targetAmount == startOverScrollAmount) {
852 cancelOverScrollAnimators(onTop);
853 ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount,
855 overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD);
856 overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
858 public void onAnimationUpdate(ValueAnimator animation) {
859 float currentOverScroll = (float) animation.getAnimatedValue();
860 mHostLayout.setOverScrollAmount(
861 currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */,
865 overScrollAnimator.setInterpolator(mFastOutSlowInInterpolator);
866 overScrollAnimator.addListener(new AnimatorListenerAdapter() {
868 public void onAnimationEnd(Animator animation) {
870 mTopOverScrollAnimator = null;
872 mBottomOverScrollAnimator = null;
876 overScrollAnimator.start();
878 mTopOverScrollAnimator = overScrollAnimator;
880 mBottomOverScrollAnimator = overScrollAnimator;
884 public void cancelOverScrollAnimators(boolean onTop) {
885 ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator;
886 if (currentAnimator != null) {
887 currentAnimator.cancel();
892 * Get the end value of the height animation running on a view or the actualHeight
893 * if no animation is running.
895 public static int getFinalActualHeight(ExpandableView view) {
899 ValueAnimator heightAnimator = getChildTag(view, TAG_ANIMATOR_HEIGHT);
900 if (heightAnimator == null) {
901 return view.getActualHeight();
903 return getChildTag(view, TAG_END_HEIGHT);