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.ObjectAnimator; 22 import android.animation.PropertyValuesHolder; 23 import android.animation.ValueAnimator; 24 import android.view.View; 25 import android.view.ViewGroup; 26 import android.view.animation.Interpolator; 27 28 import com.android.systemui.Interpolators; 29 import com.android.systemui.R; 30 import com.android.systemui.statusbar.ExpandableNotificationRow; 31 import com.android.systemui.statusbar.ExpandableView; 32 import com.android.systemui.statusbar.policy.HeadsUpManager; 33 34 import java.util.ArrayList; 35 import java.util.HashSet; 36 import java.util.Stack; 37 38 /** 39 * An stack state animator which handles animations to new StackScrollStates 40 */ 41 public class StackStateAnimator { 42 43 public static final int ANIMATION_DURATION_STANDARD = 360; 44 public static final int ANIMATION_DURATION_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_HEADS_UP_APPEAR = 650; 48 public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 230; 49 public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80; 50 public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32; 51 public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48; 52 public static final int ANIMATION_DELAY_PER_ELEMENT_DARK = 24; 53 public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2; 54 public static final int ANIMATION_DELAY_HEADS_UP = 120; 55 56 private static final int TAG_ANIMATOR_TRANSLATION_Y = R.id.translation_y_animator_tag; 57 private static final int TAG_ANIMATOR_TRANSLATION_Z = R.id.translation_z_animator_tag; 58 private static final int TAG_ANIMATOR_ALPHA = R.id.alpha_animator_tag; 59 private static final int TAG_ANIMATOR_HEIGHT = R.id.height_animator_tag; 60 private static final int TAG_ANIMATOR_TOP_INSET = R.id.top_inset_animator_tag; 61 private static final int TAG_ANIMATOR_SHADOW_ALPHA = R.id.shadow_alpha_animator_tag; 62 private static final int TAG_END_TRANSLATION_Y = R.id.translation_y_animator_end_value_tag; 63 private static final int TAG_END_TRANSLATION_Z = R.id.translation_z_animator_end_value_tag; 64 private static final int TAG_END_ALPHA = R.id.alpha_animator_end_value_tag; 65 private static final int TAG_END_HEIGHT = R.id.height_animator_end_value_tag; 66 private static final int TAG_END_TOP_INSET = R.id.top_inset_animator_end_value_tag; 67 private static final int TAG_END_SHADOW_ALPHA = R.id.shadow_alpha_animator_end_value_tag; 68 private static final int TAG_START_TRANSLATION_Y = R.id.translation_y_animator_start_value_tag; 69 private static final int TAG_START_TRANSLATION_Z = R.id.translation_z_animator_start_value_tag; 70 private static final int TAG_START_ALPHA = R.id.alpha_animator_start_value_tag; 71 private static final int TAG_START_HEIGHT = R.id.height_animator_start_value_tag; 72 private static final int TAG_START_TOP_INSET = R.id.top_inset_animator_start_value_tag; 73 private static final int TAG_START_SHADOW_ALPHA = R.id.shadow_alpha_animator_start_value_tag; 74 75 private final Interpolator mHeadsUpAppearInterpolator; 76 private final int mGoToFullShadeAppearingTranslation; 77 private final StackViewState mTmpState = new StackViewState(); 78 public NotificationStackScrollLayout mHostLayout; 79 private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents = 80 new ArrayList<>(); 81 private ArrayList<View> mNewAddChildren = new ArrayList<>(); 82 private HashSet<View> mHeadsUpAppearChildren = new HashSet<>(); 83 private HashSet<View> mHeadsUpDisappearChildren = new HashSet<>(); 84 private HashSet<Animator> mAnimatorSet = new HashSet<>(); 85 private Stack<AnimatorListenerAdapter> mAnimationListenerPool = new Stack<>(); 86 private AnimationFilter mAnimationFilter = new AnimationFilter(); 87 private long mCurrentLength; 88 private long mCurrentAdditionalDelay; 89 90 /** The current index for the last child which was not added in this event set. */ 91 private int mCurrentLastNotAddedIndex; 92 private ValueAnimator mTopOverScrollAnimator; 93 private ValueAnimator mBottomOverScrollAnimator; 94 private int mHeadsUpAppearHeightBottom; 95 private boolean mShadeExpanded; 96 private ArrayList<View> mChildrenToClearFromOverlay = new ArrayList<>(); 97 98 public StackStateAnimator(NotificationStackScrollLayout hostLayout) { 99 mHostLayout = hostLayout; 100 mGoToFullShadeAppearingTranslation = 101 hostLayout.getContext().getResources().getDimensionPixelSize( 102 R.dimen.go_to_full_shade_appearing_translation); 103 mHeadsUpAppearInterpolator = new HeadsUpAppearInterpolator(); 104 } 105 106 public boolean isRunning() { 107 return !mAnimatorSet.isEmpty(); 108 } 109 110 public void startAnimationForEvents( 111 ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents, 112 StackScrollState finalState, long additionalDelay) { 113 114 processAnimationEvents(mAnimationEvents, finalState); 115 116 int childCount = mHostLayout.getChildCount(); 117 mAnimationFilter.applyCombination(mNewEvents); 118 mCurrentAdditionalDelay = additionalDelay; 119 mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents); 120 mCurrentLastNotAddedIndex = findLastNotAddedIndex(finalState); 121 for (int i = 0; i < childCount; i++) { 122 final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i); 123 124 StackViewState viewState = finalState.getViewStateForView(child); 125 if (viewState == null || child.getVisibility() == View.GONE 126 || applyWithoutAnimation(child, viewState, finalState)) { 127 continue; 128 } 129 130 startStackAnimations(child, viewState, finalState, i, -1 /* fixedDelay */); 131 } 132 if (!isRunning()) { 133 // no child has preformed any animation, lets finish 134 onAnimationFinished(); 135 } 136 mHeadsUpAppearChildren.clear(); 137 mHeadsUpDisappearChildren.clear(); 138 mNewEvents.clear(); 139 mNewAddChildren.clear(); 140 } 141 142 /** 143 * Determines if a view should not perform an animation and applies it directly. 144 * 145 * @return true if no animation should be performed 146 */ 147 private boolean applyWithoutAnimation(ExpandableView child, StackViewState viewState, 148 StackScrollState finalState) { 149 if (mShadeExpanded) { 150 return false; 151 } 152 if (getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y) != null) { 153 // A Y translation animation is running 154 return false; 155 } 156 if (mHeadsUpDisappearChildren.contains(child) || mHeadsUpAppearChildren.contains(child)) { 157 // This is a heads up animation 158 return false; 159 } 160 if (NotificationStackScrollLayout.isPinnedHeadsUp(child)) { 161 // This is another headsUp which might move. Let's animate! 162 return false; 163 } 164 finalState.applyState(child, viewState); 165 return true; 166 } 167 168 private int findLastNotAddedIndex(StackScrollState finalState) { 169 int childCount = mHostLayout.getChildCount(); 170 for (int i = childCount - 1; i >= 0; i--) { 171 final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i); 172 173 StackViewState viewState = finalState.getViewStateForView(child); 174 if (viewState == null || child.getVisibility() == View.GONE) { 175 continue; 176 } 177 if (!mNewAddChildren.contains(child)) { 178 return viewState.notGoneIndex; 179 } 180 } 181 return -1; 182 } 183 184 185 /** 186 * Start an animation to the given {@link StackViewState}. 187 * 188 * @param child the child to start the animation on 189 * @param viewState the {@link StackViewState} of the view to animate to 190 * @param finalState the final state after the animation 191 * @param i the index of the view; only relevant if the view is the speed bump and is 192 * ignored otherwise 193 * @param fixedDelay a fixed delay if desired or -1 if the delay should be calculated 194 */ 195 public void startStackAnimations(final ExpandableView child, StackViewState viewState, 196 StackScrollState finalState, int i, long fixedDelay) { 197 boolean wasAdded = mNewAddChildren.contains(child); 198 long duration = mCurrentLength; 199 if (wasAdded && mAnimationFilter.hasGoToFullShadeEvent) { 200 child.setTranslationY(child.getTranslationY() + mGoToFullShadeAppearingTranslation); 201 float longerDurationFactor = viewState.notGoneIndex - mCurrentLastNotAddedIndex; 202 longerDurationFactor = (float) Math.pow(longerDurationFactor, 0.7f); 203 duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50 + 204 (long) (100 * longerDurationFactor); 205 } 206 boolean yTranslationChanging = child.getTranslationY() != viewState.yTranslation; 207 boolean zTranslationChanging = child.getTranslationZ() != viewState.zTranslation; 208 boolean alphaChanging = viewState.alpha != child.getAlpha(); 209 boolean heightChanging = viewState.height != child.getActualHeight(); 210 boolean shadowAlphaChanging = viewState.shadowAlpha != child.getShadowAlpha(); 211 boolean darkChanging = viewState.dark != child.isDark(); 212 boolean topInsetChanging = viewState.clipTopAmount != child.getClipTopAmount(); 213 boolean hasDelays = mAnimationFilter.hasDelays; 214 boolean isDelayRelevant = yTranslationChanging || zTranslationChanging || alphaChanging 215 || heightChanging || topInsetChanging || darkChanging || shadowAlphaChanging; 216 long delay = 0; 217 if (fixedDelay != -1) { 218 delay = fixedDelay; 219 } else if (hasDelays && isDelayRelevant || wasAdded) { 220 delay = mCurrentAdditionalDelay + calculateChildAnimationDelay(viewState, finalState); 221 } 222 223 startViewAnimations(child, viewState, delay, duration); 224 225 // start height animation 226 if (heightChanging) { 227 startHeightAnimation(child, viewState, duration, delay); 228 } else { 229 abortAnimation(child, TAG_ANIMATOR_HEIGHT); 230 } 231 232 // start shadow alpha animation 233 if (shadowAlphaChanging) { 234 startShadowAlphaAnimation(child, viewState, duration, delay); 235 } else { 236 abortAnimation(child, TAG_ANIMATOR_SHADOW_ALPHA); 237 } 238 239 // start top inset animation 240 if (topInsetChanging) { 241 startInsetAnimation(child, viewState, duration, delay); 242 } else { 243 abortAnimation(child, TAG_ANIMATOR_TOP_INSET); 244 } 245 246 // start dimmed animation 247 child.setDimmed(viewState.dimmed, mAnimationFilter.animateDimmed); 248 249 // apply speed bump state 250 child.setBelowSpeedBump(viewState.belowSpeedBump); 251 252 // start hiding sensitive animation 253 child.setHideSensitive(viewState.hideSensitive, mAnimationFilter.animateHideSensitive, 254 delay, duration); 255 256 // start dark animation 257 child.setDark(viewState.dark, mAnimationFilter.animateDark, delay); 258 259 if (wasAdded) { 260 child.performAddAnimation(delay, mCurrentLength); 261 } 262 if (child instanceof ExpandableNotificationRow) { 263 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 264 row.startChildAnimation(finalState, this, delay, duration); 265 } 266 } 267 268 /** 269 * Start an animation to a new {@link ViewState}. 270 * 271 * @param child the child to start the animation on 272 * @param viewState the {@link StackViewState} of the view to animate to 273 * @param delay a fixed delay 274 * @param duration the duration of the animation 275 */ 276 public void startViewAnimations(View child, ViewState viewState, long delay, long duration) { 277 boolean wasVisible = child.getVisibility() == View.VISIBLE; 278 final float alpha = viewState.alpha; 279 if (!wasVisible && (alpha != 0 || child.getAlpha() != 0) 280 && !viewState.gone && !viewState.hidden) { 281 child.setVisibility(View.VISIBLE); 282 } 283 boolean yTranslationChanging = child.getTranslationY() != viewState.yTranslation; 284 boolean zTranslationChanging = child.getTranslationZ() != viewState.zTranslation; 285 float childAlpha = child.getAlpha(); 286 boolean alphaChanging = viewState.alpha != childAlpha; 287 if (child instanceof ExpandableView) { 288 // We don't want views to change visibility when they are animating to GONE 289 alphaChanging &= !((ExpandableView) child).willBeGone(); 290 } 291 292 // start translationY animation 293 if (yTranslationChanging) { 294 startYTranslationAnimation(child, viewState, duration, delay); 295 } else { 296 abortAnimation(child, TAG_ANIMATOR_TRANSLATION_Y); 297 } 298 299 // start translationZ animation 300 if (zTranslationChanging) { 301 startZTranslationAnimation(child, viewState, duration, delay); 302 } else { 303 abortAnimation(child, TAG_ANIMATOR_TRANSLATION_Z); 304 } 305 306 // start alpha animation 307 if (alphaChanging && child.getTranslationX() == 0) { 308 startAlphaAnimation(child, viewState, duration, delay); 309 } else { 310 abortAnimation(child, TAG_ANIMATOR_ALPHA); 311 } 312 } 313 314 private void abortAnimation(View child, int animatorTag) { 315 Animator previousAnimator = getChildTag(child, animatorTag); 316 if (previousAnimator != null) { 317 previousAnimator.cancel(); 318 } 319 } 320 321 private long calculateChildAnimationDelay(StackViewState viewState, 322 StackScrollState finalState) { 323 if (mAnimationFilter.hasDarkEvent) { 324 return calculateDelayDark(viewState); 325 } 326 if (mAnimationFilter.hasGoToFullShadeEvent) { 327 return calculateDelayGoToFullShade(viewState); 328 } 329 if (mAnimationFilter.hasHeadsUpDisappearClickEvent) { 330 return ANIMATION_DELAY_HEADS_UP; 331 } 332 long minDelay = 0; 333 for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) { 334 long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING; 335 switch (event.animationType) { 336 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: { 337 int ownIndex = viewState.notGoneIndex; 338 int changingIndex = finalState 339 .getViewStateForView(event.changingView).notGoneIndex; 340 int difference = Math.abs(ownIndex - changingIndex); 341 difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE, 342 difference - 1)); 343 long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement; 344 minDelay = Math.max(delay, minDelay); 345 break; 346 } 347 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT: 348 delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL; 349 case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: { 350 int ownIndex = viewState.notGoneIndex; 351 boolean noNextView = event.viewAfterChangingView == null; 352 View viewAfterChangingView = noNextView 353 ? mHostLayout.getLastChildNotGone() 354 : event.viewAfterChangingView; 355 356 int nextIndex = finalState 357 .getViewStateForView(viewAfterChangingView).notGoneIndex; 358 if (ownIndex >= nextIndex) { 359 // we only have the view afterwards 360 ownIndex++; 361 } 362 int difference = Math.abs(ownIndex - nextIndex); 363 difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE, 364 difference - 1)); 365 long delay = difference * delayPerElement; 366 minDelay = Math.max(delay, minDelay); 367 break; 368 } 369 default: 370 break; 371 } 372 } 373 return minDelay; 374 } 375 376 private long calculateDelayDark(StackViewState viewState) { 377 int referenceIndex; 378 if (mAnimationFilter.darkAnimationOriginIndex == 379 NotificationStackScrollLayout.AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_ABOVE) { 380 referenceIndex = 0; 381 } else if (mAnimationFilter.darkAnimationOriginIndex == 382 NotificationStackScrollLayout.AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_BELOW) { 383 referenceIndex = mHostLayout.getNotGoneChildCount() - 1; 384 } else { 385 referenceIndex = mAnimationFilter.darkAnimationOriginIndex; 386 } 387 return Math.abs(referenceIndex - viewState.notGoneIndex) * ANIMATION_DELAY_PER_ELEMENT_DARK; 388 } 389 390 private long calculateDelayGoToFullShade(StackViewState viewState) { 391 float index = viewState.notGoneIndex; 392 index = (float) Math.pow(index, 0.7f); 393 return (long) (index * ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE); 394 } 395 396 private void startShadowAlphaAnimation(final ExpandableView child, 397 StackViewState viewState, long duration, long delay) { 398 Float previousStartValue = getChildTag(child, TAG_START_SHADOW_ALPHA); 399 Float previousEndValue = getChildTag(child, TAG_END_SHADOW_ALPHA); 400 float newEndValue = viewState.shadowAlpha; 401 if (previousEndValue != null && previousEndValue == newEndValue) { 402 return; 403 } 404 ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_SHADOW_ALPHA); 405 if (!mAnimationFilter.animateShadowAlpha) { 406 // just a local update was performed 407 if (previousAnimator != null) { 408 // we need to increase all animation keyframes of the previous animator by the 409 // relative change to the end value 410 PropertyValuesHolder[] values = previousAnimator.getValues(); 411 float relativeDiff = newEndValue - previousEndValue; 412 float newStartValue = previousStartValue + relativeDiff; 413 values[0].setFloatValues(newStartValue, newEndValue); 414 child.setTag(TAG_START_SHADOW_ALPHA, newStartValue); 415 child.setTag(TAG_END_SHADOW_ALPHA, newEndValue); 416 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); 417 return; 418 } else { 419 // no new animation needed, let's just apply the value 420 child.setShadowAlpha(newEndValue); 421 return; 422 } 423 } 424 425 ValueAnimator animator = ValueAnimator.ofFloat(child.getShadowAlpha(), newEndValue); 426 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 427 @Override 428 public void onAnimationUpdate(ValueAnimator animation) { 429 child.setShadowAlpha((float) animation.getAnimatedValue()); 430 } 431 }); 432 animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 433 long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator); 434 animator.setDuration(newDuration); 435 if (delay > 0 && (previousAnimator == null 436 || previousAnimator.getAnimatedFraction() == 0)) { 437 animator.setStartDelay(delay); 438 } 439 animator.addListener(getGlobalAnimationFinishedListener()); 440 // remove the tag when the animation is finished 441 animator.addListener(new AnimatorListenerAdapter() { 442 @Override 443 public void onAnimationEnd(Animator animation) { 444 child.setTag(TAG_ANIMATOR_SHADOW_ALPHA, null); 445 child.setTag(TAG_START_SHADOW_ALPHA, null); 446 child.setTag(TAG_END_SHADOW_ALPHA, null); 447 } 448 }); 449 startAnimator(animator); 450 child.setTag(TAG_ANIMATOR_SHADOW_ALPHA, animator); 451 child.setTag(TAG_START_SHADOW_ALPHA, child.getShadowAlpha()); 452 child.setTag(TAG_END_SHADOW_ALPHA, newEndValue); 453 } 454 455 private void startHeightAnimation(final ExpandableView child, 456 StackViewState viewState, long duration, long delay) { 457 Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT); 458 Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT); 459 int newEndValue = viewState.height; 460 if (previousEndValue != null && previousEndValue == newEndValue) { 461 return; 462 } 463 ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT); 464 if (!mAnimationFilter.animateHeight) { 465 // just a local update was performed 466 if (previousAnimator != null) { 467 // we need to increase all animation keyframes of the previous animator by the 468 // relative change to the end value 469 PropertyValuesHolder[] values = previousAnimator.getValues(); 470 int relativeDiff = newEndValue - previousEndValue; 471 int newStartValue = previousStartValue + relativeDiff; 472 values[0].setIntValues(newStartValue, newEndValue); 473 child.setTag(TAG_START_HEIGHT, newStartValue); 474 child.setTag(TAG_END_HEIGHT, newEndValue); 475 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); 476 return; 477 } else { 478 // no new animation needed, let's just apply the value 479 child.setActualHeight(newEndValue, false); 480 return; 481 } 482 } 483 484 ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), newEndValue); 485 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 486 @Override 487 public void onAnimationUpdate(ValueAnimator animation) { 488 child.setActualHeight((int) animation.getAnimatedValue(), 489 false /* notifyListeners */); 490 } 491 }); 492 animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 493 long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator); 494 animator.setDuration(newDuration); 495 if (delay > 0 && (previousAnimator == null 496 || previousAnimator.getAnimatedFraction() == 0)) { 497 animator.setStartDelay(delay); 498 } 499 animator.addListener(getGlobalAnimationFinishedListener()); 500 // remove the tag when the animation is finished 501 animator.addListener(new AnimatorListenerAdapter() { 502 boolean mWasCancelled; 503 504 @Override 505 public void onAnimationEnd(Animator animation) { 506 child.setTag(TAG_ANIMATOR_HEIGHT, null); 507 child.setTag(TAG_START_HEIGHT, null); 508 child.setTag(TAG_END_HEIGHT, null); 509 child.setActualHeightAnimating(false); 510 if (!mWasCancelled && child instanceof ExpandableNotificationRow) { 511 ((ExpandableNotificationRow) child).setGroupExpansionChanging( 512 false /* isExpansionChanging */); 513 } 514 } 515 516 @Override 517 public void onAnimationStart(Animator animation) { 518 mWasCancelled = false; 519 } 520 521 @Override 522 public void onAnimationCancel(Animator animation) { 523 mWasCancelled = true; 524 } 525 }); 526 startAnimator(animator); 527 child.setTag(TAG_ANIMATOR_HEIGHT, animator); 528 child.setTag(TAG_START_HEIGHT, child.getActualHeight()); 529 child.setTag(TAG_END_HEIGHT, newEndValue); 530 child.setActualHeightAnimating(true); 531 } 532 533 private void startInsetAnimation(final ExpandableView child, 534 StackViewState viewState, long duration, long delay) { 535 Integer previousStartValue = getChildTag(child, TAG_START_TOP_INSET); 536 Integer previousEndValue = getChildTag(child, TAG_END_TOP_INSET); 537 int newEndValue = viewState.clipTopAmount; 538 if (previousEndValue != null && previousEndValue == newEndValue) { 539 return; 540 } 541 ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TOP_INSET); 542 if (!mAnimationFilter.animateTopInset) { 543 // just a local update was performed 544 if (previousAnimator != null) { 545 // we need to increase all animation keyframes of the previous animator by the 546 // relative change to the end value 547 PropertyValuesHolder[] values = previousAnimator.getValues(); 548 int relativeDiff = newEndValue - previousEndValue; 549 int newStartValue = previousStartValue + relativeDiff; 550 values[0].setIntValues(newStartValue, newEndValue); 551 child.setTag(TAG_START_TOP_INSET, newStartValue); 552 child.setTag(TAG_END_TOP_INSET, newEndValue); 553 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); 554 return; 555 } else { 556 // no new animation needed, let's just apply the value 557 child.setClipTopAmount(newEndValue); 558 return; 559 } 560 } 561 562 ValueAnimator animator = ValueAnimator.ofInt(child.getClipTopAmount(), newEndValue); 563 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 564 @Override 565 public void onAnimationUpdate(ValueAnimator animation) { 566 child.setClipTopAmount((int) animation.getAnimatedValue()); 567 } 568 }); 569 animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 570 long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator); 571 animator.setDuration(newDuration); 572 if (delay > 0 && (previousAnimator == null 573 || previousAnimator.getAnimatedFraction() == 0)) { 574 animator.setStartDelay(delay); 575 } 576 animator.addListener(getGlobalAnimationFinishedListener()); 577 // remove the tag when the animation is finished 578 animator.addListener(new AnimatorListenerAdapter() { 579 @Override 580 public void onAnimationEnd(Animator animation) { 581 child.setTag(TAG_ANIMATOR_TOP_INSET, null); 582 child.setTag(TAG_START_TOP_INSET, null); 583 child.setTag(TAG_END_TOP_INSET, null); 584 } 585 }); 586 startAnimator(animator); 587 child.setTag(TAG_ANIMATOR_TOP_INSET, animator); 588 child.setTag(TAG_START_TOP_INSET, child.getClipTopAmount()); 589 child.setTag(TAG_END_TOP_INSET, newEndValue); 590 } 591 592 private void startAlphaAnimation(final View child, 593 final ViewState viewState, long duration, long delay) { 594 Float previousStartValue = getChildTag(child,TAG_START_ALPHA); 595 Float previousEndValue = getChildTag(child,TAG_END_ALPHA); 596 final float newEndValue = viewState.alpha; 597 if (previousEndValue != null && previousEndValue == newEndValue) { 598 return; 599 } 600 ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_ALPHA); 601 if (!mAnimationFilter.animateAlpha) { 602 // just a local update was performed 603 if (previousAnimator != null) { 604 // we need to increase all animation keyframes of the previous animator by the 605 // relative change to the end value 606 PropertyValuesHolder[] values = previousAnimator.getValues(); 607 float relativeDiff = newEndValue - previousEndValue; 608 float newStartValue = previousStartValue + relativeDiff; 609 values[0].setFloatValues(newStartValue, newEndValue); 610 child.setTag(TAG_START_ALPHA, newStartValue); 611 child.setTag(TAG_END_ALPHA, newEndValue); 612 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); 613 return; 614 } else { 615 // no new animation needed, let's just apply the value 616 child.setAlpha(newEndValue); 617 if (newEndValue == 0) { 618 child.setVisibility(View.INVISIBLE); 619 } 620 } 621 } 622 623 ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.ALPHA, 624 child.getAlpha(), newEndValue); 625 animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 626 // Handle layer type 627 child.setLayerType(View.LAYER_TYPE_HARDWARE, null); 628 animator.addListener(new AnimatorListenerAdapter() { 629 public boolean mWasCancelled; 630 631 @Override 632 public void onAnimationEnd(Animator animation) { 633 child.setLayerType(View.LAYER_TYPE_NONE, null); 634 if (newEndValue == 0 && !mWasCancelled) { 635 child.setVisibility(View.INVISIBLE); 636 } 637 // remove the tag when the animation is finished 638 child.setTag(TAG_ANIMATOR_ALPHA, null); 639 child.setTag(TAG_START_ALPHA, null); 640 child.setTag(TAG_END_ALPHA, null); 641 } 642 643 @Override 644 public void onAnimationCancel(Animator animation) { 645 mWasCancelled = true; 646 } 647 648 @Override 649 public void onAnimationStart(Animator animation) { 650 mWasCancelled = false; 651 } 652 }); 653 long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator); 654 animator.setDuration(newDuration); 655 if (delay > 0 && (previousAnimator == null 656 || previousAnimator.getAnimatedFraction() == 0)) { 657 animator.setStartDelay(delay); 658 } 659 animator.addListener(getGlobalAnimationFinishedListener()); 660 661 startAnimator(animator); 662 child.setTag(TAG_ANIMATOR_ALPHA, animator); 663 child.setTag(TAG_START_ALPHA, child.getAlpha()); 664 child.setTag(TAG_END_ALPHA, newEndValue); 665 } 666 667 private void startZTranslationAnimation(final View child, 668 final ViewState viewState, long duration, long delay) { 669 Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Z); 670 Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Z); 671 float newEndValue = viewState.zTranslation; 672 if (previousEndValue != null && previousEndValue == newEndValue) { 673 return; 674 } 675 ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Z); 676 if (!mAnimationFilter.animateZ) { 677 // just a local update was performed 678 if (previousAnimator != null) { 679 // we need to increase all animation keyframes of the previous animator by the 680 // relative change to the end value 681 PropertyValuesHolder[] values = previousAnimator.getValues(); 682 float relativeDiff = newEndValue - previousEndValue; 683 float newStartValue = previousStartValue + relativeDiff; 684 values[0].setFloatValues(newStartValue, newEndValue); 685 child.setTag(TAG_START_TRANSLATION_Z, newStartValue); 686 child.setTag(TAG_END_TRANSLATION_Z, newEndValue); 687 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); 688 return; 689 } else { 690 // no new animation needed, let's just apply the value 691 child.setTranslationZ(newEndValue); 692 } 693 } 694 695 ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Z, 696 child.getTranslationZ(), newEndValue); 697 animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 698 long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator); 699 animator.setDuration(newDuration); 700 if (delay > 0 && (previousAnimator == null 701 || previousAnimator.getAnimatedFraction() == 0)) { 702 animator.setStartDelay(delay); 703 } 704 animator.addListener(getGlobalAnimationFinishedListener()); 705 // remove the tag when the animation is finished 706 animator.addListener(new AnimatorListenerAdapter() { 707 @Override 708 public void onAnimationEnd(Animator animation) { 709 child.setTag(TAG_ANIMATOR_TRANSLATION_Z, null); 710 child.setTag(TAG_START_TRANSLATION_Z, null); 711 child.setTag(TAG_END_TRANSLATION_Z, null); 712 } 713 }); 714 startAnimator(animator); 715 child.setTag(TAG_ANIMATOR_TRANSLATION_Z, animator); 716 child.setTag(TAG_START_TRANSLATION_Z, child.getTranslationZ()); 717 child.setTag(TAG_END_TRANSLATION_Z, newEndValue); 718 } 719 720 private void startYTranslationAnimation(final View child, 721 ViewState viewState, long duration, long delay) { 722 Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Y); 723 Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Y); 724 float newEndValue = viewState.yTranslation; 725 if (previousEndValue != null && previousEndValue == newEndValue) { 726 return; 727 } 728 ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y); 729 if (!mAnimationFilter.animateY) { 730 // just a local update was performed 731 if (previousAnimator != null) { 732 // we need to increase all animation keyframes of the previous animator by the 733 // relative change to the end value 734 PropertyValuesHolder[] values = previousAnimator.getValues(); 735 float relativeDiff = newEndValue - previousEndValue; 736 float newStartValue = previousStartValue + relativeDiff; 737 values[0].setFloatValues(newStartValue, newEndValue); 738 child.setTag(TAG_START_TRANSLATION_Y, newStartValue); 739 child.setTag(TAG_END_TRANSLATION_Y, newEndValue); 740 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); 741 return; 742 } else { 743 // no new animation needed, let's just apply the value 744 child.setTranslationY(newEndValue); 745 return; 746 } 747 } 748 749 ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Y, 750 child.getTranslationY(), newEndValue); 751 Interpolator interpolator = mHeadsUpAppearChildren.contains(child) ? 752 mHeadsUpAppearInterpolator :Interpolators.FAST_OUT_SLOW_IN; 753 animator.setInterpolator(interpolator); 754 long newDuration = cancelAnimatorAndGetNewDuration(duration, previousAnimator); 755 animator.setDuration(newDuration); 756 if (delay > 0 && (previousAnimator == null 757 || previousAnimator.getAnimatedFraction() == 0)) { 758 animator.setStartDelay(delay); 759 } 760 animator.addListener(getGlobalAnimationFinishedListener()); 761 final boolean isHeadsUpDisappear = mHeadsUpDisappearChildren.contains(child); 762 // remove the tag when the animation is finished 763 animator.addListener(new AnimatorListenerAdapter() { 764 @Override 765 public void onAnimationEnd(Animator animation) { 766 HeadsUpManager.setIsClickedNotification(child, false); 767 child.setTag(TAG_ANIMATOR_TRANSLATION_Y, null); 768 child.setTag(TAG_START_TRANSLATION_Y, null); 769 child.setTag(TAG_END_TRANSLATION_Y, null); 770 if (isHeadsUpDisappear) { 771 ((ExpandableNotificationRow) child).setHeadsupDisappearRunning(false); 772 } 773 } 774 }); 775 startAnimator(animator); 776 child.setTag(TAG_ANIMATOR_TRANSLATION_Y, animator); 777 child.setTag(TAG_START_TRANSLATION_Y, child.getTranslationY()); 778 child.setTag(TAG_END_TRANSLATION_Y, newEndValue); 779 } 780 781 private void startAnimator(ValueAnimator animator) { 782 mAnimatorSet.add(animator); 783 animator.start(); 784 } 785 786 /** 787 * @return an adapter which ensures that onAnimationFinished is called once no animation is 788 * running anymore 789 */ 790 private AnimatorListenerAdapter getGlobalAnimationFinishedListener() { 791 if (!mAnimationListenerPool.empty()) { 792 return mAnimationListenerPool.pop(); 793 } 794 795 // We need to create a new one, no reusable ones found 796 return new AnimatorListenerAdapter() { 797 private boolean mWasCancelled; 798 799 @Override 800 public void onAnimationEnd(Animator animation) { 801 mAnimatorSet.remove(animation); 802 if (mAnimatorSet.isEmpty() && !mWasCancelled) { 803 onAnimationFinished(); 804 } 805 mAnimationListenerPool.push(this); 806 } 807 808 @Override 809 public void onAnimationCancel(Animator animation) { 810 mWasCancelled = true; 811 } 812 813 @Override 814 public void onAnimationStart(Animator animation) { 815 mWasCancelled = false; 816 } 817 }; 818 } 819 820 public static <T> T getChildTag(View child, int tag) { 821 return (T) child.getTag(tag); 822 } 823 824 /** 825 * Cancel the previous animator and get the duration of the new animation. 826 * 827 * @param duration the new duration 828 * @param previousAnimator the animator which was running before 829 * @return the new duration 830 */ 831 private long cancelAnimatorAndGetNewDuration(long duration, ValueAnimator previousAnimator) { 832 long newDuration = duration; 833 if (previousAnimator != null) { 834 // We take either the desired length of the new animation or the remaining time of 835 // the previous animator, whichever is longer. 836 newDuration = Math.max(previousAnimator.getDuration() 837 - previousAnimator.getCurrentPlayTime(), newDuration); 838 previousAnimator.cancel(); 839 } 840 return newDuration; 841 } 842 843 private void onAnimationFinished() { 844 mHostLayout.onChildAnimationFinished(); 845 for (View v : mChildrenToClearFromOverlay) { 846 removeFromOverlay(v); 847 } 848 mChildrenToClearFromOverlay.clear(); 849 } 850 851 /** 852 * Process the animationEvents for a new animation 853 * 854 * @param animationEvents the animation events for the animation to perform 855 * @param finalState the final state to animate to 856 */ 857 private void processAnimationEvents( 858 ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents, 859 StackScrollState finalState) { 860 for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) { 861 final ExpandableView changingView = (ExpandableView) event.changingView; 862 if (event.animationType == 863 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) { 864 865 // This item is added, initialize it's properties. 866 StackViewState viewState = finalState 867 .getViewStateForView(changingView); 868 if (viewState == null) { 869 // The position for this child was never generated, let's continue. 870 continue; 871 } 872 finalState.applyState(changingView, viewState); 873 mNewAddChildren.add(changingView); 874 875 } else if (event.animationType == 876 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) { 877 if (changingView.getVisibility() == View.GONE) { 878 removeFromOverlay(changingView); 879 continue; 880 } 881 882 // Find the amount to translate up. This is needed in order to understand the 883 // direction of the remove animation (either downwards or upwards) 884 StackViewState viewState = finalState 885 .getViewStateForView(event.viewAfterChangingView); 886 int actualHeight = changingView.getActualHeight(); 887 // upwards by default 888 float translationDirection = -1.0f; 889 if (viewState != null) { 890 // there was a view after this one, Approximate the distance the next child 891 // travelled 892 translationDirection = ((viewState.yTranslation 893 - (changingView.getTranslationY() + actualHeight / 2.0f)) * 2 / 894 actualHeight); 895 translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f); 896 897 } 898 changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR, 899 translationDirection, new Runnable() { 900 @Override 901 public void run() { 902 // remove the temporary overlay 903 removeFromOverlay(changingView); 904 } 905 }); 906 } else if (event.animationType == 907 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) { 908 // A race condition can trigger the view to be added to the overlay even though 909 // it was fully swiped out. So let's remove it 910 mHostLayout.getOverlay().remove(changingView); 911 if (Math.abs(changingView.getTranslation()) == changingView.getWidth() 912 && changingView.getTransientContainer() != null) { 913 changingView.getTransientContainer().removeTransientView(changingView); 914 } 915 } else if (event.animationType == NotificationStackScrollLayout 916 .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) { 917 ExpandableNotificationRow row = (ExpandableNotificationRow) event.changingView; 918 row.prepareExpansionChanged(finalState); 919 } else if (event.animationType == NotificationStackScrollLayout 920 .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) { 921 // This item is added, initialize it's properties. 922 StackViewState viewState = finalState.getViewStateForView(changingView); 923 mTmpState.copyFrom(viewState); 924 if (event.headsUpFromBottom) { 925 mTmpState.yTranslation = mHeadsUpAppearHeightBottom; 926 } else { 927 mTmpState.yTranslation = -mTmpState.height; 928 } 929 mHeadsUpAppearChildren.add(changingView); 930 finalState.applyState(changingView, mTmpState); 931 } else if (event.animationType == NotificationStackScrollLayout 932 .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR || 933 event.animationType == NotificationStackScrollLayout 934 .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK) { 935 mHeadsUpDisappearChildren.add(changingView); 936 if (changingView.getParent() == null) { 937 // This notification was actually removed, so we need to add it to the overlay 938 mHostLayout.getOverlay().add(changingView); 939 mTmpState.initFrom(changingView); 940 mTmpState.yTranslation = -changingView.getActualHeight(); 941 // We temporarily enable Y animations, the real filter will be combined 942 // afterwards anyway 943 mAnimationFilter.animateY = true; 944 startViewAnimations(changingView, mTmpState, 945 event.animationType == NotificationStackScrollLayout 946 .AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK 947 ? ANIMATION_DELAY_HEADS_UP 948 : 0, 949 ANIMATION_DURATION_HEADS_UP_DISAPPEAR); 950 mChildrenToClearFromOverlay.add(changingView); 951 } 952 } 953 mNewEvents.add(event); 954 } 955 } 956 957 public static void removeFromOverlay(View changingView) { 958 ViewGroup parent = (ViewGroup) changingView.getParent(); 959 if (parent != null) { 960 parent.removeView(changingView); 961 } 962 } 963 964 public void animateOverScrollToAmount(float targetAmount, final boolean onTop, 965 final boolean isRubberbanded) { 966 final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop); 967 if (targetAmount == startOverScrollAmount) { 968 return; 969 } 970 cancelOverScrollAnimators(onTop); 971 ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount, 972 targetAmount); 973 overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD); 974 overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 975 @Override 976 public void onAnimationUpdate(ValueAnimator animation) { 977 float currentOverScroll = (float) animation.getAnimatedValue(); 978 mHostLayout.setOverScrollAmount( 979 currentOverScroll, onTop, false /* animate */, false /* cancelAnimators */, 980 isRubberbanded); 981 } 982 }); 983 overScrollAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 984 overScrollAnimator.addListener(new AnimatorListenerAdapter() { 985 @Override 986 public void onAnimationEnd(Animator animation) { 987 if (onTop) { 988 mTopOverScrollAnimator = null; 989 } else { 990 mBottomOverScrollAnimator = null; 991 } 992 } 993 }); 994 overScrollAnimator.start(); 995 if (onTop) { 996 mTopOverScrollAnimator = overScrollAnimator; 997 } else { 998 mBottomOverScrollAnimator = overScrollAnimator; 999 } 1000 } 1001 1002 public void cancelOverScrollAnimators(boolean onTop) { 1003 ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator; 1004 if (currentAnimator != null) { 1005 currentAnimator.cancel(); 1006 } 1007 } 1008 1009 /** 1010 * Get the end value of the height animation running on a view or the actualHeight 1011 * if no animation is running. 1012 */ 1013 public static int getFinalActualHeight(ExpandableView view) { 1014 if (view == null) { 1015 return 0; 1016 } 1017 ValueAnimator heightAnimator = getChildTag(view, TAG_ANIMATOR_HEIGHT); 1018 if (heightAnimator == null) { 1019 return view.getActualHeight(); 1020 } else { 1021 return getChildTag(view, TAG_END_HEIGHT); 1022 } 1023 } 1024 1025 /** 1026 * Get the end value of the yTranslation animation running on a view or the yTranslation 1027 * if no animation is running. 1028 */ 1029 public static float getFinalTranslationY(View view) { 1030 if (view == null) { 1031 return 0; 1032 } 1033 ValueAnimator yAnimator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_Y); 1034 if (yAnimator == null) { 1035 return view.getTranslationY(); 1036 } else { 1037 return getChildTag(view, TAG_END_TRANSLATION_Y); 1038 } 1039 } 1040 1041 public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) { 1042 mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom; 1043 } 1044 1045 public void setShadeExpanded(boolean shadeExpanded) { 1046 mShadeExpanded = shadeExpanded; 1047 } 1048 } 1049