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