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.R;
27 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
28 import com.android.systemui.statusbar.notification.row.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_BOTTOM_INSET = R.id.bottom_inset_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_BOTTOM_INSET = R.id.bottom_inset_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_BOTTOM_INSET = R.id.bottom_inset_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 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             dimmed = svs.dimmed;
131             hideSensitive = svs.hideSensitive;
132             belowSpeedBump = svs.belowSpeedBump;
133             clipTopAmount = svs.clipTopAmount;
134             notGoneIndex = svs.notGoneIndex;
135             location = svs.location;
136             headsUpIsVisible = svs.headsUpIsVisible;
137         }
138     }
139 
140     /**
141      * Applies a {@link ExpandableViewState} to a {@link ExpandableView}.
142      */
143     @Override
applyToView(View view)144     public void applyToView(View view) {
145         super.applyToView(view);
146         if (view instanceof ExpandableView) {
147             ExpandableView expandableView = (ExpandableView) view;
148 
149             final int height = expandableView.getActualHeight();
150             final int newHeight = this.height;
151 
152             // apply height
153             if (height != newHeight) {
154                 expandableView.setActualHeight(newHeight, false /* notifyListeners */);
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 clipping
168             final float oldClipTopAmount = expandableView.getClipTopAmount();
169             if (oldClipTopAmount != this.clipTopAmount) {
170                 expandableView.setClipTopAmount(this.clipTopAmount);
171             }
172             final float oldClipBottomAmount = expandableView.getClipBottomAmount();
173             if (oldClipBottomAmount != this.clipBottomAmount) {
174                 expandableView.setClipBottomAmount(this.clipBottomAmount);
175             }
176 
177             expandableView.setTransformingInShelf(false);
178             expandableView.setInShelf(inShelf);
179 
180             if (headsUpIsVisible) {
181                 expandableView.setHeadsUpIsVisible();
182             }
183         }
184     }
185 
186     @Override
animateTo(View child, AnimationProperties properties)187     public void animateTo(View child, AnimationProperties properties) {
188         super.animateTo(child, properties);
189         if (!(child instanceof ExpandableView)) {
190             return;
191         }
192         ExpandableView expandableView = (ExpandableView) child;
193         AnimationFilter animationFilter = properties.getAnimationFilter();
194 
195         // start height animation
196         if (this.height != expandableView.getActualHeight()) {
197             startHeightAnimation(expandableView, properties);
198         }  else {
199             abortAnimation(child, TAG_ANIMATOR_HEIGHT);
200         }
201 
202         // start clip top animation
203         if (this.clipTopAmount != expandableView.getClipTopAmount()) {
204             startClipAnimation(expandableView, properties, /* clipTop */true);
205         } else {
206             abortAnimation(child, TAG_ANIMATOR_TOP_INSET);
207         }
208 
209         // start clip bottom animation
210         if (this.clipBottomAmount != expandableView.getClipBottomAmount()) {
211             startClipAnimation(expandableView, properties, /* clipTop */ false);
212         } else {
213             abortAnimation(child, TAG_ANIMATOR_BOTTOM_INSET);
214         }
215 
216         // start dimmed animation
217         expandableView.setDimmed(this.dimmed, animationFilter.animateDimmed);
218 
219         // apply below the speed bump
220         expandableView.setBelowSpeedBump(this.belowSpeedBump);
221 
222         // start hiding sensitive animation
223         expandableView.setHideSensitive(this.hideSensitive, animationFilter.animateHideSensitive,
224                 properties.delay, properties.duration);
225 
226         if (properties.wasAdded(child) && !hidden) {
227             expandableView.performAddAnimation(properties.delay, properties.duration,
228                     false /* isHeadsUpAppear */);
229         }
230 
231         if (!expandableView.isInShelf() && this.inShelf) {
232             expandableView.setTransformingInShelf(true);
233         }
234         expandableView.setInShelf(this.inShelf);
235 
236         if (headsUpIsVisible) {
237             expandableView.setHeadsUpIsVisible();
238         }
239     }
240 
startHeightAnimation(final ExpandableView child, AnimationProperties properties)241     private void startHeightAnimation(final ExpandableView child, AnimationProperties properties) {
242         Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT);
243         Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT);
244         int newEndValue = this.height;
245         if (previousEndValue != null && previousEndValue == newEndValue) {
246             return;
247         }
248         ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT);
249         AnimationFilter filter = properties.getAnimationFilter();
250         if (!filter.animateHeight) {
251             // just a local update was performed
252             if (previousAnimator != null) {
253                 // we need to increase all animation keyframes of the previous animator by the
254                 // relative change to the end value
255                 PropertyValuesHolder[] values = previousAnimator.getValues();
256                 int relativeDiff = newEndValue - previousEndValue;
257                 int newStartValue = previousStartValue + relativeDiff;
258                 values[0].setIntValues(newStartValue, newEndValue);
259                 child.setTag(TAG_START_HEIGHT, newStartValue);
260                 child.setTag(TAG_END_HEIGHT, newEndValue);
261                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
262                 return;
263             } else {
264                 // no new animation needed, let's just apply the value
265                 child.setActualHeight(newEndValue, false);
266                 return;
267             }
268         }
269 
270         ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), newEndValue);
271         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
272             @Override
273             public void onAnimationUpdate(ValueAnimator animation) {
274                 child.setActualHeight((int) animation.getAnimatedValue(),
275                         false /* notifyListeners */);
276             }
277         });
278         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
279         long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
280         animator.setDuration(newDuration);
281         if (properties.delay > 0 && (previousAnimator == null
282                 || previousAnimator.getAnimatedFraction() == 0)) {
283             animator.setStartDelay(properties.delay);
284         }
285         AnimatorListenerAdapter listener = properties.getAnimationFinishListener(
286                 null /* no property for this height */);
287         if (listener != null) {
288             animator.addListener(listener);
289         }
290         // remove the tag when the animation is finished
291         animator.addListener(new AnimatorListenerAdapter() {
292             boolean mWasCancelled;
293 
294             @Override
295             public void onAnimationEnd(Animator animation) {
296                 child.setTag(TAG_ANIMATOR_HEIGHT, null);
297                 child.setTag(TAG_START_HEIGHT, null);
298                 child.setTag(TAG_END_HEIGHT, null);
299                 child.setActualHeightAnimating(false);
300                 if (!mWasCancelled && child instanceof ExpandableNotificationRow) {
301                     ((ExpandableNotificationRow) child).setGroupExpansionChanging(
302                             false /* isExpansionChanging */);
303                 }
304             }
305 
306             @Override
307             public void onAnimationStart(Animator animation) {
308                 mWasCancelled = false;
309             }
310 
311             @Override
312             public void onAnimationCancel(Animator animation) {
313                 mWasCancelled = true;
314             }
315         });
316         startAnimator(animator, listener);
317         child.setTag(TAG_ANIMATOR_HEIGHT, animator);
318         child.setTag(TAG_START_HEIGHT, child.getActualHeight());
319         child.setTag(TAG_END_HEIGHT, newEndValue);
320         child.setActualHeightAnimating(true);
321     }
322 
startClipAnimation(final ExpandableView child, AnimationProperties properties, boolean clipTop)323     private void startClipAnimation(final ExpandableView child, AnimationProperties properties,
324             boolean clipTop) {
325         Integer previousStartValue = getChildTag(child,
326                 clipTop ? TAG_START_TOP_INSET : TAG_START_BOTTOM_INSET);
327         Integer previousEndValue = getChildTag(child,
328                 clipTop ? TAG_END_TOP_INSET : TAG_END_BOTTOM_INSET);
329         int newEndValue = clipTop ? this.clipTopAmount : this.clipBottomAmount;
330         if (previousEndValue != null && previousEndValue == newEndValue) {
331             return;
332         }
333         ValueAnimator previousAnimator = getChildTag(child,
334                 clipTop ? TAG_ANIMATOR_TOP_INSET : TAG_ANIMATOR_BOTTOM_INSET);
335         AnimationFilter filter = properties.getAnimationFilter();
336         if (clipTop && !filter.animateTopInset || !clipTop) {
337             // just a local update was performed
338             if (previousAnimator != null) {
339                 // we need to increase all animation keyframes of the previous animator by the
340                 // relative change to the end value
341                 PropertyValuesHolder[] values = previousAnimator.getValues();
342                 int relativeDiff = newEndValue - previousEndValue;
343                 int newStartValue = previousStartValue + relativeDiff;
344                 values[0].setIntValues(newStartValue, newEndValue);
345                 child.setTag(clipTop ? TAG_START_TOP_INSET : TAG_START_BOTTOM_INSET, newStartValue);
346                 child.setTag(clipTop ? TAG_END_TOP_INSET : TAG_END_BOTTOM_INSET, newEndValue);
347                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
348                 return;
349             } else {
350                 // no new animation needed, let's just apply the value
351                 if (clipTop) {
352                     child.setClipTopAmount(newEndValue);
353                 } else {
354                     child.setClipBottomAmount(newEndValue);
355                 }
356                 return;
357             }
358         }
359 
360         ValueAnimator animator = ValueAnimator.ofInt(
361                 clipTop ? child.getClipTopAmount() : child.getClipBottomAmount(), newEndValue);
362         animator.addUpdateListener(animation -> {
363             if (clipTop) {
364                 child.setClipTopAmount((int) animation.getAnimatedValue());
365             } else {
366                 child.setClipBottomAmount((int) animation.getAnimatedValue());
367             }
368         });
369         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
370         long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator);
371         animator.setDuration(newDuration);
372         if (properties.delay > 0 && (previousAnimator == null
373                 || previousAnimator.getAnimatedFraction() == 0)) {
374             animator.setStartDelay(properties.delay);
375         }
376         AnimatorListenerAdapter listener = properties.getAnimationFinishListener(
377                 null /* no property for top inset */);
378         if (listener != null) {
379             animator.addListener(listener);
380         }
381         // remove the tag when the animation is finished
382         animator.addListener(new AnimatorListenerAdapter() {
383             @Override
384             public void onAnimationEnd(Animator animation) {
385                 child.setTag(clipTop ? TAG_ANIMATOR_TOP_INSET : TAG_ANIMATOR_BOTTOM_INSET, null);
386                 child.setTag(clipTop ? TAG_START_TOP_INSET : TAG_START_BOTTOM_INSET, null);
387                 child.setTag(clipTop ? TAG_END_TOP_INSET : TAG_END_BOTTOM_INSET, null);
388             }
389         });
390         startAnimator(animator, listener);
391         child.setTag(clipTop ? TAG_ANIMATOR_TOP_INSET:TAG_ANIMATOR_BOTTOM_INSET, animator);
392         child.setTag(clipTop ? TAG_START_TOP_INSET: TAG_START_BOTTOM_INSET,
393                 clipTop ? child.getClipTopAmount() : child.getClipBottomAmount());
394         child.setTag(clipTop ? TAG_END_TOP_INSET: TAG_END_BOTTOM_INSET, newEndValue);
395     }
396 
397     /**
398      * Get the end value of the height animation running on a view or the actualHeight
399      * if no animation is running.
400      */
getFinalActualHeight(ExpandableView view)401     public static int getFinalActualHeight(ExpandableView view) {
402         if (view == null) {
403             return 0;
404         }
405         ValueAnimator heightAnimator = getChildTag(view, TAG_ANIMATOR_HEIGHT);
406         if (heightAnimator == null) {
407             return view.getActualHeight();
408         } else {
409             return getChildTag(view, TAG_END_HEIGHT);
410         }
411     }
412 
413     @Override
cancelAnimations(View view)414     public void cancelAnimations(View view) {
415         super.cancelAnimations(view);
416         Animator animator = getChildTag(view, TAG_ANIMATOR_HEIGHT);
417         if (animator != null) {
418             animator.cancel();
419         }
420         animator = getChildTag(view, TAG_ANIMATOR_TOP_INSET);
421         if (animator != null) {
422             animator.cancel();
423         }
424     }
425 }
426