1 /*
2  * Copyright (C) 2014 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.row;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.content.Context;
22 import android.util.AttributeSet;
23 import android.view.View;
24 import android.view.animation.Interpolator;
25 
26 import com.android.app.animation.Interpolators;
27 import com.android.internal.annotations.VisibleForTesting;
28 
29 import java.util.function.Consumer;
30 
31 /**
32  * A common base class for all views in the notification stack scroller which don't have a
33  * background.
34  */
35 public abstract class StackScrollerDecorView extends ExpandableView {
36 
37     protected View mContent;
38     protected View mSecondaryView;
39     private boolean mIsVisible = true;
40     private boolean mContentVisible = true;
41     private boolean mIsSecondaryVisible = true;
42     private int mAnimationDuration = 260;
43     private boolean mContentAnimating;
44     private boolean mSecondaryAnimating = false;
45 
StackScrollerDecorView(Context context, AttributeSet attrs)46     public StackScrollerDecorView(Context context, AttributeSet attrs) {
47         super(context, attrs);
48         setClipChildren(false);
49     }
50 
51     @Override
onFinishInflate()52     protected void onFinishInflate() {
53         super.onFinishInflate();
54         mContent = findContentView();
55         mSecondaryView = findSecondaryView();
56         setVisible(false /* visible */, false /* animate */);
57         setSecondaryVisible(false /* visible */, false /* animate */, null /* onAnimationEnd */);
58         setOutlineProvider(null);
59     }
60 
61     @Override
isTransparent()62     public boolean isTransparent() {
63         return true;
64     }
65 
66     /**
67      * Is this view visible. If a view is currently animating to gone, it will
68      * return {@code false}.
69      */
isVisible()70     public boolean isVisible() {
71         return mIsVisible;
72     }
73 
74     /**
75      * Make this view visible. If {@code false} is passed, the view will fade out its content
76      * and set the view Visibility to GONE. If only the content should be changed,
77      * {@link #setContentVisibleAnimated(boolean)} can be used.
78      *
79      * @param visible True if the contents should be visible.
80      * @param animate True if we should fade to new visibility.
81      */
setVisible(boolean visible, boolean animate)82     public void setVisible(boolean visible, boolean animate) {
83         if (mIsVisible != visible) {
84             mIsVisible = visible;
85             if (animate) {
86                 if (visible) {
87                     setVisibility(VISIBLE);
88                     setWillBeGone(false);
89                     notifyHeightChanged(false /* needsAnimation */);
90                 } else {
91                     setWillBeGone(true);
92                 }
93                 setContentVisible(visible, true /* animate */, null /* onAnimationEnded */);
94             } else {
95                 setVisibility(visible ? VISIBLE : GONE);
96                 setContentVisible(visible, false /* animate */, null /* onAnimationEnded */);
97                 setWillBeGone(false);
98                 notifyHeightChanged(false /* needsAnimation */);
99             }
100         }
101     }
102 
isContentVisible()103     public boolean isContentVisible() {
104         return mContentVisible;
105     }
106 
107     /**
108      * Change content visibility to {@code visible}, animated.
109      */
setContentVisibleAnimated(boolean visible)110     public void setContentVisibleAnimated(boolean visible) {
111         setContentVisible(visible, true /* animate */, null /* onAnimationEnded */);
112     }
113 
114     /**
115      * @param visible          True if the contents should be visible.
116      * @param animate          True if we should fade to new visibility.
117      * @param onAnimationEnded Callback to run after visibility updates, takes a boolean as a
118      *                         parameter that represents whether the animation was cancelled.
119      */
setContentVisible(boolean visible, boolean animate, Consumer<Boolean> onAnimationEnded)120     public void setContentVisible(boolean visible, boolean animate,
121             Consumer<Boolean> onAnimationEnded) {
122         if (mContentVisible != visible) {
123             mContentAnimating = animate;
124             mContentVisible = visible;
125             Consumer<Boolean> onAnimationEndedWrapper = (cancelled) -> {
126                 onContentVisibilityAnimationEnd();
127                 if (onAnimationEnded != null) {
128                     onAnimationEnded.accept(cancelled);
129                 }
130             };
131             setViewVisible(mContent, visible, animate, onAnimationEndedWrapper);
132         } else if (onAnimationEnded != null) {
133             // Execute onAnimationEnded immediately if there's no animation to perform.
134             onAnimationEnded.accept(true /* cancelled */);
135         }
136 
137         if (!mContentAnimating) {
138             onContentVisibilityAnimationEnd();
139         }
140     }
141 
onContentVisibilityAnimationEnd()142     private void onContentVisibilityAnimationEnd() {
143         mContentAnimating = false;
144         if (getVisibility() != View.GONE && !mIsVisible) {
145             setVisibility(GONE);
146             setWillBeGone(false);
147             notifyHeightChanged(false /* needsAnimation */);
148         }
149     }
150 
isSecondaryVisible()151     protected boolean isSecondaryVisible() {
152         return mIsSecondaryVisible;
153     }
154 
155     /**
156      * Set the secondary view of this layout to visible.
157      *
158      * @param visible          True if the contents should be visible.
159      * @param animate          True if we should fade to new visibility.
160      * @param onAnimationEnded Callback to run after visibility updates, takes a boolean as a
161      *                         parameter that represents whether the animation was cancelled.
162      */
setSecondaryVisible(boolean visible, boolean animate, Consumer<Boolean> onAnimationEnded)163     protected void setSecondaryVisible(boolean visible, boolean animate,
164             Consumer<Boolean> onAnimationEnded) {
165         if (mIsSecondaryVisible != visible) {
166             mSecondaryAnimating = animate;
167             mIsSecondaryVisible = visible;
168             Consumer<Boolean> onAnimationEndedWrapper = (cancelled) -> {
169                 onContentVisibilityAnimationEnd();
170                 if (onAnimationEnded != null) {
171                     onAnimationEnded.accept(cancelled);
172                 }
173             };
174             setViewVisible(mSecondaryView, visible, animate, onAnimationEndedWrapper);
175         }
176 
177         if (!mSecondaryAnimating) {
178             onSecondaryVisibilityAnimationEnd();
179         }
180     }
181 
onSecondaryVisibilityAnimationEnd()182     private void onSecondaryVisibilityAnimationEnd() {
183         mSecondaryAnimating = false;
184         // If we were on screen, become GONE to avoid touches
185         if (mSecondaryView == null) return;
186         if (getVisibility() != View.GONE
187                 && mSecondaryView.getVisibility() != View.GONE
188                 && !mIsSecondaryVisible) {
189             mSecondaryView.setVisibility(View.GONE);
190         }
191     }
192 
193     /**
194      * Animate a view to a new visibility.
195      *
196      * @param view             Target view, maybe content view or dismiss view.
197      * @param visible          Should it now be visible.
198      * @param animate          Should this be done in an animated way.
199      * @param onAnimationEnded Callback to run after visibility updates, takes a boolean as a
200      *                         parameter that represents whether the animation was cancelled.
201      */
setViewVisible(View view, boolean visible, boolean animate, Consumer<Boolean> onAnimationEnded)202     private void setViewVisible(View view, boolean visible,
203             boolean animate, Consumer<Boolean> onAnimationEnded) {
204         if (view == null) {
205             return;
206         }
207 
208         // Make sure we're visible so animations work
209         if (view.getVisibility() != View.VISIBLE) {
210             view.setVisibility(View.VISIBLE);
211         }
212 
213         // cancel any previous animations
214         view.animate().cancel();
215         float endValue = visible ? 1.0f : 0.0f;
216         if (!animate) {
217             view.setAlpha(endValue);
218             if (onAnimationEnded != null) {
219                 onAnimationEnded.accept(true);
220             }
221             return;
222         }
223 
224         // Animate the view alpha
225         Interpolator interpolator = visible ? Interpolators.ALPHA_IN : Interpolators.ALPHA_OUT;
226         view.animate()
227                 .alpha(endValue)
228                 .setInterpolator(interpolator)
229                 .setDuration(mAnimationDuration)
230                 .setListener(new AnimatorListenerAdapter() {
231                     boolean mCancelled;
232 
233                     @Override
234                     public void onAnimationCancel(Animator animation) {
235                         mCancelled = true;
236                     }
237 
238                     @Override
239                     public void onAnimationEnd(Animator animation) {
240                         onAnimationEnded.accept(mCancelled);
241                     }
242                 });
243     }
244 
245     @VisibleForTesting
setAnimationDuration(int animationDuration)246     public void setAnimationDuration(int animationDuration) {
247         mAnimationDuration = animationDuration;
248     }
249 
250     @Override
performRemoveAnimation(long duration, long delay, float translationDirection, boolean isHeadsUpAnimation, Runnable onStartedRunnable, Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener, ClipSide clipSide)251     public long performRemoveAnimation(long duration, long delay,
252             float translationDirection, boolean isHeadsUpAnimation,
253             Runnable onStartedRunnable,
254             Runnable onFinishedRunnable,
255             AnimatorListenerAdapter animationListener, ClipSide clipSide) {
256         // TODO: Use duration
257         if (onStartedRunnable != null) {
258             onStartedRunnable.run();
259         }
260         setContentVisible(false, true /* animate */, (cancelled) -> onFinishedRunnable.run());
261         return 0;
262     }
263 
264     @Override
performAddAnimation(long delay, long duration, boolean isHeadsUpAppear)265     public void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear) {
266         // TODO: use delay and duration
267         setContentVisibleAnimated(true);
268     }
269 
270     @Override
performAddAnimation(long delay, long duration, boolean isHeadsUpAppear, Runnable endRunnable)271     public void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear,
272             Runnable endRunnable) {
273         // TODO: use delay and duration
274         setContentVisibleAnimated(true);
275     }
276 
277     @Override
needsClippingToShelf()278     public boolean needsClippingToShelf() {
279         return false;
280     }
281 
282     @Override
hasOverlappingRendering()283     public boolean hasOverlappingRendering() {
284         return false;
285     }
286 
findContentView()287     protected abstract View findContentView();
288 
289     /**
290      * Returns a view that might not always appear while the main content view is still visible.
291      */
findSecondaryView()292     protected abstract View findSecondaryView();
293 }
294