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