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.ValueAnimator;
     22 import android.util.Property;
     23 import android.view.View;
     24 import android.view.ViewGroup;
     25 import android.view.animation.Interpolator;
     26 
     27 import com.android.systemui.Interpolators;
     28 import com.android.systemui.R;
     29 import com.android.systemui.statusbar.ExpandableNotificationRow;
     30 import com.android.systemui.statusbar.ExpandableView;
     31 import com.android.systemui.statusbar.NotificationShelf;
     32 import com.android.systemui.statusbar.StatusBarIconView;
     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_WAKEUP = 500;
     45     public static final int ANIMATION_DURATION_GO_TO_FULL_SHADE = 448;
     46     public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
     47     public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
     48     public static final int ANIMATION_DURATION_CLOSE_REMOTE_INPUT = 150;
     49     public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 550;
     50     public static final int ANIMATION_DURATION_HEADS_UP_APPEAR_CLOSED
     51             = (int) (ANIMATION_DURATION_HEADS_UP_APPEAR
     52                     * HeadsUpAppearInterpolator.getFractionUntilOvershoot());
     53     public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 300;
     54     public static final int ANIMATION_DURATION_BLOCKING_HELPER_FADE = 240;
     55     public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
     56     public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
     57     public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48;
     58     public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
     59     public static final int ANIMATION_DELAY_HEADS_UP = 120;
     60     public static final int ANIMATION_DELAY_HEADS_UP_CLICKED= 120;
     61 
     62     private final int mGoToFullShadeAppearingTranslation;
     63     private final int mPulsingAppearingTranslation;
     64     private final ExpandableViewState mTmpState = new ExpandableViewState();
     65     private final AnimationProperties mAnimationProperties;
     66     public NotificationStackScrollLayout mHostLayout;
     67     private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents =
     68             new ArrayList<>();
     69     private ArrayList<View> mNewAddChildren = new ArrayList<>();
     70     private HashSet<View> mHeadsUpAppearChildren = new HashSet<>();
     71     private HashSet<View> mHeadsUpDisappearChildren = new HashSet<>();
     72     private HashSet<Animator> mAnimatorSet = new HashSet<>();
     73     private Stack<AnimatorListenerAdapter> mAnimationListenerPool = new Stack<>();
     74     private AnimationFilter mAnimationFilter = new AnimationFilter();
     75     private long mCurrentLength;
     76     private long mCurrentAdditionalDelay;
     77 
     78     /** The current index for the last child which was not added in this event set. */
     79     private int mCurrentLastNotAddedIndex;
     80     private ValueAnimator mTopOverScrollAnimator;
     81     private ValueAnimator mBottomOverScrollAnimator;
     82     private int mHeadsUpAppearHeightBottom;
     83     private boolean mShadeExpanded;
     84     private ArrayList<ExpandableView> mTransientViewsToRemove = new ArrayList<>();
     85     private NotificationShelf mShelf;
     86     private float mStatusBarIconLocation;
     87     private int[] mTmpLocation = new int[2];
     88 
     89     public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
     90         mHostLayout = hostLayout;
     91         mGoToFullShadeAppearingTranslation =
     92                 hostLayout.getContext().getResources().getDimensionPixelSize(
     93                         R.dimen.go_to_full_shade_appearing_translation);
     94         mPulsingAppearingTranslation =
     95                 hostLayout.getContext().getResources().getDimensionPixelSize(
     96                         R.dimen.pulsing_notification_appear_translation);
     97         mAnimationProperties = new AnimationProperties() {
     98             @Override
     99             public AnimationFilter getAnimationFilter() {
    100                 return mAnimationFilter;
    101             }
    102 
    103             @Override
    104             public AnimatorListenerAdapter getAnimationFinishListener() {
    105                 return getGlobalAnimationFinishedListener();
    106             }
    107 
    108             @Override
    109             public boolean wasAdded(View view) {
    110                 return mNewAddChildren.contains(view);
    111             }
    112 
    113             @Override
    114             public Interpolator getCustomInterpolator(View child, Property property) {
    115                 if (mHeadsUpAppearChildren.contains(child) && View.TRANSLATION_Y.equals(property)) {
    116                     return Interpolators.HEADS_UP_APPEAR;
    117                 }
    118                 return null;
    119             }
    120         };
    121     }
    122 
    123     public boolean isRunning() {
    124         return !mAnimatorSet.isEmpty();
    125     }
    126 
    127     public void startAnimationForEvents(
    128             ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents,
    129             StackScrollState finalState, long additionalDelay) {
    130 
    131         processAnimationEvents(mAnimationEvents, finalState);
    132 
    133         int childCount = mHostLayout.getChildCount();
    134         mAnimationFilter.applyCombination(mNewEvents);
    135         mCurrentAdditionalDelay = additionalDelay;
    136         mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents);
    137         mCurrentLastNotAddedIndex = findLastNotAddedIndex(finalState);
    138         for (int i = 0; i < childCount; i++) {
    139             final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
    140 
    141             ExpandableViewState viewState = finalState.getViewStateForView(child);
    142             if (viewState == null || child.getVisibility() == View.GONE
    143                     || applyWithoutAnimation(child, viewState, finalState)) {
    144                 continue;
    145             }
    146 
    147             initAnimationProperties(finalState, child, viewState);
    148             viewState.animateTo(child, mAnimationProperties);
    149         }
    150         if (!isRunning()) {
    151             // no child has preformed any animation, lets finish
    152             onAnimationFinished();
    153         }
    154         mHeadsUpAppearChildren.clear();
    155         mHeadsUpDisappearChildren.clear();
    156         mNewEvents.clear();
    157         mNewAddChildren.clear();
    158     }
    159 
    160     private void initAnimationProperties(StackScrollState finalState, ExpandableView child,
    161             ExpandableViewState viewState) {
    162         boolean wasAdded = mAnimationProperties.wasAdded(child);
    163         mAnimationProperties.duration = mCurrentLength;
    164         adaptDurationWhenGoingToFullShade(child, viewState, wasAdded);
    165         mAnimationProperties.delay = 0;
    166         if (wasAdded || mAnimationFilter.hasDelays
    167                         && (viewState.yTranslation != child.getTranslationY()
    168                         || viewState.zTranslation != child.getTranslationZ()
    169                         || viewState.alpha != child.getAlpha()
    170                         || viewState.height != child.getActualHeight()
    171                         || viewState.clipTopAmount != child.getClipTopAmount()
    172                         || viewState.dark != child.isDark()
    173                         || viewState.shadowAlpha != child.getShadowAlpha())) {
    174             mAnimationProperties.delay = mCurrentAdditionalDelay
    175                     + calculateChildAnimationDelay(viewState, finalState);
    176         }
    177     }
    178 
    179     private void adaptDurationWhenGoingToFullShade(ExpandableView child,
    180             ExpandableViewState viewState, boolean wasAdded) {
    181         if (wasAdded && mAnimationFilter.hasGoToFullShadeEvent) {
    182             child.setTranslationY(child.getTranslationY() + mGoToFullShadeAppearingTranslation);
    183             float longerDurationFactor = viewState.notGoneIndex - mCurrentLastNotAddedIndex;
    184             longerDurationFactor = (float) Math.pow(longerDurationFactor, 0.7f);
    185             mAnimationProperties.duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50 +
    186                     (long) (100 * longerDurationFactor);
    187         }
    188     }
    189 
    190     /**
    191      * Determines if a view should not perform an animation and applies it directly.
    192      *
    193      * @return true if no animation should be performed
    194      */
    195     private boolean applyWithoutAnimation(ExpandableView child, ExpandableViewState viewState,
    196             StackScrollState finalState) {
    197         if (mShadeExpanded) {
    198             return false;
    199         }
    200         if (ViewState.isAnimatingY(child)) {
    201             // A Y translation animation is running
    202             return false;
    203         }
    204         if (mHeadsUpDisappearChildren.contains(child) || mHeadsUpAppearChildren.contains(child)) {
    205             // This is a heads up animation
    206             return false;
    207         }
    208         if (NotificationStackScrollLayout.isPinnedHeadsUp(child)) {
    209             // This is another headsUp which might move. Let's animate!
    210             return false;
    211         }
    212         viewState.applyToView(child);
    213         return true;
    214     }
    215 
    216     private int findLastNotAddedIndex(StackScrollState finalState) {
    217         int childCount = mHostLayout.getChildCount();
    218         for (int i = childCount - 1; i >= 0; i--) {
    219             final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
    220 
    221             ExpandableViewState viewState = finalState.getViewStateForView(child);
    222             if (viewState == null || child.getVisibility() == View.GONE) {
    223                 continue;
    224             }
    225             if (!mNewAddChildren.contains(child)) {
    226                 return viewState.notGoneIndex;
    227             }
    228         }
    229         return -1;
    230     }
    231 
    232     private long calculateChildAnimationDelay(ExpandableViewState viewState,
    233             StackScrollState finalState) {
    234         if (mAnimationFilter.hasGoToFullShadeEvent) {
    235             return calculateDelayGoToFullShade(viewState);
    236         }
    237         if (mAnimationFilter.customDelay != AnimationFilter.NO_DELAY) {
    238             return mAnimationFilter.customDelay;
    239         }
    240         long minDelay = 0;
    241         for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) {
    242             long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING;
    243             switch (event.animationType) {
    244                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: {
    245                     int ownIndex = viewState.notGoneIndex;
    246                     int changingIndex = finalState
    247                             .getViewStateForView(event.changingView).notGoneIndex;
    248                     int difference = Math.abs(ownIndex - changingIndex);
    249                     difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
    250                             difference - 1));
    251                     long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement;
    252                     minDelay = Math.max(delay, minDelay);
    253                     break;
    254                 }
    255                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT:
    256                     delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL;
    257                 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: {
    258                     int ownIndex = viewState.notGoneIndex;
    259                     boolean noNextView = event.viewAfterChangingView == null;
    260                     View viewAfterChangingView = noNextView
    261                             ? mHostLayout.getLastChildNotGone()
    262                             : event.viewAfterChangingView;
    263                     if (viewAfterChangingView == null) {
    264                         // This can happen when the last view in the list is removed.
    265                         // Since the shelf is still around and the only view, the code still goes
    266                         // in here and tries to calculate the delay for it when case its properties
    267                         // have changed.
    268                         continue;
    269                     }
    270                     int nextIndex = finalState
    271                             .getViewStateForView(viewAfterChangingView).notGoneIndex;
    272                     if (ownIndex >= nextIndex) {
    273                         // we only have the view afterwards
    274                         ownIndex++;
    275                     }
    276                     int difference = Math.abs(ownIndex - nextIndex);
    277                     difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
    278                             difference - 1));
    279                     long delay = difference * delayPerElement;
    280                     minDelay = Math.max(delay, minDelay);
    281                     break;
    282                 }
    283                 default:
    284                     break;
    285             }
    286         }
    287         return minDelay;
    288     }
    289 
    290     private long calculateDelayGoToFullShade(ExpandableViewState viewState) {
    291         int shelfIndex = mShelf.getNotGoneIndex();
    292         float index = viewState.notGoneIndex;
    293         long result = 0;
    294         if (index > shelfIndex) {
    295             float diff = index - shelfIndex;
    296             diff = (float) Math.pow(diff, 0.7f);
    297             result += (long) (diff * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE * 0.25);
    298             index = shelfIndex;
    299         }
    300         index = (float) Math.pow(index, 0.7f);
    301         result += (long) (index * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE);
    302         return result;
    303     }
    304 
    305     /**
    306      * @return an adapter which ensures that onAnimationFinished is called once no animation is
    307      *         running anymore
    308      */
    309     private AnimatorListenerAdapter getGlobalAnimationFinishedListener() {
    310         if (!mAnimationListenerPool.empty()) {
    311             return mAnimationListenerPool.pop();
    312         }
    313 
    314         // We need to create a new one, no reusable ones found
    315         return new AnimatorListenerAdapter() {
    316             private boolean mWasCancelled;
    317 
    318             @Override
    319             public void onAnimationEnd(Animator animation) {
    320                 mAnimatorSet.remove(animation);
    321                 if (mAnimatorSet.isEmpty() && !mWasCancelled) {
    322                     onAnimationFinished();
    323                 }
    324                 mAnimationListenerPool.push(this);
    325             }
    326 
    327             @Override
    328             public void onAnimationCancel(Animator animation) {
    329                 mWasCancelled = true;
    330             }
    331 
    332             @Override
    333             public void onAnimationStart(Animator animation) {
    334                 mWasCancelled = false;
    335                 mAnimatorSet.add(animation);
    336             }
    337         };
    338     }
    339 
    340     private void onAnimationFinished() {
    341         mHostLayout.onChildAnimationFinished();
    342 
    343         for (ExpandableView transientViewsToRemove : mTransientViewsToRemove) {
    344             transientViewsToRemove.getTransientContainer()
    345                     .removeTransientView(transientViewsToRemove);
    346         }
    347         mTransientViewsToRemove.clear();
    348     }
    349 
    350     /**
    351      * Process the animationEvents for a new animation
    352      *
    353      * @param animationEvents the animation events for the animation to perform
    354      * @param finalState the final state to animate to
    355      */
    356     private void processAnimationEvents(
    357             ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents,
    358             StackScrollState finalState) {
    359         for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) {
    360             final ExpandableView changingView = (ExpandableView) event.changingView;
    361             if (event.animationType ==
    362                     NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) {
    363 
    364                 // This item is added, initialize it's properties.
    365                 ExpandableViewState viewState = finalState
    366                         .getViewStateForView(changingView);
    367                 if (viewState == null || viewState.gone) {
    368                     // The position for this child was never generated, let's continue.
    369                     continue;
    370                 }
    371                 viewState.applyToView(changingView);
    372                 mNewAddChildren.add(changingView);
    373 
    374             } else if (event.animationType ==
    375                     NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) {
    376                 if (changingView.getVisibility() != View.VISIBLE) {
    377                     removeTransientView(changingView);
    378                     continue;
    379                 }
    380 
    381                 // Find the amount to translate up. This is needed in order to understand the
    382                 // direction of the remove animation (either downwards or upwards)
    383                 ExpandableViewState viewState = finalState
    384                         .getViewStateForView(event.viewAfterChangingView);
    385                 int actualHeight = changingView.getActualHeight();
    386                 // upwards by default
    387                 float translationDirection = -1.0f;
    388                 if (viewState != null) {
    389                     float ownPosition = changingView.getTranslationY();
    390                     if (changingView instanceof ExpandableNotificationRow
    391                             && event.viewAfterChangingView instanceof ExpandableNotificationRow) {
    392                         ExpandableNotificationRow changingRow =
    393                                 (ExpandableNotificationRow) changingView;
    394                         ExpandableNotificationRow nextRow =
    395                                 (ExpandableNotificationRow) event.viewAfterChangingView;
    396                         if (changingRow.isRemoved()
    397                                 && changingRow.wasChildInGroupWhenRemoved()
    398                                 && !nextRow.isChildInGroup()) {
    399                             // the next row isn't actually a child from a group! Let's
    400                             // compare absolute positions!
    401                             ownPosition = changingRow.getTranslationWhenRemoved();
    402                         }
    403                     }
    404                     // there was a view after this one, Approximate the distance the next child
    405                     // travelled
    406                     translationDirection = ((viewState.yTranslation
    407                             - (ownPosition + actualHeight / 2.0f)) * 2 /
    408                             actualHeight);
    409                     translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f);
    410 
    411                 }
    412                 changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR,
    413                         0 /* delay */, translationDirection,  false /* isHeadsUpAppear */,
    414                         0, new Runnable() {
    415                     @Override
    416                     public void run() {
    417                         removeTransientView(changingView);
    418                     }
    419                 }, null);
    420             } else if (event.animationType ==
    421                 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) {
    422                 if (Math.abs(changingView.getTranslation()) == changingView.getWidth()
    423                         && changingView.getTransientContainer() != null) {
    424                     changingView.getTransientContainer().removeTransientView(changingView);
    425                 }
    426             } else if (event.animationType == NotificationStackScrollLayout
    427                     .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) {
    428                 ExpandableNotificationRow row = (ExpandableNotificationRow) event.changingView;
    429                 row.prepareExpansionChanged(finalState);
    430             } else if (event.animationType == NotificationStackScrollLayout
    431                     .AnimationEvent.ANIMATION_TYPE_PULSE_APPEAR) {
    432                 ExpandableViewState viewState = finalState.getViewStateForView(changingView);
    433                 mTmpState.copyFrom(viewState);
    434                 mTmpState.yTranslation += mPulsingAppearingTranslation;
    435                 mTmpState.alpha = 0;
    436                 mTmpState.applyToView(changingView);
    437             } else if (event.animationType == NotificationStackScrollLayout
    438                     .AnimationEvent.ANIMATION_TYPE_PULSE_DISAPPEAR) {
    439                 ExpandableViewState viewState = finalState.getViewStateForView(changingView);
    440                 viewState.yTranslation += mPulsingAppearingTranslation;
    441                 viewState.alpha = 0;
    442             } else if (event.animationType == NotificationStackScrollLayout
    443                     .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) {
    444                 // This item is added, initialize it's properties.
    445                 ExpandableViewState viewState = finalState.getViewStateForView(changingView);
    446                 mTmpState.copyFrom(viewState);
    447                 if (event.headsUpFromBottom) {
    448                     mTmpState.yTranslation = mHeadsUpAppearHeightBottom;
    449                 } else {
    450                     mTmpState.yTranslation = 0;
    451                     changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_APPEAR_CLOSED,
    452                             true /* isHeadsUpAppear */);
    453                 }
    454                 mHeadsUpAppearChildren.add(changingView);
    455                 mTmpState.applyToView(changingView);
    456             } else if (event.animationType == NotificationStackScrollLayout
    457                             .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR ||
    458                     event.animationType == NotificationStackScrollLayout
    459                             .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK) {
    460                 mHeadsUpDisappearChildren.add(changingView);
    461                 Runnable endRunnable = null;
    462                 // We need some additional delay in case we were removed to make sure we're not
    463                 // lagging
    464                 int extraDelay = event.animationType == NotificationStackScrollLayout
    465                         .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
    466                         ? ANIMATION_DELAY_HEADS_UP_CLICKED
    467                         : 0;
    468                 if (changingView.getParent() == null) {
    469                     // This notification was actually removed, so we need to add it transiently
    470                     mHostLayout.addTransientView(changingView, 0);
    471                     changingView.setTransientContainer(mHostLayout);
    472                     mTmpState.initFrom(changingView);
    473                     mTmpState.yTranslation = 0;
    474                     // We temporarily enable Y animations, the real filter will be combined
    475                     // afterwards anyway
    476                     mAnimationFilter.animateY = true;
    477                     mAnimationProperties.delay = extraDelay + ANIMATION_DELAY_HEADS_UP;
    478                     mAnimationProperties.duration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR;
    479                     mTmpState.animateTo(changingView, mAnimationProperties);
    480                     endRunnable = () -> removeTransientView(changingView);
    481                 }
    482                 float targetLocation = 0;
    483                 boolean needsAnimation = true;
    484                 if (changingView instanceof ExpandableNotificationRow) {
    485                     ExpandableNotificationRow row = (ExpandableNotificationRow) changingView;
    486                     if (row.isDismissed()) {
    487                         needsAnimation = false;
    488                     }
    489                     StatusBarIconView icon = row.getEntry().icon;
    490                     if (icon.getParent() != null) {
    491                         icon.getLocationOnScreen(mTmpLocation);
    492                         float iconPosition = mTmpLocation[0] - icon.getTranslationX()
    493                                 + ViewState.getFinalTranslationX(icon) + icon.getWidth() * 0.25f;
    494                         mHostLayout.getLocationOnScreen(mTmpLocation);
    495                         targetLocation = iconPosition - mTmpLocation[0];
    496                     }
    497                 }
    498 
    499                 if (needsAnimation) {
    500                     // We need to add the global animation listener, since once no animations are
    501                     // running anymore, the panel will instantly hide itself. We need to wait until
    502                     // the animation is fully finished for this though.
    503                     changingView.performRemoveAnimation(ANIMATION_DURATION_HEADS_UP_DISAPPEAR
    504                                     + ANIMATION_DELAY_HEADS_UP, extraDelay, 0.0f,
    505                             true /* isHeadsUpAppear */, targetLocation, endRunnable,
    506                             getGlobalAnimationFinishedListener());
    507                 } else if (endRunnable != null) {
    508                     endRunnable.run();
    509                 }
    510             }
    511             mNewEvents.add(event);
    512         }
    513     }
    514 
    515     public static void removeTransientView(ExpandableView viewToRemove) {
    516         if (viewToRemove.getTransientContainer() != null) {
    517             viewToRemove.getTransientContainer().removeTransientView(viewToRemove);
    518         }
    519     }
    520 
    521     public void animateOverScrollToAmount(float targetAmount, final boolean onTop,
    522             final boolean isRubberbanded) {
    523         final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
    524         if (targetAmount == startOverScrollAmount) {
    525             return;
    526         }
    527         cancelOverScrollAnimators(onTop);
    528         ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount,
    529                 targetAmount);
    530         overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD);
    531         overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    532             @Override
    533             public void onAnimationUpdate(ValueAnimator animation) {
    534                 float currentOverScroll = (float) animation.getAnimatedValue();
    535                 mHostLayout.setOverScrollAmount(
    536                         currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */,
    537                         isRubberbanded);
    538             }
    539         });
    540         overScrollAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
    541         overScrollAnimator.addListener(new AnimatorListenerAdapter() {
    542             @Override
    543             public void onAnimationEnd(Animator animation) {
    544                 if (onTop) {
    545                     mTopOverScrollAnimator = null;
    546                 } else {
    547                     mBottomOverScrollAnimator = null;
    548                 }
    549             }
    550         });
    551         overScrollAnimator.start();
    552         if (onTop) {
    553             mTopOverScrollAnimator = overScrollAnimator;
    554         } else {
    555             mBottomOverScrollAnimator = overScrollAnimator;
    556         }
    557     }
    558 
    559     public void cancelOverScrollAnimators(boolean onTop) {
    560         ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator;
    561         if (currentAnimator != null) {
    562             currentAnimator.cancel();
    563         }
    564     }
    565 
    566     public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) {
    567         mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom;
    568     }
    569 
    570     public void setShadeExpanded(boolean shadeExpanded) {
    571         mShadeExpanded = shadeExpanded;
    572     }
    573 
    574     public void setShelf(NotificationShelf shelf) {
    575         mShelf = shelf;
    576     }
    577 }
    578