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