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