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