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