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