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