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 hideSensitive; 89 public boolean belowSpeedBump; 90 public boolean inShelf; 91 92 /** 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 /** 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 116 @Override copyFrom(ViewState viewState)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; 123 hideSensitive = svs.hideSensitive; 124 belowSpeedBump = svs.belowSpeedBump; 125 clipTopAmount = svs.clipTopAmount; 126 notGoneIndex = svs.notGoneIndex; 127 location = svs.location; 128 headsUpIsVisible = svs.headsUpIsVisible; 129 } 130 } 131 132 /** 133 * Applies a {@link ExpandableViewState} to a {@link ExpandableView}. 134 */ 135 @Override applyToView(View view)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 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 156 // apply below shelf speed bump 157 expandableView.setBelowSpeedBump(this.belowSpeedBump); 158 159 // apply clipping 160 float oldClipTopAmount = expandableView.getClipTopAmount(); 161 if (oldClipTopAmount != this.clipTopAmount) { 162 expandableView.setClipTopAmount(this.clipTopAmount); 163 } 164 165 expandableView.setTransformingInShelf(false); 166 expandableView.setInShelf(inShelf); 167 168 if (headsUpIsVisible) { 169 expandableView.setHeadsUpIsVisible(); 170 } 171 } 172 } 173 174 @Override animateTo(View child, AnimationProperties properties)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 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 200 // apply below the speed bump 201 expandableView.setBelowSpeedBump(this.belowSpeedBump); 202 203 // start hiding sensitive animation 204 expandableView.setHideSensitive(this.hideSensitive, animationFilter.animateHideSensitive, 205 properties.delay, properties.duration); 206 207 if (properties.wasAdded(child) && !hidden) { 208 expandableView.performAddAnimation(properties.delay, properties.duration, 209 false /* isHeadsUpAppear */); 210 } 211 212 if (!expandableView.isInShelf() && this.inShelf) { 213 expandableView.setTransformingInShelf(true); 214 } 215 expandableView.setInShelf(this.inShelf); 216 217 if (headsUpIsVisible) { 218 expandableView.setHeadsUpIsVisible(); 219 } 220 } 221 startHeightAnimation(final ExpandableView child, AnimationProperties properties)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 startInsetAnimation(final ExpandableView child, AnimationProperties properties)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 */ getFinalActualHeight(ExpandableView view)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 } 380 381 @Override cancelAnimations(View view)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 } 388 animator = getChildTag(view, TAG_ANIMATOR_TOP_INSET); 389 if (animator != null) { 390 animator.cancel(); 391 } 392 } 393 } 394