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