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;
18 
19 import android.content.Context;
20 import android.graphics.Outline;
21 import android.graphics.Paint;
22 import android.graphics.PorterDuff;
23 import android.graphics.PorterDuffXfermode;
24 import android.graphics.Rect;
25 import android.util.AttributeSet;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.view.ViewOutlineProvider;
29 import android.view.ViewTreeObserver;
30 import android.view.animation.Interpolator;
31 import android.view.animation.LinearInterpolator;
32 import android.widget.FrameLayout;
33 
34 import com.android.systemui.R;
35 
36 /**
37  * A frame layout containing the actual payload of the notification, including the contracted,
38  * expanded and heads up layout. This class is responsible for clipping the content and and
39  * switching between the expanded, contracted and the heads up view depending on its clipped size.
40  */
41 public class NotificationContentView extends FrameLayout {
42 
43     private static final long ANIMATION_DURATION_LENGTH = 170;
44     private static final int VISIBLE_TYPE_CONTRACTED = 0;
45     private static final int VISIBLE_TYPE_EXPANDED = 1;
46     private static final int VISIBLE_TYPE_HEADSUP = 2;
47 
48     private final Rect mClipBounds = new Rect();
49     private final int mSmallHeight;
50     private final int mHeadsUpHeight;
51     private final int mRoundRectRadius;
52     private final Interpolator mLinearInterpolator = new LinearInterpolator();
53     private final boolean mRoundRectClippingEnabled;
54 
55     private View mContractedChild;
56     private View mExpandedChild;
57     private View mHeadsUpChild;
58 
59     private NotificationViewWrapper mContractedWrapper;
60     private NotificationViewWrapper mExpandedWrapper;
61     private NotificationViewWrapper mHeadsUpWrapper;
62     private int mClipTopAmount;
63     private int mContentHeight;
64     private int mUnrestrictedContentHeight;
65     private int mVisibleType = VISIBLE_TYPE_CONTRACTED;
66     private boolean mDark;
67     private final Paint mFadePaint = new Paint();
68     private boolean mAnimate;
69     private boolean mIsHeadsUp;
70     private boolean mShowingLegacyBackground;
71 
72     private final ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener
73             = new ViewTreeObserver.OnPreDrawListener() {
74         @Override
75         public boolean onPreDraw() {
76             mAnimate = true;
77             getViewTreeObserver().removeOnPreDrawListener(this);
78             return true;
79         }
80     };
81 
82     private final ViewOutlineProvider mOutlineProvider = new ViewOutlineProvider() {
83         @Override
84         public void getOutline(View view, Outline outline) {
85             outline.setRoundRect(0, 0, view.getWidth(), mUnrestrictedContentHeight,
86                     mRoundRectRadius);
87         }
88     };
89 
NotificationContentView(Context context, AttributeSet attrs)90     public NotificationContentView(Context context, AttributeSet attrs) {
91         super(context, attrs);
92         mFadePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
93         mSmallHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height);
94         mHeadsUpHeight = getResources().getDimensionPixelSize(R.dimen.notification_mid_height);
95         mRoundRectRadius = getResources().getDimensionPixelSize(
96                 R.dimen.notification_material_rounded_rect_radius);
97         mRoundRectClippingEnabled = getResources().getBoolean(
98                 R.bool.config_notifications_round_rect_clipping);
99         reset(true);
100         setOutlineProvider(mOutlineProvider);
101     }
102 
103     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)104     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
105         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
106         boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
107         boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
108         int maxSize = Integer.MAX_VALUE;
109         if (hasFixedHeight || isHeightLimited) {
110             maxSize = MeasureSpec.getSize(heightMeasureSpec);
111         }
112         int maxChildHeight = 0;
113         if (mContractedChild != null) {
114             int size = Math.min(maxSize, mSmallHeight);
115             mContractedChild.measure(widthMeasureSpec,
116                     MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY));
117             maxChildHeight = Math.max(maxChildHeight, mContractedChild.getMeasuredHeight());
118         }
119         if (mExpandedChild != null) {
120             int size = maxSize;
121             ViewGroup.LayoutParams layoutParams = mExpandedChild.getLayoutParams();
122             if (layoutParams.height >= 0) {
123                 // An actual height is set
124                 size = Math.min(maxSize, layoutParams.height);
125             }
126             int spec = size == Integer.MAX_VALUE
127                     ? MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
128                     : MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
129             mExpandedChild.measure(widthMeasureSpec, spec);
130             maxChildHeight = Math.max(maxChildHeight, mExpandedChild.getMeasuredHeight());
131         }
132         if (mHeadsUpChild != null) {
133             int size = Math.min(maxSize, mHeadsUpHeight);
134             ViewGroup.LayoutParams layoutParams = mHeadsUpChild.getLayoutParams();
135             if (layoutParams.height >= 0) {
136                 // An actual height is set
137                 size = Math.min(maxSize, layoutParams.height);
138             }
139             mHeadsUpChild.measure(widthMeasureSpec,
140                     MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST));
141             maxChildHeight = Math.max(maxChildHeight, mHeadsUpChild.getMeasuredHeight());
142         }
143         int ownHeight = Math.min(maxChildHeight, maxSize);
144         int width = MeasureSpec.getSize(widthMeasureSpec);
145         setMeasuredDimension(width, ownHeight);
146     }
147 
148     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)149     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
150         super.onLayout(changed, left, top, right, bottom);
151         updateClipping();
152         invalidateOutline();
153     }
154 
155     @Override
onAttachedToWindow()156     protected void onAttachedToWindow() {
157         super.onAttachedToWindow();
158         updateVisibility();
159     }
160 
reset(boolean resetActualHeight)161     public void reset(boolean resetActualHeight) {
162         if (mContractedChild != null) {
163             mContractedChild.animate().cancel();
164         }
165         if (mExpandedChild != null) {
166             mExpandedChild.animate().cancel();
167         }
168         if (mHeadsUpChild != null) {
169             mHeadsUpChild.animate().cancel();
170         }
171         removeAllViews();
172         mContractedChild = null;
173         mExpandedChild = null;
174         mHeadsUpChild = null;
175         mVisibleType = VISIBLE_TYPE_CONTRACTED;
176         if (resetActualHeight) {
177             mContentHeight = mSmallHeight;
178         }
179     }
180 
getContractedChild()181     public View getContractedChild() {
182         return mContractedChild;
183     }
184 
getExpandedChild()185     public View getExpandedChild() {
186         return mExpandedChild;
187     }
188 
getHeadsUpChild()189     public View getHeadsUpChild() {
190         return mHeadsUpChild;
191     }
192 
setContractedChild(View child)193     public void setContractedChild(View child) {
194         if (mContractedChild != null) {
195             mContractedChild.animate().cancel();
196             removeView(mContractedChild);
197         }
198         addView(child);
199         mContractedChild = child;
200         mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child);
201         selectLayout(false /* animate */, true /* force */);
202         mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */);
203         updateRoundRectClipping();
204     }
205 
setExpandedChild(View child)206     public void setExpandedChild(View child) {
207         if (mExpandedChild != null) {
208             mExpandedChild.animate().cancel();
209             removeView(mExpandedChild);
210         }
211         addView(child);
212         mExpandedChild = child;
213         mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child);
214         selectLayout(false /* animate */, true /* force */);
215         updateRoundRectClipping();
216     }
217 
setHeadsUpChild(View child)218     public void setHeadsUpChild(View child) {
219         if (mHeadsUpChild != null) {
220             mHeadsUpChild.animate().cancel();
221             removeView(mHeadsUpChild);
222         }
223         addView(child);
224         mHeadsUpChild = child;
225         mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child);
226         selectLayout(false /* animate */, true /* force */);
227         updateRoundRectClipping();
228     }
229 
230     @Override
onVisibilityChanged(View changedView, int visibility)231     protected void onVisibilityChanged(View changedView, int visibility) {
232         super.onVisibilityChanged(changedView, visibility);
233         updateVisibility();
234     }
235 
updateVisibility()236     private void updateVisibility() {
237         setVisible(isShown());
238     }
239 
setVisible(final boolean isVisible)240     private void setVisible(final boolean isVisible) {
241         if (isVisible) {
242 
243             // We only animate if we are drawn at least once, otherwise the view might animate when
244             // it's shown the first time
245             getViewTreeObserver().addOnPreDrawListener(mEnableAnimationPredrawListener);
246         } else {
247             getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener);
248             mAnimate = false;
249         }
250     }
251 
setContentHeight(int contentHeight)252     public void setContentHeight(int contentHeight) {
253         mContentHeight = Math.max(Math.min(contentHeight, getHeight()), getMinHeight());;
254         mUnrestrictedContentHeight = Math.max(contentHeight, getMinHeight());
255         selectLayout(mAnimate /* animate */, false /* force */);
256         updateClipping();
257         invalidateOutline();
258     }
259 
getContentHeight()260     public int getContentHeight() {
261         return mContentHeight;
262     }
263 
getMaxHeight()264     public int getMaxHeight() {
265         if (mIsHeadsUp && mHeadsUpChild != null) {
266             return mHeadsUpChild.getHeight();
267         } else if (mExpandedChild != null) {
268             return mExpandedChild.getHeight();
269         }
270         return mSmallHeight;
271     }
272 
getMinHeight()273     public int getMinHeight() {
274         return mSmallHeight;
275     }
276 
setClipTopAmount(int clipTopAmount)277     public void setClipTopAmount(int clipTopAmount) {
278         mClipTopAmount = clipTopAmount;
279         updateClipping();
280     }
281 
updateRoundRectClipping()282     private void updateRoundRectClipping() {
283         boolean enabled = needsRoundRectClipping();
284         setClipToOutline(enabled);
285     }
286 
needsRoundRectClipping()287     private boolean needsRoundRectClipping() {
288         if (!mRoundRectClippingEnabled) {
289             return false;
290         }
291         boolean needsForContracted = mContractedChild != null
292                 && mContractedChild.getVisibility() == View.VISIBLE
293                 && mContractedWrapper.needsRoundRectClipping();
294         boolean needsForExpanded = mExpandedChild != null
295                 && mExpandedChild.getVisibility() == View.VISIBLE
296                 && mExpandedWrapper.needsRoundRectClipping();
297         boolean needsForHeadsUp = mExpandedChild != null
298                 && mExpandedChild.getVisibility() == View.VISIBLE
299                 && mExpandedWrapper.needsRoundRectClipping();
300         return needsForContracted || needsForExpanded || needsForHeadsUp;
301     }
302 
updateClipping()303     private void updateClipping() {
304         mClipBounds.set(0, mClipTopAmount, getWidth(), mContentHeight);
305         setClipBounds(mClipBounds);
306     }
307 
selectLayout(boolean animate, boolean force)308     private void selectLayout(boolean animate, boolean force) {
309         if (mContractedChild == null) {
310             return;
311         }
312         int visibleType = calculateVisibleType();
313         if (visibleType != mVisibleType || force) {
314             if (animate && ((visibleType == VISIBLE_TYPE_EXPANDED && mExpandedChild != null)
315                     || (visibleType == VISIBLE_TYPE_HEADSUP && mHeadsUpChild != null)
316                     || visibleType == VISIBLE_TYPE_CONTRACTED)) {
317                 runSwitchAnimation(visibleType);
318             } else {
319                 updateViewVisibilities(visibleType);
320             }
321             mVisibleType = visibleType;
322         }
323     }
324 
updateViewVisibilities(int visibleType)325     private void updateViewVisibilities(int visibleType) {
326         boolean contractedVisible = visibleType == VISIBLE_TYPE_CONTRACTED;
327         mContractedChild.setVisibility(contractedVisible ? View.VISIBLE : View.INVISIBLE);
328         mContractedChild.setAlpha(contractedVisible ? 1f : 0f);
329         mContractedChild.setLayerType(LAYER_TYPE_NONE, null);
330         if (mExpandedChild != null) {
331             boolean expandedVisible = visibleType == VISIBLE_TYPE_EXPANDED;
332             mExpandedChild.setVisibility(expandedVisible ? View.VISIBLE : View.INVISIBLE);
333             mExpandedChild.setAlpha(expandedVisible ? 1f : 0f);
334             mExpandedChild.setLayerType(LAYER_TYPE_NONE, null);
335         }
336         if (mHeadsUpChild != null) {
337             boolean headsUpVisible = visibleType == VISIBLE_TYPE_HEADSUP;
338             mHeadsUpChild.setVisibility(headsUpVisible ? View.VISIBLE : View.INVISIBLE);
339             mHeadsUpChild.setAlpha(headsUpVisible ? 1f : 0f);
340             mHeadsUpChild.setLayerType(LAYER_TYPE_NONE, null);
341         }
342         setLayerType(LAYER_TYPE_NONE, null);
343         updateRoundRectClipping();
344     }
345 
runSwitchAnimation(int visibleType)346     private void runSwitchAnimation(int visibleType) {
347         View shownView = getViewForVisibleType(visibleType);
348         View hiddenView = getViewForVisibleType(mVisibleType);
349         shownView.setVisibility(View.VISIBLE);
350         hiddenView.setVisibility(View.VISIBLE);
351         shownView.setLayerType(LAYER_TYPE_HARDWARE, mFadePaint);
352         hiddenView.setLayerType(LAYER_TYPE_HARDWARE, mFadePaint);
353         setLayerType(LAYER_TYPE_HARDWARE, null);
354         hiddenView.animate()
355                 .alpha(0f)
356                 .setDuration(ANIMATION_DURATION_LENGTH)
357                 .setInterpolator(mLinearInterpolator)
358                 .withEndAction(null); // In case we have multiple changes in one frame.
359         shownView.animate()
360                 .alpha(1f)
361                 .setDuration(ANIMATION_DURATION_LENGTH)
362                 .setInterpolator(mLinearInterpolator)
363                 .withEndAction(new Runnable() {
364                     @Override
365                     public void run() {
366                         updateViewVisibilities(mVisibleType);
367                     }
368                 });
369         updateRoundRectClipping();
370     }
371 
372     /**
373      * @param visibleType one of the static enum types in this view
374      * @return the corresponding view according to the given visible type
375      */
getViewForVisibleType(int visibleType)376     private View getViewForVisibleType(int visibleType) {
377         switch (visibleType) {
378             case VISIBLE_TYPE_EXPANDED:
379                 return mExpandedChild;
380             case VISIBLE_TYPE_HEADSUP:
381                 return mHeadsUpChild;
382             default:
383                 return mContractedChild;
384         }
385     }
386 
387     /**
388      * @return one of the static enum types in this view, calculated form the current state
389      */
calculateVisibleType()390     private int calculateVisibleType() {
391         boolean noExpandedChild = mExpandedChild == null;
392         if (mIsHeadsUp && mHeadsUpChild != null) {
393             if (mContentHeight <= mHeadsUpChild.getHeight() || noExpandedChild) {
394                 return VISIBLE_TYPE_HEADSUP;
395             } else {
396                 return VISIBLE_TYPE_EXPANDED;
397             }
398         } else {
399             if (mContentHeight <= mSmallHeight || noExpandedChild) {
400                 return VISIBLE_TYPE_CONTRACTED;
401             } else {
402                 return VISIBLE_TYPE_EXPANDED;
403             }
404         }
405     }
406 
notifyContentUpdated()407     public void notifyContentUpdated() {
408         selectLayout(false /* animate */, true /* force */);
409         if (mContractedChild != null) {
410             mContractedWrapper.notifyContentUpdated();
411             mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */);
412         }
413         if (mExpandedChild != null) {
414             mExpandedWrapper.notifyContentUpdated();
415         }
416         updateRoundRectClipping();
417     }
418 
isContentExpandable()419     public boolean isContentExpandable() {
420         return mExpandedChild != null;
421     }
422 
setDark(boolean dark, boolean fade, long delay)423     public void setDark(boolean dark, boolean fade, long delay) {
424         if (mDark == dark || mContractedChild == null) return;
425         mDark = dark;
426         mContractedWrapper.setDark(dark && !mShowingLegacyBackground, fade, delay);
427     }
428 
setHeadsUp(boolean headsUp)429     public void setHeadsUp(boolean headsUp) {
430         mIsHeadsUp = headsUp;
431         selectLayout(false /* animate */, true /* force */);
432     }
433 
434     @Override
hasOverlappingRendering()435     public boolean hasOverlappingRendering() {
436 
437         // This is not really true, but good enough when fading from the contracted to the expanded
438         // layout, and saves us some layers.
439         return false;
440     }
441 
setShowingLegacyBackground(boolean showing)442     public void setShowingLegacyBackground(boolean showing) {
443         mShowingLegacyBackground = showing;
444     }
445 }
446