• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 com.android.internal.widget;
18 
19 import static android.app.Flags.notificationsRedesignTemplates;
20 import static android.app.Notification.CallStyle.DEBUG_NEW_ACTION_LAYOUT;
21 import static android.app.Flags.evenlyDividedCallStyleActionLayout;
22 
23 import android.annotation.DimenRes;
24 import android.app.Flags;
25 import android.app.Notification;
26 import android.content.Context;
27 import android.content.res.TypedArray;
28 import android.graphics.drawable.RippleDrawable;
29 import android.util.AttributeSet;
30 import android.util.Log;
31 import android.view.Gravity;
32 import android.view.RemotableViewMethod;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.widget.LinearLayout;
36 import android.widget.RemoteViews;
37 import android.widget.TextView;
38 
39 import com.android.internal.R;
40 
41 import java.util.ArrayList;
42 import java.util.Comparator;
43 
44 /**
45  * Layout for notification actions that ensures that no action consumes more than their share of
46  * the remaining available width, and the last action consumes the remaining space.
47  */
48 @RemoteViews.RemoteView
49 public class NotificationActionListLayout extends LinearLayout {
50     private final int mGravity;
51     private int mTotalWidth = 0;
52     private int mExtraStartPadding = 0;
53     private ArrayList<TextViewInfo> mMeasureOrderTextViews = new ArrayList<>();
54     private ArrayList<View> mMeasureOrderOther = new ArrayList<>();
55     private boolean mEmphasizedMode;
56     private boolean mEvenlyDividedMode;
57     private int mDefaultPaddingBottom;
58     private int mDefaultPaddingTop;
59     private int mEmphasizedPaddingTop;
60     private int mEmphasizedPaddingBottom;
61     private int mEmphasizedHeight;
62     private int mRegularHeight;
63     @DimenRes private int mCollapsibleIndentDimen;
64     int mNumNotGoneChildren;
65     int mNumPriorityChildren;
66 
NotificationActionListLayout(Context context, AttributeSet attrs)67     public NotificationActionListLayout(Context context, AttributeSet attrs) {
68         this(context, attrs, 0);
69     }
70 
NotificationActionListLayout(Context context, AttributeSet attrs, int defStyleAttr)71     public NotificationActionListLayout(Context context, AttributeSet attrs, int defStyleAttr) {
72         this(context, attrs, defStyleAttr, 0);
73     }
74 
NotificationActionListLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)75     public NotificationActionListLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
76         super(context, attrs, defStyleAttr, defStyleRes);
77 
78         mCollapsibleIndentDimen = Flags.notificationsRedesignTemplates()
79                 ? R.dimen.notification_2025_actions_margin_start
80                 : R.dimen.notification_actions_padding_start;
81 
82         int[] attrIds = { android.R.attr.gravity };
83         TypedArray ta = context.obtainStyledAttributes(attrs, attrIds, defStyleAttr, defStyleRes);
84         mGravity = ta.getInt(0, 0);
85         ta.recycle();
86     }
87 
isPriority(View actionView)88     private static boolean isPriority(View actionView) {
89         return actionView instanceof EmphasizedNotificationButton
90                 && ((EmphasizedNotificationButton) actionView).isPriority();
91     }
92 
countAndRebuildMeasureOrder()93     private void countAndRebuildMeasureOrder() {
94         final int numChildren = getChildCount();
95         int textViews = 0;
96         int otherViews = 0;
97         mNumNotGoneChildren = 0;
98         mNumPriorityChildren = 0;
99 
100         for (int i = 0; i < numChildren; i++) {
101             View c = getChildAt(i);
102             if (c instanceof TextView) {
103                 textViews++;
104             } else {
105                 otherViews++;
106             }
107             if (c.getVisibility() != GONE) {
108                 mNumNotGoneChildren++;
109                 if (isPriority(c)) {
110                     mNumPriorityChildren++;
111                 }
112             }
113         }
114 
115         // Rebuild the measure order if the number of children changed or the text length of
116         // any of the children changed.
117         boolean needRebuild = false;
118         if (textViews != mMeasureOrderTextViews.size()
119                 || otherViews != mMeasureOrderOther.size()) {
120             needRebuild = true;
121         }
122         if (!needRebuild) {
123             final int size = mMeasureOrderTextViews.size();
124             for (int i = 0; i < size; i++) {
125                 if (mMeasureOrderTextViews.get(i).needsRebuild()) {
126                     needRebuild = true;
127                     break;
128                 }
129             }
130         }
131 
132         if (needRebuild) {
133             rebuildMeasureOrder(textViews, otherViews);
134         }
135     }
136 
measureAndReturnEvenlyDividedWidth(int heightMeasureSpec, int innerWidth)137     private int measureAndReturnEvenlyDividedWidth(int heightMeasureSpec, int innerWidth) {
138         final int numChildren = getChildCount();
139         int childMarginSum = 0;
140         for (int i = 0; i < numChildren; i++) {
141             final View child = getChildAt(i);
142             if (child.getVisibility() != GONE) {
143                 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
144                 childMarginSum += lp.leftMargin + lp.rightMargin;
145             }
146         }
147 
148         final int innerWidthMinusChildMargins = innerWidth - childMarginSum;
149         final int childWidth = innerWidthMinusChildMargins / mNumNotGoneChildren;
150         final int childWidthMeasureSpec =
151                 MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
152 
153         if (DEBUG_NEW_ACTION_LAYOUT) {
154             Log.v(TAG, "measuring evenly divided width: "
155                     + "numChildren = " + numChildren + ", "
156                     + "innerWidth = " + innerWidth + "px, "
157                     + "childMarginSum = " + childMarginSum + "px, "
158                     + "innerWidthMinusChildMargins = " + innerWidthMinusChildMargins + "px, "
159                     + "childWidth = " + childWidth + "px, "
160                     + "childWidthMeasureSpec = " + MeasureSpec.toString(childWidthMeasureSpec));
161         }
162 
163         for (int i = 0; i < numChildren; i++) {
164             final View child = getChildAt(i);
165             if (child.getVisibility() != GONE) {
166                 child.measure(childWidthMeasureSpec, heightMeasureSpec);
167             }
168         }
169 
170         return innerWidth;
171     }
172 
measureAndGetUsedWidth(int widthMeasureSpec, int heightMeasureSpec, int innerWidth, boolean collapsePriorityActions)173     private int measureAndGetUsedWidth(int widthMeasureSpec, int heightMeasureSpec, int innerWidth,
174             boolean collapsePriorityActions) {
175         final int numChildren = getChildCount();
176         final boolean constrained =
177                 MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED;
178         final int otherSize = mMeasureOrderOther.size();
179         int usedWidth = 0;
180 
181         int maxPriorityWidth = 0;
182         int measuredChildren = 0;
183         int measuredPriorityChildren = 0;
184         for (int i = 0; i < numChildren; i++) {
185             // Measure shortest children first. To avoid measuring twice, we approximate by looking
186             // at the text length.
187             final boolean isPriority;
188             final View c;
189             if (i < otherSize) {
190                 c = mMeasureOrderOther.get(i);
191                 isPriority = false;
192             } else {
193                 TextViewInfo info = mMeasureOrderTextViews.get(i - otherSize);
194                 c = info.mTextView;
195                 isPriority = info.mIsPriority;
196             }
197             if (c.getVisibility() == GONE) {
198                 continue;
199             }
200             MarginLayoutParams lp = (MarginLayoutParams) c.getLayoutParams();
201 
202             int usedWidthForChild = usedWidth;
203             if (constrained) {
204                 // Make sure that this child doesn't consume more than its share of the remaining
205                 // total available space. Not used space will benefit subsequent views. Since we
206                 // measure in the order of (approx.) size, a large view can still take more than its
207                 // share if the others are small.
208                 int availableWidth = innerWidth - usedWidth;
209                 int unmeasuredChildren = mNumNotGoneChildren - measuredChildren;
210                 int maxWidthForChild = availableWidth / unmeasuredChildren;
211                 if (isPriority && collapsePriorityActions) {
212                     // Collapsing the actions to just the width required to show the icon.
213                     if (maxPriorityWidth == 0) {
214                         maxPriorityWidth = getResources().getDimensionPixelSize(
215                                 R.dimen.notification_actions_collapsed_priority_width);
216                     }
217                     maxWidthForChild = maxPriorityWidth + lp.leftMargin + lp.rightMargin;
218                 } else if (isPriority) {
219                     // Priority children get a larger maximum share of the total space:
220                     //  maximum priority share = (nPriority + 1) / (MAX + 1)
221                     int unmeasuredPriorityChildren = mNumPriorityChildren
222                             - measuredPriorityChildren;
223                     int unmeasuredOtherChildren = unmeasuredChildren - unmeasuredPriorityChildren;
224                     int widthReservedForOtherChildren = innerWidth * unmeasuredOtherChildren
225                             / (Notification.MAX_ACTION_BUTTONS + 1);
226                     int widthAvailableForPriority = availableWidth - widthReservedForOtherChildren;
227                     maxWidthForChild = widthAvailableForPriority / unmeasuredPriorityChildren;
228                 }
229 
230                 usedWidthForChild = innerWidth - maxWidthForChild;
231             }
232 
233             measureChildWithMargins(c, widthMeasureSpec, usedWidthForChild,
234                     heightMeasureSpec, 0 /* usedHeight */);
235 
236             usedWidth += c.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
237             measuredChildren++;
238             if (isPriority) {
239                 measuredPriorityChildren++;
240             }
241         }
242 
243         int collapsibleIndent = mCollapsibleIndentDimen == 0 ? 0
244                 : getResources().getDimensionPixelOffset(mCollapsibleIndentDimen);
245         if (innerWidth - usedWidth > collapsibleIndent) {
246             mExtraStartPadding = collapsibleIndent;
247         } else {
248             mExtraStartPadding = 0;
249         }
250         return usedWidth;
251     }
252 
253     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)254     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
255         countAndRebuildMeasureOrder();
256         final int innerWidth = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight;
257         int usedWidth;
258         if (mEvenlyDividedMode) {
259             usedWidth = measureAndReturnEvenlyDividedWidth(heightMeasureSpec, innerWidth);
260         } else {
261             usedWidth = measureAndGetUsedWidth(widthMeasureSpec, heightMeasureSpec, innerWidth,
262                     false /* collapsePriorityButtons */);
263             if (mNumPriorityChildren != 0 && usedWidth >= innerWidth) {
264                 usedWidth = measureAndGetUsedWidth(widthMeasureSpec, heightMeasureSpec, innerWidth,
265                         true /* collapsePriorityButtons */);
266             }
267         }
268 
269         mTotalWidth = usedWidth + mPaddingRight + mPaddingLeft + mExtraStartPadding;
270         setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec),
271                 resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec));
272     }
273 
rebuildMeasureOrder(int capacityText, int capacityOther)274     private void rebuildMeasureOrder(int capacityText, int capacityOther) {
275         clearMeasureOrder();
276         mMeasureOrderTextViews.ensureCapacity(capacityText);
277         mMeasureOrderOther.ensureCapacity(capacityOther);
278         final int childCount = getChildCount();
279         for (int i = 0; i < childCount; i++) {
280             View c = getChildAt(i);
281             if (c instanceof TextView && ((TextView) c).getText().length() > 0) {
282                 mMeasureOrderTextViews.add(new TextViewInfo((TextView) c));
283             } else {
284                 mMeasureOrderOther.add(c);
285             }
286         }
287         mMeasureOrderTextViews.sort(MEASURE_ORDER_COMPARATOR);
288     }
289 
clearMeasureOrder()290     private void clearMeasureOrder() {
291         mMeasureOrderOther.clear();
292         mMeasureOrderTextViews.clear();
293     }
294 
295     @Override
onViewAdded(View child)296     public void onViewAdded(View child) {
297         super.onViewAdded(child);
298         clearMeasureOrder();
299         // For some reason ripples + notification actions seem to be an unhappy combination
300         // b/69474443 so just turn them off for now.
301         if (child.getBackground() instanceof RippleDrawable) {
302             ((RippleDrawable)child.getBackground()).setForceSoftware(true);
303         }
304     }
305 
306     @Override
onViewRemoved(View child)307     public void onViewRemoved(View child) {
308         super.onViewRemoved(child);
309         clearMeasureOrder();
310     }
311 
312     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)313     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
314         final boolean isLayoutRtl = isLayoutRtl();
315         final int paddingTop = mPaddingTop;
316         final boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0;
317 
318         int childTop;
319         int childLeft;
320         if (centerAligned) {
321             childLeft = mPaddingLeft + left + (right - left) / 2 - mTotalWidth / 2;
322         } else {
323             childLeft = mPaddingLeft;
324             int absoluteGravity = Gravity.getAbsoluteGravity(Gravity.START, getLayoutDirection());
325             if (absoluteGravity == Gravity.RIGHT) {
326                 childLeft += right - left - mTotalWidth;
327             } else {
328                 // Put the extra start padding (if any) on the left when LTR
329                 childLeft += mExtraStartPadding;
330             }
331         }
332 
333 
334         // Where bottom of child should go
335         final int height = bottom - top;
336 
337         // Space available for child
338         int innerHeight = height - paddingTop - mPaddingBottom;
339 
340         final int count = getChildCount();
341 
342         int start = 0;
343         int dir = 1;
344         //In case of RTL, start drawing from the last child.
345         if (isLayoutRtl) {
346             start = count - 1;
347             dir = -1;
348         }
349 
350         for (int i = 0; i < count; i++) {
351             final int childIndex = start + dir * i;
352             final View child = getChildAt(childIndex);
353             if (child.getVisibility() != GONE) {
354                 final int childWidth = child.getMeasuredWidth();
355                 final int childHeight = child.getMeasuredHeight();
356 
357                 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
358 
359                 childTop = paddingTop + ((innerHeight - childHeight) / 2)
360                             + lp.topMargin - lp.bottomMargin;
361 
362                 childLeft += lp.leftMargin;
363                 child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
364                 childLeft += childWidth + lp.rightMargin;
365             }
366         }
367     }
368 
369     @Override
onFinishInflate()370     protected void onFinishInflate() {
371         super.onFinishInflate();
372         if (!notificationsRedesignTemplates()) {
373             mDefaultPaddingBottom = getPaddingBottom();
374             mDefaultPaddingTop = getPaddingTop();
375             updateHeights();
376         }
377     }
378 
updateHeights()379     private void updateHeights() {
380         if (notificationsRedesignTemplates()) {
381             return;
382         }
383         int inset = getResources().getDimensionPixelSize(
384                 com.android.internal.R.dimen.button_inset_vertical_material);
385         mEmphasizedPaddingTop = getResources().getDimensionPixelSize(
386                 com.android.internal.R.dimen.notification_content_margin) - inset;
387         // same padding on bottom and at end
388         mEmphasizedPaddingBottom = getResources().getDimensionPixelSize(
389                 com.android.internal.R.dimen.notification_content_margin_end) - inset;
390         mEmphasizedHeight = mEmphasizedPaddingTop + mEmphasizedPaddingBottom
391                 + getResources().getDimensionPixelSize(
392                         com.android.internal.R.dimen.notification_action_emphasized_height);
393         mRegularHeight = getResources().getDimensionPixelSize(
394                 com.android.internal.R.dimen.notification_action_list_height);
395     }
396 
397     /**
398      * When buttons are in wrap mode, this is a padding that will be applied at the start of the
399      * layout of the actions, but only when those actions would fit with the entire padding
400      * visible.  Otherwise, this padding will be omitted entirely.
401      */
402     @RemotableViewMethod
setCollapsibleIndentDimen(@imenRes int collapsibleIndentDimen)403     public void setCollapsibleIndentDimen(@DimenRes int collapsibleIndentDimen) {
404         if (mCollapsibleIndentDimen != collapsibleIndentDimen) {
405             mCollapsibleIndentDimen = collapsibleIndentDimen;
406             requestLayout();
407         }
408     }
409 
410     /**
411      * Sets whether the available width should be distributed evenly among the action buttons.
412      *
413      * When enabled, the available width (after subtracting this layout's padding and all of the
414      * buttons' margins) is divided by the number of (not-GONE) buttons, and each button is forced
415      * to that exact width, even if it is less <em>or more</em> width than they need.
416      *
417      * When disabled, the available width is allocated as buttons need; if that exceeds the
418      * available width, priority buttons are collapsed to just their icon to save space.
419      *
420      * @param evenlyDividedMode whether to enable evenly divided mode
421      */
422     @RemotableViewMethod
setEvenlyDividedMode(boolean evenlyDividedMode)423     public void setEvenlyDividedMode(boolean evenlyDividedMode) {
424         if (evenlyDividedMode && !evenlyDividedCallStyleActionLayout()) {
425             Log.e(TAG, "setEvenlyDividedMode(true) called with new action layout disabled; "
426                     + "leaving evenly divided mode disabled");
427             return;
428         }
429 
430         if (evenlyDividedMode == mEvenlyDividedMode) {
431             return;
432         }
433 
434         if (DEBUG_NEW_ACTION_LAYOUT) {
435             Log.v(TAG, "evenlyDividedMode changed to " + evenlyDividedMode + "; "
436                     + "requesting layout");
437         }
438         mEvenlyDividedMode = evenlyDividedMode;
439         requestLayout();
440     }
441 
442     /**
443      * Set whether the list is in a mode where some actions are emphasized. This will trigger an
444      * equal measuring where all actions are full height and change a few parameters like
445      * the padding.
446      */
447     @RemotableViewMethod
setEmphasizedMode(boolean emphasizedMode)448     public void setEmphasizedMode(boolean emphasizedMode) {
449         if (notificationsRedesignTemplates()) {
450             return;
451         }
452         mEmphasizedMode = emphasizedMode;
453         int height;
454         if (emphasizedMode) {
455             setPaddingRelative(getPaddingStart(),
456                     mEmphasizedPaddingTop,
457                     getPaddingEnd(),
458                     mEmphasizedPaddingBottom);
459             setMinimumHeight(mEmphasizedHeight);
460             height = ViewGroup.LayoutParams.WRAP_CONTENT;
461         } else {
462             setPaddingRelative(getPaddingStart(),
463                     mDefaultPaddingTop,
464                     getPaddingEnd(),
465                     mDefaultPaddingBottom);
466             height = mRegularHeight;
467         }
468         ViewGroup.LayoutParams layoutParams = getLayoutParams();
469         layoutParams.height = height;
470         setLayoutParams(layoutParams);
471     }
472 
getExtraMeasureHeight()473     public int getExtraMeasureHeight() {
474         // Note: the emphasized height is no longer different from the regular height when the
475         // notificationsRedesignTemplates flag is on.
476         if (!notificationsRedesignTemplates() && mEmphasizedMode) {
477             return mEmphasizedHeight - mRegularHeight;
478         }
479         return 0;
480     }
481 
482     public static final Comparator<TextViewInfo> MEASURE_ORDER_COMPARATOR = (a, b) -> {
483         int priorityComparison = -Boolean.compare(a.mIsPriority, b.mIsPriority);
484         return priorityComparison != 0
485                 ? priorityComparison
486                 : Integer.compare(a.mTextLength, b.mTextLength);
487     };
488 
489     private static final class TextViewInfo {
490         final boolean mIsPriority;
491         final int mTextLength;
492         final TextView mTextView;
493 
TextViewInfo(TextView textView)494         TextViewInfo(TextView textView) {
495             this.mIsPriority = isPriority(textView);
496             this.mTextLength = textView.getText().length();
497             this.mTextView = textView;
498         }
499 
needsRebuild()500         boolean needsRebuild() {
501             return mTextView.getText().length() != mTextLength
502                     || isPriority(mTextView) != mIsPriority;
503         }
504     }
505 
506     private static final String TAG = "NotificationActionListLayout";
507 }
508