• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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.Flags.physicalNotificationMovement;
20 import static com.android.systemui.statusbar.notification.row.ExpandableView.HEIGHT_PROPERTY;
21 import static com.android.systemui.statusbar.notification.row.ExpandableView.TAG_ANIMATOR_HEIGHT;
22 
23 import android.animation.Animator;
24 import android.animation.AnimatorListenerAdapter;
25 import android.animation.PropertyValuesHolder;
26 import android.animation.ValueAnimator;
27 import android.util.FloatProperty;
28 import android.view.View;
29 
30 import androidx.annotation.NonNull;
31 
32 import com.android.app.animation.Interpolators;
33 import com.android.internal.dynamicanimation.animation.DynamicAnimation;
34 import com.android.systemui.res.R;
35 import com.android.systemui.statusbar.notification.PhysicsProperty;
36 import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator;
37 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
38 import com.android.systemui.statusbar.notification.row.ExpandableView;
39 
40 /**
41  * A state of an expandable view
42  */
43 public class ExpandableViewState extends ViewState {
44 
45     private static final int TAG_ANIMATOR_TOP_INSET = R.id.top_inset_animator_tag;
46     private static final int TAG_ANIMATOR_BOTTOM_INSET = R.id.bottom_inset_animator_tag;
47     private static final int TAG_END_HEIGHT = R.id.height_animator_end_value_tag;
48     private static final int TAG_END_TOP_INSET = R.id.top_inset_animator_end_value_tag;
49     private static final int TAG_END_BOTTOM_INSET = R.id.bottom_inset_animator_end_value_tag;
50     private static final int TAG_START_HEIGHT = R.id.height_animator_start_value_tag;
51     private static final int TAG_START_TOP_INSET = R.id.top_inset_animator_start_value_tag;
52     private static final int TAG_START_BOTTOM_INSET = R.id.bottom_inset_animator_start_value_tag;
53 
54     // These are flags such that we can create masks for filtering.
55 
56     /**
57      * No known location. This is the default and should not be set after an invocation of the
58      * algorithm.
59      */
60     public static final int LOCATION_UNKNOWN = 0x00;
61 
62     /**
63      * The location is the first heads up notification, so on the very top.
64      */
65     public static final int LOCATION_FIRST_HUN = 0x01;
66 
67     /**
68      * The location is hidden / scrolled away on the top.
69      */
70     public static final int LOCATION_HIDDEN_TOP = 0x02;
71 
72     /**
73      * The location is in the main area of the screen and visible.
74      */
75     public static final int LOCATION_MAIN_AREA = 0x04;
76 
77     /**
78      * The location is in the bottom stack and it's peeking
79      */
80     public static final int LOCATION_BOTTOM_STACK_PEEKING = 0x08;
81 
82     /**
83      * The location is in the bottom stack and it's hidden.
84      */
85     public static final int LOCATION_BOTTOM_STACK_HIDDEN = 0x10;
86 
87     /**
88      * The view isn't laid out at all.
89      */
90     public static final int LOCATION_GONE = 0x40;
91 
92     /**
93      * The visible locations of a view.
94      */
95     public static final int VISIBLE_LOCATIONS = ExpandableViewState.LOCATION_FIRST_HUN
96             | ExpandableViewState.LOCATION_MAIN_AREA;
97 
98     public int height;
99     public boolean hideSensitive;
100     public boolean belowSpeedBump;
101     public boolean inShelf;
102 
103     /**
104      * A state indicating whether a headsup is currently fully visible, even when not scrolled.
105      * Only valid if the view is heads upped.
106      */
107     public boolean headsUpIsVisible;
108 
109     /**
110      * How much the child overlaps on top with the child above.
111      */
112     public int clipTopAmount;
113 
114     /**
115      * How much the child overlaps on bottom with the child above. This is used to
116      * show the background properly when the child on top is translating away.
117      */
118     public int clipBottomAmount;
119 
120     /**
121      * The index of the view, only accounting for views not equal to GONE
122      */
123     public int notGoneIndex;
124 
125     /**
126      * The location this view is currently rendered at.
127      *
128      * <p>See <code>LOCATION_</code> flags.</p>
129      */
130     public int location;
131 
132     @Override
copyFrom(ViewState viewState)133     public void copyFrom(ViewState viewState) {
134         super.copyFrom(viewState);
135         if (viewState instanceof ExpandableViewState) {
136             ExpandableViewState svs = (ExpandableViewState) viewState;
137             height = svs.height;
138             hideSensitive = svs.hideSensitive;
139             belowSpeedBump = svs.belowSpeedBump;
140             clipTopAmount = svs.clipTopAmount;
141             notGoneIndex = svs.notGoneIndex;
142             location = svs.location;
143             headsUpIsVisible = svs.headsUpIsVisible;
144         }
145     }
146 
147     /**
148      * Applies a {@link ExpandableViewState} to a {@link ExpandableView}.
149      */
150     @Override
applyToView(View view)151     public void applyToView(View view) {
152         super.applyToView(view);
153         if (view instanceof ExpandableView) {
154             ExpandableView expandableView = (ExpandableView) view;
155 
156             final int height = expandableView.getActualHeight();
157             final int newHeight = this.height;
158 
159             // apply height
160             if (height != newHeight) {
161                 expandableView.setFinalActualHeight(newHeight);
162             }
163 
164             // apply hiding sensitive
165             expandableView.setHideSensitive(
166                     this.hideSensitive, false /* animated */, 0 /* delay */, 0 /* duration */);
167 
168             // apply clipping
169             final float oldClipTopAmount = expandableView.getClipTopAmount();
170             if (oldClipTopAmount != this.clipTopAmount) {
171                 expandableView.setClipTopAmount(this.clipTopAmount);
172             }
173             final float oldClipBottomAmount = expandableView.getClipBottomAmount();
174             if (oldClipBottomAmount != this.clipBottomAmount) {
175                 expandableView.setClipBottomAmount(this.clipBottomAmount);
176             }
177 
178             expandableView.setTransformingInShelf(false);
179             expandableView.setInShelf(inShelf);
180 
181             if (headsUpIsVisible) {
182                 expandableView.markHeadsUpSeen();
183             }
184         }
185     }
186 
187     @Override
animateTo(View child, AnimationProperties properties)188     public void animateTo(View child, AnimationProperties properties) {
189         super.animateTo(child, properties);
190         if (!(child instanceof ExpandableView)) {
191             return;
192         }
193         ExpandableView expandableView = (ExpandableView) child;
194         AnimationFilter animationFilter = properties.getAnimationFilter();
195 
196         // start height animation
197         if (this.height != expandableView.getActualHeight()) {
198             if (mUsePhysicsForMovement) {
199                 boolean animateHeight = properties.getAnimationFilter().animateHeight;
200                 if (animateHeight) {
201                     expandableView.setActualHeightAnimating(true);
202                 }
203                 DynamicAnimation.OnAnimationEndListener endListener = null;
204                 if (!ViewState.isAnimating(expandableView, HEIGHT_PROPERTY)) {
205                     // only Add the end listener if we haven't already
206                     endListener = (animation, canceled, value, velocity) -> {
207                         expandableView.setActualHeightAnimating(false);
208                         if (!canceled && child instanceof ExpandableNotificationRow row) {
209                             row.setGroupExpansionChanging(false /* isExpansionChanging */);
210                         }
211                     };
212                 }
213                 PhysicsPropertyAnimator.setProperty(child, HEIGHT_PROPERTY, this.height, properties,
214                         animateHeight,
215                         endListener);
216             } else {
217                 startHeightAnimationInterpolator(expandableView, properties);
218             }
219         } else {
220             abortAnimation(child, TAG_ANIMATOR_HEIGHT);
221         }
222 
223         // start clip top animation
224         if (this.clipTopAmount != expandableView.getClipTopAmount()) {
225             startClipAnimation(expandableView, properties, /* clipTop */true);
226         } else {
227             abortAnimation(child, TAG_ANIMATOR_TOP_INSET);
228         }
229 
230         // start clip bottom animation
231         if (this.clipBottomAmount != expandableView.getClipBottomAmount()) {
232             startClipAnimation(expandableView, properties, /* clipTop */ false);
233         } else {
234             abortAnimation(child, TAG_ANIMATOR_BOTTOM_INSET);
235         }
236 
237         // start hiding sensitive animation
238         expandableView.setHideSensitive(this.hideSensitive, animationFilter.animateHideSensitive,
239                 properties.delay, properties.duration);
240 
241         if (properties.wasAdded(child) && !hidden) {
242             expandableView.performAddAnimation(properties.delay, properties.duration,
243                     false /* isHeadsUpAppear */);
244         }
245 
246         if (!expandableView.isInShelf() && this.inShelf) {
247             expandableView.setTransformingInShelf(true);
248         }
249         expandableView.setInShelf(this.inShelf);
250 
251         if (headsUpIsVisible) {
252             expandableView.markHeadsUpSeen();
253         }
254     }
255 
startHeightAnimationInterpolator(final ExpandableView child, AnimationProperties properties)256     private void startHeightAnimationInterpolator(final ExpandableView child,
257             AnimationProperties properties) {
258         Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT);
259         Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT);
260         int newEndValue = this.height;
261         if (previousEndValue != null && previousEndValue == newEndValue) {
262             return;
263         }
264         ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT);
265         AnimationFilter filter = properties.getAnimationFilter();
266         if (!filter.animateHeight) {
267             // just a local update was performed
268             if (previousAnimator != null) {
269                 // we need to increase all animation keyframes of the previous animator by the
270                 // relative change to the end value
271                 PropertyValuesHolder[] values = previousAnimator.getValues();
272                 int relativeDiff = newEndValue - previousEndValue;
273                 int newStartValue = previousStartValue + relativeDiff;
274                 values[0].setIntValues(newStartValue, newEndValue);
275                 child.setTag(TAG_START_HEIGHT, newStartValue);
276                 child.setTag(TAG_END_HEIGHT, newEndValue);
277                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
278                 return;
279             } else {
280                 // no new animation needed, let's just apply the value
281                 child.setActualHeight(newEndValue, false);
282                 return;
283             }
284         }
285 
286         ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), newEndValue);
287         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
288             @Override
289             public void onAnimationUpdate(ValueAnimator animation) {
290                 child.setActualHeight((int) animation.getAnimatedValue(),
291                         false /* notifyListeners */);
292             }
293         });
294         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
295         long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
296         animator.setDuration(newDuration);
297         if (properties.delay > 0 && (previousAnimator == null
298                 || previousAnimator.getAnimatedFraction() == 0)) {
299             animator.setStartDelay(properties.delay);
300         }
301         AnimatorListenerAdapter listener = properties.getAnimationFinishListener(
302                 null /* no property for this height */);
303         if (listener != null) {
304             animator.addListener(listener);
305         }
306         // remove the tag when the animation is finished
307         animator.addListener(new AnimatorListenerAdapter() {
308             boolean mWasCancelled;
309 
310             @Override
311             public void onAnimationEnd(Animator animation) {
312                 child.setTag(TAG_ANIMATOR_HEIGHT, null);
313                 child.setTag(TAG_START_HEIGHT, null);
314                 child.setTag(TAG_END_HEIGHT, null);
315                 child.setActualHeightAnimating(false);
316                 if (!mWasCancelled && child instanceof ExpandableNotificationRow) {
317                     ((ExpandableNotificationRow) child).setGroupExpansionChanging(
318                             false /* isExpansionChanging */);
319                 }
320             }
321 
322             @Override
323             public void onAnimationStart(Animator animation) {
324                 mWasCancelled = false;
325             }
326 
327             @Override
328             public void onAnimationCancel(Animator animation) {
329                 mWasCancelled = true;
330             }
331         });
332         startAnimator(animator, listener);
333         child.setTag(TAG_ANIMATOR_HEIGHT, animator);
334         child.setTag(TAG_START_HEIGHT, child.getActualHeight());
335         child.setTag(TAG_END_HEIGHT, newEndValue);
336         child.setActualHeightAnimating(true);
337     }
338 
startClipAnimation(final ExpandableView child, AnimationProperties properties, boolean clipTop)339     private void startClipAnimation(final ExpandableView child, AnimationProperties properties,
340             boolean clipTop) {
341         Integer previousStartValue = getChildTag(child,
342                 clipTop ? TAG_START_TOP_INSET : TAG_START_BOTTOM_INSET);
343         Integer previousEndValue = getChildTag(child,
344                 clipTop ? TAG_END_TOP_INSET : TAG_END_BOTTOM_INSET);
345         int newEndValue = clipTop ? this.clipTopAmount : this.clipBottomAmount;
346         if (previousEndValue != null && previousEndValue == newEndValue) {
347             return;
348         }
349         ValueAnimator previousAnimator = getChildTag(child,
350                 clipTop ? TAG_ANIMATOR_TOP_INSET : TAG_ANIMATOR_BOTTOM_INSET);
351         AnimationFilter filter = properties.getAnimationFilter();
352         if (clipTop && !filter.animateTopInset || !clipTop) {
353             // just a local update was performed
354             if (previousAnimator != null) {
355                 // we need to increase all animation keyframes of the previous animator by the
356                 // relative change to the end value
357                 PropertyValuesHolder[] values = previousAnimator.getValues();
358                 int relativeDiff = newEndValue - previousEndValue;
359                 int newStartValue = previousStartValue + relativeDiff;
360                 values[0].setIntValues(newStartValue, newEndValue);
361                 child.setTag(clipTop ? TAG_START_TOP_INSET : TAG_START_BOTTOM_INSET, newStartValue);
362                 child.setTag(clipTop ? TAG_END_TOP_INSET : TAG_END_BOTTOM_INSET, newEndValue);
363                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
364                 return;
365             } else {
366                 // no new animation needed, let's just apply the value
367                 if (clipTop) {
368                     child.setClipTopAmount(newEndValue);
369                 } else {
370                     child.setClipBottomAmount(newEndValue);
371                 }
372                 return;
373             }
374         }
375 
376         ValueAnimator animator = ValueAnimator.ofInt(
377                 clipTop ? child.getClipTopAmount() : child.getClipBottomAmount(), newEndValue);
378         animator.addUpdateListener(animation -> {
379             if (clipTop) {
380                 child.setClipTopAmount((int) animation.getAnimatedValue());
381             } else {
382                 child.setClipBottomAmount((int) animation.getAnimatedValue());
383             }
384         });
385         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
386         long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
387         animator.setDuration(newDuration);
388         if (properties.delay > 0 && (previousAnimator == null
389                 || previousAnimator.getAnimatedFraction() == 0)) {
390             animator.setStartDelay(properties.delay);
391         }
392         AnimatorListenerAdapter listener = properties.getAnimationFinishListener(
393                 null /* no property for top inset */);
394         if (listener != null) {
395             animator.addListener(listener);
396         }
397         // remove the tag when the animation is finished
398         animator.addListener(new AnimatorListenerAdapter() {
399             @Override
400             public void onAnimationEnd(Animator animation) {
401                 child.setTag(clipTop ? TAG_ANIMATOR_TOP_INSET : TAG_ANIMATOR_BOTTOM_INSET, null);
402                 child.setTag(clipTop ? TAG_START_TOP_INSET : TAG_START_BOTTOM_INSET, null);
403                 child.setTag(clipTop ? TAG_END_TOP_INSET : TAG_END_BOTTOM_INSET, null);
404             }
405         });
406         startAnimator(animator, listener);
407         child.setTag(clipTop ? TAG_ANIMATOR_TOP_INSET : TAG_ANIMATOR_BOTTOM_INSET, animator);
408         child.setTag(clipTop ? TAG_START_TOP_INSET : TAG_START_BOTTOM_INSET,
409                 clipTop ? child.getClipTopAmount() : child.getClipBottomAmount());
410         child.setTag(clipTop ? TAG_END_TOP_INSET : TAG_END_BOTTOM_INSET, newEndValue);
411     }
412 
413     @Override
cancelAnimations(View view)414     public void cancelAnimations(View view) {
415         super.cancelAnimations(view);
416         abortAnimation(view, TAG_ANIMATOR_HEIGHT);
417         abortAnimation(view, TAG_ANIMATOR_TOP_INSET);
418     }
419 }
420