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