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