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