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 static android.app.Flags.notificationsRedesignTemplates; 20 import static android.util.MathUtils.abs; 21 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 22 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.compat.annotation.UnsupportedAppUsage; 27 import android.content.Context; 28 import android.content.res.Resources; 29 import android.graphics.Canvas; 30 import android.graphics.Outline; 31 import android.graphics.Rect; 32 import android.graphics.drawable.Drawable; 33 import android.os.Build; 34 import android.util.AttributeSet; 35 import android.widget.FrameLayout; 36 import android.widget.RelativeLayout; 37 import android.widget.RemoteViews; 38 import android.widget.TextView; 39 40 import com.android.internal.R; 41 import com.android.internal.widget.CachingIconView; 42 import com.android.internal.widget.NotificationExpandButton; 43 44 import java.util.ArrayList; 45 46 /** 47 * A header of a notification view 48 * 49 * @hide 50 */ 51 @RemoteViews.RemoteView 52 public class NotificationHeaderView extends RelativeLayout { 53 private final int mTouchableHeight; 54 private OnClickListener mExpandClickListener; 55 private HeaderTouchListener mTouchListener = new HeaderTouchListener(); 56 private NotificationTopLineView mTopLineView; 57 private NotificationExpandButton mExpandButton; 58 private View mAltExpandTarget; 59 private CachingIconView mIcon; 60 private Drawable mBackground; 61 private boolean mEntireHeaderClickable; 62 private boolean mExpandOnlyOnButton; 63 private boolean mAcceptAllTouches; 64 private float mTopLineTranslation; 65 private float mExpandButtonTranslation; 66 67 ViewOutlineProvider mProvider = new ViewOutlineProvider() { 68 @Override 69 public void getOutline(View view, Outline outline) { 70 if (mBackground != null) { 71 outline.setRect(0, 0, getWidth(), getHeight()); 72 outline.setAlpha(1f); 73 } 74 } 75 }; 76 NotificationHeaderView(Context context)77 public NotificationHeaderView(Context context) { 78 this(context, null); 79 } 80 81 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) NotificationHeaderView(Context context, @Nullable AttributeSet attrs)82 public NotificationHeaderView(Context context, @Nullable AttributeSet attrs) { 83 this(context, attrs, 0); 84 } 85 NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)86 public NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 87 this(context, attrs, defStyleAttr, 0); 88 } 89 NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)90 public NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, 91 int defStyleRes) { 92 super(context, attrs, defStyleAttr, defStyleRes); 93 Resources res = getResources(); 94 mTouchableHeight = res.getDimensionPixelSize(R.dimen.notification_header_touchable_height); 95 mEntireHeaderClickable = res.getBoolean(R.bool.config_notificationHeaderClickableForExpand); 96 } 97 98 @Override onFinishInflate()99 protected void onFinishInflate() { 100 super.onFinishInflate(); 101 mIcon = findViewById(R.id.icon); 102 mTopLineView = findViewById(R.id.notification_top_line); 103 mExpandButton = findViewById(R.id.expand_button); 104 mAltExpandTarget = findViewById(R.id.alternate_expand_target); 105 setClipToPadding(false); 106 } 107 108 /** 109 * Set a {@link Drawable} to be displayed as a background on the header. 110 */ setHeaderBackgroundDrawable(Drawable drawable)111 public void setHeaderBackgroundDrawable(Drawable drawable) { 112 if (drawable != null) { 113 setWillNotDraw(false); 114 mBackground = drawable; 115 mBackground.setCallback(this); 116 setOutlineProvider(mProvider); 117 } else { 118 setWillNotDraw(true); 119 mBackground = null; 120 setOutlineProvider(null); 121 } 122 invalidate(); 123 } 124 125 @Override onDraw(Canvas canvas)126 protected void onDraw(Canvas canvas) { 127 if (mBackground != null) { 128 mBackground.setBounds(0, 0, getWidth(), getHeight()); 129 mBackground.draw(canvas); 130 } 131 } 132 133 @Override verifyDrawable(@onNull Drawable who)134 protected boolean verifyDrawable(@NonNull Drawable who) { 135 return super.verifyDrawable(who) || who == mBackground; 136 } 137 138 @Override drawableStateChanged()139 protected void drawableStateChanged() { 140 if (mBackground != null && mBackground.isStateful()) { 141 mBackground.setState(getDrawableState()); 142 } 143 } 144 updateTouchListener()145 private void updateTouchListener() { 146 if (mExpandClickListener == null) { 147 setOnTouchListener(null); 148 return; 149 } 150 setOnTouchListener(mTouchListener); 151 mTouchListener.bindTouchRects(); 152 } 153 154 @Override setOnClickListener(@ullable OnClickListener l)155 public void setOnClickListener(@Nullable OnClickListener l) { 156 mExpandClickListener = l; 157 mExpandButton.setOnClickListener(mExpandClickListener); 158 mAltExpandTarget.setOnClickListener(mExpandClickListener); 159 updateTouchListener(); 160 } 161 162 /** 163 * Sets the extra margin at the end of the top line of left-aligned text + icons. 164 * This value will have the margin required to accommodate the expand button added to it. 165 * 166 * @param extraMarginEnd extra margin in px 167 */ setTopLineExtraMarginEnd(int extraMarginEnd)168 public void setTopLineExtraMarginEnd(int extraMarginEnd) { 169 mTopLineView.setHeaderTextMarginEnd(extraMarginEnd); 170 } 171 172 /** 173 * Sets the extra margin at the end of the top line of left-aligned text + icons. 174 * This value will have the margin required to accommodate the expand button added to it. 175 * 176 * @param extraMarginEndDp extra margin in dp 177 */ 178 @RemotableViewMethod setTopLineExtraMarginEndDp(float extraMarginEndDp)179 public void setTopLineExtraMarginEndDp(float extraMarginEndDp) { 180 setTopLineExtraMarginEnd( 181 (int) (extraMarginEndDp * getResources().getDisplayMetrics().density)); 182 } 183 184 /** 185 * Center top line and expand button vertically. 186 */ 187 @RemotableViewMethod centerTopLine(boolean center)188 public void centerTopLine(boolean center) { 189 if (notificationsRedesignTemplates()) { 190 // The content of the top line view is already center-aligned, but since the height 191 // matches the content by default, it looks top-aligned. If the height matches the 192 // parent instead, the text ends up correctly centered in the parent. 193 ViewGroup.LayoutParams lp = mTopLineView.getLayoutParams(); 194 lp.height = center ? MATCH_PARENT : WRAP_CONTENT; 195 mTopLineView.setLayoutParams(lp); 196 197 centerExpandButton(center); 198 } 199 } 200 201 /** Center expand button vertically. */ centerExpandButton(boolean center)202 private void centerExpandButton(boolean center) { 203 ViewGroup.LayoutParams lp = mExpandButton.getLayoutParams(); 204 lp.height = center ? MATCH_PARENT : WRAP_CONTENT; 205 if (lp instanceof FrameLayout.LayoutParams flp) { 206 flp.gravity = center ? Gravity.CENTER : (Gravity.TOP | Gravity.END); 207 } 208 mExpandButton.setLayoutParams(lp); 209 } 210 211 /** The view containing the app name, timestamp etc at the top of the notification. */ getTopLineView()212 public NotificationTopLineView getTopLineView() { 213 return mTopLineView; 214 } 215 216 /** The view containing the button to expand the notification. */ getExpandButton()217 public NotificationExpandButton getExpandButton() { 218 return mExpandButton; 219 } 220 221 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)222 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 223 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 224 225 if (notificationsRedesignTemplates()) { 226 // TODO: b/378660052 - These should never be null in practice, consider using 227 // requireViewById() in the onFinishInflate. 228 if (mTopLineView != null) { 229 mTopLineTranslation = measureCenterTranslation(mTopLineView); 230 } 231 if (mExpandButton != null) { 232 mExpandButtonTranslation = measureCenterTranslation(mExpandButton); 233 } 234 } 235 } 236 measureCenterTranslation(View view)237 private float measureCenterTranslation(View view) { 238 // When the view is centered (see centerTopLine), its height is MATCH_PARENT 239 int parentHeight = getMeasuredHeight(); 240 // When the view is top-aligned, its height is WRAP_CONTENT 241 float wrapContentHeight = view.getMeasuredHeight(); 242 // Calculate the translation needed between the two alignments 243 final MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams(); 244 return abs((parentHeight - wrapContentHeight) / 2f - lp.topMargin); 245 } 246 247 /** 248 * The vertical translation necessary between the two positions of the top line, to be used in 249 * the animation. See also {@link NotificationHeaderView#centerTopLine(boolean)}. 250 */ getTopLineTranslation()251 public float getTopLineTranslation() { 252 return mTopLineTranslation; 253 } 254 255 /** 256 * The vertical translation necessary between the two positions of the expander, to be used in 257 * the animation. See also {@link NotificationHeaderView#centerTopLine(boolean)}. 258 */ getExpandButtonTranslation()259 public float getExpandButtonTranslation() { 260 return mExpandButtonTranslation; 261 } 262 263 /** 264 * This is used to make the low-priority header show the bolded text of a title. 265 * 266 * @param styleTextAsTitle true if this header's text is to have the style of a title 267 */ 268 @RemotableViewMethod styleTextAsTitle(boolean styleTextAsTitle)269 public void styleTextAsTitle(boolean styleTextAsTitle) { 270 int styleResId = styleTextAsTitle 271 ? R.style.TextAppearance_DeviceDefault_Notification_Title 272 : R.style.TextAppearance_DeviceDefault_Notification_Info; 273 // Most of the time, we're showing text in the minimized state 274 View headerText = findViewById(R.id.header_text); 275 if (headerText instanceof TextView) { 276 ((TextView) headerText).setTextAppearance(styleResId); 277 } 278 // If there's no summary or text, we show the app name instead of nothing 279 View appNameText = findViewById(R.id.app_name_text); 280 if (appNameText instanceof TextView) { 281 ((TextView) appNameText).setTextAppearance(styleResId); 282 } 283 } 284 285 /** 286 * Handles clicks on the header based on the region tapped. 287 */ 288 public class HeaderTouchListener implements OnTouchListener { 289 290 private final ArrayList<Rect> mTouchRects = new ArrayList<>(); 291 private Rect mExpandButtonRect; 292 private Rect mAltExpandTargetRect; 293 private int mTouchSlop; 294 private boolean mTrackGesture; 295 private float mDownX; 296 private float mDownY; 297 HeaderTouchListener()298 public HeaderTouchListener() { 299 } 300 bindTouchRects()301 public void bindTouchRects() { 302 mTouchRects.clear(); 303 addRectAroundView(mIcon); 304 mExpandButtonRect = addRectAroundView(mExpandButton); 305 mAltExpandTargetRect = addRectAroundView(mAltExpandTarget); 306 addWidthRect(); 307 mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 308 } 309 addWidthRect()310 private void addWidthRect() { 311 Rect r = new Rect(); 312 r.top = 0; 313 r.bottom = mTouchableHeight; 314 r.left = 0; 315 r.right = getWidth(); 316 mTouchRects.add(r); 317 } 318 addRectAroundView(View view)319 private Rect addRectAroundView(View view) { 320 final Rect r = getRectAroundView(view); 321 mTouchRects.add(r); 322 return r; 323 } 324 getRectAroundView(View view)325 private Rect getRectAroundView(View view) { 326 float size = 48 * getResources().getDisplayMetrics().density; 327 float width = Math.max(size, view.getWidth()); 328 float height = Math.max(size, view.getHeight()); 329 final Rect r = new Rect(); 330 if (view.getVisibility() == GONE) { 331 view = getFirstChildNotGone(); 332 r.left = (int) (view.getLeft() - width / 2.0f); 333 } else { 334 r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - width / 2.0f); 335 } 336 r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - height / 2.0f); 337 r.bottom = (int) (r.top + height); 338 r.right = (int) (r.left + width); 339 return r; 340 } 341 342 @Override onTouch(View v, MotionEvent event)343 public boolean onTouch(View v, MotionEvent event) { 344 float x = event.getX(); 345 float y = event.getY(); 346 switch (event.getActionMasked() & MotionEvent.ACTION_MASK) { 347 case MotionEvent.ACTION_DOWN: 348 mTrackGesture = false; 349 if (isInside(x, y)) { 350 mDownX = x; 351 mDownY = y; 352 mTrackGesture = true; 353 return true; 354 } 355 break; 356 case MotionEvent.ACTION_MOVE: 357 if (mTrackGesture) { 358 if (Math.abs(mDownX - x) > mTouchSlop 359 || Math.abs(mDownY - y) > mTouchSlop) { 360 mTrackGesture = false; 361 } 362 } 363 break; 364 case MotionEvent.ACTION_UP: 365 if (mTrackGesture) { 366 float topLineX = mTopLineView.getX(); 367 float topLineY = mTopLineView.getY(); 368 if (mTopLineView.onTouchUp(x - topLineX, y - topLineY, 369 mDownX - topLineX, mDownY - topLineY)) { 370 break; 371 } 372 mExpandButton.performClick(); 373 } 374 break; 375 } 376 return mTrackGesture; 377 } 378 isInside(float x, float y)379 private boolean isInside(float x, float y) { 380 if (mAcceptAllTouches) { 381 return true; 382 } 383 if (mExpandOnlyOnButton) { 384 return mExpandButtonRect.contains((int) x, (int) y) 385 || mAltExpandTargetRect.contains((int) x, (int) y); 386 } 387 for (int i = 0; i < mTouchRects.size(); i++) { 388 Rect r = mTouchRects.get(i); 389 if (r.contains((int) x, (int) y)) { 390 return true; 391 } 392 } 393 float topLineX = x - mTopLineView.getX(); 394 float topLineY = y - mTopLineView.getY(); 395 return mTopLineView.isInTouchRect(topLineX, topLineY); 396 } 397 } 398 getFirstChildNotGone()399 private View getFirstChildNotGone() { 400 for (int i = 0; i < getChildCount(); i++) { 401 final View child = getChildAt(i); 402 if (child.getVisibility() != GONE) { 403 return child; 404 } 405 } 406 return this; 407 } 408 409 @Override hasOverlappingRendering()410 public boolean hasOverlappingRendering() { 411 return false; 412 } 413 isInTouchRect(float x, float y)414 public boolean isInTouchRect(float x, float y) { 415 if (mExpandClickListener == null) { 416 return false; 417 } 418 return mTouchListener.isInside(x, y); 419 } 420 421 /** 422 * Sets whether or not all touches to this header view will register as a click. Note that 423 * if the config value for {@code config_notificationHeaderClickableForExpand} is {@code true}, 424 * then calling this method with {@code false} will not override that configuration. 425 */ 426 @RemotableViewMethod setAcceptAllTouches(boolean acceptAllTouches)427 public void setAcceptAllTouches(boolean acceptAllTouches) { 428 mAcceptAllTouches = mEntireHeaderClickable || acceptAllTouches; 429 } 430 431 /** 432 * Sets whether only the expand icon itself should serve as the expand target. 433 */ 434 @RemotableViewMethod setExpandOnlyOnButton(boolean expandOnlyOnButton)435 public void setExpandOnlyOnButton(boolean expandOnlyOnButton) { 436 mExpandOnlyOnButton = expandOnlyOnButton; 437 } 438 } 439