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