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