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