1 /* 2 * Copyright (C) 2018 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.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_MEDIA_CONTROLS; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.animation.ObjectAnimator; 24 import android.animation.PropertyValuesHolder; 25 import android.graphics.Rect; 26 import android.view.View; 27 import android.view.animation.Interpolator; 28 29 import com.android.app.animation.Interpolators; 30 import com.android.systemui.statusbar.notification.row.ExpandableView; 31 32 /** 33 * Represents the bounds of a section of the notification shade and handles animation when the 34 * bounds change. 35 */ 36 public class NotificationSection { 37 private @PriorityBucket final int mBucket; 38 private final View mOwningView; 39 private final Rect mBounds = new Rect(); 40 private final Rect mCurrentBounds = new Rect(-1, -1, -1, -1); 41 private final Rect mStartAnimationRect = new Rect(); 42 private final Rect mEndAnimationRect = new Rect(); 43 private ObjectAnimator mTopAnimator = null; 44 private ObjectAnimator mBottomAnimator = null; 45 private ExpandableView mFirstVisibleChild; 46 private ExpandableView mLastVisibleChild; 47 NotificationSection(View owningView, @PriorityBucket int bucket)48 NotificationSection(View owningView, @PriorityBucket int bucket) { 49 mOwningView = owningView; 50 mBucket = bucket; 51 } 52 cancelAnimators()53 public void cancelAnimators() { 54 if (mBottomAnimator != null) { 55 mBottomAnimator.cancel(); 56 } 57 if (mTopAnimator != null) { 58 mTopAnimator.cancel(); 59 } 60 } 61 getCurrentBounds()62 public Rect getCurrentBounds() { 63 return mCurrentBounds; 64 } 65 getBounds()66 public Rect getBounds() { 67 return mBounds; 68 } 69 didBoundsChange()70 public boolean didBoundsChange() { 71 return !mCurrentBounds.equals(mBounds); 72 } 73 areBoundsAnimating()74 public boolean areBoundsAnimating() { 75 return mBottomAnimator != null || mTopAnimator != null; 76 } 77 78 @PriorityBucket getBucket()79 public int getBucket() { 80 return mBucket; 81 } 82 startBackgroundAnimation(boolean animateTop, boolean animateBottom)83 public void startBackgroundAnimation(boolean animateTop, boolean animateBottom) { 84 // Left and right bounds are always applied immediately. 85 mCurrentBounds.left = mBounds.left; 86 mCurrentBounds.right = mBounds.right; 87 startBottomAnimation(animateBottom); 88 startTopAnimation(animateTop); 89 } 90 91 startTopAnimation(boolean animate)92 private void startTopAnimation(boolean animate) { 93 int previousEndValue = mEndAnimationRect.top; 94 int newEndValue = mBounds.top; 95 ObjectAnimator previousAnimator = mTopAnimator; 96 if (previousAnimator != null && previousEndValue == newEndValue) { 97 return; 98 } 99 if (!animate) { 100 // just a local update was performed 101 if (previousAnimator != null) { 102 // we need to increase all animation keyframes of the previous animator by the 103 // relative change to the end value 104 int previousStartValue = mStartAnimationRect.top; 105 PropertyValuesHolder[] values = previousAnimator.getValues(); 106 values[0].setIntValues(previousStartValue, newEndValue); 107 mStartAnimationRect.top = previousStartValue; 108 mEndAnimationRect.top = newEndValue; 109 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); 110 return; 111 } else { 112 // no new animation needed, let's just apply the value 113 setBackgroundTop(newEndValue); 114 return; 115 } 116 } 117 if (previousAnimator != null) { 118 previousAnimator.cancel(); 119 } 120 ObjectAnimator animator = ObjectAnimator.ofInt(this, "backgroundTop", 121 mCurrentBounds.top, newEndValue); 122 Interpolator interpolator = Interpolators.FAST_OUT_SLOW_IN; 123 animator.setInterpolator(interpolator); 124 animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 125 // remove the tag when the animation is finished 126 animator.addListener(new AnimatorListenerAdapter() { 127 @Override 128 public void onAnimationEnd(Animator animation) { 129 mStartAnimationRect.top = -1; 130 mEndAnimationRect.top = -1; 131 mTopAnimator = null; 132 } 133 }); 134 animator.start(); 135 mStartAnimationRect.top = mCurrentBounds.top; 136 mEndAnimationRect.top = newEndValue; 137 mTopAnimator = animator; 138 } 139 startBottomAnimation(boolean animate)140 private void startBottomAnimation(boolean animate) { 141 int previousStartValue = mStartAnimationRect.bottom; 142 int previousEndValue = mEndAnimationRect.bottom; 143 int newEndValue = mBounds.bottom; 144 ObjectAnimator previousAnimator = mBottomAnimator; 145 if (previousAnimator != null && previousEndValue == newEndValue) { 146 return; 147 } 148 if (!animate) { 149 // just a local update was performed 150 if (previousAnimator != null) { 151 // we need to increase all animation keyframes of the previous animator by the 152 // relative change to the end value 153 PropertyValuesHolder[] values = previousAnimator.getValues(); 154 values[0].setIntValues(previousStartValue, newEndValue); 155 mStartAnimationRect.bottom = previousStartValue; 156 mEndAnimationRect.bottom = newEndValue; 157 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime()); 158 return; 159 } else { 160 // no new animation needed, let's just apply the value 161 setBackgroundBottom(newEndValue); 162 return; 163 } 164 } 165 if (previousAnimator != null) { 166 previousAnimator.cancel(); 167 } 168 ObjectAnimator animator = ObjectAnimator.ofInt(this, "backgroundBottom", 169 mCurrentBounds.bottom, newEndValue); 170 Interpolator interpolator = Interpolators.FAST_OUT_SLOW_IN; 171 animator.setInterpolator(interpolator); 172 animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 173 // remove the tag when the animation is finished 174 animator.addListener(new AnimatorListenerAdapter() { 175 @Override 176 public void onAnimationEnd(Animator animation) { 177 mStartAnimationRect.bottom = -1; 178 mEndAnimationRect.bottom = -1; 179 mBottomAnimator = null; 180 } 181 }); 182 animator.start(); 183 mStartAnimationRect.bottom = mCurrentBounds.bottom; 184 mEndAnimationRect.bottom = newEndValue; 185 mBottomAnimator = animator; 186 } 187 setBackgroundTop(int top)188 private void setBackgroundTop(int top) { 189 mCurrentBounds.top = top; 190 mOwningView.invalidate(); 191 } 192 setBackgroundBottom(int bottom)193 private void setBackgroundBottom(int bottom) { 194 mCurrentBounds.bottom = bottom; 195 mOwningView.invalidate(); 196 } 197 getFirstVisibleChild()198 public ExpandableView getFirstVisibleChild() { 199 return mFirstVisibleChild; 200 } 201 getLastVisibleChild()202 public ExpandableView getLastVisibleChild() { 203 return mLastVisibleChild; 204 } 205 setFirstVisibleChild(ExpandableView child)206 public boolean setFirstVisibleChild(ExpandableView child) { 207 boolean changed = mFirstVisibleChild != child; 208 mFirstVisibleChild = child; 209 return changed; 210 } 211 setLastVisibleChild(ExpandableView child)212 public boolean setLastVisibleChild(ExpandableView child) { 213 boolean changed = mLastVisibleChild != child; 214 mLastVisibleChild = child; 215 return changed; 216 } 217 resetCurrentBounds()218 public void resetCurrentBounds() { 219 mCurrentBounds.set(mBounds); 220 } 221 222 /** 223 * Returns true if {@code top} is equal to the top of this section (if not currently animating) 224 * or where the top of this section will be when animation completes. 225 */ isTargetTop(int top)226 public boolean isTargetTop(int top) { 227 return (mTopAnimator == null && mCurrentBounds.top == top) 228 || (mTopAnimator != null && mEndAnimationRect.top == top); 229 } 230 231 /** 232 * Returns true if {@code bottom} is equal to the bottom of this section (if not currently 233 * animating) or where the bottom of this section will be when animation completes. 234 */ isTargetBottom(int bottom)235 public boolean isTargetBottom(int bottom) { 236 return (mBottomAnimator == null && mCurrentBounds.bottom == bottom) 237 || (mBottomAnimator != null && mEndAnimationRect.bottom == bottom); 238 } 239 240 /** 241 * Update the bounds of this section based on it's views 242 * 243 * @param minTopPosition the minimum position that the top needs to have 244 * @param minBottomPosition the minimum position that the bottom needs to have 245 * @return the position of the new bottom 246 */ updateBounds(int minTopPosition, int minBottomPosition, boolean shiftBackgroundWithFirst)247 public int updateBounds(int minTopPosition, int minBottomPosition, 248 boolean shiftBackgroundWithFirst) { 249 int top = minTopPosition; 250 int bottom = minTopPosition; 251 ExpandableView firstView = getFirstVisibleChild(); 252 if (firstView != null) { 253 // Round Y up to avoid seeing the background during animation 254 int finalTranslationY = (int) Math.ceil(ViewState.getFinalTranslationY(firstView)); 255 // TODO: look into the already animating part 256 int newTop; 257 if (isTargetTop(finalTranslationY)) { 258 // we're ending up at the same location as we are now, let's just skip the 259 // animation 260 newTop = finalTranslationY; 261 } else { 262 newTop = (int) Math.ceil(firstView.getTranslationY()); 263 } 264 top = Math.max(newTop, top); 265 if (firstView.showingPulsing()) { 266 // If we're pulsing, the notification can actually go below! 267 bottom = Math.max(bottom, finalTranslationY 268 + ExpandableViewState.getFinalActualHeight(firstView)); 269 if (shiftBackgroundWithFirst) { 270 mBounds.left += Math.max(firstView.getTranslation(), 0); 271 mBounds.right += Math.min(firstView.getTranslation(), 0); 272 } 273 } 274 } 275 ExpandableView lastView = getLastVisibleChild(); 276 if (lastView != null) { 277 float finalTranslationY = ViewState.getFinalTranslationY(lastView); 278 int finalHeight = ExpandableViewState.getFinalActualHeight(lastView); 279 // Round Y down to avoid seeing the background during animation 280 int finalBottom = (int) Math.floor( 281 finalTranslationY + finalHeight - lastView.getClipBottomAmount()); 282 int newBottom; 283 if (isTargetBottom(finalBottom)) { 284 // we're ending up at the same location as we are now, lets just skip the animation 285 newBottom = finalBottom; 286 } else { 287 newBottom = (int) (lastView.getTranslationY() + lastView.getActualHeight() 288 - lastView.getClipBottomAmount()); 289 // The background can never be lower than the end of the last view 290 minBottomPosition = (int) Math.min( 291 lastView.getTranslationY() + lastView.getActualHeight(), 292 minBottomPosition); 293 } 294 bottom = Math.max(bottom, Math.max(newBottom, minBottomPosition)); 295 } 296 bottom = Math.max(top, bottom); 297 mBounds.top = top; 298 mBounds.bottom = bottom; 299 return bottom; 300 } 301 needsBackground()302 public boolean needsBackground() { 303 return mFirstVisibleChild != null && mBucket != BUCKET_MEDIA_CONTROLS; 304 } 305 } 306