blob: 9048f87ef512f6371850028f92f46896b4b840e4 [file] [log] [blame]
Adrian Roos61254352016-04-25 15:45:04 -07001/*
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
17package com.android.internal.widget;
18
19import android.content.Context;
Lucas Dupinbd9798f2017-10-24 18:04:51 -070020import android.content.res.TypedArray;
John Reck4aef5262017-12-07 14:55:26 -080021import android.graphics.drawable.RippleDrawable;
Adrian Roos61254352016-04-25 15:45:04 -070022import android.util.AttributeSet;
23import android.util.Pair;
24import android.view.Gravity;
Selim Cinek06e9e1f2016-07-08 17:14:16 -070025import android.view.RemotableViewMethod;
Adrian Roos61254352016-04-25 15:45:04 -070026import android.view.View;
Selim Cinek396caca2018-04-10 17:46:46 -070027import android.view.ViewGroup;
Selim Cinek06e9e1f2016-07-08 17:14:16 -070028import android.widget.LinearLayout;
Adrian Roos61254352016-04-25 15:45:04 -070029import android.widget.RemoteViews;
30import android.widget.TextView;
31
32import java.util.ArrayList;
33import java.util.Comparator;
34
35/**
36 * Layout for notification actions that ensures that no action consumes more than their share of
37 * the remaining available width, and the last action consumes the remaining space.
38 */
39@RemoteViews.RemoteView
Selim Cinek06e9e1f2016-07-08 17:14:16 -070040public class NotificationActionListLayout extends LinearLayout {
Adrian Roos61254352016-04-25 15:45:04 -070041
Lucas Dupinbd9798f2017-10-24 18:04:51 -070042 private final int mGravity;
Adrian Roos61254352016-04-25 15:45:04 -070043 private int mTotalWidth = 0;
44 private ArrayList<Pair<Integer, TextView>> mMeasureOrderTextViews = new ArrayList<>();
45 private ArrayList<View> mMeasureOrderOther = new ArrayList<>();
Selim Cinek396caca2018-04-10 17:46:46 -070046 private boolean mEmphasizedMode;
47 private int mDefaultPaddingBottom;
48 private int mDefaultPaddingTop;
49 private int mEmphasizedHeight;
50 private int mRegularHeight;
Adrian Roos61254352016-04-25 15:45:04 -070051
52 public NotificationActionListLayout(Context context, AttributeSet attrs) {
Lucas Dupinbd9798f2017-10-24 18:04:51 -070053 this(context, attrs, 0);
54 }
55
56 public NotificationActionListLayout(Context context, AttributeSet attrs, int defStyleAttr) {
57 this(context, attrs, defStyleAttr, 0);
58 }
59
60 public NotificationActionListLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
61 super(context, attrs, defStyleAttr, defStyleRes);
62
63 int[] attrIds = { android.R.attr.gravity };
64 TypedArray ta = context.obtainStyledAttributes(attrs, attrIds, defStyleAttr, defStyleRes);
65 mGravity = ta.getInt(0, 0);
66 ta.recycle();
Adrian Roos61254352016-04-25 15:45:04 -070067 }
68
69 @Override
70 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Selim Cinek396caca2018-04-10 17:46:46 -070071 if (mEmphasizedMode) {
Selim Cinek06e9e1f2016-07-08 17:14:16 -070072 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
73 return;
74 }
Adrian Roos61254352016-04-25 15:45:04 -070075 final int N = getChildCount();
76 int textViews = 0;
77 int otherViews = 0;
78 int notGoneChildren = 0;
79
Adrian Roos61254352016-04-25 15:45:04 -070080 for (int i = 0; i < N; i++) {
81 View c = getChildAt(i);
82 if (c instanceof TextView) {
83 textViews++;
84 } else {
85 otherViews++;
86 }
87 if (c.getVisibility() != GONE) {
88 notGoneChildren++;
Adrian Roos61254352016-04-25 15:45:04 -070089 }
90 }
91
92 // Rebuild the measure order if the number of children changed or the text length of
93 // any of the children changed.
94 boolean needRebuild = false;
95 if (textViews != mMeasureOrderTextViews.size()
96 || otherViews != mMeasureOrderOther.size()) {
97 needRebuild = true;
98 }
99 if (!needRebuild) {
100 final int size = mMeasureOrderTextViews.size();
101 for (int i = 0; i < size; i++) {
102 Pair<Integer, TextView> pair = mMeasureOrderTextViews.get(i);
103 if (pair.first != pair.second.getText().length()) {
104 needRebuild = true;
105 }
106 }
107 }
Lucas Dupinfc7036c2018-04-12 17:45:29 -0700108
shawnlina7045c92018-04-27 18:23:13 +0800109 if (needRebuild) {
Adrian Roos61254352016-04-25 15:45:04 -0700110 rebuildMeasureOrder(textViews, otherViews);
111 }
112
113 final boolean constrained =
114 MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED;
115
116 final int innerWidth = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight;
117 final int otherSize = mMeasureOrderOther.size();
118 int usedWidth = 0;
119
Adrian Roos61254352016-04-25 15:45:04 -0700120 int measuredChildren = 0;
shawnlina7045c92018-04-27 18:23:13 +0800121 for (int i = 0; i < N; i++) {
Adrian Roos61254352016-04-25 15:45:04 -0700122 // Measure shortest children first. To avoid measuring twice, we approximate by looking
123 // at the text length.
124 View c;
125 if (i < otherSize) {
126 c = mMeasureOrderOther.get(i);
127 } else {
128 c = mMeasureOrderTextViews.get(i - otherSize).second;
129 }
130 if (c.getVisibility() == GONE) {
131 continue;
132 }
133 MarginLayoutParams lp = (MarginLayoutParams) c.getLayoutParams();
134
135 int usedWidthForChild = usedWidth;
136 if (constrained) {
137 // Make sure that this child doesn't consume more than its share of the remaining
138 // total available space. Not used space will benefit subsequent views. Since we
139 // measure in the order of (approx.) size, a large view can still take more than its
140 // share if the others are small.
141 int availableWidth = innerWidth - usedWidth;
142 int maxWidthForChild = availableWidth / (notGoneChildren - measuredChildren);
143
144 usedWidthForChild = innerWidth - maxWidthForChild;
145 }
146
147 measureChildWithMargins(c, widthMeasureSpec, usedWidthForChild,
148 heightMeasureSpec, 0 /* usedHeight */);
149
150 usedWidth += c.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
151 measuredChildren++;
152 }
153
Adrian Roos61254352016-04-25 15:45:04 -0700154 mTotalWidth = usedWidth + mPaddingRight + mPaddingLeft;
155 setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec),
156 resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec));
157 }
158
159 private void rebuildMeasureOrder(int capacityText, int capacityOther) {
160 clearMeasureOrder();
161 mMeasureOrderTextViews.ensureCapacity(capacityText);
162 mMeasureOrderOther.ensureCapacity(capacityOther);
163 final int childCount = getChildCount();
164 for (int i = 0; i < childCount; i++) {
165 View c = getChildAt(i);
166 if (c instanceof TextView && ((TextView) c).getText().length() > 0) {
167 mMeasureOrderTextViews.add(Pair.create(((TextView) c).getText().length(),
168 (TextView)c));
169 } else {
170 mMeasureOrderOther.add(c);
171 }
172 }
173 mMeasureOrderTextViews.sort(MEASURE_ORDER_COMPARATOR);
174 }
175
176 private void clearMeasureOrder() {
177 mMeasureOrderOther.clear();
178 mMeasureOrderTextViews.clear();
179 }
180
181 @Override
182 public void onViewAdded(View child) {
183 super.onViewAdded(child);
184 clearMeasureOrder();
John Reck4aef5262017-12-07 14:55:26 -0800185 // For some reason ripples + notification actions seem to be an unhappy combination
186 // b/69474443 so just turn them off for now.
187 if (child.getBackground() instanceof RippleDrawable) {
188 ((RippleDrawable)child.getBackground()).setForceSoftware(true);
189 }
Adrian Roos61254352016-04-25 15:45:04 -0700190 }
191
192 @Override
193 public void onViewRemoved(View child) {
194 super.onViewRemoved(child);
195 clearMeasureOrder();
196 }
197
198 @Override
199 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Selim Cinek396caca2018-04-10 17:46:46 -0700200 if (mEmphasizedMode) {
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700201 super.onLayout(changed, left, top, right, bottom);
202 return;
203 }
Adrian Roos61254352016-04-25 15:45:04 -0700204 final boolean isLayoutRtl = isLayoutRtl();
205 final int paddingTop = mPaddingTop;
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700206 final boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0;
Adrian Roos61254352016-04-25 15:45:04 -0700207
208 int childTop;
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700209 int childLeft = centerAligned ? left + (right - left) / 2 - mTotalWidth / 2 : 0;
Adrian Roos61254352016-04-25 15:45:04 -0700210
211 // Where bottom of child should go
212 final int height = bottom - top;
213
214 // Space available for child
215 int innerHeight = height - paddingTop - mPaddingBottom;
216
217 final int count = getChildCount();
218
219 final int layoutDirection = getLayoutDirection();
220 switch (Gravity.getAbsoluteGravity(Gravity.START, layoutDirection)) {
221 case Gravity.RIGHT:
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700222 childLeft += mPaddingLeft + right - left - mTotalWidth;
Adrian Roos61254352016-04-25 15:45:04 -0700223 break;
224
225 case Gravity.LEFT:
226 default:
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700227 childLeft += mPaddingLeft;
Adrian Roos61254352016-04-25 15:45:04 -0700228 break;
229 }
230
231 int start = 0;
232 int dir = 1;
233 //In case of RTL, start drawing from the last child.
234 if (isLayoutRtl) {
235 start = count - 1;
236 dir = -1;
237 }
238
239 for (int i = 0; i < count; i++) {
240 final int childIndex = start + dir * i;
241 final View child = getChildAt(childIndex);
242 if (child.getVisibility() != GONE) {
243 final int childWidth = child.getMeasuredWidth();
244 final int childHeight = child.getMeasuredHeight();
245
246 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
247
248 childTop = paddingTop + ((innerHeight - childHeight) / 2)
249 + lp.topMargin - lp.bottomMargin;
250
251 childLeft += lp.leftMargin;
252 child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
253 childLeft += childWidth + lp.rightMargin;
254 }
255 }
256 }
257
258 @Override
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700259 protected void onFinishInflate() {
260 super.onFinishInflate();
Selim Cinek396caca2018-04-10 17:46:46 -0700261 mDefaultPaddingBottom = getPaddingBottom();
262 mDefaultPaddingTop = getPaddingTop();
263 updateHeights();
264 }
265
266 private void updateHeights() {
267 int paddingTop = getResources().getDimensionPixelSize(
268 com.android.internal.R.dimen.notification_content_margin);
269 // same padding on bottom and at end
270 int paddingBottom = getResources().getDimensionPixelSize(
271 com.android.internal.R.dimen.notification_content_margin_end);
272 mEmphasizedHeight = paddingBottom + paddingTop + getResources().getDimensionPixelSize(
273 com.android.internal.R.dimen.notification_action_emphasized_height);
274 mRegularHeight = getResources().getDimensionPixelSize(
275 com.android.internal.R.dimen.notification_action_list_height);
Adrian Roos61254352016-04-25 15:45:04 -0700276 }
277
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700278 /**
279 * Set whether the list is in a mode where some actions are emphasized. This will trigger an
280 * equal measuring where all actions are full height and change a few parameters like
281 * the padding.
282 */
283 @RemotableViewMethod
284 public void setEmphasizedMode(boolean emphasizedMode) {
Selim Cinek396caca2018-04-10 17:46:46 -0700285 mEmphasizedMode = emphasizedMode;
286 int height;
287 if (emphasizedMode) {
288 int paddingTop = getResources().getDimensionPixelSize(
289 com.android.internal.R.dimen.notification_content_margin);
290 // same padding on bottom and at end
291 int paddingBottom = getResources().getDimensionPixelSize(
292 com.android.internal.R.dimen.notification_content_margin_end);
293 height = mEmphasizedHeight;
294 int buttonPaddingInternal = getResources().getDimensionPixelSize(
295 com.android.internal.R.dimen.button_inset_vertical_material);
296 setPaddingRelative(getPaddingStart(),
297 paddingTop - buttonPaddingInternal,
298 getPaddingEnd(),
299 paddingBottom - buttonPaddingInternal);
300 } else {
301 setPaddingRelative(getPaddingStart(),
302 mDefaultPaddingTop,
303 getPaddingEnd(),
304 mDefaultPaddingBottom);
305 height = mRegularHeight;
306 }
307 ViewGroup.LayoutParams layoutParams = getLayoutParams();
308 layoutParams.height = height;
309 setLayoutParams(layoutParams);
310 }
311
312 public int getExtraMeasureHeight() {
313 if (mEmphasizedMode) {
314 return mEmphasizedHeight - mRegularHeight;
315 }
316 return 0;
Adrian Roos61254352016-04-25 15:45:04 -0700317 }
318
319 public static final Comparator<Pair<Integer, TextView>> MEASURE_ORDER_COMPARATOR
320 = (a, b) -> a.first.compareTo(b.first);
321}