Home | History | Annotate | Download | only in view
      1 /*
      2  * Copyright (C) 2015 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 android.view;
     18 
     19 import android.annotation.Nullable;
     20 import android.app.Notification;
     21 import android.content.Context;
     22 import android.content.res.Resources;
     23 import android.graphics.Canvas;
     24 import android.graphics.Outline;
     25 import android.graphics.Rect;
     26 import android.graphics.drawable.Drawable;
     27 import android.util.AttributeSet;
     28 import android.widget.ImageView;
     29 import android.widget.RemoteViews;
     30 
     31 import com.android.internal.R;
     32 import com.android.internal.widget.CachingIconView;
     33 
     34 import java.util.ArrayList;
     35 
     36 /**
     37  * A header of a notification view
     38  *
     39  * @hide
     40  */
     41 @RemoteViews.RemoteView
     42 public class NotificationHeaderView extends ViewGroup {
     43     public static final int NO_COLOR = Notification.COLOR_INVALID;
     44     private final int mChildMinWidth;
     45     private final int mContentEndMargin;
     46     private View mAppName;
     47     private View mHeaderText;
     48     private OnClickListener mExpandClickListener;
     49     private HeaderTouchListener mTouchListener = new HeaderTouchListener();
     50     private ImageView mExpandButton;
     51     private CachingIconView mIcon;
     52     private View mProfileBadge;
     53     private View mInfo;
     54     private int mIconColor;
     55     private int mOriginalNotificationColor;
     56     private boolean mExpanded;
     57     private boolean mShowExpandButtonAtEnd;
     58     private boolean mShowWorkBadgeAtEnd;
     59     private Drawable mBackground;
     60     private int mHeaderBackgroundHeight;
     61     private boolean mEntireHeaderClickable;
     62     private boolean mExpandOnlyOnButton;
     63     private boolean mAcceptAllTouches;
     64 
     65     ViewOutlineProvider mProvider = new ViewOutlineProvider() {
     66         @Override
     67         public void getOutline(View view, Outline outline) {
     68             if (mBackground != null) {
     69                 outline.setRect(0, 0, getWidth(), mHeaderBackgroundHeight);
     70                 outline.setAlpha(1f);
     71             }
     72         }
     73     };
     74 
     75     public NotificationHeaderView(Context context) {
     76         this(context, null);
     77     }
     78 
     79     public NotificationHeaderView(Context context, @Nullable AttributeSet attrs) {
     80         this(context, attrs, 0);
     81     }
     82 
     83     public NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
     84         this(context, attrs, defStyleAttr, 0);
     85     }
     86 
     87     public NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
     88         super(context, attrs, defStyleAttr, defStyleRes);
     89         Resources res = getResources();
     90         mChildMinWidth = res.getDimensionPixelSize(R.dimen.notification_header_shrink_min_width);
     91         mContentEndMargin = res.getDimensionPixelSize(R.dimen.notification_content_margin_end);
     92         mHeaderBackgroundHeight = res.getDimensionPixelSize(
     93                 R.dimen.notification_header_background_height);
     94         mEntireHeaderClickable = res.getBoolean(R.bool.config_notificationHeaderClickableForExpand);
     95     }
     96 
     97     @Override
     98     protected void onFinishInflate() {
     99         super.onFinishInflate();
    100         mAppName = findViewById(com.android.internal.R.id.app_name_text);
    101         mHeaderText = findViewById(com.android.internal.R.id.header_text);
    102         mExpandButton = findViewById(com.android.internal.R.id.expand_button);
    103         mIcon = findViewById(com.android.internal.R.id.icon);
    104         mProfileBadge = findViewById(com.android.internal.R.id.profile_badge);
    105     }
    106 
    107     @Override
    108     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    109         final int givenWidth = MeasureSpec.getSize(widthMeasureSpec);
    110         final int givenHeight = MeasureSpec.getSize(heightMeasureSpec);
    111         int wrapContentWidthSpec = MeasureSpec.makeMeasureSpec(givenWidth,
    112                 MeasureSpec.AT_MOST);
    113         int wrapContentHeightSpec = MeasureSpec.makeMeasureSpec(givenHeight,
    114                 MeasureSpec.AT_MOST);
    115         int totalWidth = getPaddingStart() + getPaddingEnd();
    116         for (int i = 0; i < getChildCount(); i++) {
    117             final View child = getChildAt(i);
    118             if (child.getVisibility() == GONE) {
    119                 // We'll give it the rest of the space in the end
    120                 continue;
    121             }
    122             final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    123             int childWidthSpec = getChildMeasureSpec(wrapContentWidthSpec,
    124                     lp.leftMargin + lp.rightMargin, lp.width);
    125             int childHeightSpec = getChildMeasureSpec(wrapContentHeightSpec,
    126                     lp.topMargin + lp.bottomMargin, lp.height);
    127             child.measure(childWidthSpec, childHeightSpec);
    128             totalWidth += lp.leftMargin + lp.rightMargin + child.getMeasuredWidth();
    129         }
    130         if (totalWidth > givenWidth) {
    131             int overFlow = totalWidth - givenWidth;
    132             // We are overflowing, lets shrink the app name first
    133             final int appWidth = mAppName.getMeasuredWidth();
    134             if (overFlow > 0 && mAppName.getVisibility() != GONE && appWidth > mChildMinWidth) {
    135                 int newSize = appWidth - Math.min(appWidth - mChildMinWidth, overFlow);
    136                 int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
    137                 mAppName.measure(childWidthSpec, wrapContentHeightSpec);
    138                 overFlow -= appWidth - newSize;
    139             }
    140             // still overflowing, finaly we shrink the header text
    141             if (overFlow > 0 && mHeaderText.getVisibility() != GONE) {
    142                 // we're still too big
    143                 final int textWidth = mHeaderText.getMeasuredWidth();
    144                 int newSize = Math.max(0, textWidth - overFlow);
    145                 int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
    146                 mHeaderText.measure(childWidthSpec, wrapContentHeightSpec);
    147             }
    148         }
    149         setMeasuredDimension(givenWidth, givenHeight);
    150     }
    151 
    152     @Override
    153     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    154         int left = getPaddingStart();
    155         int end = getMeasuredWidth();
    156         int childCount = getChildCount();
    157         int ownHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
    158         for (int i = 0; i < childCount; i++) {
    159             View child = getChildAt(i);
    160             if (child.getVisibility() == GONE) {
    161                 continue;
    162             }
    163             int childHeight = child.getMeasuredHeight();
    164             MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
    165             left += params.getMarginStart();
    166             int right = left + child.getMeasuredWidth();
    167             int top = (int) (getPaddingTop() + (ownHeight - childHeight) / 2.0f);
    168             int bottom = top + childHeight;
    169             int layoutLeft = left;
    170             int layoutRight = right;
    171             if (child == mExpandButton && mShowExpandButtonAtEnd) {
    172                 layoutRight = end - mContentEndMargin;
    173                 end = layoutLeft = layoutRight - child.getMeasuredWidth();
    174             }
    175             if (child == mProfileBadge) {
    176                 int paddingEnd = getPaddingEnd();
    177                 if (mShowWorkBadgeAtEnd) {
    178                     paddingEnd = mContentEndMargin;
    179                 }
    180                 layoutRight = end - paddingEnd;
    181                 end = layoutLeft = layoutRight - child.getMeasuredWidth();
    182             }
    183             if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
    184                 int ltrLeft = layoutLeft;
    185                 layoutLeft = getWidth() - layoutRight;
    186                 layoutRight = getWidth() - ltrLeft;
    187             }
    188             child.layout(layoutLeft, top, layoutRight, bottom);
    189             left = right + params.getMarginEnd();
    190         }
    191         updateTouchListener();
    192     }
    193 
    194     @Override
    195     public LayoutParams generateLayoutParams(AttributeSet attrs) {
    196         return new ViewGroup.MarginLayoutParams(getContext(), attrs);
    197     }
    198 
    199     /**
    200      * Set a {@link Drawable} to be displayed as a background on the header.
    201      */
    202     public void setHeaderBackgroundDrawable(Drawable drawable) {
    203         if (drawable != null) {
    204             setWillNotDraw(false);
    205             mBackground = drawable;
    206             mBackground.setCallback(this);
    207             setOutlineProvider(mProvider);
    208         } else {
    209             setWillNotDraw(true);
    210             mBackground = null;
    211             setOutlineProvider(null);
    212         }
    213         invalidate();
    214     }
    215 
    216     @Override
    217     protected void onDraw(Canvas canvas) {
    218         if (mBackground != null) {
    219             mBackground.setBounds(0, 0, getWidth(), mHeaderBackgroundHeight);
    220             mBackground.draw(canvas);
    221         }
    222     }
    223 
    224     @Override
    225     protected boolean verifyDrawable(Drawable who) {
    226         return super.verifyDrawable(who) || who == mBackground;
    227     }
    228 
    229     @Override
    230     protected void drawableStateChanged() {
    231         if (mBackground != null && mBackground.isStateful()) {
    232             mBackground.setState(getDrawableState());
    233         }
    234     }
    235 
    236     private void updateTouchListener() {
    237         if (mExpandClickListener != null) {
    238             mTouchListener.bindTouchRects();
    239         }
    240     }
    241 
    242     @Override
    243     public void setOnClickListener(@Nullable OnClickListener l) {
    244         mExpandClickListener = l;
    245         setOnTouchListener(mExpandClickListener != null ? mTouchListener : null);
    246         mExpandButton.setOnClickListener(mExpandClickListener);
    247         updateTouchListener();
    248     }
    249 
    250     @RemotableViewMethod
    251     public void setOriginalIconColor(int color) {
    252         mIconColor = color;
    253     }
    254 
    255     public int getOriginalIconColor() {
    256         return mIconColor;
    257     }
    258 
    259     @RemotableViewMethod
    260     public void setOriginalNotificationColor(int color) {
    261         mOriginalNotificationColor = color;
    262     }
    263 
    264     public int getOriginalNotificationColor() {
    265         return mOriginalNotificationColor;
    266     }
    267 
    268     @RemotableViewMethod
    269     public void setExpanded(boolean expanded) {
    270         mExpanded = expanded;
    271         updateExpandButton();
    272     }
    273 
    274     private void updateExpandButton() {
    275         int drawableId;
    276         int contentDescriptionId;
    277         if (mExpanded) {
    278             drawableId = R.drawable.ic_collapse_notification;
    279             contentDescriptionId = R.string.expand_button_content_description_expanded;
    280         } else {
    281             drawableId = R.drawable.ic_expand_notification;
    282             contentDescriptionId = R.string.expand_button_content_description_collapsed;
    283         }
    284         mExpandButton.setImageDrawable(getContext().getDrawable(drawableId));
    285         mExpandButton.setColorFilter(mOriginalNotificationColor);
    286         mExpandButton.setContentDescription(mContext.getText(contentDescriptionId));
    287     }
    288 
    289     public void setShowWorkBadgeAtEnd(boolean showWorkBadgeAtEnd) {
    290         if (showWorkBadgeAtEnd != mShowWorkBadgeAtEnd) {
    291             setClipToPadding(!showWorkBadgeAtEnd);
    292             mShowWorkBadgeAtEnd = showWorkBadgeAtEnd;
    293         }
    294     }
    295 
    296     /**
    297      * Sets whether or not the expand button appears at the end of the NotificationHeaderView. If
    298      * both this and {@link #setShowWorkBadgeAtEnd(boolean)} have been set to true, then the
    299      * expand button will appear closer to the end than the work badge.
    300      */
    301     public void setShowExpandButtonAtEnd(boolean showExpandButtonAtEnd) {
    302         if (showExpandButtonAtEnd != mShowExpandButtonAtEnd) {
    303             setClipToPadding(!showExpandButtonAtEnd);
    304             mShowExpandButtonAtEnd = showExpandButtonAtEnd;
    305         }
    306     }
    307 
    308     public View getWorkProfileIcon() {
    309         return mProfileBadge;
    310     }
    311 
    312     public CachingIconView getIcon() {
    313         return mIcon;
    314     }
    315 
    316     public class HeaderTouchListener implements View.OnTouchListener {
    317 
    318         private final ArrayList<Rect> mTouchRects = new ArrayList<>();
    319         private Rect mExpandButtonRect;
    320         private int mTouchSlop;
    321         private boolean mTrackGesture;
    322         private float mDownX;
    323         private float mDownY;
    324 
    325         public HeaderTouchListener() {
    326         }
    327 
    328         public void bindTouchRects() {
    329             mTouchRects.clear();
    330             addRectAroundView(mIcon);
    331             mExpandButtonRect = addRectAroundView(mExpandButton);
    332             addWidthRect();
    333             mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    334         }
    335 
    336         private void addWidthRect() {
    337             Rect r = new Rect();
    338             r.top = 0;
    339             r.bottom = (int) (32 * getResources().getDisplayMetrics().density);
    340             r.left = 0;
    341             r.right = getWidth();
    342             mTouchRects.add(r);
    343         }
    344 
    345         private Rect addRectAroundView(View view) {
    346             final Rect r = getRectAroundView(view);
    347             mTouchRects.add(r);
    348             return r;
    349         }
    350 
    351         private Rect getRectAroundView(View view) {
    352             float size = 48 * getResources().getDisplayMetrics().density;
    353             final Rect r = new Rect();
    354             if (view.getVisibility() == GONE) {
    355                 view = getFirstChildNotGone();
    356                 r.left = (int) (view.getLeft() - size / 2.0f);
    357             } else {
    358                 r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - size / 2.0f);
    359             }
    360             r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - size / 2.0f);
    361             r.bottom = (int) (r.top + size);
    362             r.right = (int) (r.left + size);
    363             return r;
    364         }
    365 
    366         @Override
    367         public boolean onTouch(View v, MotionEvent event) {
    368             float x = event.getX();
    369             float y = event.getY();
    370             switch (event.getActionMasked() & MotionEvent.ACTION_MASK) {
    371                 case MotionEvent.ACTION_DOWN:
    372                     mTrackGesture = false;
    373                     if (isInside(x, y)) {
    374                         mDownX = x;
    375                         mDownY = y;
    376                         mTrackGesture = true;
    377                         return true;
    378                     }
    379                     break;
    380                 case MotionEvent.ACTION_MOVE:
    381                     if (mTrackGesture) {
    382                         if (Math.abs(mDownX - x) > mTouchSlop
    383                                 || Math.abs(mDownY - y) > mTouchSlop) {
    384                             mTrackGesture = false;
    385                         }
    386                     }
    387                     break;
    388                 case MotionEvent.ACTION_UP:
    389                     if (mTrackGesture) {
    390                         mExpandButton.performClick();
    391                     }
    392                     break;
    393             }
    394             return mTrackGesture;
    395         }
    396 
    397         private boolean isInside(float x, float y) {
    398             if (mAcceptAllTouches) {
    399                 return true;
    400             }
    401             if (mExpandOnlyOnButton) {
    402                 return mExpandButtonRect.contains((int) x, (int) y);
    403             }
    404             for (int i = 0; i < mTouchRects.size(); i++) {
    405                 Rect r = mTouchRects.get(i);
    406                 if (r.contains((int) x, (int) y)) {
    407                     return true;
    408                 }
    409             }
    410             return false;
    411         }
    412     }
    413 
    414     private View getFirstChildNotGone() {
    415         for (int i = 0; i < getChildCount(); i++) {
    416             final View child = getChildAt(i);
    417             if (child.getVisibility() != GONE) {
    418                 return child;
    419             }
    420         }
    421         return this;
    422     }
    423 
    424     public ImageView getExpandButton() {
    425         return mExpandButton;
    426     }
    427 
    428     @Override
    429     public boolean hasOverlappingRendering() {
    430         return false;
    431     }
    432 
    433     public boolean isInTouchRect(float x, float y) {
    434         if (mExpandClickListener == null) {
    435             return false;
    436         }
    437         return mTouchListener.isInside(x, y);
    438     }
    439 
    440     /**
    441      * Sets whether or not all touches to this header view will register as a click. Note that
    442      * if the config value for {@code config_notificationHeaderClickableForExpand} is {@code true},
    443      * then calling this method with {@code false} will not override that configuration.
    444      */
    445     @RemotableViewMethod
    446     public void setAcceptAllTouches(boolean acceptAllTouches) {
    447         mAcceptAllTouches = mEntireHeaderClickable || acceptAllTouches;
    448     }
    449 
    450     /**
    451      * Sets whether only the expand icon itself should serve as the expand target.
    452      */
    453     @RemotableViewMethod
    454     public void setExpandOnlyOnButton(boolean expandOnlyOnButton) {
    455         mExpandOnlyOnButton = expandOnlyOnButton;
    456     }
    457 }
    458