1 /* 2 * Copyright (C) 2015 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.PropertyValuesHolder; 22 import android.animation.ValueAnimator; 23 import android.view.View; 24 25 import com.android.systemui.Interpolators; 26 import com.android.systemui.R; 27 import com.android.systemui.statusbar.ExpandableNotificationRow; 28 import com.android.systemui.statusbar.ExpandableView; 29 30 /** 31 * A state of an expandable view 32 */ 33 public class ExpandableViewState extends ViewState { 34 35 private static final int TAG_ANIMATOR_HEIGHT = R.id.height_animator_tag; 36 private static final int TAG_ANIMATOR_TOP_INSET = R.id.top_inset_animator_tag; 37 private static final int TAG_ANIMATOR_SHADOW_ALPHA = R.id.shadow_alpha_animator_tag; 38 private static final int TAG_END_HEIGHT = R.id.height_animator_end_value_tag; 39 private static final int TAG_END_TOP_INSET = R.id.top_inset_animator_end_value_tag; 40 private static final int TAG_END_SHADOW_ALPHA = R.id.shadow_alpha_animator_end_value_tag; 41 private static final int TAG_START_HEIGHT = R.id.height_animator_start_value_tag; 42 private static final int TAG_START_TOP_INSET = R.id.top_inset_animator_start_value_tag; 43 private static final int TAG_START_SHADOW_ALPHA = R.id.shadow_alpha_animator_start_value_tag; 44 45 // These are flags such that we can create masks for filtering. 46 47 /** 48 * No known location. This is the default and should not be set after an invocation of the 49 * algorithm. 50 */ 51 public static final int LOCATION_UNKNOWN = 0x00; 52 53 /** 54 * The location is the first heads up notification, so on the very top. 55 */ 56 public static final int LOCATION_FIRST_HUN = 0x01; 57 58 /** 59 * The location is hidden / scrolled away on the top. 60 */ 61 public static final int LOCATION_HIDDEN_TOP = 0x02; 62 63 /** 64 * The location is in the main area of the screen and visible. 65 */ 66 public static final int LOCATION_MAIN_AREA = 0x04; 67 68 /** 69 * The location is in the bottom stack and it's peeking 70 */ 71 public static final int LOCATION_BOTTOM_STACK_PEEKING = 0x08; 72 73 /** 74 * The location is in the bottom stack and it's hidden. 75 */ 76 public static final int LOCATION_BOTTOM_STACK_HIDDEN = 0x10; 77 78 /** 79 * The view isn't laid out at all. 80 */ 81 public static final int LOCATION_GONE = 0x40; 82 83 /** 84 * The visible locations of a view. 85 */ 86 public static final int VISIBLE_LOCATIONS = ExpandableViewState.LOCATION_FIRST_HUN 87 | ExpandableViewState.LOCATION_MAIN_AREA; 88 89 public int height; 90 public boolean dimmed; 91 public boolean dark; 92 public boolean hideSensitive; 93 public boolean belowSpeedBump; 94 public float shadowAlpha; 95 public boolean inShelf; 96 97 /** 98 * A state indicating whether a headsup is currently fully visible, even when not scrolled. 99 * Only valid if the view is heads upped. 100 */ 101 public boolean headsUpIsVisible; 102 103 /** 104 * How much the child overlaps with the previous child on top. This is used to 105 * show the background properly when the child on top is translating away. 106 */ 107 public int clipTopAmount; 108 109 /** 110 * The index of the view, only accounting for views not equal to GONE 111 */ 112 public int notGoneIndex; 113 114 /** 115 * The location this view is currently rendered at. 116 * 117 * <p>See <code>LOCATION_</code> flags.</p> 118 */ 119 public int location; 120 121 @Override copyFrom(ViewState viewState)122 public void copyFrom(ViewState viewState) { 123 super.copyFrom(viewState); 124 if (viewState instanceof ExpandableViewState) { 125 ExpandableViewState svs = (ExpandableViewState) viewState; 126 height = svs.height; 127 dimmed = svs.dimmed; 128 shadowAlpha = svs.shadowAlpha; 129 dark = svs.dark; 130 hideSensitive = svs.hideSensitive; 131 belowSpeedBump = svs.belowSpeedBump; 132 clipTopAmount = svs.clipTopAmount; 133 notGoneIndex = svs.notGoneIndex; 134 location = svs.location; 135 headsUpIsVisible = svs.headsUpIsVisible; 136 } 137 } 138 139 /** 140 * Applies a {@link ExpandableViewState} to a {@link ExpandableView}. 141 */ 142 @Override applyToView(View view)143 public void applyToView(View view) { 144 super.applyToView(view); 145 if (view instanceof ExpandableView) { 146 ExpandableView expandableView = (ExpandableView) view; 147 148 int height = expandableView.getActualHeight(); 149 int newHeight = this.height; 150 151 // apply height 152 if (height != newHeight) { 153 expandableView.setActualHeight(newHeight, false /* notifyListeners */); 154 } 155 156 float shadowAlpha = expandableView.getShadowAlpha(); 157 float newShadowAlpha = this.shadowAlpha; 158 159 // apply shadowAlpha 160 if (shadowAlpha != newShadowAlpha) { 161 expandableView.setShadowAlpha(newShadowAlpha); 162 } 163 164 // apply dimming 165 expandableView.setDimmed(this.dimmed, false /* animate */); 166 167 // apply hiding sensitive 168 expandableView.setHideSensitive( 169 this.hideSensitive, false /* animated */, 0 /* delay */, 0 /* duration */); 170 171 // apply below shelf speed bump 172 expandableView.setBelowSpeedBump(this.belowSpeedBump); 173 174 // apply dark 175 expandableView.setDark(this.dark, false /* animate */, 0 /* delay */); 176 177 // apply clipping 178 float oldClipTopAmount = expandableView.getClipTopAmount(); 179 if (oldClipTopAmount != this.clipTopAmount) { 180 expandableView.setClipTopAmount(this.clipTopAmount); 181 } 182 183 expandableView.setTransformingInShelf(false); 184 expandableView.setInShelf(inShelf); 185 186 if (headsUpIsVisible) { 187 expandableView.setHeadsUpIsVisible(); 188 } 189 } 190 } 191 192 @Override animateTo(View child, AnimationProperties properties)193 public void animateTo(View child, AnimationProperties properties) { 194 super.animateTo(child, properties); 195 if (!(child instanceof ExpandableView)) { 196 return; 197 } 198 ExpandableView expandableView = (ExpandableView) child; 199 AnimationFilter animationFilter = properties.getAnimationFilter(); 200 201 // start height animation 202 if (this.height != expandableView.getActualHeight()) { 203 startHeightAnimation(expandableView, properties); 204 } else { 205 abortAnimation(child, TAG_ANIMATOR_HEIGHT); 206 } 207 208 // start shadow alpha animation 209 if (this.shadowAlpha != expandableView.getShadowAlpha()) { 210 startShadowAlphaAnimation(expandableView, properties); 211 } else { 212 abortAnimation(child, TAG_ANIMATOR_SHADOW_ALPHA); 213 } 214 215 // start top inset animation 216 if (this.clipTopAmount != expandableView.getClipTopAmount()) { 217 startInsetAnimation(expandableView, properties); 218 } else { 219 abortAnimation(child, TAG_ANIMATOR_TOP_INSET); 220 } 221 222 // start dimmed animation 223 expandableView.setDimmed(this.dimmed, animationFilter.animateDimmed); 224 225 // apply below the speed bump 226 expandableView.setBelowSpeedBump(this.belowSpeedBump); 227 228 // start hiding sensitive animation 229 expandableView.setHideSensitive(this.hideSensitive, animationFilter.animateHideSensitive, 230 properties.delay, properties.duration); 231 232 // start dark animation 233 expandableView.setDark(this.dark, animationFilter.animateDark, properties.delay); 234 235 if (properties.wasAdded(child) && !hidden) { 236 expandableView.performAddAnimation(properties.delay, properties.duration, 237 false /* isHeadsUpAppear */); 238 } 239 240 if (!expandableView.isInShelf() && this.inShelf) { 241 expandableView.setTransformingInShelf(true); 242 } 243 expandableView.setInShelf(this.inShelf); 244 245 if (headsUpIsVisible) { 246 expandableView.setHeadsUpIsVisible(); 247 } 248 } 249 startHeightAnimation(final ExpandableView child, AnimationProperties properties)250 private void startHeightAnimation(final ExpandableView child, AnimationProperties properties) { 251 Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT); 252 Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT); 253 int newEndValue = this.height; 254 if (previousEndValue != null && previousEndValue == newEndValue) { 255 return; 256 } 257 ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT); 258 AnimationFilter filter = properties.getAnimationFilter(); 259 if (!filter.animateHeight) { 260 // just a local update was performed 261 if (previousAnimator != null) { 262 // we need to increase all animation keyframes of the previous animator by the 263 // relative change to the end value 264 PropertyValuesHolder[] values = previousAnimator.getValues(); 265 int relativeDiff = newEndValue - previousEndValue; 266 int newStartValue = previousStartValue + relativeDiff; 267 values[0].setIntValues(newStartValue, newEndValue); 268 child.setTag(TAG_START_HEIGHT, newStartValue); 269 child.setTag(TAG_END_HEIGHT, newEndValue); 270 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); 271 return; 272 } else { 273 // no new animation needed, let's just apply the value 274 child.setActualHeight(newEndValue, false); 275 return; 276 } 277 } 278 279 ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), newEndValue); 280 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 281 @Override 282 public void onAnimationUpdate(ValueAnimator animation) { 283 child.setActualHeight((int) animation.getAnimatedValue(), 284 false /* notifyListeners */); 285 } 286 }); 287 animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 288 long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); 289 animator.setDuration(newDuration); 290 if (properties.delay > 0 && (previousAnimator == null 291 || previousAnimator.getAnimatedFraction() == 0)) { 292 animator.setStartDelay(properties.delay); 293 } 294 AnimatorListenerAdapter listener = properties.getAnimationFinishListener(); 295 if (listener != null) { 296 animator.addListener(listener); 297 } 298 // remove the tag when the animation is finished 299 animator.addListener(new AnimatorListenerAdapter() { 300 boolean mWasCancelled; 301 302 @Override 303 public void onAnimationEnd(Animator animation) { 304 child.setTag(TAG_ANIMATOR_HEIGHT, null); 305 child.setTag(TAG_START_HEIGHT, null); 306 child.setTag(TAG_END_HEIGHT, null); 307 child.setActualHeightAnimating(false); 308 if (!mWasCancelled && child instanceof ExpandableNotificationRow) { 309 ((ExpandableNotificationRow) child).setGroupExpansionChanging( 310 false /* isExpansionChanging */); 311 } 312 } 313 314 @Override 315 public void onAnimationStart(Animator animation) { 316 mWasCancelled = false; 317 } 318 319 @Override 320 public void onAnimationCancel(Animator animation) { 321 mWasCancelled = true; 322 } 323 }); 324 startAnimator(animator, listener); 325 child.setTag(TAG_ANIMATOR_HEIGHT, animator); 326 child.setTag(TAG_START_HEIGHT, child.getActualHeight()); 327 child.setTag(TAG_END_HEIGHT, newEndValue); 328 child.setActualHeightAnimating(true); 329 } 330 startShadowAlphaAnimation(final ExpandableView child, AnimationProperties properties)331 private void startShadowAlphaAnimation(final ExpandableView child, 332 AnimationProperties properties) { 333 Float previousStartValue = getChildTag(child, TAG_START_SHADOW_ALPHA); 334 Float previousEndValue = getChildTag(child, TAG_END_SHADOW_ALPHA); 335 float newEndValue = this.shadowAlpha; 336 if (previousEndValue != null && previousEndValue == newEndValue) { 337 return; 338 } 339 ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_SHADOW_ALPHA); 340 AnimationFilter filter = properties.getAnimationFilter(); 341 if (!filter.animateShadowAlpha) { 342 // just a local update was performed 343 if (previousAnimator != null) { 344 // we need to increase all animation keyframes of the previous animator by the 345 // relative change to the end value 346 PropertyValuesHolder[] values = previousAnimator.getValues(); 347 float relativeDiff = newEndValue - previousEndValue; 348 float newStartValue = previousStartValue + relativeDiff; 349 values[0].setFloatValues(newStartValue, newEndValue); 350 child.setTag(TAG_START_SHADOW_ALPHA, newStartValue); 351 child.setTag(TAG_END_SHADOW_ALPHA, newEndValue); 352 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); 353 return; 354 } else { 355 // no new animation needed, let's just apply the value 356 child.setShadowAlpha(newEndValue); 357 return; 358 } 359 } 360 361 ValueAnimator animator = ValueAnimator.ofFloat(child.getShadowAlpha(), newEndValue); 362 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 363 @Override 364 public void onAnimationUpdate(ValueAnimator animation) { 365 child.setShadowAlpha((float) animation.getAnimatedValue()); 366 } 367 }); 368 animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 369 long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); 370 animator.setDuration(newDuration); 371 if (properties.delay > 0 && (previousAnimator == null 372 || previousAnimator.getAnimatedFraction() == 0)) { 373 animator.setStartDelay(properties.delay); 374 } 375 AnimatorListenerAdapter listener = properties.getAnimationFinishListener(); 376 if (listener != null) { 377 animator.addListener(listener); 378 } 379 // remove the tag when the animation is finished 380 animator.addListener(new AnimatorListenerAdapter() { 381 @Override 382 public void onAnimationEnd(Animator animation) { 383 child.setTag(TAG_ANIMATOR_SHADOW_ALPHA, null); 384 child.setTag(TAG_START_SHADOW_ALPHA, null); 385 child.setTag(TAG_END_SHADOW_ALPHA, null); 386 } 387 }); 388 startAnimator(animator, listener); 389 child.setTag(TAG_ANIMATOR_SHADOW_ALPHA, animator); 390 child.setTag(TAG_START_SHADOW_ALPHA, child.getShadowAlpha()); 391 child.setTag(TAG_END_SHADOW_ALPHA, newEndValue); 392 } 393 startInsetAnimation(final ExpandableView child, AnimationProperties properties)394 private void startInsetAnimation(final ExpandableView child, AnimationProperties properties) { 395 Integer previousStartValue = getChildTag(child, TAG_START_TOP_INSET); 396 Integer previousEndValue = getChildTag(child, TAG_END_TOP_INSET); 397 int newEndValue = this.clipTopAmount; 398 if (previousEndValue != null && previousEndValue == newEndValue) { 399 return; 400 } 401 ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TOP_INSET); 402 AnimationFilter filter = properties.getAnimationFilter(); 403 if (!filter.animateTopInset) { 404 // just a local update was performed 405 if (previousAnimator != null) { 406 // we need to increase all animation keyframes of the previous animator by the 407 // relative change to the end value 408 PropertyValuesHolder[] values = previousAnimator.getValues(); 409 int relativeDiff = newEndValue - previousEndValue; 410 int newStartValue = previousStartValue + relativeDiff; 411 values[0].setIntValues(newStartValue, newEndValue); 412 child.setTag(TAG_START_TOP_INSET, newStartValue); 413 child.setTag(TAG_END_TOP_INSET, newEndValue); 414 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); 415 return; 416 } else { 417 // no new animation needed, let's just apply the value 418 child.setClipTopAmount(newEndValue); 419 return; 420 } 421 } 422 423 ValueAnimator animator = ValueAnimator.ofInt(child.getClipTopAmount(), newEndValue); 424 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 425 @Override 426 public void onAnimationUpdate(ValueAnimator animation) { 427 child.setClipTopAmount((int) animation.getAnimatedValue()); 428 } 429 }); 430 animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 431 long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); 432 animator.setDuration(newDuration); 433 if (properties.delay > 0 && (previousAnimator == null 434 || previousAnimator.getAnimatedFraction() == 0)) { 435 animator.setStartDelay(properties.delay); 436 } 437 AnimatorListenerAdapter listener = properties.getAnimationFinishListener(); 438 if (listener != null) { 439 animator.addListener(listener); 440 } 441 // remove the tag when the animation is finished 442 animator.addListener(new AnimatorListenerAdapter() { 443 @Override 444 public void onAnimationEnd(Animator animation) { 445 child.setTag(TAG_ANIMATOR_TOP_INSET, null); 446 child.setTag(TAG_START_TOP_INSET, null); 447 child.setTag(TAG_END_TOP_INSET, null); 448 } 449 }); 450 startAnimator(animator, listener); 451 child.setTag(TAG_ANIMATOR_TOP_INSET, animator); 452 child.setTag(TAG_START_TOP_INSET, child.getClipTopAmount()); 453 child.setTag(TAG_END_TOP_INSET, newEndValue); 454 } 455 456 /** 457 * Get the end value of the height animation running on a view or the actualHeight 458 * if no animation is running. 459 */ getFinalActualHeight(ExpandableView view)460 public static int getFinalActualHeight(ExpandableView view) { 461 if (view == null) { 462 return 0; 463 } 464 ValueAnimator heightAnimator = getChildTag(view, TAG_ANIMATOR_HEIGHT); 465 if (heightAnimator == null) { 466 return view.getActualHeight(); 467 } else { 468 return getChildTag(view, TAG_END_HEIGHT); 469 } 470 } 471 472 @Override cancelAnimations(View view)473 public void cancelAnimations(View view) { 474 super.cancelAnimations(view); 475 Animator animator = getChildTag(view, TAG_ANIMATOR_HEIGHT); 476 if (animator != null) { 477 animator.cancel(); 478 } 479 animator = getChildTag(view, TAG_ANIMATOR_SHADOW_ALPHA); 480 if (animator != null) { 481 animator.cancel(); 482 } 483 animator = getChildTag(view, TAG_ANIMATOR_TOP_INSET); 484 if (animator != null) { 485 animator.cancel(); 486 } 487 } 488 } 489