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