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.ViewGroup;
     26 import android.view.animation.Interpolator;
     27 
     28 import com.android.systemui.Interpolators;
     29 import com.android.systemui.R;
     30 import com.android.systemui.statusbar.ExpandableNotificationRow;
     31 import com.android.systemui.statusbar.ExpandableView;
     32 import com.android.systemui.statusbar.policy.HeadsUpManager;
     33 
     34 import java.util.ArrayList;
     35 import java.util.HashSet;
     36 import java.util.Stack;
     37 
     38 /**
     39  * An stack state animator which handles animations to new StackScrollStates
     40  */
     41 public class StackStateAnimator {
     42 
     43     public static final int ANIMATION_DURATION_STANDARD = 360;
     44     public static final int ANIMATION_DURATION_GO_TO_FULL_SHADE = 448;
     45     public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
     46     public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
     47     public static final int ANIMATION_DURATION_CLOSE_REMOTE_INPUT = 150;
     48     public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 650;
     49     public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 230;
     50     public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
     51     public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
     52     public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48;
     53     public static final int ANIMATION_DELAY_PER_ELEMENT_DARK = 24;
     54     public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
     55     public static final int ANIMATION_DELAY_HEADS_UP = 120;
     56 
     57     private static final int TAG_ANIMATOR_TRANSLATION_Y = R.id.translation_y_animator_tag;
     58     private static final int TAG_ANIMATOR_TRANSLATION_Z = R.id.translation_z_animator_tag;
     59     private static final int TAG_ANIMATOR_ALPHA = R.id.alpha_animator_tag;
     60     private static final int TAG_ANIMATOR_HEIGHT = R.id.height_animator_tag;
     61     private static final int TAG_ANIMATOR_TOP_INSET = R.id.top_inset_animator_tag;
     62     private static final int TAG_ANIMATOR_SHADOW_ALPHA = R.id.shadow_alpha_animator_tag;
     63     private static final int TAG_END_TRANSLATION_Y = R.id.translation_y_animator_end_value_tag;
     64     private static final int TAG_END_TRANSLATION_Z = R.id.translation_z_animator_end_value_tag;
     65     private static final int TAG_END_ALPHA = R.id.alpha_animator_end_value_tag;
     66     private static final int TAG_END_HEIGHT = R.id.height_animator_end_value_tag;
     67     private static final int TAG_END_TOP_INSET = R.id.top_inset_animator_end_value_tag;
     68     private static final int TAG_END_SHADOW_ALPHA = R.id.shadow_alpha_animator_end_value_tag;
     69     private static final int TAG_START_TRANSLATION_Y = R.id.translation_y_animator_start_value_tag;
     70     private static final int TAG_START_TRANSLATION_Z = R.id.translation_z_animator_start_value_tag;
     71     private static final int TAG_START_ALPHA = R.id.alpha_animator_start_value_tag;
     72     private static final int TAG_START_HEIGHT = R.id.height_animator_start_value_tag;
     73     private static final int TAG_START_TOP_INSET = R.id.top_inset_animator_start_value_tag;
     74     private static final int TAG_START_SHADOW_ALPHA = R.id.shadow_alpha_animator_start_value_tag;
     75 
     76     private final Interpolator mHeadsUpAppearInterpolator;
     77     private final int mGoToFullShadeAppearingTranslation;
     78     private final StackViewState mTmpState = new StackViewState();
     79     public NotificationStackScrollLayout mHostLayout;
     80     private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents =
     81             new ArrayList<>();
     82     private ArrayList<View> mNewAddChildren = new ArrayList<>();
     83     private HashSet<View> mHeadsUpAppearChildren = new HashSet<>();
     84     private HashSet<View> mHeadsUpDisappearChildren = new HashSet<>();
     85     private HashSet<Animator> mAnimatorSet = new HashSet<>();
     86     private Stack<AnimatorListenerAdapter> mAnimationListenerPool = new Stack<>();
     87     private AnimationFilter mAnimationFilter = new AnimationFilter();
     88     private long mCurrentLength;
     89     private long mCurrentAdditionalDelay;
     90 
     91     /** The current index for the last child which was not added in this event set. */
     92     private int mCurrentLastNotAddedIndex;
     93     private ValueAnimator mTopOverScrollAnimator;
     94     private ValueAnimator mBottomOverScrollAnimator;
     95     private int mHeadsUpAppearHeightBottom;
     96     private boolean mShadeExpanded;
     97     private ArrayList<View> mChildrenToClearFromOverlay = new ArrayList<>();
     98 
     99     public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
    100         mHostLayout = hostLayout;
    101         mGoToFullShadeAppearingTranslation =
    102                 hostLayout.getContext().getResources().getDimensionPixelSize(
    103                         R.dimen.go_to_full_shade_appearing_translation);
    104         mHeadsUpAppearInterpolator = new HeadsUpAppearInterpolator();
    105     }
    106 
    107     public boolean isRunning() {
    108         return !mAnimatorSet.isEmpty();
    109     }
    110 
    111     public void startAnimationForEvents(
    112             ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents,
    113             StackScrollState finalState, long additionalDelay) {
    114 
    115         processAnimationEvents(mAnimationEvents, finalState);
    116 
    117         int childCount = mHostLayout.getChildCount();
    118         mAnimationFilter.applyCombination(mNewEvents);
    119         mCurrentAdditionalDelay = additionalDelay;
    120         mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents);
    121         mCurrentLastNotAddedIndex = findLastNotAddedIndex(finalState);
    122         for (int i = 0; i < childCount; i++) {
    123             final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
    124 
    125             StackViewState viewState = finalState.getViewStateForView(child);
    126             if (viewState == null || child.getVisibility() == View.GONE
    127                     || applyWithoutAnimation(child, viewState, finalState)) {
    128                 continue;
    129             }
    130 
    131             startStackAnimations(child, viewState, finalState, i, -1 /* fixedDelay */);
    132         }
    133         if (!isRunning()) {
    134             // no child has preformed any animation, lets finish
    135             onAnimationFinished();
    136         }
    137         mHeadsUpAppearChildren.clear();
    138         mHeadsUpDisappearChildren.clear();
    139         mNewEvents.clear();
    140         mNewAddChildren.clear();
    141     }
    142 
    143     /**
    144      * Determines if a view should not perform an animation and applies it directly.
    145      *
    146      * @return true if no animation should be performed
    147      */
    148     private boolean applyWithoutAnimation(ExpandableView child, StackViewState viewState,
    149             StackScrollState finalState) {
    150         if (mShadeExpanded) {
    151             return false;
    152         }
    153         if (getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y) != null) {
    154             // A Y translation animation is running
    155             return false;
    156         }
    157         if (mHeadsUpDisappearChildren.contains(child) || mHeadsUpAppearChildren.contains(child)) {
    158             // This is a heads up animation
    159             return false;
    160         }
    161         if (NotificationStackScrollLayout.isPinnedHeadsUp(child)) {
    162             // This is another headsUp which might move. Let's animate!
    163             return false;
    164         }
    165         finalState.applyState(child, viewState);
    166         return true;
    167     }
    168 
    169     private int findLastNotAddedIndex(StackScrollState finalState) {
    170         int childCount = mHostLayout.getChildCount();
    171         for (int i = childCount - 1; i >= 0; i--) {
    172             final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
    173 
    174             StackViewState viewState = finalState.getViewStateForView(child);
    175             if (viewState == null || child.getVisibility() == View.GONE) {
    176                 continue;
    177             }
    178             if (!mNewAddChildren.contains(child)) {
    179                 return viewState.notGoneIndex;
    180             }
    181         }
    182         return -1;
    183     }
    184 
    185 
    186     /**
    187      * Start an animation to the given  {@link StackViewState}.
    188      *
    189      * @param child the child to start the animation on
    190      * @param viewState the {@link StackViewState} of the view to animate to
    191      * @param finalState the final state after the animation
    192      * @param i the index of the view; only relevant if the view is the speed bump and is
    193      *          ignored otherwise
    194      * @param fixedDelay a fixed delay if desired or -1 if the delay should be calculated
    195      */
    196     public void startStackAnimations(final ExpandableView child, StackViewState viewState,
    197             StackScrollState finalState, int i, long fixedDelay) {
    198         boolean wasAdded = mNewAddChildren.contains(child);
    199         long duration = mCurrentLength;
    200         if (wasAdded && mAnimationFilter.hasGoToFullShadeEvent) {
    201             child.setTranslationY(child.getTranslationY() + mGoToFullShadeAppearingTranslation);
    202             float longerDurationFactor = viewState.notGoneIndex - mCurrentLastNotAddedIndex;
    203             longerDurationFactor = (float) Math.pow(longerDurationFactor, 0.7f);
    204             duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50 +
    205                     (long) (100 * longerDurationFactor);
    206         }
    207         boolean yTranslationChanging = child.getTranslationY() != viewState.yTranslation;
    208         boolean zTranslationChanging = child.getTranslationZ() != viewState.zTranslation;
    209         boolean alphaChanging = viewState.alpha != child.getAlpha();
    210         boolean heightChanging = viewState.height != child.getActualHeight();
    211         boolean shadowAlphaChanging = viewState.shadowAlpha != child.getShadowAlpha();
    212         boolean darkChanging = viewState.dark != child.isDark();
    213         boolean topInsetChanging = viewState.clipTopAmount != child.getClipTopAmount();
    214         boolean hasDelays = mAnimationFilter.hasDelays;
    215         boolean isDelayRelevant = yTranslationChanging || zTranslationChanging || alphaChanging
    216                 || heightChanging || topInsetChanging || darkChanging || shadowAlphaChanging;
    217         long delay = 0;
    218         if (fixedDelay != -1) {
    219             delay = fixedDelay;
    220         } else if (hasDelays && isDelayRelevant || wasAdded) {
    221             delay = mCurrentAdditionalDelay + calculateChildAnimationDelay(viewState, finalState);
    222         }
    223 
    224         startViewAnimations(child, viewState, delay, duration);
    225 
    226         // start height animation
    227         if (heightChanging) {
    228             startHeightAnimation(child, viewState, duration, delay);
    229         }  else {
    230             abortAnimation(child, TAG_ANIMATOR_HEIGHT);
    231         }
    232 
    233         // start shadow alpha animation
    234         if (shadowAlphaChanging) {
    235             startShadowAlphaAnimation(child, viewState, duration, delay);
    236         } else {
    237             abortAnimation(child, TAG_ANIMATOR_SHADOW_ALPHA);
    238         }
    239 
    240         // start top inset animation
    241         if (topInsetChanging) {
    242             startInsetAnimation(child, viewState, duration, delay);
    243         } else {
    244             abortAnimation(child, TAG_ANIMATOR_TOP_INSET);
    245         }
    246 
    247         // start dimmed animation
    248         child.setDimmed(viewState.dimmed, mAnimationFilter.animateDimmed);
    249 
    250         // apply speed bump state
    251         child.setBelowSpeedBump(viewState.belowSpeedBump);
    252 
    253         // start hiding sensitive animation
    254         child.setHideSensitive(viewState.hideSensitive, mAnimationFilter.animateHideSensitive,
    255                 delay, duration);
    256 
    257         // start dark animation
    258         child.setDark(viewState.dark, mAnimationFilter.animateDark, delay);
    259 
    260         if (wasAdded) {
    261             child.performAddAnimation(delay, mCurrentLength);
    262         }
    263         if (child instanceof ExpandableNotificationRow) {
    264             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
    265             row.startChildAnimation(finalState, this, delay, duration);
    266         }
    267     }
    268 
    269     /**
    270      * Start an animation to a new {@link ViewState}.
    271      *
    272      * @param child the child to start the animation on
    273      * @param viewState the  {@link StackViewState} of the view to animate to
    274      * @param delay a fixed delay
    275      * @param duration the duration of the animation
    276      */
    277     public void startViewAnimations(View child, ViewState viewState, long delay, long duration) {
    278         boolean wasVisible = child.getVisibility() == View.VISIBLE;
    279         final float alpha = viewState.alpha;
    280         if (!wasVisible && (alpha != 0 || child.getAlpha() != 0)
    281                 && !viewState.gone && !viewState.hidden) {
    282             child.setVisibility(View.VISIBLE);
    283         }
    284         boolean yTranslationChanging = child.getTranslationY() != viewState.yTranslation;
    285         boolean zTranslationChanging = child.getTranslationZ() != viewState.zTranslation;
    286         float childAlpha = child.getAlpha();
    287         boolean alphaChanging = viewState.alpha != childAlpha;
    288         if (child instanceof ExpandableView) {
    289             // We don't want views to change visibility when they are animating to GONE
    290             alphaChanging &= !((ExpandableView) child).willBeGone();
    291         }
    292 
    293         // start translationY animation
    294         if (yTranslationChanging) {
    295             startYTranslationAnimation(child, viewState, duration, delay);
    296         } else {
    297             abortAnimation(child, TAG_ANIMATOR_TRANSLATION_Y);
    298         }
    299 
    300         // start translationZ animation
    301         if (zTranslationChanging) {
    302             startZTranslationAnimation(child, viewState, duration, delay);
    303         } else {
    304             abortAnimation(child, TAG_ANIMATOR_TRANSLATION_Z);
    305         }
    306 
    307         // start alpha animation
    308         if (alphaChanging && child.getTranslationX() == 0) {
    309             startAlphaAnimation(child, viewState, duration, delay);
    310         }  else {
    311             abortAnimation(child, TAG_ANIMATOR_ALPHA);
    312         }
    313     }
    314 
    315     private void abortAnimation(View child, int animatorTag) {
    316         Animator previousAnimator = getChildTag(child, animatorTag);
    317         if (previousAnimator != null) {
    318             previousAnimator.cancel();
    319         }
    320     }
    321 
    322     private long calculateChildAnimationDelay(StackViewState viewState,
    323             StackScrollState finalState) {
    324         if (mAnimationFilter.hasDarkEvent) {
    325             return calculateDelayDark(viewState);
    326         }
    327         if (mAnimationFilter.hasGoToFullShadeEvent) {
    328             return calculateDelayGoToFullShade(viewState);
    329         }
    330         if (mAnimationFilter.hasHeadsUpDisappearClickEvent) {
    331             return ANIMATION_DELAY_HEADS_UP;
    332         }
    333         long minDelay = 0;
    334         for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) {
    335             long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING;
    336             switch (event.animationType) {
    337                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: {
    338                     int ownIndex = viewState.notGoneIndex;
    339                     int changingIndex = finalState
    340                             .getViewStateForView(event.changingView).notGoneIndex;
    341                     int difference = Math.abs(ownIndex - changingIndex);
    342                     difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
    343                             difference - 1));
    344                     long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement;
    345                     minDelay = Math.max(delay, minDelay);
    346                     break;
    347                 }
    348                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT:
    349                     delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL;
    350                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: {
    351                     int ownIndex = viewState.notGoneIndex;
    352                     boolean noNextView = event.viewAfterChangingView == null;
    353                     View viewAfterChangingView = noNextView
    354                             ? mHostLayout.getLastChildNotGone()
    355                             : event.viewAfterChangingView;
    356 
    357                     int nextIndex = finalState
    358                             .getViewStateForView(viewAfterChangingView).notGoneIndex;
    359                     if (ownIndex >= nextIndex) {
    360                         // we only have the view afterwards
    361                         ownIndex++;
    362                     }
    363                     int difference = Math.abs(ownIndex - nextIndex);
    364                     difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
    365                             difference - 1));
    366                     long delay = difference * delayPerElement;
    367                     minDelay = Math.max(delay, minDelay);
    368                     break;
    369                 }
    370                 default:
    371                     break;
    372             }
    373         }
    374         return minDelay;
    375     }
    376 
    377     private long calculateDelayDark(StackViewState viewState) {
    378         int referenceIndex;
    379         if (mAnimationFilter.darkAnimationOriginIndex ==
    380                 NotificationStackScrollLayout.AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_ABOVE) {
    381             referenceIndex = 0;
    382         } else if (mAnimationFilter.darkAnimationOriginIndex ==
    383                 NotificationStackScrollLayout.AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_BELOW) {
    384             referenceIndex = mHostLayout.getNotGoneChildCount() - 1;
    385         } else {
    386             referenceIndex = mAnimationFilter.darkAnimationOriginIndex;
    387         }
    388         return Math.abs(referenceIndex - viewState.notGoneIndex) * ANIMATION_DELAY_PER_ELEMENT_DARK;
    389     }
    390 
    391     private long calculateDelayGoToFullShade(StackViewState viewState) {
    392         float index = viewState.notGoneIndex;
    393         index = (float) Math.pow(index, 0.7f);
    394         return (long) (index * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE);
    395     }
    396 
    397     private void startShadowAlphaAnimation(final ExpandableView child,
    398             StackViewState viewState, long duration, long delay) {
    399         Float previousStartValue = getChildTag(child, TAG_START_SHADOW_ALPHA);
    400         Float previousEndValue = getChildTag(child, TAG_END_SHADOW_ALPHA);
    401         float newEndValue = viewState.shadowAlpha;
    402         if (previousEndValue != null && previousEndValue == newEndValue) {
    403             return;
    404         }
    405         ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_SHADOW_ALPHA);
    406         if (!mAnimationFilter.animateShadowAlpha) {
    407             // just a local update was performed
    408             if (previousAnimator != null) {
    409                 // we need to increase all animation keyframes of the previous animator by the
    410                 // relative change to the end value
    411                 PropertyValuesHolder[] values = previousAnimator.getValues();
    412                 float relativeDiff = newEndValue - previousEndValue;
    413                 float newStartValue = previousStartValue + relativeDiff;
    414                 values[0].setFloatValues(newStartValue, newEndValue);
    415                 child.setTag(TAG_START_SHADOW_ALPHA, newStartValue);
    416                 child.setTag(TAG_END_SHADOW_ALPHA, newEndValue);
    417                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
    418                 return;
    419             } else {
    420                 // no new animation needed, let's just apply the value
    421                 child.setShadowAlpha(newEndValue);
    422                 return;
    423             }
    424         }
    425 
    426         ValueAnimator animator = ValueAnimator.ofFloat(child.getShadowAlpha(), newEndValue);
    427         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    428             @Override
    429             public void onAnimationUpdate(ValueAnimator animation) {
    430                 child.setShadowAlpha((float) animation.getAnimatedValue());
    431             }
    432         });
    433         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
    434         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
    435         animator.setDuration(newDuration);
    436         if (delay > 0 && (previousAnimator == null
    437                 || previousAnimator.getAnimatedFraction() == 0)) {
    438             animator.setStartDelay(delay);
    439         }
    440         animator.addListener(getGlobalAnimationFinishedListener());
    441         // remove the tag when the animation is finished
    442         animator.addListener(new AnimatorListenerAdapter() {
    443             @Override
    444             public void onAnimationEnd(Animator animation) {
    445                 child.setTag(TAG_ANIMATOR_SHADOW_ALPHA, null);
    446                 child.setTag(TAG_START_SHADOW_ALPHA, null);
    447                 child.setTag(TAG_END_SHADOW_ALPHA, null);
    448             }
    449         });
    450         startAnimator(animator);
    451         child.setTag(TAG_ANIMATOR_SHADOW_ALPHA, animator);
    452         child.setTag(TAG_START_SHADOW_ALPHA, child.getShadowAlpha());
    453         child.setTag(TAG_END_SHADOW_ALPHA, newEndValue);
    454     }
    455 
    456     private void startHeightAnimation(final ExpandableView child,
    457             StackViewState viewState, long duration, long delay) {
    458         Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT);
    459         Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT);
    460         int newEndValue = viewState.height;
    461         if (previousEndValue != null && previousEndValue == newEndValue) {
    462             return;
    463         }
    464         ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT);
    465         if (!mAnimationFilter.animateHeight) {
    466             // just a local update was performed
    467             if (previousAnimator != null) {
    468                 // we need to increase all animation keyframes of the previous animator by the
    469                 // relative change to the end value
    470                 PropertyValuesHolder[] values = previousAnimator.getValues();
    471                 int relativeDiff = newEndValue - previousEndValue;
    472                 int newStartValue = previousStartValue + relativeDiff;
    473                 values[0].setIntValues(newStartValue, newEndValue);
    474                 child.setTag(TAG_START_HEIGHT, newStartValue);
    475                 child.setTag(TAG_END_HEIGHT, newEndValue);
    476                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
    477                 return;
    478             } else {
    479                 // no new animation needed, let's just apply the value
    480                 child.setActualHeight(newEndValue, false);
    481                 return;
    482             }
    483         }
    484 
    485         ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), newEndValue);
    486         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    487             @Override
    488             public void onAnimationUpdate(ValueAnimator animation) {
    489                 child.setActualHeight((int) animation.getAnimatedValue(),
    490                         false /* notifyListeners */);
    491             }
    492         });
    493         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
    494         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
    495         animator.setDuration(newDuration);
    496         if (delay > 0 && (previousAnimator == null
    497                 || previousAnimator.getAnimatedFraction() == 0)) {
    498             animator.setStartDelay(delay);
    499         }
    500         animator.addListener(getGlobalAnimationFinishedListener());
    501         // remove the tag when the animation is finished
    502         animator.addListener(new AnimatorListenerAdapter() {
    503             boolean mWasCancelled;
    504 
    505             @Override
    506             public void onAnimationEnd(Animator animation) {
    507                 child.setTag(TAG_ANIMATOR_HEIGHT, null);
    508                 child.setTag(TAG_START_HEIGHT, null);
    509                 child.setTag(TAG_END_HEIGHT, null);
    510                 child.setActualHeightAnimating(false);
    511                 if (!mWasCancelled && child instanceof ExpandableNotificationRow) {
    512                     ((ExpandableNotificationRow) child).setGroupExpansionChanging(
    513                             false /* isExpansionChanging */);
    514                 }
    515             }
    516 
    517             @Override
    518             public void onAnimationStart(Animator animation) {
    519                 mWasCancelled = false;
    520             }
    521 
    522             @Override
    523             public void onAnimationCancel(Animator animation) {
    524                 mWasCancelled = true;
    525             }
    526         });
    527         startAnimator(animator);
    528         child.setTag(TAG_ANIMATOR_HEIGHT, animator);
    529         child.setTag(TAG_START_HEIGHT, child.getActualHeight());
    530         child.setTag(TAG_END_HEIGHT, newEndValue);
    531         child.setActualHeightAnimating(true);
    532     }
    533 
    534     private void startInsetAnimation(final ExpandableView child,
    535             StackViewState viewState, long duration, long delay) {
    536         Integer previousStartValue = getChildTag(child, TAG_START_TOP_INSET);
    537         Integer previousEndValue = getChildTag(child, TAG_END_TOP_INSET);
    538         int newEndValue = viewState.clipTopAmount;
    539         if (previousEndValue != null && previousEndValue == newEndValue) {
    540             return;
    541         }
    542         ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TOP_INSET);
    543         if (!mAnimationFilter.animateTopInset) {
    544             // just a local update was performed
    545             if (previousAnimator != null) {
    546                 // we need to increase all animation keyframes of the previous animator by the
    547                 // relative change to the end value
    548                 PropertyValuesHolder[] values = previousAnimator.getValues();
    549                 int relativeDiff = newEndValue - previousEndValue;
    550                 int newStartValue = previousStartValue + relativeDiff;
    551                 values[0].setIntValues(newStartValue, newEndValue);
    552                 child.setTag(TAG_START_TOP_INSET, newStartValue);
    553                 child.setTag(TAG_END_TOP_INSET, newEndValue);
    554                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
    555                 return;
    556             } else {
    557                 // no new animation needed, let's just apply the value
    558                 child.setClipTopAmount(newEndValue);
    559                 return;
    560             }
    561         }
    562 
    563         ValueAnimator animator = ValueAnimator.ofInt(child.getClipTopAmount(), newEndValue);
    564         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    565             @Override
    566             public void onAnimationUpdate(ValueAnimator animation) {
    567                 child.setClipTopAmount((int) animation.getAnimatedValue());
    568             }
    569         });
    570         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
    571         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
    572         animator.setDuration(newDuration);
    573         if (delay > 0 && (previousAnimator == null
    574                 || previousAnimator.getAnimatedFraction() == 0)) {
    575             animator.setStartDelay(delay);
    576         }
    577         animator.addListener(getGlobalAnimationFinishedListener());
    578         // remove the tag when the animation is finished
    579         animator.addListener(new AnimatorListenerAdapter() {
    580             @Override
    581             public void onAnimationEnd(Animator animation) {
    582                 child.setTag(TAG_ANIMATOR_TOP_INSET, null);
    583                 child.setTag(TAG_START_TOP_INSET, null);
    584                 child.setTag(TAG_END_TOP_INSET, null);
    585             }
    586         });
    587         startAnimator(animator);
    588         child.setTag(TAG_ANIMATOR_TOP_INSET, animator);
    589         child.setTag(TAG_START_TOP_INSET, child.getClipTopAmount());
    590         child.setTag(TAG_END_TOP_INSET, newEndValue);
    591     }
    592 
    593     private void startAlphaAnimation(final View child,
    594             final ViewState viewState, long duration, long delay) {
    595         Float previousStartValue = getChildTag(child,TAG_START_ALPHA);
    596         Float previousEndValue = getChildTag(child,TAG_END_ALPHA);
    597         final float newEndValue = viewState.alpha;
    598         if (previousEndValue != null && previousEndValue == newEndValue) {
    599             return;
    600         }
    601         ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_ALPHA);
    602         if (!mAnimationFilter.animateAlpha) {
    603             // just a local update was performed
    604             if (previousAnimator != null) {
    605                 // we need to increase all animation keyframes of the previous animator by the
    606                 // relative change to the end value
    607                 PropertyValuesHolder[] values = previousAnimator.getValues();
    608                 float relativeDiff = newEndValue - previousEndValue;
    609                 float newStartValue = previousStartValue + relativeDiff;
    610                 values[0].setFloatValues(newStartValue, newEndValue);
    611                 child.setTag(TAG_START_ALPHA, newStartValue);
    612                 child.setTag(TAG_END_ALPHA, newEndValue);
    613                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
    614                 return;
    615             } else {
    616                 // no new animation needed, let's just apply the value
    617                 child.setAlpha(newEndValue);
    618                 if (newEndValue == 0) {
    619                     child.setVisibility(View.INVISIBLE);
    620                 }
    621             }
    622         }
    623 
    624         ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.ALPHA,
    625                 child.getAlpha(), newEndValue);
    626         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
    627         // Handle layer type
    628         child.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    629         animator.addListener(new AnimatorListenerAdapter() {
    630             public boolean mWasCancelled;
    631 
    632             @Override
    633             public void onAnimationEnd(Animator animation) {
    634                 child.setLayerType(View.LAYER_TYPE_NONE, null);
    635                 if (newEndValue == 0 && !mWasCancelled) {
    636                     child.setVisibility(View.INVISIBLE);
    637                 }
    638                 // remove the tag when the animation is finished
    639                 child.setTag(TAG_ANIMATOR_ALPHA, null);
    640                 child.setTag(TAG_START_ALPHA, null);
    641                 child.setTag(TAG_END_ALPHA, null);
    642             }
    643 
    644             @Override
    645             public void onAnimationCancel(Animator animation) {
    646                 mWasCancelled = true;
    647             }
    648 
    649             @Override
    650             public void onAnimationStart(Animator animation) {
    651                 mWasCancelled = false;
    652             }
    653         });
    654         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
    655         animator.setDuration(newDuration);
    656         if (delay > 0 && (previousAnimator == null
    657                 || previousAnimator.getAnimatedFraction() == 0)) {
    658             animator.setStartDelay(delay);
    659         }
    660         animator.addListener(getGlobalAnimationFinishedListener());
    661 
    662         startAnimator(animator);
    663         child.setTag(TAG_ANIMATOR_ALPHA, animator);
    664         child.setTag(TAG_START_ALPHA, child.getAlpha());
    665         child.setTag(TAG_END_ALPHA, newEndValue);
    666     }
    667 
    668     private void startZTranslationAnimation(final View child,
    669             final ViewState viewState, long duration, long delay) {
    670         Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Z);
    671         Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Z);
    672         float newEndValue = viewState.zTranslation;
    673         if (previousEndValue != null && previousEndValue == newEndValue) {
    674             return;
    675         }
    676         ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Z);
    677         if (!mAnimationFilter.animateZ) {
    678             // just a local update was performed
    679             if (previousAnimator != null) {
    680                 // we need to increase all animation keyframes of the previous animator by the
    681                 // relative change to the end value
    682                 PropertyValuesHolder[] values = previousAnimator.getValues();
    683                 float relativeDiff = newEndValue - previousEndValue;
    684                 float newStartValue = previousStartValue + relativeDiff;
    685                 values[0].setFloatValues(newStartValue, newEndValue);
    686                 child.setTag(TAG_START_TRANSLATION_Z, newStartValue);
    687                 child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
    688                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
    689                 return;
    690             } else {
    691                 // no new animation needed, let's just apply the value
    692                 child.setTranslationZ(newEndValue);
    693             }
    694         }
    695 
    696         ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Z,
    697                 child.getTranslationZ(), newEndValue);
    698         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
    699         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
    700         animator.setDuration(newDuration);
    701         if (delay > 0 && (previousAnimator == null
    702                 || previousAnimator.getAnimatedFraction() == 0)) {
    703             animator.setStartDelay(delay);
    704         }
    705         animator.addListener(getGlobalAnimationFinishedListener());
    706         // remove the tag when the animation is finished
    707         animator.addListener(new AnimatorListenerAdapter() {
    708             @Override
    709             public void onAnimationEnd(Animator animation) {
    710                 child.setTag(TAG_ANIMATOR_TRANSLATION_Z, null);
    711                 child.setTag(TAG_START_TRANSLATION_Z, null);
    712                 child.setTag(TAG_END_TRANSLATION_Z, null);
    713             }
    714         });
    715         startAnimator(animator);
    716         child.setTag(TAG_ANIMATOR_TRANSLATION_Z, animator);
    717         child.setTag(TAG_START_TRANSLATION_Z, child.getTranslationZ());
    718         child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
    719     }
    720 
    721     private void startYTranslationAnimation(final View child,
    722             ViewState viewState, long duration, long delay) {
    723         Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Y);
    724         Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Y);
    725         float newEndValue = viewState.yTranslation;
    726         if (previousEndValue != null && previousEndValue == newEndValue) {
    727             return;
    728         }
    729         ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y);
    730         if (!mAnimationFilter.animateY) {
    731             // just a local update was performed
    732             if (previousAnimator != null) {
    733                 // we need to increase all animation keyframes of the previous animator by the
    734                 // relative change to the end value
    735                 PropertyValuesHolder[] values = previousAnimator.getValues();
    736                 float relativeDiff = newEndValue - previousEndValue;
    737                 float newStartValue = previousStartValue + relativeDiff;
    738                 values[0].setFloatValues(newStartValue, newEndValue);
    739                 child.setTag(TAG_START_TRANSLATION_Y, newStartValue);
    740                 child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
    741                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
    742                 return;
    743             } else {
    744                 // no new animation needed, let's just apply the value
    745                 child.setTranslationY(newEndValue);
    746                 return;
    747             }
    748         }
    749 
    750         ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Y,
    751                 child.getTranslationY(), newEndValue);
    752         Interpolator interpolator = mHeadsUpAppearChildren.contains(child) ?
    753                 mHeadsUpAppearInterpolator :Interpolators.FAST_OUT_SLOW_IN;
    754         animator.setInterpolator(interpolator);
    755         long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator);
    756         animator.setDuration(newDuration);
    757         if (delay > 0 && (previousAnimator == null
    758                 || previousAnimator.getAnimatedFraction() == 0)) {
    759             animator.setStartDelay(delay);
    760         }
    761         animator.addListener(getGlobalAnimationFinishedListener());
    762         final boolean isHeadsUpDisappear = mHeadsUpDisappearChildren.contains(child);
    763         // remove the tag when the animation is finished
    764         animator.addListener(new AnimatorListenerAdapter() {
    765             @Override
    766             public void onAnimationEnd(Animator animation) {
    767                 HeadsUpManager.setIsClickedNotification(child, false);
    768                 child.setTag(TAG_ANIMATOR_TRANSLATION_Y, null);
    769                 child.setTag(TAG_START_TRANSLATION_Y, null);
    770                 child.setTag(TAG_END_TRANSLATION_Y, null);
    771                 if (isHeadsUpDisappear) {
    772                     ((ExpandableNotificationRow) child).setHeadsupDisappearRunning(false);
    773                 }
    774             }
    775         });
    776         startAnimator(animator);
    777         child.setTag(TAG_ANIMATOR_TRANSLATION_Y, animator);
    778         child.setTag(TAG_START_TRANSLATION_Y, child.getTranslationY());
    779         child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
    780     }
    781 
    782     private void startAnimator(ValueAnimator animator) {
    783         mAnimatorSet.add(animator);
    784         animator.start();
    785     }
    786 
    787     /**
    788      * @return an adapter which ensures that onAnimationFinished is called once no animation is
    789      *         running anymore
    790      */
    791     private AnimatorListenerAdapter getGlobalAnimationFinishedListener() {
    792         if (!mAnimationListenerPool.empty()) {
    793             return mAnimationListenerPool.pop();
    794         }
    795 
    796         // We need to create a new one, no reusable ones found
    797         return new AnimatorListenerAdapter() {
    798             private boolean mWasCancelled;
    799 
    800             @Override
    801             public void onAnimationEnd(Animator animation) {
    802                 mAnimatorSet.remove(animation);
    803                 if (mAnimatorSet.isEmpty() && !mWasCancelled) {
    804                     onAnimationFinished();
    805                 }
    806                 mAnimationListenerPool.push(this);
    807             }
    808 
    809             @Override
    810             public void onAnimationCancel(Animator animation) {
    811                 mWasCancelled = true;
    812             }
    813 
    814             @Override
    815             public void onAnimationStart(Animator animation) {
    816                 mWasCancelled = false;
    817             }
    818         };
    819     }
    820 
    821     public static <T> T getChildTag(View child, int tag) {
    822         return (T) child.getTag(tag);
    823     }
    824 
    825     /**
    826      * Cancel the previous animator and get the duration of the new animation.
    827      *
    828      * @param duration the new duration
    829      * @param previousAnimator the animator which was running before
    830      * @return the new duration
    831      */
    832     private long cancelAnimatorAndGetNewDuration(long duration, ValueAnimator previousAnimator) {
    833         long newDuration = duration;
    834         if (previousAnimator != null) {
    835             // We take either the desired length of the new animation or the remaining time of
    836             // the previous animator, whichever is longer.
    837             newDuration = Math.max(previousAnimator.getDuration()
    838                     - previousAnimator.getCurrentPlayTime(), newDuration);
    839             previousAnimator.cancel();
    840         }
    841         return newDuration;
    842     }
    843 
    844     private void onAnimationFinished() {
    845         mHostLayout.onChildAnimationFinished();
    846         for (View v : mChildrenToClearFromOverlay) {
    847             removeFromOverlay(v);
    848         }
    849         mChildrenToClearFromOverlay.clear();
    850     }
    851 
    852     /**
    853      * Process the animationEvents for a new animation
    854      *
    855      * @param animationEvents the animation events for the animation to perform
    856      * @param finalState the final state to animate to
    857      */
    858     private void processAnimationEvents(
    859             ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents,
    860             StackScrollState finalState) {
    861         for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) {
    862             final ExpandableView changingView = (ExpandableView) event.changingView;
    863             if (event.animationType ==
    864                     NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) {
    865 
    866                 // This item is added, initialize it's properties.
    867                 StackViewState viewState = finalState
    868                         .getViewStateForView(changingView);
    869                 if (viewState == null) {
    870                     // The position for this child was never generated, let's continue.
    871                     continue;
    872                 }
    873                 finalState.applyState(changingView, viewState);
    874                 mNewAddChildren.add(changingView);
    875 
    876             } else if (event.animationType ==
    877                     NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) {
    878                 if (changingView.getVisibility() == View.GONE) {
    879                     removeFromOverlay(changingView);
    880                     continue;
    881                 }
    882 
    883                 // Find the amount to translate up. This is needed in order to understand the
    884                 // direction of the remove animation (either downwards or upwards)
    885                 StackViewState viewState = finalState
    886                         .getViewStateForView(event.viewAfterChangingView);
    887                 int actualHeight = changingView.getActualHeight();
    888                 // upwards by default
    889                 float translationDirection = -1.0f;
    890                 if (viewState != null) {
    891                     // there was a view after this one, Approximate the distance the next child
    892                     // travelled
    893                     translationDirection = ((viewState.yTranslation
    894                             - (changingView.getTranslationY() + actualHeight / 2.0f)) * 2 /
    895                             actualHeight);
    896                     translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f);
    897 
    898                 }
    899                 changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR,
    900                         translationDirection, new Runnable() {
    901                     @Override
    902                     public void run() {
    903                         // remove the temporary overlay
    904                         removeFromOverlay(changingView);
    905                     }
    906                 });
    907             } else if (event.animationType ==
    908                 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) {
    909                 // A race condition can trigger the view to be added to the overlay even though
    910                 // it was fully swiped out. So let's remove it
    911                 mHostLayout.getOverlay().remove(changingView);
    912                 if (Math.abs(changingView.getTranslation()) == changingView.getWidth()
    913                         && changingView.getTransientContainer() != null) {
    914                     changingView.getTransientContainer().removeTransientView(changingView);
    915                 }
    916             } else if (event.animationType == NotificationStackScrollLayout
    917                     .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) {
    918                 ExpandableNotificationRow row = (ExpandableNotificationRow) event.changingView;
    919                 row.prepareExpansionChanged(finalState);
    920             } else if (event.animationType == NotificationStackScrollLayout
    921                     .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) {
    922                 // This item is added, initialize it's properties.
    923                 StackViewState viewState = finalState.getViewStateForView(changingView);
    924                 mTmpState.copyFrom(viewState);
    925                 if (event.headsUpFromBottom) {
    926                     mTmpState.yTranslation = mHeadsUpAppearHeightBottom;
    927                 } else {
    928                     mTmpState.yTranslation = -mTmpState.height;
    929                 }
    930                 mHeadsUpAppearChildren.add(changingView);
    931                 finalState.applyState(changingView, mTmpState);
    932             } else if (event.animationType == NotificationStackScrollLayout
    933                             .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR ||
    934                     event.animationType == NotificationStackScrollLayout
    935                             .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK) {
    936                 mHeadsUpDisappearChildren.add(changingView);
    937                 if (changingView.getParent() == null) {
    938                     // This notification was actually removed, so we need to add it to the overlay
    939                     mHostLayout.getOverlay().add(changingView);
    940                     mTmpState.initFrom(changingView);
    941                     mTmpState.yTranslation = -changingView.getActualHeight();
    942                     // We temporarily enable Y animations, the real filter will be combined
    943                     // afterwards anyway
    944                     mAnimationFilter.animateY = true;
    945                     startViewAnimations(changingView, mTmpState,
    946                             event.animationType == NotificationStackScrollLayout
    947                                     .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
    948                                             ? ANIMATION_DELAY_HEADS_UP
    949                                             : 0,
    950                             ANIMATION_DURATION_HEADS_UP_DISAPPEAR);
    951                     mChildrenToClearFromOverlay.add(changingView);
    952                 }
    953             }
    954             mNewEvents.add(event);
    955         }
    956     }
    957 
    958     public static void removeFromOverlay(View changingView) {
    959         ViewGroup parent = (ViewGroup) changingView.getParent();
    960         if (parent != null) {
    961             parent.removeView(changingView);
    962         }
    963     }
    964 
    965     public void animateOverScrollToAmount(float targetAmount, final boolean onTop,
    966             final boolean isRubberbanded) {
    967         final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
    968         if (targetAmount == startOverScrollAmount) {
    969             return;
    970         }
    971         cancelOverScrollAnimators(onTop);
    972         ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount,
    973                 targetAmount);
    974         overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD);
    975         overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    976             @Override
    977             public void onAnimationUpdate(ValueAnimator animation) {
    978                 float currentOverScroll = (float) animation.getAnimatedValue();
    979                 mHostLayout.setOverScrollAmount(
    980                         currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */,
    981                         isRubberbanded);
    982             }
    983         });
    984         overScrollAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
    985         overScrollAnimator.addListener(new AnimatorListenerAdapter() {
    986             @Override
    987             public void onAnimationEnd(Animator animation) {
    988                 if (onTop) {
    989                     mTopOverScrollAnimator = null;
    990                 } else {
    991                     mBottomOverScrollAnimator = null;
    992                 }
    993             }
    994         });
    995         overScrollAnimator.start();
    996         if (onTop) {
    997             mTopOverScrollAnimator = overScrollAnimator;
    998         } else {
    999             mBottomOverScrollAnimator = overScrollAnimator;
   1000         }
   1001     }
   1002 
   1003     public void cancelOverScrollAnimators(boolean onTop) {
   1004         ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator;
   1005         if (currentAnimator != null) {
   1006             currentAnimator.cancel();
   1007         }
   1008     }
   1009 
   1010     /**
   1011      * Get the end value of the height animation running on a view or the actualHeight
   1012      * if no animation is running.
   1013      */
   1014     public static int getFinalActualHeight(ExpandableView view) {
   1015         if (view == null) {
   1016             return 0;
   1017         }
   1018         ValueAnimator heightAnimator = getChildTag(view, TAG_ANIMATOR_HEIGHT);
   1019         if (heightAnimator == null) {
   1020             return view.getActualHeight();
   1021         } else {
   1022             return getChildTag(view, TAG_END_HEIGHT);
   1023         }
   1024     }
   1025 
   1026     /**
   1027      * Get the end value of the yTranslation animation running on a view or the yTranslation
   1028      * if no animation is running.
   1029      */
   1030     public static float getFinalTranslationY(View view) {
   1031         if (view == null) {
   1032             return 0;
   1033         }
   1034         ValueAnimator yAnimator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_Y);
   1035         if (yAnimator == null) {
   1036             return view.getTranslationY();
   1037         } else {
   1038             return getChildTag(view, TAG_END_TRANSLATION_Y);
   1039         }
   1040     }
   1041 
   1042     public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) {
   1043         mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom;
   1044     }
   1045 
   1046     public void setShadeExpanded(boolean shadeExpanded) {
   1047         mShadeExpanded = shadeExpanded;
   1048     }
   1049 }
   1050