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