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