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.notification.stack; 18 19 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR; 20 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_CYCLING_IN; 21 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_CYCLING_OUT; 22 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR; 23 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK; 24 25 import android.animation.Animator; 26 import android.animation.AnimatorListenerAdapter; 27 import android.animation.ValueAnimator; 28 import android.content.Context; 29 import android.util.Property; 30 import android.view.View; 31 32 import com.android.app.animation.Interpolators; 33 import com.android.internal.annotations.VisibleForTesting; 34 import com.android.systemui.res.R; 35 import com.android.systemui.shared.clocks.AnimatableClockView; 36 import com.android.systemui.statusbar.NotificationShelf; 37 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 38 import com.android.systemui.statusbar.notification.row.ExpandableView; 39 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; 40 import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling; 41 import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation; 42 43 import java.util.ArrayList; 44 import java.util.HashSet; 45 import java.util.Stack; 46 47 /** 48 * An stack state animator which handles animations to new StackScrollStates 49 */ 50 public class StackStateAnimator { 51 52 public static final int ANIMATION_DURATION_STANDARD = 360; 53 public static final int ANIMATION_DURATION_CORNER_RADIUS = 200; 54 public static final int ANIMATION_DURATION_WAKEUP = 500; 55 public static final int ANIMATION_DURATION_WAKEUP_SCRIM = 667; 56 public static final int ANIMATION_DURATION_GO_TO_FULL_SHADE = 448; 57 public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464; 58 public static final int ANIMATION_DURATION_SWIPE = 200; 59 public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220; 60 public static final int ANIMATION_DURATION_CLOSE_REMOTE_INPUT = 150; 61 public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 400; 62 public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 400; 63 public static final int ANIMATION_DURATION_HEADS_UP_CYCLING = 400; 64 public static final int ANIMATION_DURATION_FOLD_TO_AOD = 65 AnimatableClockView.ANIMATION_DURATION_FOLD_TO_AOD; 66 public static final int ANIMATION_DURATION_PRIORITY_CHANGE = 500; 67 public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80; 68 public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32; 69 public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48; 70 public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2; 71 private static final int MAX_STAGGER_COUNT = 5; 72 73 @VisibleForTesting int mGoToFullShadeAppearingTranslation; 74 @VisibleForTesting float mHeadsUpAppearStartAboveScreen; 75 // Padding between the old and new heads up notifications for the hun cycling animation 76 private float mHeadsUpCyclingPadding; 77 private final ExpandableViewState mTmpState = new ExpandableViewState(); 78 private final AnimationProperties mAnimationProperties; 79 public NotificationStackScrollLayout mHostLayout; 80 private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents = 81 new ArrayList<>(); 82 private ArrayList<View> mNewAddChildren = new ArrayList<>(); 83 private HashSet<View> mHeadsUpAppearChildren = new HashSet<>(); 84 private HashSet<View> mHeadsUpDisappearChildren = new HashSet<>(); 85 private HashSet<Animator> mAnimatorSet = new HashSet<>(); 86 private Stack<AnimatorListenerAdapter> mAnimationListenerPool = new Stack<>(); 87 private AnimationFilter mAnimationFilter = new AnimationFilter(); 88 private long mCurrentLength; 89 private long mCurrentAdditionalDelay; 90 91 private ValueAnimator mTopOverScrollAnimator; 92 private ValueAnimator mBottomOverScrollAnimator; 93 private int mHeadsUpAppearHeightBottom; 94 private int mStackTopMargin; 95 private boolean mShadeExpanded; 96 private ArrayList<ExpandableView> mTransientViewsToRemove = new ArrayList<>(); 97 private NotificationShelf mShelf; 98 private StackStateLogger mLogger; 99 StackStateAnimator(Context context, NotificationStackScrollLayout hostLayout)100 public StackStateAnimator(Context context, NotificationStackScrollLayout hostLayout) { 101 mHostLayout = hostLayout; 102 initView(context); 103 mAnimationProperties = new AnimationProperties() { 104 @Override 105 public AnimationFilter getAnimationFilter() { 106 return mAnimationFilter; 107 } 108 109 @Override 110 public AnimatorListenerAdapter getAnimationFinishListener(Property property) { 111 return getGlobalAnimationFinishedListener(); 112 } 113 114 @Override 115 public boolean wasAdded(View view) { 116 return mNewAddChildren.contains(view); 117 } 118 }; 119 } 120 121 /** 122 * Needs to be called on configuration changes, to update cached resource values. 123 */ initView(Context context)124 public void initView(Context context) { 125 updateResources(context); 126 } 127 updateResources(Context context)128 private void updateResources(Context context) { 129 mGoToFullShadeAppearingTranslation = 130 context.getResources().getDimensionPixelSize( 131 R.dimen.go_to_full_shade_appearing_translation); 132 mHeadsUpAppearStartAboveScreen = context.getResources() 133 .getDimensionPixelSize(R.dimen.heads_up_appear_y_above_screen); 134 mHeadsUpCyclingPadding = context.getResources() 135 .getDimensionPixelSize(R.dimen.heads_up_cycling_padding); 136 } 137 setLogger(StackStateLogger logger)138 protected void setLogger(StackStateLogger logger) { 139 mLogger = logger; 140 } 141 isRunning()142 public boolean isRunning() { 143 return !mAnimatorSet.isEmpty(); 144 } 145 startAnimationForEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents, long additionalDelay)146 public void startAnimationForEvents( 147 ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents, 148 long additionalDelay) { 149 150 // Animation events might generate custom animations, which are started async 151 boolean anyCustomAnimationCreated = processAnimationEvents(mAnimationEvents); 152 153 int childCount = mHostLayout.getChildCount(); 154 mAnimationFilter.applyCombination(mNewEvents); 155 mCurrentAdditionalDelay = additionalDelay; 156 mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents); 157 // Used to stagger concurrent animations' delays and durations for visual effect 158 int animationStaggerCount = 0; 159 for (int i = 0; i < childCount; i++) { 160 final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i); 161 162 ExpandableViewState viewState = child.getViewState(); 163 if (viewState == null || child.getVisibility() == View.GONE 164 || applyWithoutAnimation(child, viewState)) { 165 continue; 166 } 167 168 if (mAnimationProperties.wasAdded(child) && animationStaggerCount < MAX_STAGGER_COUNT) { 169 animationStaggerCount++; 170 } 171 initAnimationProperties(child, viewState, animationStaggerCount); 172 viewState.animateTo(child, mAnimationProperties); 173 } 174 if (!isRunning() && !anyCustomAnimationCreated) { 175 // no child has performed any animation or is about to animate, lets finish 176 onAnimationFinished(); 177 } 178 mHeadsUpAppearChildren.clear(); 179 mHeadsUpDisappearChildren.clear(); 180 mNewEvents.clear(); 181 mNewAddChildren.clear(); 182 if (NotificationsImprovedHunAnimation.isEnabled() 183 || NotificationHeadsUpCycling.isEnabled()) { 184 mAnimationProperties.resetCustomInterpolators(); 185 } 186 } 187 initAnimationProperties(ExpandableView child, ExpandableViewState viewState, int animationStaggerCount)188 private void initAnimationProperties(ExpandableView child, 189 ExpandableViewState viewState, int animationStaggerCount) { 190 boolean wasAdded = mAnimationProperties.wasAdded(child); 191 mAnimationProperties.duration = mCurrentLength; 192 adaptDurationWhenGoingToFullShade(child, viewState, wasAdded, animationStaggerCount); 193 mAnimationProperties.delay = 0; 194 if (wasAdded || mAnimationFilter.hasDelays 195 && (viewState.getYTranslation() != child.getTranslationY() 196 || viewState.getZTranslation() != child.getTranslationZ() 197 || viewState.getAlpha() != child.getAlpha() 198 || viewState.height != child.getActualHeight() 199 || viewState.clipTopAmount != child.getClipTopAmount())) { 200 mAnimationProperties.delay = mCurrentAdditionalDelay 201 + calculateChildAnimationDelay(viewState, animationStaggerCount); 202 } 203 } 204 adaptDurationWhenGoingToFullShade(ExpandableView child, ExpandableViewState viewState, boolean wasAdded, int animationStaggerCount)205 private void adaptDurationWhenGoingToFullShade(ExpandableView child, 206 ExpandableViewState viewState, boolean wasAdded, int animationStaggerCount) { 207 boolean isDecorView = child instanceof StackScrollerDecorView; 208 boolean needsAdjustment = wasAdded || isDecorView; 209 if (needsAdjustment && mAnimationFilter.hasGoToFullShadeEvent) { 210 int startOffset = 0; 211 if (!isDecorView) { 212 startOffset = mGoToFullShadeAppearingTranslation; 213 float longerDurationFactor = (float) Math.pow(animationStaggerCount, 0.7f); 214 mAnimationProperties.duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50 215 + (long) (100 * longerDurationFactor); 216 } 217 child.setTranslationY(viewState.getYTranslation() + startOffset); 218 } 219 } 220 221 /** 222 * Determines if a view should not perform an animation and applies it directly. 223 * 224 * @return true if no animation should be performed 225 */ applyWithoutAnimation(ExpandableView child, ExpandableViewState viewState)226 private boolean applyWithoutAnimation(ExpandableView child, ExpandableViewState viewState) { 227 if (mShadeExpanded) { 228 return false; 229 } 230 if (ViewState.isAnimatingY(child)) { 231 // A Y translation animation is running 232 return false; 233 } 234 if (mHeadsUpDisappearChildren.contains(child) || mHeadsUpAppearChildren.contains(child)) { 235 // This is a heads up animation 236 return false; 237 } 238 if (NotificationStackScrollLayout.isPinnedHeadsUp(child)) { 239 // This is another headsUp which might move. Let's animate! 240 return false; 241 } 242 viewState.applyToView(child); 243 return true; 244 } 245 calculateChildAnimationDelay(ExpandableViewState viewState, int animationStaggerCount)246 private long calculateChildAnimationDelay(ExpandableViewState viewState, 247 int animationStaggerCount) { 248 if (mAnimationFilter.hasGoToFullShadeEvent) { 249 return calculateDelayGoToFullShade(viewState, animationStaggerCount); 250 } 251 if (mAnimationFilter.customDelay != AnimationFilter.NO_DELAY) { 252 return mAnimationFilter.customDelay; 253 } 254 long minDelay = 0; 255 for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) { 256 long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING; 257 switch (event.animationType) { 258 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: { 259 int ownIndex = viewState.notGoneIndex; 260 int changingIndex = 261 ((ExpandableView) (event.mChangingView)).getViewState().notGoneIndex; 262 int difference = Math.abs(ownIndex - changingIndex); 263 difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE, 264 difference - 1)); 265 long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement; 266 minDelay = Math.max(delay, minDelay); 267 break; 268 } 269 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT: 270 delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL; 271 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: { 272 int ownIndex = viewState.notGoneIndex; 273 boolean noNextView = event.viewAfterChangingView == null; 274 ExpandableView viewAfterChangingView = noNextView 275 ? mHostLayout.getLastChildNotGone() 276 : (ExpandableView) event.viewAfterChangingView; 277 if (viewAfterChangingView == null) { 278 // This can happen when the last view in the list is removed. 279 // Since the shelf is still around and the only view, the code still goes 280 // in here and tries to calculate the delay for it when case its properties 281 // have changed. 282 continue; 283 } 284 int nextIndex = viewAfterChangingView.getViewState().notGoneIndex; 285 if (ownIndex >= nextIndex) { 286 // we only have the view afterwards 287 ownIndex++; 288 } 289 int difference = Math.abs(ownIndex - nextIndex); 290 difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE, 291 difference - 1)); 292 long delay = difference * delayPerElement; 293 minDelay = Math.max(delay, minDelay); 294 break; 295 } 296 default: 297 break; 298 } 299 } 300 return minDelay; 301 } 302 calculateDelayGoToFullShade(ExpandableViewState viewState, int animationStaggerCount)303 private long calculateDelayGoToFullShade(ExpandableViewState viewState, 304 int animationStaggerCount) { 305 int shelfIndex = mShelf.getNotGoneIndex(); 306 float index = viewState.notGoneIndex; 307 long result = 0; 308 if (index > shelfIndex) { 309 float diff = (float) Math.pow(animationStaggerCount, 0.7f); 310 result += (long) (diff * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE * 0.25); 311 index = shelfIndex; 312 } 313 index = (float) Math.pow(index, 0.7f); 314 result += (long) (index * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE); 315 return result; 316 } 317 318 /** 319 * @return an adapter which ensures that onAnimationFinished is called once no animation is 320 * running anymore 321 */ getGlobalAnimationFinishedListener()322 private AnimatorListenerAdapter getGlobalAnimationFinishedListener() { 323 if (!mAnimationListenerPool.empty()) { 324 return mAnimationListenerPool.pop(); 325 } 326 327 // We need to create a new one, no reusable ones found 328 return new AnimatorListenerAdapter() { 329 private boolean mWasCancelled; 330 331 @Override 332 public void onAnimationEnd(Animator animation) { 333 mAnimatorSet.remove(animation); 334 if (mAnimatorSet.isEmpty() && !mWasCancelled) { 335 onAnimationFinished(); 336 } 337 mAnimationListenerPool.push(this); 338 } 339 340 @Override 341 public void onAnimationCancel(Animator animation) { 342 mWasCancelled = true; 343 } 344 345 @Override 346 public void onAnimationStart(Animator animation) { 347 mWasCancelled = false; 348 mAnimatorSet.add(animation); 349 } 350 }; 351 } 352 onAnimationFinished()353 private void onAnimationFinished() { 354 mHostLayout.onChildAnimationFinished(); 355 356 for (ExpandableView transientViewToRemove : mTransientViewsToRemove) { 357 transientViewToRemove.removeFromTransientContainer(); 358 } 359 mTransientViewsToRemove.clear(); 360 } 361 362 /** 363 * Process the animationEvents for a new animation. Here is the place to do something custom, 364 * like to modify the ViewState or to create a custom animation for an event. 365 * 366 * @param animationEvents the animation events for the animation to perform 367 * @return true if any custom animation was created 368 */ processAnimationEvents( ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents)369 private boolean processAnimationEvents( 370 ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents) { 371 boolean needsCustomAnimation = false; 372 for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) { 373 final ExpandableView changingView = event.mChangingView; 374 boolean loggable = false; 375 boolean isHeadsUp = false; 376 String key = null; 377 if (changingView instanceof ExpandableNotificationRow && mLogger != null) { 378 loggable = true; 379 isHeadsUp = ((ExpandableNotificationRow) changingView).isHeadsUp(); 380 key = ((ExpandableNotificationRow) changingView).getEntry().getKey(); 381 } 382 if (event.animationType == 383 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) { 384 385 // This item is added, initialize its properties. 386 ExpandableViewState viewState = changingView.getViewState(); 387 if (viewState == null || viewState.gone) { 388 // The position for this child was never generated, let's continue. 389 continue; 390 } 391 if (loggable && isHeadsUp) { 392 mLogger.logHUNViewAppearingWithAddEvent(key); 393 } 394 viewState.applyToView(changingView); 395 mNewAddChildren.add(changingView); 396 397 } else if (event.animationType == 398 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) { 399 int changingViewVisibility = changingView.getVisibility(); 400 if (loggable) { 401 mLogger.processAnimationEventsRemoval(key, changingViewVisibility, isHeadsUp); 402 } 403 if (changingViewVisibility != View.VISIBLE) { 404 changingView.removeFromTransientContainer(); 405 continue; 406 } 407 408 // Find the amount to translate up. This is needed in order to understand the 409 // direction of the remove animation (either downwards or upwards) 410 // upwards by default 411 float translationDirection = -1.0f; 412 if (event.viewAfterChangingView != null) { 413 float ownPosition = changingView.getTranslationY(); 414 if (changingView instanceof ExpandableNotificationRow 415 && event.viewAfterChangingView instanceof ExpandableNotificationRow) { 416 ExpandableNotificationRow changingRow = 417 (ExpandableNotificationRow) changingView; 418 ExpandableNotificationRow nextRow = 419 (ExpandableNotificationRow) event.viewAfterChangingView; 420 if (changingRow.isRemoved() 421 && changingRow.wasChildInGroupWhenRemoved() 422 && !nextRow.isChildInGroup()) { 423 // the next row isn't actually a child from a group! Let's 424 // compare absolute positions! 425 ownPosition = changingRow.getTranslationWhenRemoved(); 426 } 427 } 428 int actualHeight = changingView.getActualHeight(); 429 // there was a view after this one, Approximate the distance the next child 430 // travelled 431 ExpandableViewState viewState = 432 ((ExpandableView) event.viewAfterChangingView).getViewState(); 433 translationDirection = ((viewState.getYTranslation() 434 - (ownPosition + actualHeight / 2.0f)) * 2 / 435 actualHeight); 436 translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f); 437 438 } 439 Runnable postAnimation; 440 Runnable startAnimation; 441 if (loggable) { 442 String finalKey = key; 443 final boolean finalIsHeadsHp = isHeadsUp; 444 startAnimation = () -> { 445 mLogger.animationStart(finalKey, "ANIMATION_TYPE_REMOVE", finalIsHeadsHp); 446 changingView.setInRemovalAnimation(true); 447 }; 448 postAnimation = () -> { 449 mLogger.animationEnd(finalKey, "ANIMATION_TYPE_REMOVE", finalIsHeadsHp); 450 changingView.setInRemovalAnimation(false); 451 changingView.removeFromTransientContainer(); 452 }; 453 } else { 454 startAnimation = ()-> { 455 changingView.setInRemovalAnimation(true); 456 }; 457 postAnimation = () -> { 458 changingView.setInRemovalAnimation(false); 459 changingView.removeFromTransientContainer(); 460 }; 461 } 462 changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR, 463 0 /* delay */, translationDirection, false /* isHeadsUpAppear */, 464 startAnimation, postAnimation, getGlobalAnimationFinishedListener(), 465 ExpandableView.ClipSide.BOTTOM); 466 needsCustomAnimation = true; 467 } else if (event.animationType == 468 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) { 469 boolean isFullySwipedOut = mHostLayout.isFullySwipedOut(changingView); 470 if (loggable) { 471 mLogger.processAnimationEventsRemoveSwipeOut(key, isFullySwipedOut, isHeadsUp); 472 } 473 if (isFullySwipedOut) { 474 changingView.removeFromTransientContainer(); 475 } 476 } else if (event.animationType == NotificationStackScrollLayout 477 .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) { 478 ExpandableNotificationRow row = (ExpandableNotificationRow) event.mChangingView; 479 row.prepareExpansionChanged(); 480 } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_CYCLING_IN) { 481 mHeadsUpAppearChildren.add(changingView); 482 483 mTmpState.copyFrom(changingView.getViewState()); 484 mTmpState.setYTranslation(changingView.getViewState().getYTranslation() 485 + getHeadsUpCyclingInYTranslationStart(event.headsUpFromBottom)); 486 mTmpState.applyToView(changingView); 487 488 // TODO(b/339519404): use a different interpolator 489 Runnable onAnimationEnd = null; 490 if (loggable) { 491 // This only captures HEADS_UP_APPEAR animations, but HUNs can appear with 492 // normal ADD animations, which would not be logged here. 493 String finalKey = key; 494 mLogger.logHUNViewAppearing(key); 495 onAnimationEnd = () -> { 496 mLogger.appearAnimationEnded(finalKey); 497 }; 498 } 499 changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_CYCLING, 500 /* isHeadsUpAppear= */ true, onAnimationEnd); 501 } else if (NotificationsImprovedHunAnimation.isEnabled() 502 && (event.animationType == ANIMATION_TYPE_HEADS_UP_APPEAR)) { 503 mHeadsUpAppearChildren.add(changingView); 504 505 mTmpState.copyFrom(changingView.getViewState()); 506 // translate the HUN in from the top, or the bottom of the screen 507 mTmpState.setYTranslation(getHeadsUpYTranslationStart(event.headsUpFromBottom)); 508 // set the height and the initial position 509 mTmpState.applyToView(changingView); 510 mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y, 511 Interpolators.FAST_OUT_SLOW_IN); 512 513 Runnable onAnimationEnd = null; 514 if (loggable) { 515 // This only captures HEADS_UP_APPEAR animations, but HUNs can appear with 516 // normal ADD animations, which would not be logged here. 517 String finalKey = key; 518 mLogger.logHUNViewAppearing(key); 519 onAnimationEnd = () -> mLogger.appearAnimationEnded(finalKey); 520 } 521 changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_APPEAR, 522 /* isHeadsUpAppear= */ true, onAnimationEnd); 523 } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_CYCLING_OUT) { 524 mHeadsUpDisappearChildren.add(changingView); 525 Runnable endRunnable = null; 526 mTmpState.copyFrom(changingView.getViewState()); 527 528 if (changingView.getParent() == null) { 529 // This notification was actually removed, so we need to add it 530 // transiently 531 mHostLayout.addTransientView(changingView, 0); 532 changingView.setTransientContainer(mHostLayout); 533 // TODO(b/316404716): remove the hard-coded height 534 // StackScrollAlgorithm cannot find this view because it has been removed 535 // from the NSSL. To correctly translate the view to the top or bottom of 536 // the screen (where it animated from), we need to update its translation. 537 mTmpState.setYTranslation( 538 mTmpState.getYTranslation() + 10 539 ); 540 endRunnable = changingView::removeFromTransientContainer; 541 } 542 543 boolean needsAnimation = true; 544 if (changingView instanceof ExpandableNotificationRow) { 545 ExpandableNotificationRow row = 546 (ExpandableNotificationRow) changingView; 547 if (row.isDismissed()) { 548 needsAnimation = false; 549 } 550 } 551 if (needsAnimation) { 552 // We need to add the global animation listener, since once no animations are 553 // running anymore, the panel will instantly hide itself. We need to wait until 554 // the animation is fully finished for this though. 555 final Runnable tmpEndRunnable = endRunnable; 556 Runnable postAnimation; 557 Runnable startAnimation; 558 if (loggable) { 559 String finalKey1 = key; 560 final boolean finalIsHeadsUp = isHeadsUp; 561 final String type = "ANIMATION_TYPE_HEADS_UP_CYCLING_OUT"; 562 startAnimation = () -> { 563 mLogger.animationStart(finalKey1, type, finalIsHeadsUp); 564 changingView.setInRemovalAnimation(true); 565 }; 566 postAnimation = () -> { 567 mLogger.animationEnd(finalKey1, type, finalIsHeadsUp); 568 changingView.setInRemovalAnimation(false); 569 if (tmpEndRunnable != null) { 570 tmpEndRunnable.run(); 571 } 572 573 }; 574 } else { 575 postAnimation = () -> { 576 changingView.setInRemovalAnimation(false); 577 if (tmpEndRunnable != null) { 578 tmpEndRunnable.run(); 579 } 580 }; 581 startAnimation = () -> { 582 changingView.setInRemovalAnimation(true); 583 }; 584 } 585 long removeAnimationDelay = changingView.performRemoveAnimation( 586 ANIMATION_DURATION_HEADS_UP_CYCLING, 587 /* delay= */ 0, 588 // It's a shame that translationDirection isn't where we do the y 589 // translation, the actual translation is in StackScrollAlgorithm. 590 /* translationDirection= */ 0.0f, 591 /* isHeadsUpAnimation= */ true, 592 startAnimation, postAnimation, 593 getGlobalAnimationFinishedListener(), ExpandableView.ClipSide.TOP); 594 mAnimationProperties.delay += removeAnimationDelay; 595 mAnimationProperties.duration = ANIMATION_DURATION_HEADS_UP_CYCLING; 596 mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y, 597 Interpolators.LINEAR); 598 mAnimationProperties.getAnimationFilter().animateY = true; 599 mTmpState.animateTo(changingView, mAnimationProperties); 600 mAnimationProperties.resetCustomInterpolators(); 601 } else if (endRunnable != null) { 602 endRunnable.run(); 603 } 604 needsCustomAnimation |= needsAnimation; 605 } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_APPEAR) { 606 NotificationsImprovedHunAnimation.assertInLegacyMode(); 607 // This item is added, initialize its properties. 608 ExpandableViewState viewState = changingView.getViewState(); 609 mTmpState.copyFrom(viewState); 610 if (event.headsUpFromBottom) { 611 mTmpState.setYTranslation(mHeadsUpAppearHeightBottom); 612 } else { 613 Runnable onAnimationEnd = null; 614 if (loggable) { 615 String finalKey = key; 616 onAnimationEnd = () -> mLogger.appearAnimationEnded(finalKey); 617 } 618 changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_APPEAR, 619 true /* isHeadsUpAppear */, onAnimationEnd); 620 } 621 mHeadsUpAppearChildren.add(changingView); 622 // this only captures HEADS_UP_APPEAR animations, but HUNs can appear with normal 623 // ADD animations, which would not be logged here. 624 if (loggable) { 625 mLogger.logHUNViewAppearing(key); 626 } 627 628 mTmpState.applyToView(changingView); 629 } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_DISAPPEAR 630 || event.animationType == ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK) { 631 mHeadsUpDisappearChildren.add(changingView); 632 Runnable endRunnable = null; 633 mTmpState.copyFrom(changingView.getViewState()); 634 if (changingView.getParent() == null) { 635 // This notification was actually removed, so we need to add it 636 // transiently 637 mHostLayout.addTransientView(changingView, 0); 638 changingView.setTransientContainer(mHostLayout); 639 if (NotificationsImprovedHunAnimation.isEnabled()) { 640 // StackScrollAlgorithm cannot find this view because it has been removed 641 // from the NSSL. To correctly translate the view to the top or bottom of 642 // the screen (where it animated from), we need to update its translation. 643 mTmpState.setYTranslation( 644 getHeadsUpYTranslationStart(event.headsUpFromBottom) 645 ); 646 } 647 endRunnable = changingView::removeFromTransientContainer; 648 } 649 650 boolean needsAnimation = true; 651 if (changingView instanceof ExpandableNotificationRow) { 652 ExpandableNotificationRow row = 653 (ExpandableNotificationRow) changingView; 654 if (row.isDismissed()) { 655 needsAnimation = false; 656 } 657 } 658 if (needsAnimation) { 659 // We need to add the global animation listener, since once no animations are 660 // running anymore, the panel will instantly hide itself. We need to wait until 661 // the animation is fully finished for this though. 662 final Runnable tmpEndRunnable = endRunnable; 663 Runnable postAnimation; 664 Runnable startAnimation; 665 if (loggable) { 666 String finalKey1 = key; 667 final boolean finalIsHeadsUp = isHeadsUp; 668 final String type = 669 event.animationType == ANIMATION_TYPE_HEADS_UP_DISAPPEAR 670 ? "ANIMATION_TYPE_HEADS_UP_DISAPPEAR" 671 : "ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK"; 672 startAnimation = () -> { 673 mLogger.animationStart(finalKey1, type, finalIsHeadsUp); 674 changingView.setInRemovalAnimation(true); 675 }; 676 postAnimation = () -> { 677 mLogger.animationEnd(finalKey1, type, finalIsHeadsUp); 678 changingView.setInRemovalAnimation(false); 679 if (tmpEndRunnable != null) { 680 tmpEndRunnable.run(); 681 } 682 }; 683 } else { 684 startAnimation = () -> { 685 changingView.setInRemovalAnimation(true); 686 }; 687 postAnimation = () -> { 688 changingView.setInRemovalAnimation(false); 689 if (tmpEndRunnable != null) { 690 tmpEndRunnable.run(); 691 } 692 }; 693 } 694 long removeAnimationDelay = changingView.performRemoveAnimation( 695 ANIMATION_DURATION_HEADS_UP_DISAPPEAR, 696 0, 0.0f, true /* isHeadsUpAppear */, 697 startAnimation, postAnimation, 698 getGlobalAnimationFinishedListener(), ExpandableView.ClipSide.BOTTOM); 699 mAnimationProperties.delay += removeAnimationDelay; 700 if (NotificationsImprovedHunAnimation.isEnabled()) { 701 mAnimationProperties.duration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR; 702 mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y, 703 Interpolators.FAST_OUT_SLOW_IN_REVERSE); 704 mAnimationProperties.getAnimationFilter().animateY = true; 705 mTmpState.animateTo(changingView, mAnimationProperties); 706 mAnimationProperties.resetCustomInterpolators(); 707 } 708 } else if (endRunnable != null) { 709 endRunnable.run(); 710 } 711 needsCustomAnimation |= needsAnimation; 712 } 713 mNewEvents.add(event); 714 } 715 return needsCustomAnimation; 716 } 717 getHeadsUpYTranslationStart(boolean headsUpFromBottom)718 private float getHeadsUpYTranslationStart(boolean headsUpFromBottom) { 719 if (headsUpFromBottom) { 720 // start from the bottom of the screen 721 return mHeadsUpAppearHeightBottom + mHeadsUpAppearStartAboveScreen; 722 } 723 // start from the top of the screen 724 return -mStackTopMargin - mHeadsUpAppearStartAboveScreen; 725 } 726 727 /** 728 * @param headsUpFromBottom Whether we are showing the HUNs at the bottom of the screen 729 * @return The start y translation of the HUN cycling in animation 730 */ getHeadsUpCyclingInYTranslationStart(boolean headsUpFromBottom)731 private float getHeadsUpCyclingInYTranslationStart(boolean headsUpFromBottom) { 732 if (headsUpFromBottom) { 733 // start from the bottom of the screen 734 return mHeadsUpAppearHeightBottom + mHeadsUpCyclingPadding; 735 } 736 // start from the top of the screen 737 return -mHeadsUpCyclingPadding; 738 } 739 740 /** 741 * @param headsUpFromBottom Whether we are showing the HUNs at the bottom of the screen 742 * @param oldHunHeight Height of the old HUN 743 * @param newHunHeight Height of the new HUN 744 * @return The y translation target value of the HUN cycling out animation 745 */ getHeadsUpCyclingOutYTranslation( boolean headsUpFromBottom, int oldHunHeight, int newHunHeight )746 private float getHeadsUpCyclingOutYTranslation( 747 boolean headsUpFromBottom, 748 int oldHunHeight, 749 int newHunHeight 750 ) { 751 final float translationDistance = mHeadsUpCyclingPadding + newHunHeight - oldHunHeight; 752 if (headsUpFromBottom) { 753 // start from the bottom of the screen 754 return mHeadsUpAppearHeightBottom - translationDistance; 755 } 756 return translationDistance; 757 } 758 animateOverScrollToAmount(float targetAmount, final boolean onTop, final boolean isRubberbanded)759 public void animateOverScrollToAmount(float targetAmount, final boolean onTop, 760 final boolean isRubberbanded) { 761 final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop); 762 if (targetAmount == startOverScrollAmount) { 763 return; 764 } 765 cancelOverScrollAnimators(onTop); 766 ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount, 767 targetAmount); 768 overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD); 769 overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 770 @Override 771 public void onAnimationUpdate(ValueAnimator animation) { 772 float currentOverScroll = (float) animation.getAnimatedValue(); 773 mHostLayout.setOverScrollAmount( 774 currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */, 775 isRubberbanded); 776 } 777 }); 778 overScrollAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 779 overScrollAnimator.addListener(new AnimatorListenerAdapter() { 780 @Override 781 public void onAnimationEnd(Animator animation) { 782 if (onTop) { 783 mTopOverScrollAnimator = null; 784 } else { 785 mBottomOverScrollAnimator = null; 786 } 787 } 788 }); 789 overScrollAnimator.start(); 790 if (onTop) { 791 mTopOverScrollAnimator = overScrollAnimator; 792 } else { 793 mBottomOverScrollAnimator = overScrollAnimator; 794 } 795 } 796 cancelOverScrollAnimators(boolean onTop)797 public void cancelOverScrollAnimators(boolean onTop) { 798 ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator; 799 if (currentAnimator != null) { 800 currentAnimator.cancel(); 801 } 802 } 803 setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom)804 public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) { 805 mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom; 806 } 807 setStackTopMargin(int stackTopMargin)808 public void setStackTopMargin(int stackTopMargin) { 809 mStackTopMargin = stackTopMargin; 810 } 811 setShadeExpanded(boolean shadeExpanded)812 public void setShadeExpanded(boolean shadeExpanded) { 813 mShadeExpanded = shadeExpanded; 814 } 815 setShelf(NotificationShelf shelf)816 public void setShelf(NotificationShelf shelf) { 817 mShelf = shelf; 818 } 819 } 820