Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 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 | |
Rohan Shah | 20790b8 | 2018-07-02 17:21:04 -0700 | [diff] [blame] | 17 | package com.android.systemui.statusbar.notification.stack; |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 18 | |
Selim Cinek | 0cfbef4 | 2016-11-09 19:06:36 -0800 | [diff] [blame] | 19 | import android.animation.Animator; |
| 20 | import android.animation.AnimatorListenerAdapter; |
| 21 | import android.animation.PropertyValuesHolder; |
| 22 | import android.animation.ValueAnimator; |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 23 | import android.view.View; |
| 24 | |
Selim Cinek | 0cfbef4 | 2016-11-09 19:06:36 -0800 | [diff] [blame] | 25 | import com.android.systemui.Interpolators; |
| 26 | import com.android.systemui.R; |
Rohan Shah | 20790b8 | 2018-07-02 17:21:04 -0700 | [diff] [blame] | 27 | import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; |
| 28 | import com.android.systemui.statusbar.notification.row.ExpandableView; |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 29 | |
| 30 | /** |
| 31 | * A state of an expandable view |
| 32 | */ |
| 33 | public class ExpandableViewState extends ViewState { |
| 34 | |
Selim Cinek | 0cfbef4 | 2016-11-09 19:06:36 -0800 | [diff] [blame] | 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; |
Selim Cinek | 0cfbef4 | 2016-11-09 19:06:36 -0800 | [diff] [blame] | 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; |
Selim Cinek | 0cfbef4 | 2016-11-09 19:06:36 -0800 | [diff] [blame] | 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; |
Selim Cinek | 0cfbef4 | 2016-11-09 19:06:36 -0800 | [diff] [blame] | 41 | |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 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 | /** |
Selim Cinek | 281c202 | 2016-10-13 19:14:43 -0700 | [diff] [blame] | 76 | * The view isn't laid out at all. |
| 77 | */ |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 78 | public static final int LOCATION_GONE = 0x40; |
| 79 | |
Selim Cinek | a7d4f82 | 2016-12-06 14:34:47 -0800 | [diff] [blame] | 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 | |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 86 | public int height; |
| 87 | public boolean dimmed; |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 88 | public boolean hideSensitive; |
Selim Cinek | db16737 | 2016-11-17 15:41:17 -0800 | [diff] [blame] | 89 | public boolean belowSpeedBump; |
Selim Cinek | eccb5de | 2016-10-28 15:04:05 -0700 | [diff] [blame] | 90 | public boolean inShelf; |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 91 | |
| 92 | /** |
Selim Cinek | 9b9d6e1 | 2017-11-30 12:29:47 +0100 | [diff] [blame] | 93 | * A state indicating whether a headsup is currently fully visible, even when not scrolled. |
| 94 | * Only valid if the view is heads upped. |
| 95 | */ |
| 96 | public boolean headsUpIsVisible; |
| 97 | |
| 98 | /** |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 99 | * How much the child overlaps with the previous child on top. This is used to |
| 100 | * show the background properly when the child on top is translating away. |
| 101 | */ |
| 102 | public int clipTopAmount; |
| 103 | |
| 104 | /** |
| 105 | * The index of the view, only accounting for views not equal to GONE |
| 106 | */ |
| 107 | public int notGoneIndex; |
| 108 | |
| 109 | /** |
| 110 | * The location this view is currently rendered at. |
| 111 | * |
| 112 | * <p>See <code>LOCATION_</code> flags.</p> |
| 113 | */ |
| 114 | public int location; |
| 115 | |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 116 | @Override |
| 117 | public void copyFrom(ViewState viewState) { |
| 118 | super.copyFrom(viewState); |
| 119 | if (viewState instanceof ExpandableViewState) { |
| 120 | ExpandableViewState svs = (ExpandableViewState) viewState; |
| 121 | height = svs.height; |
| 122 | dimmed = svs.dimmed; |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 123 | hideSensitive = svs.hideSensitive; |
Selim Cinek | db16737 | 2016-11-17 15:41:17 -0800 | [diff] [blame] | 124 | belowSpeedBump = svs.belowSpeedBump; |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 125 | clipTopAmount = svs.clipTopAmount; |
| 126 | notGoneIndex = svs.notGoneIndex; |
| 127 | location = svs.location; |
Selim Cinek | 9b9d6e1 | 2017-11-30 12:29:47 +0100 | [diff] [blame] | 128 | headsUpIsVisible = svs.headsUpIsVisible; |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 129 | } |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * Applies a {@link ExpandableViewState} to a {@link ExpandableView}. |
| 134 | */ |
| 135 | @Override |
| 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 | |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 149 | // apply dimming |
| 150 | expandableView.setDimmed(this.dimmed, false /* animate */); |
| 151 | |
| 152 | // apply hiding sensitive |
| 153 | expandableView.setHideSensitive( |
| 154 | this.hideSensitive, false /* animated */, 0 /* delay */, 0 /* duration */); |
| 155 | |
Selim Cinek | db16737 | 2016-11-17 15:41:17 -0800 | [diff] [blame] | 156 | // apply below shelf speed bump |
| 157 | expandableView.setBelowSpeedBump(this.belowSpeedBump); |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 158 | |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 159 | // apply clipping |
| 160 | float oldClipTopAmount = expandableView.getClipTopAmount(); |
| 161 | if (oldClipTopAmount != this.clipTopAmount) { |
| 162 | expandableView.setClipTopAmount(this.clipTopAmount); |
| 163 | } |
Selim Cinek | eccb5de | 2016-10-28 15:04:05 -0700 | [diff] [blame] | 164 | |
| 165 | expandableView.setTransformingInShelf(false); |
| 166 | expandableView.setInShelf(inShelf); |
Selim Cinek | 9b9d6e1 | 2017-11-30 12:29:47 +0100 | [diff] [blame] | 167 | |
| 168 | if (headsUpIsVisible) { |
| 169 | expandableView.setHeadsUpIsVisible(); |
| 170 | } |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 171 | } |
| 172 | } |
Selim Cinek | 0cfbef4 | 2016-11-09 19:06:36 -0800 | [diff] [blame] | 173 | |
| 174 | @Override |
| 175 | public void animateTo(View child, AnimationProperties properties) { |
| 176 | super.animateTo(child, properties); |
| 177 | if (!(child instanceof ExpandableView)) { |
| 178 | return; |
| 179 | } |
| 180 | ExpandableView expandableView = (ExpandableView) child; |
| 181 | AnimationFilter animationFilter = properties.getAnimationFilter(); |
| 182 | |
| 183 | // start height animation |
| 184 | if (this.height != expandableView.getActualHeight()) { |
| 185 | startHeightAnimation(expandableView, properties); |
| 186 | } else { |
| 187 | abortAnimation(child, TAG_ANIMATOR_HEIGHT); |
| 188 | } |
| 189 | |
Selim Cinek | 0cfbef4 | 2016-11-09 19:06:36 -0800 | [diff] [blame] | 190 | // start top inset animation |
| 191 | if (this.clipTopAmount != expandableView.getClipTopAmount()) { |
| 192 | startInsetAnimation(expandableView, properties); |
| 193 | } else { |
| 194 | abortAnimation(child, TAG_ANIMATOR_TOP_INSET); |
| 195 | } |
| 196 | |
| 197 | // start dimmed animation |
| 198 | expandableView.setDimmed(this.dimmed, animationFilter.animateDimmed); |
| 199 | |
Selim Cinek | db16737 | 2016-11-17 15:41:17 -0800 | [diff] [blame] | 200 | // apply below the speed bump |
| 201 | expandableView.setBelowSpeedBump(this.belowSpeedBump); |
Selim Cinek | 0cfbef4 | 2016-11-09 19:06:36 -0800 | [diff] [blame] | 202 | |
| 203 | // start hiding sensitive animation |
| 204 | expandableView.setHideSensitive(this.hideSensitive, animationFilter.animateHideSensitive, |
| 205 | properties.delay, properties.duration); |
| 206 | |
Selim Cinek | 5b5beb01 | 2016-11-08 18:11:58 -0800 | [diff] [blame] | 207 | if (properties.wasAdded(child) && !hidden) { |
Selim Cinek | 332c23f | 2018-03-16 17:37:50 -0700 | [diff] [blame] | 208 | expandableView.performAddAnimation(properties.delay, properties.duration, |
| 209 | false /* isHeadsUpAppear */); |
Selim Cinek | 0cfbef4 | 2016-11-09 19:06:36 -0800 | [diff] [blame] | 210 | } |
Selim Cinek | eccb5de | 2016-10-28 15:04:05 -0700 | [diff] [blame] | 211 | |
| 212 | if (!expandableView.isInShelf() && this.inShelf) { |
| 213 | expandableView.setTransformingInShelf(true); |
| 214 | } |
| 215 | expandableView.setInShelf(this.inShelf); |
Selim Cinek | 9b9d6e1 | 2017-11-30 12:29:47 +0100 | [diff] [blame] | 216 | |
| 217 | if (headsUpIsVisible) { |
| 218 | expandableView.setHeadsUpIsVisible(); |
| 219 | } |
Selim Cinek | 0cfbef4 | 2016-11-09 19:06:36 -0800 | [diff] [blame] | 220 | } |
| 221 | |
| 222 | private void startHeightAnimation(final ExpandableView child, AnimationProperties properties) { |
| 223 | Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT); |
| 224 | Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT); |
| 225 | int newEndValue = this.height; |
| 226 | if (previousEndValue != null && previousEndValue == newEndValue) { |
| 227 | return; |
| 228 | } |
| 229 | ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT); |
| 230 | AnimationFilter filter = properties.getAnimationFilter(); |
| 231 | if (!filter.animateHeight) { |
| 232 | // just a local update was performed |
| 233 | if (previousAnimator != null) { |
| 234 | // we need to increase all animation keyframes of the previous animator by the |
| 235 | // relative change to the end value |
| 236 | PropertyValuesHolder[] values = previousAnimator.getValues(); |
| 237 | int relativeDiff = newEndValue - previousEndValue; |
| 238 | int newStartValue = previousStartValue + relativeDiff; |
| 239 | values[0].setIntValues(newStartValue, newEndValue); |
| 240 | child.setTag(TAG_START_HEIGHT, newStartValue); |
| 241 | child.setTag(TAG_END_HEIGHT, newEndValue); |
| 242 | previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); |
| 243 | return; |
| 244 | } else { |
| 245 | // no new animation needed, let's just apply the value |
| 246 | child.setActualHeight(newEndValue, false); |
| 247 | return; |
| 248 | } |
| 249 | } |
| 250 | |
| 251 | ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), newEndValue); |
| 252 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
| 253 | @Override |
| 254 | public void onAnimationUpdate(ValueAnimator animation) { |
| 255 | child.setActualHeight((int) animation.getAnimatedValue(), |
| 256 | false /* notifyListeners */); |
| 257 | } |
| 258 | }); |
| 259 | animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); |
| 260 | long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); |
| 261 | animator.setDuration(newDuration); |
| 262 | if (properties.delay > 0 && (previousAnimator == null |
| 263 | || previousAnimator.getAnimatedFraction() == 0)) { |
| 264 | animator.setStartDelay(properties.delay); |
| 265 | } |
| 266 | AnimatorListenerAdapter listener = properties.getAnimationFinishListener(); |
| 267 | if (listener != null) { |
| 268 | animator.addListener(listener); |
| 269 | } |
| 270 | // remove the tag when the animation is finished |
| 271 | animator.addListener(new AnimatorListenerAdapter() { |
| 272 | boolean mWasCancelled; |
| 273 | |
| 274 | @Override |
| 275 | public void onAnimationEnd(Animator animation) { |
| 276 | child.setTag(TAG_ANIMATOR_HEIGHT, null); |
| 277 | child.setTag(TAG_START_HEIGHT, null); |
| 278 | child.setTag(TAG_END_HEIGHT, null); |
| 279 | child.setActualHeightAnimating(false); |
| 280 | if (!mWasCancelled && child instanceof ExpandableNotificationRow) { |
| 281 | ((ExpandableNotificationRow) child).setGroupExpansionChanging( |
| 282 | false /* isExpansionChanging */); |
| 283 | } |
| 284 | } |
| 285 | |
| 286 | @Override |
| 287 | public void onAnimationStart(Animator animation) { |
| 288 | mWasCancelled = false; |
| 289 | } |
| 290 | |
| 291 | @Override |
| 292 | public void onAnimationCancel(Animator animation) { |
| 293 | mWasCancelled = true; |
| 294 | } |
| 295 | }); |
| 296 | startAnimator(animator, listener); |
| 297 | child.setTag(TAG_ANIMATOR_HEIGHT, animator); |
| 298 | child.setTag(TAG_START_HEIGHT, child.getActualHeight()); |
| 299 | child.setTag(TAG_END_HEIGHT, newEndValue); |
| 300 | child.setActualHeightAnimating(true); |
| 301 | } |
| 302 | |
Selim Cinek | 0cfbef4 | 2016-11-09 19:06:36 -0800 | [diff] [blame] | 303 | private void startInsetAnimation(final ExpandableView child, AnimationProperties properties) { |
| 304 | Integer previousStartValue = getChildTag(child, TAG_START_TOP_INSET); |
| 305 | Integer previousEndValue = getChildTag(child, TAG_END_TOP_INSET); |
| 306 | int newEndValue = this.clipTopAmount; |
| 307 | if (previousEndValue != null && previousEndValue == newEndValue) { |
| 308 | return; |
| 309 | } |
| 310 | ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TOP_INSET); |
| 311 | AnimationFilter filter = properties.getAnimationFilter(); |
| 312 | if (!filter.animateTopInset) { |
| 313 | // just a local update was performed |
| 314 | if (previousAnimator != null) { |
| 315 | // we need to increase all animation keyframes of the previous animator by the |
| 316 | // relative change to the end value |
| 317 | PropertyValuesHolder[] values = previousAnimator.getValues(); |
| 318 | int relativeDiff = newEndValue - previousEndValue; |
| 319 | int newStartValue = previousStartValue + relativeDiff; |
| 320 | values[0].setIntValues(newStartValue, newEndValue); |
| 321 | child.setTag(TAG_START_TOP_INSET, newStartValue); |
| 322 | child.setTag(TAG_END_TOP_INSET, newEndValue); |
| 323 | previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); |
| 324 | return; |
| 325 | } else { |
| 326 | // no new animation needed, let's just apply the value |
| 327 | child.setClipTopAmount(newEndValue); |
| 328 | return; |
| 329 | } |
| 330 | } |
| 331 | |
| 332 | ValueAnimator animator = ValueAnimator.ofInt(child.getClipTopAmount(), newEndValue); |
| 333 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
| 334 | @Override |
| 335 | public void onAnimationUpdate(ValueAnimator animation) { |
| 336 | child.setClipTopAmount((int) animation.getAnimatedValue()); |
| 337 | } |
| 338 | }); |
| 339 | animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); |
| 340 | long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); |
| 341 | animator.setDuration(newDuration); |
| 342 | if (properties.delay > 0 && (previousAnimator == null |
| 343 | || previousAnimator.getAnimatedFraction() == 0)) { |
| 344 | animator.setStartDelay(properties.delay); |
| 345 | } |
| 346 | AnimatorListenerAdapter listener = properties.getAnimationFinishListener(); |
| 347 | if (listener != null) { |
| 348 | animator.addListener(listener); |
| 349 | } |
| 350 | // remove the tag when the animation is finished |
| 351 | animator.addListener(new AnimatorListenerAdapter() { |
| 352 | @Override |
| 353 | public void onAnimationEnd(Animator animation) { |
| 354 | child.setTag(TAG_ANIMATOR_TOP_INSET, null); |
| 355 | child.setTag(TAG_START_TOP_INSET, null); |
| 356 | child.setTag(TAG_END_TOP_INSET, null); |
| 357 | } |
| 358 | }); |
| 359 | startAnimator(animator, listener); |
| 360 | child.setTag(TAG_ANIMATOR_TOP_INSET, animator); |
| 361 | child.setTag(TAG_START_TOP_INSET, child.getClipTopAmount()); |
| 362 | child.setTag(TAG_END_TOP_INSET, newEndValue); |
| 363 | } |
| 364 | |
| 365 | /** |
| 366 | * Get the end value of the height animation running on a view or the actualHeight |
| 367 | * if no animation is running. |
| 368 | */ |
| 369 | public static int getFinalActualHeight(ExpandableView view) { |
| 370 | if (view == null) { |
| 371 | return 0; |
| 372 | } |
| 373 | ValueAnimator heightAnimator = getChildTag(view, TAG_ANIMATOR_HEIGHT); |
| 374 | if (heightAnimator == null) { |
| 375 | return view.getActualHeight(); |
| 376 | } else { |
| 377 | return getChildTag(view, TAG_END_HEIGHT); |
| 378 | } |
| 379 | } |
Selim Cinek | 2627d72 | 2018-01-19 12:16:49 -0800 | [diff] [blame] | 380 | |
| 381 | @Override |
| 382 | public void cancelAnimations(View view) { |
| 383 | super.cancelAnimations(view); |
| 384 | Animator animator = getChildTag(view, TAG_ANIMATOR_HEIGHT); |
| 385 | if (animator != null) { |
| 386 | animator.cancel(); |
| 387 | } |
Selim Cinek | 2627d72 | 2018-01-19 12:16:49 -0800 | [diff] [blame] | 388 | animator = getChildTag(view, TAG_ANIMATOR_TOP_INSET); |
| 389 | if (animator != null) { |
| 390 | animator.cancel(); |
| 391 | } |
| 392 | } |
Selim Cinek | bbcebde | 2016-11-09 18:28:20 -0800 | [diff] [blame] | 393 | } |