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
17package com.android.systemui.statusbar;
18
19import android.content.Context;
20import android.graphics.Outline;
21import android.graphics.Paint;
22import android.graphics.PorterDuff;
23import android.graphics.PorterDuffXfermode;
24import android.graphics.Rect;
25import android.util.AttributeSet;
26import android.view.View;
27import android.view.ViewGroup;
28import android.view.ViewOutlineProvider;
29import android.view.ViewTreeObserver;
30import android.view.animation.Interpolator;
31import android.view.animation.LinearInterpolator;
32import android.widget.FrameLayout;
33
34import 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 */
41public 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
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
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
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
156    protected void onAttachedToWindow() {
157        super.onAttachedToWindow();
158        updateVisibility();
159    }
160
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
181    public View getContractedChild() {
182        return mContractedChild;
183    }
184
185    public View getExpandedChild() {
186        return mExpandedChild;
187    }
188
189    public View getHeadsUpChild() {
190        return mHeadsUpChild;
191    }
192
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
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
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
231    protected void onVisibilityChanged(View changedView, int visibility) {
232        super.onVisibilityChanged(changedView, visibility);
233        updateVisibility();
234    }
235
236    private void updateVisibility() {
237        setVisible(isShown());
238    }
239
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
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
260    public int getContentHeight() {
261        return mContentHeight;
262    }
263
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
273    public int getMinHeight() {
274        return mSmallHeight;
275    }
276
277    public void setClipTopAmount(int clipTopAmount) {
278        mClipTopAmount = clipTopAmount;
279        updateClipping();
280    }
281
282    private void updateRoundRectClipping() {
283        boolean enabled = needsRoundRectClipping();
284        setClipToOutline(enabled);
285    }
286
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
303    private void updateClipping() {
304        mClipBounds.set(0, mClipTopAmount, getWidth(), mContentHeight);
305        setClipBounds(mClipBounds);
306    }
307
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
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
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     */
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     */
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
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
419    public boolean isContentExpandable() {
420        return mExpandedChild != null;
421    }
422
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
429    public void setHeadsUp(boolean headsUp) {
430        mIsHeadsUp = headsUp;
431        selectLayout(false /* animate */, true /* force */);
432    }
433
434    @Override
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
442    public void setShowingLegacyBackground(boolean showing) {
443        mShowingLegacyBackground = showing;
444    }
445}
446