• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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