blob: dd6d3833f147d144f1056bbda814a98a89518de0 [file] [log] [blame]
Gus Prevase2d6f042018-10-17 15:25:30 -04001/*
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
17package com.android.systemui.statusbar.notification.stack;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.PropertyValuesHolder;
23import android.graphics.Rect;
24import android.view.View;
25import android.view.animation.Interpolator;
26
27import com.android.systemui.Interpolators;
28import com.android.systemui.statusbar.notification.ShadeViewRefactor;
29import 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 */
35class 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
46 NotificationSection(View owningView) {
47 mOwningView = owningView;
48 }
49
50 public void cancelAnimators() {
51 if (mBottomAnimator != null) {
52 mBottomAnimator.cancel();
53 }
54 if (mTopAnimator != null) {
55 mTopAnimator.cancel();
56 }
57 }
58
59 public Rect getCurrentBounds() {
60 return mCurrentBounds;
61 }
62
63 public Rect getBounds() {
64 return mBounds;
65 }
66
67 public boolean didBoundsChange() {
68 return !mCurrentBounds.equals(mBounds);
69 }
70
71 public boolean areBoundsAnimating() {
72 return mBottomAnimator != null || mTopAnimator != null;
73 }
74
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)
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)
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)
183 private void setBackgroundTop(int top) {
184 mCurrentBounds.top = top;
185 mOwningView.invalidate();
186 }
187
188 @ShadeViewRefactor(ShadeViewRefactor.RefactorComponent.SHADE_VIEW)
189 private void setBackgroundBottom(int bottom) {
190 mCurrentBounds.bottom = bottom;
191 mOwningView.invalidate();
192 }
193
194 public ActivatableNotificationView getFirstVisibleChild() {
195 return mFirstVisibleChild;
196 }
197
198 public ActivatableNotificationView getLastVisibleChild() {
199 return mLastVisibleChild;
200 }
201
202 public void setFirstVisibleChild(ActivatableNotificationView child) {
203 mFirstVisibleChild = child;
204 }
205
206 public void setLastVisibleChild(ActivatableNotificationView child) {
207 mLastVisibleChild = child;
208 }
209
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 */
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 */
227 public boolean isTargetBottom(int bottom) {
228 return (mBottomAnimator == null && mCurrentBounds.bottom == bottom)
229 || (mBottomAnimator != null && mEndAnimationRect.bottom == bottom);
230 }
Selim Cinek3fe7e7e2019-02-15 18:40:53 -0800231
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 */
Selim Cinekae55d832019-02-22 17:43:43 -0800239 public int updateBounds(int minTopPosition, int minBottomPosition,
240 boolean shiftBackgroundWithFirst) {
Selim Cinek3fe7e7e2019-02-15 18:40:53 -0800241 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);
Selim Cinek459aee32019-02-20 11:18:56 -0800257 if (firstView.showingAmbientPulsing()) {
Selim Cinek5040f2e2019-02-14 18:22:42 -0800258 // If we're pulsing, the notification can actually go below!
259 bottom = Math.max(bottom, finalTranslationY
260 + ExpandableViewState.getFinalActualHeight(firstView));
Selim Cinekae55d832019-02-22 17:43:43 -0800261 if (shiftBackgroundWithFirst) {
262 mBounds.left += Math.max(firstView.getTranslation(), 0);
263 }
Selim Cinek5040f2e2019-02-14 18:22:42 -0800264 }
Selim Cinek3fe7e7e2019-02-15 18:40:53 -0800265 }
266 top = Math.max(minTopPosition, top);
267 ActivatableNotificationView lastView = getLastVisibleChild();
268 if (lastView != null) {
269 float finalTranslationY = ViewState.getFinalTranslationY(lastView);
270 int finalHeight = ExpandableViewState.getFinalActualHeight(lastView);
271 // Round Y down to avoid seeing the background during animation
272 int finalBottom = (int) Math.floor(
273 finalTranslationY + finalHeight - lastView.getClipBottomAmount());
274 int newBottom;
275 if (isTargetBottom(finalBottom)) {
276 // we're ending up at the same location as we are now, lets just skip the animation
277 newBottom = finalBottom;
278 } else {
279 newBottom = (int) (lastView.getTranslationY() + lastView.getActualHeight()
280 - lastView.getClipBottomAmount());
281 // The background can never be lower than the end of the last view
282 minBottomPosition = (int) Math.min(
283 lastView.getTranslationY() + lastView.getActualHeight(),
284 minBottomPosition);
285 }
286 bottom = Math.max(bottom, Math.max(newBottom, minBottomPosition));
287 }
288 bottom = Math.max(top, bottom);
289 mBounds.top = top;
290 mBounds.bottom = bottom;
291 return bottom;
292 }
293
Gus Prevase2d6f042018-10-17 15:25:30 -0400294}