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