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.res.R; 27 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 28 import com.android.systemui.statusbar.notification.row.ExpandableView; 29 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; 30 31 /** 32 * A state of an expandable view 33 */ 34 public class ExpandableViewState extends ViewState { 35 36 private static final int TAG_ANIMATOR_HEIGHT = R.id.height_animator_tag; 37 private static final int TAG_ANIMATOR_TOP_INSET = R.id.top_inset_animator_tag; 38 private static final int TAG_ANIMATOR_BOTTOM_INSET = R.id.bottom_inset_animator_tag; 39 private static final int TAG_END_HEIGHT = R.id.height_animator_end_value_tag; 40 private static final int TAG_END_TOP_INSET = R.id.top_inset_animator_end_value_tag; 41 private static final int TAG_END_BOTTOM_INSET = R.id.bottom_inset_animator_end_value_tag; 42 private static final int TAG_START_HEIGHT = R.id.height_animator_start_value_tag; 43 private static final int TAG_START_TOP_INSET = R.id.top_inset_animator_start_value_tag; 44 private static final int TAG_START_BOTTOM_INSET = R.id.bottom_inset_animator_start_value_tag; 45 46 // These are flags such that we can create masks for filtering. 47 48 /** 49 * No known location. This is the default and should not be set after an invocation of the 50 * algorithm. 51 */ 52 public static final int LOCATION_UNKNOWN = 0x00; 53 54 /** 55 * The location is the first heads up notification, so on the very top. 56 */ 57 public static final int LOCATION_FIRST_HUN = 0x01; 58 59 /** 60 * The location is hidden / scrolled away on the top. 61 */ 62 public static final int LOCATION_HIDDEN_TOP = 0x02; 63 64 /** 65 * The location is in the main area of the screen and visible. 66 */ 67 public static final int LOCATION_MAIN_AREA = 0x04; 68 69 /** 70 * The location is in the bottom stack and it's peeking 71 */ 72 public static final int LOCATION_BOTTOM_STACK_PEEKING = 0x08; 73 74 /** 75 * The location is in the bottom stack and it's hidden. 76 */ 77 public static final int LOCATION_BOTTOM_STACK_HIDDEN = 0x10; 78 79 /** 80 * The view isn't laid out at all. 81 */ 82 public static final int LOCATION_GONE = 0x40; 83 84 /** 85 * The visible locations of a view. 86 */ 87 public static final int VISIBLE_LOCATIONS = ExpandableViewState.LOCATION_FIRST_HUN 88 | ExpandableViewState.LOCATION_MAIN_AREA; 89 90 public int height; 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 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 final int height = expandableView.getActualHeight(); 149 final int newHeight = this.height; 150 151 // apply height 152 if (height != newHeight) { 153 expandableView.setActualHeight(newHeight, false /* notifyListeners */); 154 } 155 156 // apply hiding sensitive 157 expandableView.setHideSensitive( 158 this.hideSensitive, false /* animated */, 0 /* delay */, 0 /* duration */); 159 160 // apply below shelf speed bump 161 if (!NotificationIconContainerRefactor.isEnabled()) { 162 expandableView.setBelowSpeedBump(this.belowSpeedBump); 163 } 164 165 // apply clipping 166 final float oldClipTopAmount = expandableView.getClipTopAmount(); 167 if (oldClipTopAmount != this.clipTopAmount) { 168 expandableView.setClipTopAmount(this.clipTopAmount); 169 } 170 final float oldClipBottomAmount = expandableView.getClipBottomAmount(); 171 if (oldClipBottomAmount != this.clipBottomAmount) { 172 expandableView.setClipBottomAmount(this.clipBottomAmount); 173 } 174 175 expandableView.setTransformingInShelf(false); 176 expandableView.setInShelf(inShelf); 177 178 if (headsUpIsVisible) { 179 expandableView.setHeadsUpIsVisible(); 180 } 181 } 182 } 183 184 @Override animateTo(View child, AnimationProperties properties)185 public void animateTo(View child, AnimationProperties properties) { 186 super.animateTo(child, properties); 187 if (!(child instanceof ExpandableView)) { 188 return; 189 } 190 ExpandableView expandableView = (ExpandableView) child; 191 AnimationFilter animationFilter = properties.getAnimationFilter(); 192 193 // start height animation 194 if (this.height != expandableView.getActualHeight()) { 195 startHeightAnimation(expandableView, properties); 196 } else { 197 abortAnimation(child, TAG_ANIMATOR_HEIGHT); 198 } 199 200 // start clip top animation 201 if (this.clipTopAmount != expandableView.getClipTopAmount()) { 202 startClipAnimation(expandableView, properties, /* clipTop */true); 203 } else { 204 abortAnimation(child, TAG_ANIMATOR_TOP_INSET); 205 } 206 207 // start clip bottom animation 208 if (this.clipBottomAmount != expandableView.getClipBottomAmount()) { 209 startClipAnimation(expandableView, properties, /* clipTop */ false); 210 } else { 211 abortAnimation(child, TAG_ANIMATOR_BOTTOM_INSET); 212 } 213 214 // apply below the speed bump 215 if (!NotificationIconContainerRefactor.isEnabled()) { 216 expandableView.setBelowSpeedBump(this.belowSpeedBump); 217 } 218 219 // start hiding sensitive animation 220 expandableView.setHideSensitive(this.hideSensitive, animationFilter.animateHideSensitive, 221 properties.delay, properties.duration); 222 223 if (properties.wasAdded(child) && !hidden) { 224 expandableView.performAddAnimation(properties.delay, properties.duration, 225 false /* isHeadsUpAppear */); 226 } 227 228 if (!expandableView.isInShelf() && this.inShelf) { 229 expandableView.setTransformingInShelf(true); 230 } 231 expandableView.setInShelf(this.inShelf); 232 233 if (headsUpIsVisible) { 234 expandableView.setHeadsUpIsVisible(); 235 } 236 } 237 startHeightAnimation(final ExpandableView child, AnimationProperties properties)238 private void startHeightAnimation(final ExpandableView child, AnimationProperties properties) { 239 Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT); 240 Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT); 241 int newEndValue = this.height; 242 if (previousEndValue != null && previousEndValue == newEndValue) { 243 return; 244 } 245 ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT); 246 AnimationFilter filter = properties.getAnimationFilter(); 247 if (!filter.animateHeight) { 248 // just a local update was performed 249 if (previousAnimator != null) { 250 // we need to increase all animation keyframes of the previous animator by the 251 // relative change to the end value 252 PropertyValuesHolder[] values = previousAnimator.getValues(); 253 int relativeDiff = newEndValue - previousEndValue; 254 int newStartValue = previousStartValue + relativeDiff; 255 values[0].setIntValues(newStartValue, newEndValue); 256 child.setTag(TAG_START_HEIGHT, newStartValue); 257 child.setTag(TAG_END_HEIGHT, newEndValue); 258 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); 259 return; 260 } else { 261 // no new animation needed, let's just apply the value 262 child.setActualHeight(newEndValue, false); 263 return; 264 } 265 } 266 267 ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), newEndValue); 268 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 269 @Override 270 public void onAnimationUpdate(ValueAnimator animation) { 271 child.setActualHeight((int) animation.getAnimatedValue(), 272 false /* notifyListeners */); 273 } 274 }); 275 animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 276 long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); 277 animator.setDuration(newDuration); 278 if (properties.delay > 0 && (previousAnimator == null 279 || previousAnimator.getAnimatedFraction() == 0)) { 280 animator.setStartDelay(properties.delay); 281 } 282 AnimatorListenerAdapter listener = properties.getAnimationFinishListener( 283 null /* no property for this height */); 284 if (listener != null) { 285 animator.addListener(listener); 286 } 287 // remove the tag when the animation is finished 288 animator.addListener(new AnimatorListenerAdapter() { 289 boolean mWasCancelled; 290 291 @Override 292 public void onAnimationEnd(Animator animation) { 293 child.setTag(TAG_ANIMATOR_HEIGHT, null); 294 child.setTag(TAG_START_HEIGHT, null); 295 child.setTag(TAG_END_HEIGHT, null); 296 child.setActualHeightAnimating(false); 297 if (!mWasCancelled && child instanceof ExpandableNotificationRow) { 298 ((ExpandableNotificationRow) child).setGroupExpansionChanging( 299 false /* isExpansionChanging */); 300 } 301 } 302 303 @Override 304 public void onAnimationStart(Animator animation) { 305 mWasCancelled = false; 306 } 307 308 @Override 309 public void onAnimationCancel(Animator animation) { 310 mWasCancelled = true; 311 } 312 }); 313 startAnimator(animator, listener); 314 child.setTag(TAG_ANIMATOR_HEIGHT, animator); 315 child.setTag(TAG_START_HEIGHT, child.getActualHeight()); 316 child.setTag(TAG_END_HEIGHT, newEndValue); 317 child.setActualHeightAnimating(true); 318 } 319 startClipAnimation(final ExpandableView child, AnimationProperties properties, boolean clipTop)320 private void startClipAnimation(final ExpandableView child, AnimationProperties properties, 321 boolean clipTop) { 322 Integer previousStartValue = getChildTag(child, 323 clipTop ? TAG_START_TOP_INSET : TAG_START_BOTTOM_INSET); 324 Integer previousEndValue = getChildTag(child, 325 clipTop ? TAG_END_TOP_INSET : TAG_END_BOTTOM_INSET); 326 int newEndValue = clipTop ? this.clipTopAmount : this.clipBottomAmount; 327 if (previousEndValue != null && previousEndValue == newEndValue) { 328 return; 329 } 330 ValueAnimator previousAnimator = getChildTag(child, 331 clipTop ? TAG_ANIMATOR_TOP_INSET : TAG_ANIMATOR_BOTTOM_INSET); 332 AnimationFilter filter = properties.getAnimationFilter(); 333 if (clipTop && !filter.animateTopInset || !clipTop) { 334 // just a local update was performed 335 if (previousAnimator != null) { 336 // we need to increase all animation keyframes of the previous animator by the 337 // relative change to the end value 338 PropertyValuesHolder[] values = previousAnimator.getValues(); 339 int relativeDiff = newEndValue - previousEndValue; 340 int newStartValue = previousStartValue + relativeDiff; 341 values[0].setIntValues(newStartValue, newEndValue); 342 child.setTag(clipTop ? TAG_START_TOP_INSET : TAG_START_BOTTOM_INSET, newStartValue); 343 child.setTag(clipTop ? TAG_END_TOP_INSET : TAG_END_BOTTOM_INSET, newEndValue); 344 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); 345 return; 346 } else { 347 // no new animation needed, let's just apply the value 348 if (clipTop) { 349 child.setClipTopAmount(newEndValue); 350 } else { 351 child.setClipBottomAmount(newEndValue); 352 } 353 return; 354 } 355 } 356 357 ValueAnimator animator = ValueAnimator.ofInt( 358 clipTop ? child.getClipTopAmount() : child.getClipBottomAmount(), newEndValue); 359 animator.addUpdateListener(animation -> { 360 if (clipTop) { 361 child.setClipTopAmount((int) animation.getAnimatedValue()); 362 } else { 363 child.setClipBottomAmount((int) animation.getAnimatedValue()); 364 } 365 }); 366 animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 367 long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); 368 animator.setDuration(newDuration); 369 if (properties.delay > 0 && (previousAnimator == null 370 || previousAnimator.getAnimatedFraction() == 0)) { 371 animator.setStartDelay(properties.delay); 372 } 373 AnimatorListenerAdapter listener = properties.getAnimationFinishListener( 374 null /* no property for top inset */); 375 if (listener != null) { 376 animator.addListener(listener); 377 } 378 // remove the tag when the animation is finished 379 animator.addListener(new AnimatorListenerAdapter() { 380 @Override 381 public void onAnimationEnd(Animator animation) { 382 child.setTag(clipTop ? TAG_ANIMATOR_TOP_INSET : TAG_ANIMATOR_BOTTOM_INSET, null); 383 child.setTag(clipTop ? TAG_START_TOP_INSET : TAG_START_BOTTOM_INSET, null); 384 child.setTag(clipTop ? TAG_END_TOP_INSET : TAG_END_BOTTOM_INSET, null); 385 } 386 }); 387 startAnimator(animator, listener); 388 child.setTag(clipTop ? TAG_ANIMATOR_TOP_INSET:TAG_ANIMATOR_BOTTOM_INSET, animator); 389 child.setTag(clipTop ? TAG_START_TOP_INSET: TAG_START_BOTTOM_INSET, 390 clipTop ? child.getClipTopAmount() : child.getClipBottomAmount()); 391 child.setTag(clipTop ? TAG_END_TOP_INSET: TAG_END_BOTTOM_INSET, newEndValue); 392 } 393 394 /** 395 * Get the end value of the height animation running on a view or the actualHeight 396 * if no animation is running. 397 */ getFinalActualHeight(ExpandableView view)398 public static int getFinalActualHeight(ExpandableView view) { 399 if (view == null) { 400 return 0; 401 } 402 ValueAnimator heightAnimator = getChildTag(view, TAG_ANIMATOR_HEIGHT); 403 if (heightAnimator == null) { 404 return view.getActualHeight(); 405 } else { 406 return getChildTag(view, TAG_END_HEIGHT); 407 } 408 } 409 410 @Override cancelAnimations(View view)411 public void cancelAnimations(View view) { 412 super.cancelAnimations(view); 413 Animator animator = getChildTag(view, TAG_ANIMATOR_HEIGHT); 414 if (animator != null) { 415 animator.cancel(); 416 } 417 animator = getChildTag(view, TAG_ANIMATOR_TOP_INSET); 418 if (animator != null) { 419 animator.cancel(); 420 } 421 } 422 } 423