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 * How much the child overlaps with the previous child on top. This is used to 99 * show the background properly when the child on top is translating away. 100 */ 101 public int clipTopAmount; 102 103 /** 104 * The index of the view, only accounting for views not equal to GONE 105 */ 106 public int notGoneIndex; 107 108 /** 109 * The location this view is currently rendered at. 110 * 111 * <p>See <code>LOCATION_</code> flags.</p> 112 */ 113 public int location; 114 115 @Override copyFrom(ViewState viewState)116 public void copyFrom(ViewState viewState) { 117 super.copyFrom(viewState); 118 if (viewState instanceof ExpandableViewState) { 119 ExpandableViewState svs = (ExpandableViewState) viewState; 120 height = svs.height; 121 dimmed = svs.dimmed; 122 shadowAlpha = svs.shadowAlpha; 123 dark = svs.dark; 124 hideSensitive = svs.hideSensitive; 125 belowSpeedBump = svs.belowSpeedBump; 126 clipTopAmount = svs.clipTopAmount; 127 notGoneIndex = svs.notGoneIndex; 128 location = svs.location; 129 } 130 } 131 132 /** 133 * Applies a {@link ExpandableViewState} to a {@link ExpandableView}. 134 */ 135 @Override applyToView(View view)136 public void applyToView(View view) { 137 super.applyToView(view); 138 if (view instanceof ExpandableView) { 139 ExpandableView expandableView = (ExpandableView) view; 140 141 int height = expandableView.getActualHeight(); 142 int newHeight = this.height; 143 144 // apply height 145 if (height != newHeight) { 146 expandableView.setActualHeight(newHeight, false /* notifyListeners */); 147 } 148 149 float shadowAlpha = expandableView.getShadowAlpha(); 150 float newShadowAlpha = this.shadowAlpha; 151 152 // apply shadowAlpha 153 if (shadowAlpha != newShadowAlpha) { 154 expandableView.setShadowAlpha(newShadowAlpha); 155 } 156 157 // apply dimming 158 expandableView.setDimmed(this.dimmed, false /* animate */); 159 160 // apply hiding sensitive 161 expandableView.setHideSensitive( 162 this.hideSensitive, false /* animated */, 0 /* delay */, 0 /* duration */); 163 164 // apply below shelf speed bump 165 expandableView.setBelowSpeedBump(this.belowSpeedBump); 166 167 // apply dark 168 expandableView.setDark(this.dark, false /* animate */, 0 /* delay */); 169 170 // apply clipping 171 float oldClipTopAmount = expandableView.getClipTopAmount(); 172 if (oldClipTopAmount != this.clipTopAmount) { 173 expandableView.setClipTopAmount(this.clipTopAmount); 174 } 175 176 expandableView.setTransformingInShelf(false); 177 expandableView.setInShelf(inShelf); 178 } 179 } 180 181 @Override animateTo(View child, AnimationProperties properties)182 public void animateTo(View child, AnimationProperties properties) { 183 super.animateTo(child, properties); 184 if (!(child instanceof ExpandableView)) { 185 return; 186 } 187 ExpandableView expandableView = (ExpandableView) child; 188 AnimationFilter animationFilter = properties.getAnimationFilter(); 189 190 // start height animation 191 if (this.height != expandableView.getActualHeight()) { 192 startHeightAnimation(expandableView, properties); 193 } else { 194 abortAnimation(child, TAG_ANIMATOR_HEIGHT); 195 } 196 197 // start shadow alpha animation 198 if (this.shadowAlpha != expandableView.getShadowAlpha()) { 199 startShadowAlphaAnimation(expandableView, properties); 200 } else { 201 abortAnimation(child, TAG_ANIMATOR_SHADOW_ALPHA); 202 } 203 204 // start top inset animation 205 if (this.clipTopAmount != expandableView.getClipTopAmount()) { 206 startInsetAnimation(expandableView, properties); 207 } else { 208 abortAnimation(child, TAG_ANIMATOR_TOP_INSET); 209 } 210 211 // start dimmed animation 212 expandableView.setDimmed(this.dimmed, animationFilter.animateDimmed); 213 214 // apply below the speed bump 215 expandableView.setBelowSpeedBump(this.belowSpeedBump); 216 217 // start hiding sensitive animation 218 expandableView.setHideSensitive(this.hideSensitive, animationFilter.animateHideSensitive, 219 properties.delay, properties.duration); 220 221 // start dark animation 222 expandableView.setDark(this.dark, animationFilter.animateDark, properties.delay); 223 224 if (properties.wasAdded(child) && !hidden) { 225 expandableView.performAddAnimation(properties.delay, properties.duration); 226 } 227 228 if (!expandableView.isInShelf() && this.inShelf) { 229 expandableView.setTransformingInShelf(true); 230 } 231 expandableView.setInShelf(this.inShelf); 232 } 233 startHeightAnimation(final ExpandableView child, AnimationProperties properties)234 private void startHeightAnimation(final ExpandableView child, AnimationProperties properties) { 235 Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT); 236 Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT); 237 int newEndValue = this.height; 238 if (previousEndValue != null && previousEndValue == newEndValue) { 239 return; 240 } 241 ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT); 242 AnimationFilter filter = properties.getAnimationFilter(); 243 if (!filter.animateHeight) { 244 // just a local update was performed 245 if (previousAnimator != null) { 246 // we need to increase all animation keyframes of the previous animator by the 247 // relative change to the end value 248 PropertyValuesHolder[] values = previousAnimator.getValues(); 249 int relativeDiff = newEndValue - previousEndValue; 250 int newStartValue = previousStartValue + relativeDiff; 251 values[0].setIntValues(newStartValue, newEndValue); 252 child.setTag(TAG_START_HEIGHT, newStartValue); 253 child.setTag(TAG_END_HEIGHT, newEndValue); 254 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); 255 return; 256 } else { 257 // no new animation needed, let's just apply the value 258 child.setActualHeight(newEndValue, false); 259 return; 260 } 261 } 262 263 ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), newEndValue); 264 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 265 @Override 266 public void onAnimationUpdate(ValueAnimator animation) { 267 child.setActualHeight((int) animation.getAnimatedValue(), 268 false /* notifyListeners */); 269 } 270 }); 271 animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 272 long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); 273 animator.setDuration(newDuration); 274 if (properties.delay > 0 && (previousAnimator == null 275 || previousAnimator.getAnimatedFraction() == 0)) { 276 animator.setStartDelay(properties.delay); 277 } 278 AnimatorListenerAdapter listener = properties.getAnimationFinishListener(); 279 if (listener != null) { 280 animator.addListener(listener); 281 } 282 // remove the tag when the animation is finished 283 animator.addListener(new AnimatorListenerAdapter() { 284 boolean mWasCancelled; 285 286 @Override 287 public void onAnimationEnd(Animator animation) { 288 child.setTag(TAG_ANIMATOR_HEIGHT, null); 289 child.setTag(TAG_START_HEIGHT, null); 290 child.setTag(TAG_END_HEIGHT, null); 291 child.setActualHeightAnimating(false); 292 if (!mWasCancelled && child instanceof ExpandableNotificationRow) { 293 ((ExpandableNotificationRow) child).setGroupExpansionChanging( 294 false /* isExpansionChanging */); 295 } 296 } 297 298 @Override 299 public void onAnimationStart(Animator animation) { 300 mWasCancelled = false; 301 } 302 303 @Override 304 public void onAnimationCancel(Animator animation) { 305 mWasCancelled = true; 306 } 307 }); 308 startAnimator(animator, listener); 309 child.setTag(TAG_ANIMATOR_HEIGHT, animator); 310 child.setTag(TAG_START_HEIGHT, child.getActualHeight()); 311 child.setTag(TAG_END_HEIGHT, newEndValue); 312 child.setActualHeightAnimating(true); 313 } 314 startShadowAlphaAnimation(final ExpandableView child, AnimationProperties properties)315 private void startShadowAlphaAnimation(final ExpandableView child, 316 AnimationProperties properties) { 317 Float previousStartValue = getChildTag(child, TAG_START_SHADOW_ALPHA); 318 Float previousEndValue = getChildTag(child, TAG_END_SHADOW_ALPHA); 319 float newEndValue = this.shadowAlpha; 320 if (previousEndValue != null && previousEndValue == newEndValue) { 321 return; 322 } 323 ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_SHADOW_ALPHA); 324 AnimationFilter filter = properties.getAnimationFilter(); 325 if (!filter.animateShadowAlpha) { 326 // just a local update was performed 327 if (previousAnimator != null) { 328 // we need to increase all animation keyframes of the previous animator by the 329 // relative change to the end value 330 PropertyValuesHolder[] values = previousAnimator.getValues(); 331 float relativeDiff = newEndValue - previousEndValue; 332 float newStartValue = previousStartValue + relativeDiff; 333 values[0].setFloatValues(newStartValue, newEndValue); 334 child.setTag(TAG_START_SHADOW_ALPHA, newStartValue); 335 child.setTag(TAG_END_SHADOW_ALPHA, newEndValue); 336 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); 337 return; 338 } else { 339 // no new animation needed, let's just apply the value 340 child.setShadowAlpha(newEndValue); 341 return; 342 } 343 } 344 345 ValueAnimator animator = ValueAnimator.ofFloat(child.getShadowAlpha(), newEndValue); 346 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 347 @Override 348 public void onAnimationUpdate(ValueAnimator animation) { 349 child.setShadowAlpha((float) animation.getAnimatedValue()); 350 } 351 }); 352 animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 353 long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); 354 animator.setDuration(newDuration); 355 if (properties.delay > 0 && (previousAnimator == null 356 || previousAnimator.getAnimatedFraction() == 0)) { 357 animator.setStartDelay(properties.delay); 358 } 359 AnimatorListenerAdapter listener = properties.getAnimationFinishListener(); 360 if (listener != null) { 361 animator.addListener(listener); 362 } 363 // remove the tag when the animation is finished 364 animator.addListener(new AnimatorListenerAdapter() { 365 @Override 366 public void onAnimationEnd(Animator animation) { 367 child.setTag(TAG_ANIMATOR_SHADOW_ALPHA, null); 368 child.setTag(TAG_START_SHADOW_ALPHA, null); 369 child.setTag(TAG_END_SHADOW_ALPHA, null); 370 } 371 }); 372 startAnimator(animator, listener); 373 child.setTag(TAG_ANIMATOR_SHADOW_ALPHA, animator); 374 child.setTag(TAG_START_SHADOW_ALPHA, child.getShadowAlpha()); 375 child.setTag(TAG_END_SHADOW_ALPHA, newEndValue); 376 } 377 startInsetAnimation(final ExpandableView child, AnimationProperties properties)378 private void startInsetAnimation(final ExpandableView child, AnimationProperties properties) { 379 Integer previousStartValue = getChildTag(child, TAG_START_TOP_INSET); 380 Integer previousEndValue = getChildTag(child, TAG_END_TOP_INSET); 381 int newEndValue = this.clipTopAmount; 382 if (previousEndValue != null && previousEndValue == newEndValue) { 383 return; 384 } 385 ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TOP_INSET); 386 AnimationFilter filter = properties.getAnimationFilter(); 387 if (!filter.animateTopInset) { 388 // just a local update was performed 389 if (previousAnimator != null) { 390 // we need to increase all animation keyframes of the previous animator by the 391 // relative change to the end value 392 PropertyValuesHolder[] values = previousAnimator.getValues(); 393 int relativeDiff = newEndValue - previousEndValue; 394 int newStartValue = previousStartValue + relativeDiff; 395 values[0].setIntValues(newStartValue, newEndValue); 396 child.setTag(TAG_START_TOP_INSET, newStartValue); 397 child.setTag(TAG_END_TOP_INSET, newEndValue); 398 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); 399 return; 400 } else { 401 // no new animation needed, let's just apply the value 402 child.setClipTopAmount(newEndValue); 403 return; 404 } 405 } 406 407 ValueAnimator animator = ValueAnimator.ofInt(child.getClipTopAmount(), newEndValue); 408 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 409 @Override 410 public void onAnimationUpdate(ValueAnimator animation) { 411 child.setClipTopAmount((int) animation.getAnimatedValue()); 412 } 413 }); 414 animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 415 long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); 416 animator.setDuration(newDuration); 417 if (properties.delay > 0 && (previousAnimator == null 418 || previousAnimator.getAnimatedFraction() == 0)) { 419 animator.setStartDelay(properties.delay); 420 } 421 AnimatorListenerAdapter listener = properties.getAnimationFinishListener(); 422 if (listener != null) { 423 animator.addListener(listener); 424 } 425 // remove the tag when the animation is finished 426 animator.addListener(new AnimatorListenerAdapter() { 427 @Override 428 public void onAnimationEnd(Animator animation) { 429 child.setTag(TAG_ANIMATOR_TOP_INSET, null); 430 child.setTag(TAG_START_TOP_INSET, null); 431 child.setTag(TAG_END_TOP_INSET, null); 432 } 433 }); 434 startAnimator(animator, listener); 435 child.setTag(TAG_ANIMATOR_TOP_INSET, animator); 436 child.setTag(TAG_START_TOP_INSET, child.getClipTopAmount()); 437 child.setTag(TAG_END_TOP_INSET, newEndValue); 438 } 439 440 /** 441 * Get the end value of the height animation running on a view or the actualHeight 442 * if no animation is running. 443 */ getFinalActualHeight(ExpandableView view)444 public static int getFinalActualHeight(ExpandableView view) { 445 if (view == null) { 446 return 0; 447 } 448 ValueAnimator heightAnimator = getChildTag(view, TAG_ANIMATOR_HEIGHT); 449 if (heightAnimator == null) { 450 return view.getActualHeight(); 451 } else { 452 return getChildTag(view, TAG_END_HEIGHT); 453 } 454 } 455 } 456