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