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 21 import android.annotation.Nullable; 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.content.res.TypedArray; 25 import android.graphics.Rect; 26 import android.os.Trace; 27 import android.util.AttributeSet; 28 import android.widget.RemoteViews; 29 30 import com.android.internal.R; 31 32 import java.util.HashSet; 33 import java.util.Set; 34 35 /** 36 * The top line of content in a notification view. 37 * This includes the text views and badges but excludes the icon and the expander. 38 * 39 * @hide 40 */ 41 @RemoteViews.RemoteView 42 public class NotificationTopLineView extends ViewGroup { 43 private final OverflowAdjuster mOverflowAdjuster = new OverflowAdjuster(); 44 private final int mGravityY; 45 private final int mChildMinWidth; 46 private final int mChildHideWidth; 47 @Nullable private View mAppName; 48 @Nullable private View mTitle; 49 private View mHeaderText; 50 private View mHeaderTextDivider; 51 private View mSecondaryHeaderText; 52 private View mSecondaryHeaderTextDivider; 53 private OnClickListener mFeedbackListener; 54 private HeaderTouchListener mTouchListener = new HeaderTouchListener(); 55 private View mFeedbackIcon; 56 private int mHeaderTextMarginEnd; 57 58 private Set<View> mViewsToDisappear = new HashSet<>(); 59 60 private int mMaxAscent; 61 private int mMaxDescent; 62 NotificationTopLineView(Context context)63 public NotificationTopLineView(Context context) { 64 this(context, null); 65 } 66 NotificationTopLineView(Context context, @Nullable AttributeSet attrs)67 public NotificationTopLineView(Context context, @Nullable AttributeSet attrs) { 68 this(context, attrs, 0); 69 } 70 NotificationTopLineView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)71 public NotificationTopLineView(Context context, @Nullable AttributeSet attrs, 72 int defStyleAttr) { 73 this(context, attrs, defStyleAttr, 0); 74 } 75 NotificationTopLineView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)76 public NotificationTopLineView(Context context, AttributeSet attrs, int defStyleAttr, 77 int defStyleRes) { 78 super(context, attrs, defStyleAttr, defStyleRes); 79 Resources res = getResources(); 80 mChildMinWidth = res.getDimensionPixelSize(R.dimen.notification_header_shrink_min_width); 81 mChildHideWidth = res.getDimensionPixelSize(R.dimen.notification_header_shrink_hide_width); 82 83 // NOTE: Implementation only supports TOP, BOTTOM, and CENTER_VERTICAL gravities, 84 // with CENTER_VERTICAL being the default. 85 int[] attrIds = {android.R.attr.gravity}; 86 TypedArray ta = context.obtainStyledAttributes(attrs, attrIds, defStyleAttr, defStyleRes); 87 int gravity = ta.getInt(0, 0); 88 ta.recycle(); 89 if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) { 90 mGravityY = Gravity.BOTTOM; 91 } else if ((gravity & Gravity.TOP) == Gravity.TOP) { 92 mGravityY = Gravity.TOP; 93 } else { 94 mGravityY = Gravity.CENTER_VERTICAL; 95 } 96 } 97 98 @Override onFinishInflate()99 protected void onFinishInflate() { 100 super.onFinishInflate(); 101 mAppName = findViewById(R.id.app_name_text); 102 mTitle = findViewById(R.id.title); 103 mHeaderText = findViewById(R.id.header_text); 104 mHeaderTextDivider = findViewById(R.id.header_text_divider); 105 mSecondaryHeaderText = findViewById(R.id.header_text_secondary); 106 mSecondaryHeaderTextDivider = findViewById(R.id.header_text_secondary_divider); 107 mFeedbackIcon = findViewById(R.id.feedback); 108 } 109 110 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)111 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 112 Trace.beginSection("NotificationTopLineView#onMeasure"); 113 final int givenWidth = MeasureSpec.getSize(widthMeasureSpec); 114 final int givenHeight = MeasureSpec.getSize(heightMeasureSpec); 115 final boolean wrapHeight = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST; 116 int wrapContentWidthSpec = MeasureSpec.makeMeasureSpec(givenWidth, MeasureSpec.AT_MOST); 117 int heightSpec = notificationsRedesignTemplates() 118 ? MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) 119 : MeasureSpec.makeMeasureSpec(givenHeight, MeasureSpec.AT_MOST); 120 int totalWidth = getPaddingStart(); 121 int maxChildHeight = -1; 122 mMaxAscent = -1; 123 mMaxDescent = -1; 124 for (int i = 0; i < getChildCount(); i++) { 125 final View child = getChildAt(i); 126 if (child.getVisibility() == GONE) { 127 // We'll give it the rest of the space in the end 128 continue; 129 } 130 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 131 int childWidthSpec = getChildMeasureSpec(wrapContentWidthSpec, 132 lp.leftMargin + lp.rightMargin, lp.width); 133 int childHeightSpec = getChildMeasureSpec(heightSpec, 134 lp.topMargin + lp.bottomMargin, lp.height); 135 child.measure(childWidthSpec, childHeightSpec); 136 totalWidth += lp.leftMargin + lp.rightMargin + child.getMeasuredWidth(); 137 int childBaseline = child.getBaseline(); 138 int childHeight = child.getMeasuredHeight(); 139 if (childBaseline != -1) { 140 mMaxAscent = Math.max(mMaxAscent, childBaseline); 141 mMaxDescent = Math.max(mMaxDescent, childHeight - childBaseline); 142 } 143 maxChildHeight = Math.max(maxChildHeight, childHeight); 144 } 145 146 mViewsToDisappear.clear(); 147 // Ensure that there is at least enough space for the icons 148 int endMargin = Math.max(mHeaderTextMarginEnd, getPaddingEnd()); 149 if (totalWidth > givenWidth - endMargin) { 150 int overFlow = totalWidth - givenWidth + endMargin; 151 152 mOverflowAdjuster.resetForOverflow(overFlow, heightSpec) 153 // First shrink the app name, down to a minimum size 154 .adjust(mAppName, null, mChildMinWidth) 155 // Next, shrink the header text (this usually has subText) 156 // This shrinks the subtext first, but not all the way (yet!) 157 .adjust(mHeaderText, mHeaderTextDivider, mChildMinWidth) 158 // Next, shrink the secondary header text (this rarely has conversationTitle) 159 .adjust(mSecondaryHeaderText, mSecondaryHeaderTextDivider, 0) 160 // Next, shrink the title text (this has contentTitle; only in headerless views) 161 .adjust(mTitle, null, mChildMinWidth) 162 // Next, shrink the header down to 0 if still necessary. 163 .adjust(mHeaderText, mHeaderTextDivider, 0) 164 // Finally, shrink the title to 0 if necessary (media is super cramped) 165 .adjust(mTitle, null, 0) 166 // Clean up 167 .finish(); 168 } 169 setMeasuredDimension(givenWidth, wrapHeight ? maxChildHeight : givenHeight); 170 Trace.endSection(); 171 } 172 173 @Override onLayout(boolean changed, int l, int t, int r, int b)174 protected void onLayout(boolean changed, int l, int t, int r, int b) { 175 final boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; 176 final int width = getWidth(); 177 int start = getPaddingStart(); 178 int childCount = getChildCount(); 179 int ownHeight = b - t; 180 int childSpace = ownHeight - mPaddingTop - mPaddingBottom; 181 182 // Instead of centering the baseline, pick a baseline that centers views which align to it. 183 // Only used when mGravityY is CENTER_VERTICAL 184 int baselineY = mPaddingTop + ((childSpace - (mMaxAscent + mMaxDescent)) / 2) + mMaxAscent; 185 186 for (int i = 0; i < childCount; i++) { 187 View child = getChildAt(i); 188 if (child.getVisibility() == GONE) { 189 continue; 190 } 191 int childHeight = child.getMeasuredHeight(); 192 MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams(); 193 194 // Calculate vertical alignment of the views, accounting for the view baselines 195 int childTop; 196 int childBaseline = child.getBaseline(); 197 switch (mGravityY) { 198 case Gravity.TOP: 199 childTop = mPaddingTop + params.topMargin; 200 if (childBaseline != -1) { 201 childTop += mMaxAscent - childBaseline; 202 } 203 break; 204 case Gravity.CENTER_VERTICAL: 205 if (childBaseline != -1) { 206 // Align baselines vertically only if the child is smaller than us 207 if (childSpace - childHeight > 0) { 208 childTop = baselineY - childBaseline; 209 } else { 210 childTop = mPaddingTop + (childSpace - childHeight) / 2; 211 } 212 } else { 213 childTop = mPaddingTop + ((childSpace - childHeight) / 2) 214 + params.topMargin - params.bottomMargin; 215 } 216 break; 217 case Gravity.BOTTOM: 218 int childBottom = ownHeight - mPaddingBottom; 219 childTop = childBottom - childHeight - params.bottomMargin; 220 if (childBaseline != -1) { 221 int descent = childHeight - childBaseline; 222 childTop -= (mMaxDescent - descent); 223 } 224 break; 225 default: 226 childTop = mPaddingTop; 227 } 228 if (mViewsToDisappear.contains(child)) { 229 child.layout(start, childTop, start, childTop + childHeight); 230 } else { 231 start += params.getMarginStart(); 232 int end = start + child.getMeasuredWidth(); 233 int layoutLeft = isRtl ? width - end : start; 234 int layoutRight = isRtl ? width - start : end; 235 start = end + params.getMarginEnd(); 236 child.layout(layoutLeft, childTop, layoutRight, childTop + childHeight); 237 } 238 } 239 updateTouchListener(); 240 } 241 242 @Override generateLayoutParams(AttributeSet attrs)243 public LayoutParams generateLayoutParams(AttributeSet attrs) { 244 return new MarginLayoutParams(getContext(), attrs); 245 } 246 updateTouchListener()247 private void updateTouchListener() { 248 if (mFeedbackListener == null) { 249 setOnTouchListener(null); 250 return; 251 } 252 setOnTouchListener(mTouchListener); 253 mTouchListener.bindTouchRects(); 254 } 255 256 /** 257 * Sets onclick listener for feedback icon. 258 */ setFeedbackOnClickListener(OnClickListener l)259 public void setFeedbackOnClickListener(OnClickListener l) { 260 mFeedbackListener = l; 261 mFeedbackIcon.setOnClickListener(mFeedbackListener); 262 updateTouchListener(); 263 } 264 265 /** 266 * Sets the margin end for the text portion of the header, excluding right-aligned elements 267 * 268 * @param headerTextMarginEnd margin size 269 */ setHeaderTextMarginEnd(int headerTextMarginEnd)270 public void setHeaderTextMarginEnd(int headerTextMarginEnd) { 271 if (mHeaderTextMarginEnd != headerTextMarginEnd) { 272 mHeaderTextMarginEnd = headerTextMarginEnd; 273 requestLayout(); 274 } 275 } 276 277 /** 278 * Get the current margin end value for the header text 279 * 280 * @return margin size 281 */ getHeaderTextMarginEnd()282 public int getHeaderTextMarginEnd() { 283 return mHeaderTextMarginEnd; 284 } 285 286 /** 287 * Set padding at the start of the view. 288 */ setPaddingStart(int paddingStart)289 public void setPaddingStart(int paddingStart) { 290 setPaddingRelative(paddingStart, getPaddingTop(), getPaddingEnd(), getPaddingBottom()); 291 } 292 293 private class HeaderTouchListener implements OnTouchListener { 294 295 private Rect mFeedbackRect; 296 private int mTouchSlop; 297 private boolean mTrackGesture; 298 private float mDownX; 299 private float mDownY; 300 HeaderTouchListener()301 HeaderTouchListener() { 302 } 303 bindTouchRects()304 public void bindTouchRects() { 305 mFeedbackRect = getRectAroundView(mFeedbackIcon); 306 mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 307 } 308 getRectAroundView(View view)309 private Rect getRectAroundView(View view) { 310 float size = 48 * getResources().getDisplayMetrics().density; 311 float width = Math.max(size, view.getWidth()); 312 float height = Math.max(size, view.getHeight()); 313 final Rect r = new Rect(); 314 if (view.getVisibility() == GONE) { 315 view = getFirstChildNotGone(); 316 r.left = (int) (view.getLeft() - width / 2.0f); 317 } else { 318 r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - width / 2.0f); 319 } 320 r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - height / 2.0f); 321 r.bottom = (int) (r.top + height); 322 r.right = (int) (r.left + width); 323 return r; 324 } 325 326 @Override onTouch(View v, MotionEvent event)327 public boolean onTouch(View v, MotionEvent event) { 328 float x = event.getX(); 329 float y = event.getY(); 330 switch (event.getActionMasked() & MotionEvent.ACTION_MASK) { 331 case MotionEvent.ACTION_DOWN: 332 mTrackGesture = false; 333 if (isInside(x, y)) { 334 mDownX = x; 335 mDownY = y; 336 mTrackGesture = true; 337 return true; 338 } 339 break; 340 case MotionEvent.ACTION_MOVE: 341 if (mTrackGesture) { 342 if (Math.abs(mDownX - x) > mTouchSlop 343 || Math.abs(mDownY - y) > mTouchSlop) { 344 mTrackGesture = false; 345 } 346 } 347 break; 348 case MotionEvent.ACTION_UP: 349 if (mTrackGesture && onTouchUp(x, y, mDownX, mDownY)) { 350 return true; 351 } 352 break; 353 } 354 return mTrackGesture; 355 } 356 onTouchUp(float upX, float upY, float downX, float downY)357 private boolean onTouchUp(float upX, float upY, float downX, float downY) { 358 if (mFeedbackIcon.isVisibleToUser() 359 && (mFeedbackRect.contains((int) upX, (int) upY) 360 || mFeedbackRect.contains((int) downX, (int) downY))) { 361 mFeedbackIcon.performClick(); 362 return true; 363 } 364 return false; 365 } 366 isInside(float x, float y)367 private boolean isInside(float x, float y) { 368 return mFeedbackRect.contains((int) x, (int) y); 369 } 370 } 371 getFirstChildNotGone()372 private View getFirstChildNotGone() { 373 for (int i = 0; i < getChildCount(); i++) { 374 final View child = getChildAt(i); 375 if (child.getVisibility() != GONE) { 376 return child; 377 } 378 } 379 return this; 380 } 381 382 @Override hasOverlappingRendering()383 public boolean hasOverlappingRendering() { 384 return false; 385 } 386 387 /** 388 * Returns whether the title is present. 389 */ isTitlePresent()390 public boolean isTitlePresent() { 391 return mTitle != null; 392 } 393 394 /** 395 * Determine if the given point is touching an active part of the top line. 396 */ isInTouchRect(float x, float y)397 public boolean isInTouchRect(float x, float y) { 398 if (mFeedbackListener == null) { 399 return false; 400 } 401 return mTouchListener.isInside(x, y); 402 } 403 404 /** 405 * Perform a click on an active part of the top line, if touching. 406 */ onTouchUp(float upX, float upY, float downX, float downY)407 public boolean onTouchUp(float upX, float upY, float downX, float downY) { 408 if (mFeedbackListener == null) { 409 return false; 410 } 411 return mTouchListener.onTouchUp(upX, upY, downX, downY); 412 } 413 414 private final class OverflowAdjuster { 415 private int mOverflow; 416 private int mHeightSpec; 417 private View mRegrowView; 418 resetForOverflow(int overflow, int heightSpec)419 OverflowAdjuster resetForOverflow(int overflow, int heightSpec) { 420 mOverflow = overflow; 421 mHeightSpec = heightSpec; 422 mRegrowView = null; 423 return this; 424 } 425 426 /** 427 * Shrink the targetView's width by up to overFlow, down to minimumWidth. 428 * @param targetView the view to shrink the width of 429 * @param targetDivider a divider view which should be set to 0 width if the targetView is 430 * @param minimumWidth the minimum width allowed for the targetView 431 * @return this object 432 */ adjust(View targetView, View targetDivider, int minimumWidth)433 OverflowAdjuster adjust(View targetView, View targetDivider, int minimumWidth) { 434 if (mOverflow <= 0 || targetView == null || targetView.getVisibility() == View.GONE) { 435 return this; 436 } 437 final int oldWidth = targetView.getMeasuredWidth(); 438 if (oldWidth <= minimumWidth) { 439 return this; 440 } 441 // we're too big 442 int newSize = Math.max(minimumWidth, oldWidth - mOverflow); 443 if (minimumWidth == 0 && newSize < mChildHideWidth 444 && mRegrowView != null && mRegrowView != targetView) { 445 // View is so small it's better to hide it entirely (and its divider and margins) 446 // so we can give that space back to another previously shrunken view. 447 newSize = 0; 448 } 449 450 int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST); 451 targetView.measure(childWidthSpec, mHeightSpec); 452 mOverflow -= oldWidth - newSize; 453 454 if (newSize == 0) { 455 mViewsToDisappear.add(targetView); 456 mOverflow -= getHorizontalMargins(targetView); 457 if (targetDivider != null && targetDivider.getVisibility() != GONE) { 458 mViewsToDisappear.add(targetDivider); 459 int oldDividerWidth = targetDivider.getMeasuredWidth(); 460 int dividerWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.AT_MOST); 461 targetDivider.measure(dividerWidthSpec, mHeightSpec); 462 mOverflow -= (oldDividerWidth + getHorizontalMargins(targetDivider)); 463 } 464 } 465 if (mOverflow < 0 && mRegrowView != null) { 466 // We're now under-flowing, so regrow the last view. 467 final int regrowCurrentSize = mRegrowView.getMeasuredWidth(); 468 final int maxSize = regrowCurrentSize - mOverflow; 469 int regrowWidthSpec = MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.AT_MOST); 470 mRegrowView.measure(regrowWidthSpec, mHeightSpec); 471 finish(); 472 return this; 473 } 474 475 if (newSize != 0) { 476 // if we shrunk this view (but did not completely hide it) store it for potential 477 // re-growth if we proactively shorten a future view. 478 mRegrowView = targetView; 479 } 480 return this; 481 } 482 finish()483 void finish() { 484 resetForOverflow(0, 0); 485 } 486 getHorizontalMargins(View view)487 private int getHorizontalMargins(View view) { 488 MarginLayoutParams params = (MarginLayoutParams) view.getLayoutParams(); 489 return params.getMarginStart() + params.getMarginEnd(); 490 } 491 } 492 } 493