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