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