Home | History | Annotate | Download | only in statusbar
      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 
     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