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