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 StackStateAnimator(NotificationStackScrollLayout hostLayout)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 isRunning()111 public boolean isRunning() { 112 return !mAnimatorSet.isEmpty(); 113 } 114 startAnimationForEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents, StackScrollState finalState, long additionalDelay)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 initAnimationProperties(StackScrollState finalState, ExpandableView child, ExpandableViewState viewState)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 adaptDurationWhenGoingToFullShade(ExpandableView child, ExpandableViewState viewState, boolean wasAdded)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 */ applyWithoutAnimation(ExpandableView child, ExpandableViewState viewState, StackScrollState finalState)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 findLastNotAddedIndex(StackScrollState finalState)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 calculateChildAnimationDelay(ExpandableViewState viewState, StackScrollState finalState)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 calculateDelayGoToFullShade(ExpandableViewState viewState)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 */ getGlobalAnimationFinishedListener()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 onAnimationFinished()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 */ processAnimationEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents, StackScrollState finalState)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 removeFromOverlay(View changingView)458 public static void removeFromOverlay(View changingView) { 459 ViewGroup parent = (ViewGroup) changingView.getParent(); 460 if (parent != null) { 461 parent.removeView(changingView); 462 } 463 } 464 animateOverScrollToAmount(float targetAmount, final boolean onTop, final boolean isRubberbanded)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 cancelOverScrollAnimators(boolean onTop)503 public void cancelOverScrollAnimators(boolean onTop) { 504 ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator; 505 if (currentAnimator != null) { 506 currentAnimator.cancel(); 507 } 508 } 509 setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom)510 public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) { 511 mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom; 512 } 513 setShadeExpanded(boolean shadeExpanded)514 public void setShadeExpanded(boolean shadeExpanded) { 515 mShadeExpanded = shadeExpanded; 516 } 517 setShelf(NotificationShelf shelf)518 public void setShelf(NotificationShelf shelf) { 519 mShelf = shelf; 520 } 521 } 522