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