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