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