Home | History | Annotate | Download | only in stack
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      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
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     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
     15  */
     16 
     17 package com.android.systemui.statusbar.stack;
     18 
     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;
     27 
     28 import com.android.systemui.R;
     29 import com.android.systemui.statusbar.ExpandableView;
     30 import com.android.systemui.statusbar.SpeedBumpView;
     31 
     32 import java.util.ArrayList;
     33 import java.util.HashSet;
     34 import java.util.Set;
     35 import java.util.Stack;
     36 
     37 /**
     38  * An stack state animator which handles animations to new StackScrollStates
     39  */
     40 public class StackStateAnimator {
     41 
     42     public static final int ANIMATION_DURATION_STANDARD = 360;
     43     public static final int ANIMATION_DURATION_GO_TO_FULL_SHADE = 448;
     44     public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
     45     public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
     46     public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
     47     public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
     48     public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48;
     49     private static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
     50 
     51     private static final int TAG_ANIMATOR_TRANSLATION_Y = R.id.translation_y_animator_tag;
     52     private static final int TAG_ANIMATOR_TRANSLATION_Z = R.id.translation_z_animator_tag;
     53     private static final int TAG_ANIMATOR_SCALE = R.id.scale_animator_tag;
     54     private static final int TAG_ANIMATOR_ALPHA = R.id.alpha_animator_tag;
     55     private static final int TAG_ANIMATOR_HEIGHT = R.id.height_animator_tag;
     56     private static final int TAG_ANIMATOR_TOP_INSET = R.id.top_inset_animator_tag;
     57     private static final int TAG_END_TRANSLATION_Y = R.id.translation_y_animator_end_value_tag;
     58     private static final int TAG_END_TRANSLATION_Z = R.id.translation_z_animator_end_value_tag;
     59     private static final int TAG_END_SCALE = R.id.scale_animator_end_value_tag;
     60     private static final int TAG_END_ALPHA = R.id.alpha_animator_end_value_tag;
     61     private static final int TAG_END_HEIGHT = R.id.height_animator_end_value_tag;
     62     private static final int TAG_END_TOP_INSET = R.id.top_inset_animator_end_value_tag;
     63     private static final int TAG_START_TRANSLATION_Y = R.id.translation_y_animator_start_value_tag;
     64     private static final int TAG_START_TRANSLATION_Z = R.id.translation_z_animator_start_value_tag;
     65     private static final int TAG_START_SCALE = R.id.scale_animator_start_value_tag;
     66     private static final int TAG_START_ALPHA = R.id.alpha_animator_start_value_tag;
     67     private static final int TAG_START_HEIGHT = R.id.height_animator_start_value_tag;
     68     private static final int TAG_START_TOP_INSET = R.id.top_inset_animator_start_value_tag;
     69 
     70     private final Interpolator mFastOutSlowInInterpolator;
     71     private final int mGoToFullShadeAppearingTranslation;
     72     public NotificationStackScrollLayout mHostLayout;
     73     private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents =
     74             new ArrayList<>();
     75     private ArrayList<View> mNewAddChildren = new ArrayList<>();
     76     private Set<Animator> mAnimatorSet = new HashSet<>();
     77     private Stack<AnimatorListenerAdapter> mAnimationListenerPool = new Stack<>();
     78     private AnimationFilter mAnimationFilter = new AnimationFilter();
     79     private long mCurrentLength;
     80     private long mCurrentAdditionalDelay;
     81 
     82     /** The current index for the last child which was not added in this event set. */
     83     private int mCurrentLastNotAddedIndex;
     84 
     85     private ValueAnimator mTopOverScrollAnimator;
     86     private ValueAnimator mBottomOverScrollAnimator;
     87 
     88     public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
     89         mHostLayout = hostLayout;
     90         mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(hostLayout.getContext(),
     91                 android.R.interpolator.fast_out_slow_in);
     92         mGoToFullShadeAppearingTranslation =
     93                 hostLayout.getContext().getResources().getDimensionPixelSize(
     94                         R.dimen.go_to_full_shade_appearing_translation);
     95     }
     96 
     97     public boolean isRunning() {
     98         return !mAnimatorSet.isEmpty();
     99     }
    100 
    101     public void startAnimationForEvents(
    102             ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents,
    103             StackScrollState finalState, long additionalDelay) {
    104 
    105         processAnimationEvents(mAnimationEvents, finalState);
    106 
    107         int childCount = mHostLayout.getChildCount();
    108         mAnimationFilter.applyCombination(mNewEvents);
    109         mCurrentAdditionalDelay = additionalDelay;
    110         mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents);
    111         mCurrentLastNotAddedIndex = findLastNotAddedIndex(finalState);
    112         for (int i = 0; i < childCount; i++) {
    113             final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
    114 
    115             StackScrollState.ViewState viewState = finalState.getViewStateForView(child);
    116             if (viewState == null || child.getVisibility() == View.GONE) {
    117                 continue;
    118             }
    119 
    120             child.setClipBounds(null);
    121             startAnimations(child, viewState, finalState, i);
    122         }
    123         if (!isRunning()) {
    124             // no child has preformed any animation, lets finish
    125             onAnimationFinished();
    126         }
    127         mNewEvents.clear();
    128         mNewAddChildren.clear();
    129     }
    130 
    131     private int findLastNotAddedIndex(StackScrollState finalState) {
    132         int childCount = mHostLayout.getChildCount();
    133         for (int i = childCount - 1; i >= 0; i--) {
    134             final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
    135 
    136             StackScrollState.ViewState viewState = finalState.getViewStateForView(child);
    137             if (viewState == null || child.getVisibility() == View.GONE) {
    138                 continue;
    139             }
    140             if (!mNewAddChildren.contains(child)) {
    141                 return viewState.notGoneIndex;
    142             }
    143         }
    144         return -1;
    145     }
    146 
    147     /**
    148      * Start an animation to the given viewState
    149      */
    150     private void startAnimations(final ExpandableView child, StackScrollState.ViewState viewState,
    151             StackScrollState finalState, int i) {
    152         int childVisibility = child.getVisibility();
    153         boolean wasVisible = childVisibility == View.VISIBLE;
    154         final float alpha = viewState.alpha;
    155         if (!wasVisible && alpha != 0 && !viewState.gone) {
    156             child.setVisibility(View.VISIBLE);
    157         }
    158 
    159         boolean yTranslationChanging = child.getTranslationY() != viewState.yTranslation;
    160         boolean zTranslationChanging = child.getTranslationZ() != viewState.zTranslation;
    161         boolean scaleChanging = child.getScaleX() != viewState.scale;
    162         boolean alphaChanging = alpha != child.getAlpha();
    163         boolean heightChanging = viewState.height != child.getActualHeight();
    164         boolean topInsetChanging = viewState.clipTopAmount != child.getClipTopAmount();
    165         boolean wasAdded = mNewAddChildren.contains(child);
    166         boolean hasDelays = mAnimationFilter.hasDelays;
    167         boolean isDelayRelevant = yTranslationChanging || zTranslationChanging || scaleChanging ||
    168                 alphaChanging || heightChanging || topInsetChanging;
    169         boolean noAnimation = wasAdded;
    170         long delay = 0;
    171         long duration = mCurrentLength;
    172         if (hasDelays && isDelayRelevant || wasAdded) {
    173             delay = mCurrentAdditionalDelay + calculateChildAnimationDelay(viewState, finalState);
    174         }
    175 
    176         if (wasAdded && mAnimationFilter.hasGoToFullShadeEvent) {
    177             child.setTranslationY(child.getTranslationY() + mGoToFullShadeAppearingTranslation);
    178             yTranslationChanging = true;
    179             float longerDurationFactor = viewState.notGoneIndex - mCurrentLastNotAddedIndex;
    180             longerDurationFactor = (float) Math.pow(longerDurationFactor, 0.7f);
    181             duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50 +
    182                     (long) (100 * longerDurationFactor);
    183         }
    184 
    185         // start translationY animation
    186         if (yTranslationChanging) {
    187             if (noAnimation && !mAnimationFilter.hasGoToFullShadeEvent) {
    188                 child.setTranslationY(viewState.yTranslation);
    189             } else {
    190                 startYTranslationAnimation(child, viewState, duration, delay);
    191             }
    192         }
    193 
    194         // start translationZ animation
    195         if (zTranslationChanging) {
    196             if (noAnimation) {
    197                 child.setTranslationZ(viewState.zTranslation);
    198             } else {
    199                 startZTranslationAnimation(child, viewState, duration, delay);
    200             }
    201         }
    202 
    203         // start scale animation
    204         if (scaleChanging) {
    205             if (noAnimation) {
    206                 child.setScaleX(viewState.scale);
    207                 child.setScaleY(viewState.scale);
    208             } else {
    209                 startScaleAnimation(child, viewState, duration);
    210             }
    211         }
    212 
    213         // start alpha animation
    214         if (alphaChanging && child.getTranslationX() == 0) {
    215             if (noAnimation) {
    216                 child.setAlpha(viewState.alpha);
    217             } else {
    218                 startAlphaAnimation(child, viewState, duration, delay);
    219             }
    220         }
    221 
    222         // start height animation
    223         if (heightChanging && child.getActualHeight() != 0) {
    224             if (noAnimation) {
    225                 child.setActualHeight(viewState.height, false);
    226             } else {
    227                 startHeightAnimation(child, viewState, duration, delay);
    228             }
    229         }
    230 
    231         // start top inset animation
    232         if (topInsetChanging) {
    233             if (noAnimation) {
    234                 child.setClipTopAmount(viewState.clipTopAmount);
    235             } else {
    236                 startInsetAnimation(child, viewState, duration, delay);
    237             }
    238         }
    239 
    240         // start dimmed animation
    241         child.setDimmed(viewState.dimmed, mAnimationFilter.animateDimmed && !wasAdded
    242                 && !noAnimation);
    243 
    244         // start dark animation
    245         child.setDark(viewState.dark, mAnimationFilter.animateDark && !noAnimation);
    246 
    247         // apply speed bump state
    248         child.setBelowSpeedBump(viewState.belowSpeedBump);
    249 
    250         // start hiding sensitive animation
    251         child.setHideSensitive(viewState.hideSensitive, mAnimationFilter.animateHideSensitive &&
    252                 !wasAdded && !noAnimation, delay, duration);
    253 
    254         if (wasAdded) {
    255             child.performAddAnimation(delay, mCurrentLength);
    256         }
    257         if (child instanceof SpeedBumpView) {
    258             finalState.performSpeedBumpAnimation(i, (SpeedBumpView) child, viewState,
    259                     delay + duration);
    260         }
    261     }
    262 
    263     private long calculateChildAnimationDelay(StackScrollState.ViewState viewState,
    264             StackScrollState finalState) {
    265         if (mAnimationFilter.hasGoToFullShadeEvent) {
    266             return calculateDelayGoToFullShade(viewState);
    267         }
    268         long minDelay = 0;
    269         for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) {
    270             long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING;
    271             switch (event.animationType) {
    272                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: {
    273                     int ownIndex = viewState.notGoneIndex;
    274                     int changingIndex = finalState
    275                             .getViewStateForView(event.changingView).notGoneIndex;
    276                     int difference = Math.abs(ownIndex - changingIndex);
    277                     difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
    278                             difference - 1));
    279                     long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement;
    280                     minDelay = Math.max(delay, minDelay);
    281                     break;
    282                 }
    283                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT:
    284                     delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL;
    285                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: {
    286                     int ownIndex = viewState.notGoneIndex;
    287                     boolean noNextView = event.viewAfterChangingView == null;
    288                     View viewAfterChangingView = noNextView
    289                             ? mHostLayout.getLastChildNotGone()
    290                             : event.viewAfterChangingView;
    291 
    292                     int nextIndex = finalState
    293                             .getViewStateForView(viewAfterChangingView).notGoneIndex;
    294                     if (ownIndex >= nextIndex) {
    295                         // we only have the view afterwards
    296                         ownIndex++;
    297                     }
    298                     int difference = Math.abs(ownIndex - nextIndex);
    299                     difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
    300                             difference - 1));
    301                     long delay = difference * delayPerElement;
    302                     minDelay = Math.max(delay, minDelay);
    303                     break;
    304                 }
    305                 default:
    306                     break;
    307             }
    308         }
    309         return minDelay;
    310     }
    311 
    312     private long calculateDelayGoToFullShade(StackScrollState.ViewState viewState) {
    313         float index = viewState.notGoneIndex;
    314         index = (float) Math.pow(index, 0.7f);
    315         return (long) (index * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE);
    316     }
    317 
    318     private void startHeightAnimation(final ExpandableView child,
    319             StackScrollState.ViewState viewState, long duration, long delay) {
    320         Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT);
    321         Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT);
    322         int newEndValue = viewState.height;
    323         if (previousEndValue != null && previousEndValue == newEndValue) {
    324             return;
    325         }
    326         ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT);
    327         if (!mAnimationFilter.animateHeight) {
    328             // just a local update was performed
    329             if (previousAnimator != null) {
    330                 // we need to increase all animation keyframes of the previous animator by the
    331                 // relative change to the end value
    332                 PropertyValuesHolder[] values = previousAnimator.getValues();
    333                 int relativeDiff = newEndValue - previousEndValue;
    334                 int newStartValue = previousStartValue + relativeDiff;
    335                 values[0].setIntValues(newStartValue, newEndValue);
    336                 child.setTag(TAG_START_HEIGHT, newStartValue);
    337                 child.setTag(TAG_END_HEIGHT, newEndValue);
    338                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
    339                 return;
    340             } else {
    341                 // no new animation needed, let's just apply the value
    342                 child.setActualHeight(newEndValue, false);
    343                 return;
    344             }
    345         }
    346 
    347         ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), newEndValue);
    348         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    349             @Override
    350             public void onAnimationUpdate(ValueAnimator animation) {
    351                 child.setActualHeight((int) animation.getAnimatedValue(),
    352                         false /* notifyListeners */);
    353             }
    354         });
    355         animator.setInterpolator(mFastOutSlowInInterpolator);
    356         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
    357         animator.setDuration(newDuration);
    358         if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
    359             animator.setStartDelay(delay);
    360         }
    361         animator.addListener(getGlobalAnimationFinishedListener());
    362         // remove the tag when the animation is finished
    363         animator.addListener(new AnimatorListenerAdapter() {
    364             @Override
    365             public void onAnimationEnd(Animator animation) {
    366                 child.setTag(TAG_ANIMATOR_HEIGHT, null);
    367                 child.setTag(TAG_START_HEIGHT, null);
    368                 child.setTag(TAG_END_HEIGHT, null);
    369             }
    370         });
    371         startAnimator(animator);
    372         child.setTag(TAG_ANIMATOR_HEIGHT, animator);
    373         child.setTag(TAG_START_HEIGHT, child.getActualHeight());
    374         child.setTag(TAG_END_HEIGHT, newEndValue);
    375     }
    376 
    377     private void startInsetAnimation(final ExpandableView child,
    378             StackScrollState.ViewState viewState, long duration, long delay) {
    379         Integer previousStartValue = getChildTag(child, TAG_START_TOP_INSET);
    380         Integer previousEndValue = getChildTag(child, TAG_END_TOP_INSET);
    381         int newEndValue = viewState.clipTopAmount;
    382         if (previousEndValue != null && previousEndValue == newEndValue) {
    383             return;
    384         }
    385         ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TOP_INSET);
    386         if (!mAnimationFilter.animateTopInset) {
    387             // just a local update was performed
    388             if (previousAnimator != null) {
    389                 // we need to increase all animation keyframes of the previous animator by the
    390                 // relative change to the end value
    391                 PropertyValuesHolder[] values = previousAnimator.getValues();
    392                 int relativeDiff = newEndValue - previousEndValue;
    393                 int newStartValue = previousStartValue + relativeDiff;
    394                 values[0].setIntValues(newStartValue, newEndValue);
    395                 child.setTag(TAG_START_TOP_INSET, newStartValue);
    396                 child.setTag(TAG_END_TOP_INSET, newEndValue);
    397                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
    398                 return;
    399             } else {
    400                 // no new animation needed, let's just apply the value
    401                 child.setClipTopAmount(newEndValue);
    402                 return;
    403             }
    404         }
    405 
    406         ValueAnimator animator = ValueAnimator.ofInt(child.getClipTopAmount(), newEndValue);
    407         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    408             @Override
    409             public void onAnimationUpdate(ValueAnimator animation) {
    410                 child.setClipTopAmount((int) animation.getAnimatedValue());
    411             }
    412         });
    413         animator.setInterpolator(mFastOutSlowInInterpolator);
    414         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
    415         animator.setDuration(newDuration);
    416         if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
    417             animator.setStartDelay(delay);
    418         }
    419         animator.addListener(getGlobalAnimationFinishedListener());
    420         // remove the tag when the animation is finished
    421         animator.addListener(new AnimatorListenerAdapter() {
    422             @Override
    423             public void onAnimationEnd(Animator animation) {
    424                 child.setTag(TAG_ANIMATOR_TOP_INSET, null);
    425                 child.setTag(TAG_START_TOP_INSET, null);
    426                 child.setTag(TAG_END_TOP_INSET, null);
    427             }
    428         });
    429         startAnimator(animator);
    430         child.setTag(TAG_ANIMATOR_TOP_INSET, animator);
    431         child.setTag(TAG_START_TOP_INSET, child.getClipTopAmount());
    432         child.setTag(TAG_END_TOP_INSET, newEndValue);
    433     }
    434 
    435     private void startAlphaAnimation(final ExpandableView child,
    436             final StackScrollState.ViewState viewState, long duration, long delay) {
    437         Float previousStartValue = getChildTag(child,TAG_START_ALPHA);
    438         Float previousEndValue = getChildTag(child,TAG_END_ALPHA);
    439         final float newEndValue = viewState.alpha;
    440         if (previousEndValue != null && previousEndValue == newEndValue) {
    441             return;
    442         }
    443         ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_ALPHA);
    444         if (!mAnimationFilter.animateAlpha) {
    445             // just a local update was performed
    446             if (previousAnimator != null) {
    447                 // we need to increase all animation keyframes of the previous animator by the
    448                 // relative change to the end value
    449                 PropertyValuesHolder[] values = previousAnimator.getValues();
    450                 float relativeDiff = newEndValue - previousEndValue;
    451                 float newStartValue = previousStartValue + relativeDiff;
    452                 values[0].setFloatValues(newStartValue, newEndValue);
    453                 child.setTag(TAG_START_ALPHA, newStartValue);
    454                 child.setTag(TAG_END_ALPHA, newEndValue);
    455                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
    456                 return;
    457             } else {
    458                 // no new animation needed, let's just apply the value
    459                 child.setAlpha(newEndValue);
    460                 if (newEndValue == 0) {
    461                     child.setVisibility(View.INVISIBLE);
    462                 }
    463             }
    464         }
    465 
    466         ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.ALPHA,
    467                 child.getAlpha(), newEndValue);
    468         animator.setInterpolator(mFastOutSlowInInterpolator);
    469         // Handle layer type
    470         child.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    471         animator.addListener(new AnimatorListenerAdapter() {
    472             public boolean mWasCancelled;
    473 
    474             @Override
    475             public void onAnimationEnd(Animator animation) {
    476                 child.setLayerType(View.LAYER_TYPE_NONE, null);
    477                 if (newEndValue == 0 && !mWasCancelled) {
    478                     child.setVisibility(View.INVISIBLE);
    479                 }
    480                 child.setTag(TAG_ANIMATOR_ALPHA, null);
    481                 child.setTag(TAG_START_ALPHA, null);
    482                 child.setTag(TAG_END_ALPHA, null);
    483             }
    484 
    485             @Override
    486             public void onAnimationCancel(Animator animation) {
    487                 mWasCancelled = true;
    488             }
    489 
    490             @Override
    491             public void onAnimationStart(Animator animation) {
    492                 mWasCancelled = false;
    493             }
    494         });
    495         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
    496         animator.setDuration(newDuration);
    497         if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
    498             animator.setStartDelay(delay);
    499         }
    500         animator.addListener(getGlobalAnimationFinishedListener());
    501         // remove the tag when the animation is finished
    502         animator.addListener(new AnimatorListenerAdapter() {
    503             @Override
    504             public void onAnimationEnd(Animator animation) {
    505 
    506             }
    507         });
    508         startAnimator(animator);
    509         child.setTag(TAG_ANIMATOR_ALPHA, animator);
    510         child.setTag(TAG_START_ALPHA, child.getAlpha());
    511         child.setTag(TAG_END_ALPHA, newEndValue);
    512     }
    513 
    514     private void startZTranslationAnimation(final ExpandableView child,
    515             final StackScrollState.ViewState viewState, long duration, long delay) {
    516         Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Z);
    517         Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Z);
    518         float newEndValue = viewState.zTranslation;
    519         if (previousEndValue != null && previousEndValue == newEndValue) {
    520             return;
    521         }
    522         ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Z);
    523         if (!mAnimationFilter.animateZ) {
    524             // just a local update was performed
    525             if (previousAnimator != null) {
    526                 // we need to increase all animation keyframes of the previous animator by the
    527                 // relative change to the end value
    528                 PropertyValuesHolder[] values = previousAnimator.getValues();
    529                 float relativeDiff = newEndValue - previousEndValue;
    530                 float newStartValue = previousStartValue + relativeDiff;
    531                 values[0].setFloatValues(newStartValue, newEndValue);
    532                 child.setTag(TAG_START_TRANSLATION_Z, newStartValue);
    533                 child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
    534                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
    535                 return;
    536             } else {
    537                 // no new animation needed, let's just apply the value
    538                 child.setTranslationZ(newEndValue);
    539             }
    540         }
    541 
    542         ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Z,
    543                 child.getTranslationZ(), newEndValue);
    544         animator.setInterpolator(mFastOutSlowInInterpolator);
    545         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
    546         animator.setDuration(newDuration);
    547         if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
    548             animator.setStartDelay(delay);
    549         }
    550         animator.addListener(getGlobalAnimationFinishedListener());
    551         // remove the tag when the animation is finished
    552         animator.addListener(new AnimatorListenerAdapter() {
    553             @Override
    554             public void onAnimationEnd(Animator animation) {
    555                 child.setTag(TAG_ANIMATOR_TRANSLATION_Z, null);
    556                 child.setTag(TAG_START_TRANSLATION_Z, null);
    557                 child.setTag(TAG_END_TRANSLATION_Z, null);
    558             }
    559         });
    560         startAnimator(animator);
    561         child.setTag(TAG_ANIMATOR_TRANSLATION_Z, animator);
    562         child.setTag(TAG_START_TRANSLATION_Z, child.getTranslationZ());
    563         child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
    564     }
    565 
    566     private void startYTranslationAnimation(final ExpandableView child,
    567             StackScrollState.ViewState viewState, long duration, long delay) {
    568         Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Y);
    569         Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Y);
    570         float newEndValue = viewState.yTranslation;
    571         if (previousEndValue != null && previousEndValue == newEndValue) {
    572             return;
    573         }
    574         ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y);
    575         if (!mAnimationFilter.animateY) {
    576             // just a local update was performed
    577             if (previousAnimator != null) {
    578                 // we need to increase all animation keyframes of the previous animator by the
    579                 // relative change to the end value
    580                 PropertyValuesHolder[] values = previousAnimator.getValues();
    581                 float relativeDiff = newEndValue - previousEndValue;
    582                 float newStartValue = previousStartValue + relativeDiff;
    583                 values[0].setFloatValues(newStartValue, newEndValue);
    584                 child.setTag(TAG_START_TRANSLATION_Y, newStartValue);
    585                 child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
    586                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
    587                 return;
    588             } else {
    589                 // no new animation needed, let's just apply the value
    590                 child.setTranslationY(newEndValue);
    591                 return;
    592             }
    593         }
    594 
    595         ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Y,
    596                 child.getTranslationY(), newEndValue);
    597         animator.setInterpolator(mFastOutSlowInInterpolator);
    598         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
    599         animator.setDuration(newDuration);
    600         if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
    601             animator.setStartDelay(delay);
    602         }
    603         animator.addListener(getGlobalAnimationFinishedListener());
    604         // remove the tag when the animation is finished
    605         animator.addListener(new AnimatorListenerAdapter() {
    606             @Override
    607             public void onAnimationEnd(Animator animation) {
    608                 child.setTag(TAG_ANIMATOR_TRANSLATION_Y, null);
    609                 child.setTag(TAG_START_TRANSLATION_Y, null);
    610                 child.setTag(TAG_END_TRANSLATION_Y, null);
    611             }
    612         });
    613         startAnimator(animator);
    614         child.setTag(TAG_ANIMATOR_TRANSLATION_Y, animator);
    615         child.setTag(TAG_START_TRANSLATION_Y, child.getTranslationY());
    616         child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
    617     }
    618 
    619     private void startScaleAnimation(final ExpandableView child,
    620             StackScrollState.ViewState viewState, long duration) {
    621         Float previousStartValue = getChildTag(child, TAG_START_SCALE);
    622         Float previousEndValue = getChildTag(child, TAG_END_SCALE);
    623         float newEndValue = viewState.scale;
    624         if (previousEndValue != null && previousEndValue == newEndValue) {
    625             return;
    626         }
    627         ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_SCALE);
    628         if (!mAnimationFilter.animateScale) {
    629             // just a local update was performed
    630             if (previousAnimator != null) {
    631                 // we need to increase all animation keyframes of the previous animator by the
    632                 // relative change to the end value
    633                 PropertyValuesHolder[] values = previousAnimator.getValues();
    634                 float relativeDiff = newEndValue - previousEndValue;
    635                 float newStartValue = previousStartValue + relativeDiff;
    636                 values[0].setFloatValues(newStartValue, newEndValue);
    637                 values[1].setFloatValues(newStartValue, newEndValue);
    638                 child.setTag(TAG_START_SCALE, newStartValue);
    639                 child.setTag(TAG_END_SCALE, newEndValue);
    640                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
    641                 return;
    642             } else {
    643                 // no new animation needed, let's just apply the value
    644                 child.setScaleX(newEndValue);
    645                 child.setScaleY(newEndValue);
    646             }
    647         }
    648 
    649         PropertyValuesHolder holderX =
    650                 PropertyValuesHolder.ofFloat(View.SCALE_X, child.getScaleX(), newEndValue);
    651         PropertyValuesHolder holderY =
    652                 PropertyValuesHolder.ofFloat(View.SCALE_Y, child.getScaleY(), newEndValue);
    653         ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(child, holderX, holderY);
    654         animator.setInterpolator(mFastOutSlowInInterpolator);
    655         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
    656         animator.setDuration(newDuration);
    657         animator.addListener(getGlobalAnimationFinishedListener());
    658         // remove the tag when the animation is finished
    659         animator.addListener(new AnimatorListenerAdapter() {
    660             @Override
    661             public void onAnimationEnd(Animator animation) {
    662                 child.setTag(TAG_ANIMATOR_SCALE, null);
    663                 child.setTag(TAG_START_SCALE, null);
    664                 child.setTag(TAG_END_SCALE, null);
    665             }
    666         });
    667         startAnimator(animator);
    668         child.setTag(TAG_ANIMATOR_SCALE, animator);
    669         child.setTag(TAG_START_SCALE, child.getScaleX());
    670         child.setTag(TAG_END_SCALE, newEndValue);
    671     }
    672 
    673     private void startAnimator(ValueAnimator animator) {
    674         mAnimatorSet.add(animator);
    675         animator.start();
    676     }
    677 
    678     /**
    679      * @return an adapter which ensures that onAnimationFinished is called once no animation is
    680      *         running anymore
    681      */
    682     private AnimatorListenerAdapter getGlobalAnimationFinishedListener() {
    683         if (!mAnimationListenerPool.empty()) {
    684             return mAnimationListenerPool.pop();
    685         }
    686 
    687         // We need to create a new one, no reusable ones found
    688         return new AnimatorListenerAdapter() {
    689             private boolean mWasCancelled;
    690 
    691             @Override
    692             public void onAnimationEnd(Animator animation) {
    693                 mAnimatorSet.remove(animation);
    694                 if (mAnimatorSet.isEmpty() && !mWasCancelled) {
    695                     onAnimationFinished();
    696                 }
    697                 mAnimationListenerPool.push(this);
    698             }
    699 
    700             @Override
    701             public void onAnimationCancel(Animator animation) {
    702                 mWasCancelled = true;
    703             }
    704 
    705             @Override
    706             public void onAnimationStart(Animator animation) {
    707                 mWasCancelled = false;
    708             }
    709         };
    710     }
    711 
    712     private static <T> T getChildTag(View child, int tag) {
    713         return (T) child.getTag(tag);
    714     }
    715 
    716     /**
    717      * Cancel the previous animator and get the duration of the new animation.
    718      *
    719      * @param duration the new duration
    720      * @param previousAnimator the animator which was running before
    721      * @return the new duration
    722      */
    723     private long cancelAnimatorAndGetNewDuration(long duration, ValueAnimator previousAnimator) {
    724         long newDuration = duration;
    725         if (previousAnimator != null) {
    726             // We take either the desired length of the new animation or the remaining time of
    727             // the previous animator, whichever is longer.
    728             newDuration = Math.max(previousAnimator.getDuration()
    729                     - previousAnimator.getCurrentPlayTime(), newDuration);
    730             previousAnimator.cancel();
    731         }
    732         return newDuration;
    733     }
    734 
    735     private void onAnimationFinished() {
    736         mHostLayout.onChildAnimationFinished();
    737     }
    738 
    739     /**
    740      * Process the animationEvents for a new animation
    741      *
    742      * @param animationEvents the animation events for the animation to perform
    743      * @param finalState the final state to animate to
    744      */
    745     private void processAnimationEvents(
    746             ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents,
    747             StackScrollState finalState) {
    748         for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) {
    749             final ExpandableView changingView = (ExpandableView) event.changingView;
    750             if (event.animationType ==
    751                     NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) {
    752 
    753                 // This item is added, initialize it's properties.
    754                 StackScrollState.ViewState viewState = finalState
    755                         .getViewStateForView(changingView);
    756                 if (viewState == null) {
    757                     // The position for this child was never generated, let's continue.
    758                     continue;
    759                 }
    760                 if (changingView.getVisibility() == View.GONE) {
    761                     // The view was set to gone but the state never removed
    762                     finalState.removeViewStateForView(changingView);
    763                     continue;
    764                 }
    765                 changingView.setAlpha(viewState.alpha);
    766                 changingView.setTranslationY(viewState.yTranslation);
    767                 changingView.setTranslationZ(viewState.zTranslation);
    768                 changingView.setActualHeight(viewState.height, false);
    769                 mNewAddChildren.add(changingView);
    770 
    771             } else if (event.animationType ==
    772                     NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) {
    773                 if (changingView.getVisibility() == View.GONE) {
    774                     mHostLayout.getOverlay().remove(changingView);
    775                     continue;
    776                 }
    777 
    778                 // Find the amount to translate up. This is needed in order to understand the
    779                 // direction of the remove animation (either downwards or upwards)
    780                 StackScrollState.ViewState viewState = finalState
    781                         .getViewStateForView(event.viewAfterChangingView);
    782                 int actualHeight = changingView.getActualHeight();
    783                 // upwards by default
    784                 float translationDirection = -1.0f;
    785                 if (viewState != null) {
    786                     // there was a view after this one, Approximate the distance the next child
    787                     // travelled
    788                     translationDirection = ((viewState.yTranslation
    789                             - (changingView.getTranslationY() + actualHeight / 2.0f)) * 2 /
    790                             actualHeight);
    791                     translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f);
    792 
    793                 }
    794                 changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR,
    795                         translationDirection, new Runnable() {
    796                     @Override
    797                     public void run() {
    798                         // remove the temporary overlay
    799                         mHostLayout.getOverlay().remove(changingView);
    800                     }
    801                 });
    802             }
    803             mNewEvents.add(event);
    804         }
    805     }
    806 
    807     public void animateOverScrollToAmount(float targetAmount, final boolean onTop,
    808             final boolean isRubberbanded) {
    809         final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
    810         if (targetAmount == startOverScrollAmount) {
    811             return;
    812         }
    813         cancelOverScrollAnimators(onTop);
    814         ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount,
    815                 targetAmount);
    816         overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD);
    817         overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    818             @Override
    819             public void onAnimationUpdate(ValueAnimator animation) {
    820                 float currentOverScroll = (float) animation.getAnimatedValue();
    821                 mHostLayout.setOverScrollAmount(
    822                         currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */,
    823                         isRubberbanded);
    824             }
    825         });
    826         overScrollAnimator.setInterpolator(mFastOutSlowInInterpolator);
    827         overScrollAnimator.addListener(new AnimatorListenerAdapter() {
    828             @Override
    829             public void onAnimationEnd(Animator animation) {
    830                 if (onTop) {
    831                     mTopOverScrollAnimator = null;
    832                 } else {
    833                     mBottomOverScrollAnimator = null;
    834                 }
    835             }
    836         });
    837         overScrollAnimator.start();
    838         if (onTop) {
    839             mTopOverScrollAnimator = overScrollAnimator;
    840         } else {
    841             mBottomOverScrollAnimator = overScrollAnimator;
    842         }
    843     }
    844 
    845     public void cancelOverScrollAnimators(boolean onTop) {
    846         ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator;
    847         if (currentAnimator != null) {
    848             currentAnimator.cancel();
    849         }
    850     }
    851 
    852     /**
    853      * Get the end value of the height animation running on a view or the actualHeight
    854      * if no animation is running.
    855      */
    856     public static int getFinalActualHeight(ExpandableView view) {
    857         if (view == null) {
    858             return 0;
    859         }
    860         ValueAnimator heightAnimator = getChildTag(view, TAG_ANIMATOR_HEIGHT);
    861         if (heightAnimator == null) {
    862             return view.getActualHeight();
    863         } else {
    864             return getChildTag(view, TAG_END_HEIGHT);
    865         }
    866     }
    867 }
    868