blob: c7ea781b2793f59638bf880dee51f1fc68a78a45 [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 Dupin71e38a12018-05-22 15:02:01 -0700209 int childLeft;
210 if (centerAligned) {
211 childLeft = mPaddingLeft + left + (right - left) / 2 - mTotalWidth / 2;
212 } else {
213 childLeft = mPaddingLeft;
214 int absoluteGravity = Gravity.getAbsoluteGravity(Gravity.START, getLayoutDirection());
215 if (absoluteGravity == Gravity.RIGHT) {
216 childLeft += right - left - mTotalWidth;
217 }
218 }
219
Adrian Roos61254352016-04-25 15:45:04 -0700220
221 // Where bottom of child should go
222 final int height = bottom - top;
223
224 // Space available for child
225 int innerHeight = height - paddingTop - mPaddingBottom;
226
227 final int count = getChildCount();
228
Adrian Roos61254352016-04-25 15:45:04 -0700229 int start = 0;
230 int dir = 1;
231 //In case of RTL, start drawing from the last child.
232 if (isLayoutRtl) {
233 start = count - 1;
234 dir = -1;
235 }
236
237 for (int i = 0; i < count; i++) {
238 final int childIndex = start + dir * i;
239 final View child = getChildAt(childIndex);
240 if (child.getVisibility() != GONE) {
241 final int childWidth = child.getMeasuredWidth();
242 final int childHeight = child.getMeasuredHeight();
243
244 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
245
246 childTop = paddingTop + ((innerHeight - childHeight) / 2)
247 + lp.topMargin - lp.bottomMargin;
248
249 childLeft += lp.leftMargin;
250 child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
251 childLeft += childWidth + lp.rightMargin;
252 }
253 }
254 }
255
256 @Override
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700257 protected void onFinishInflate() {
258 super.onFinishInflate();
Selim Cinek396caca2018-04-10 17:46:46 -0700259 mDefaultPaddingBottom = getPaddingBottom();
260 mDefaultPaddingTop = getPaddingTop();
261 updateHeights();
262 }
263
264 private void updateHeights() {
265 int paddingTop = getResources().getDimensionPixelSize(
266 com.android.internal.R.dimen.notification_content_margin);
267 // same padding on bottom and at end
268 int paddingBottom = getResources().getDimensionPixelSize(
269 com.android.internal.R.dimen.notification_content_margin_end);
270 mEmphasizedHeight = paddingBottom + paddingTop + getResources().getDimensionPixelSize(
271 com.android.internal.R.dimen.notification_action_emphasized_height);
272 mRegularHeight = getResources().getDimensionPixelSize(
273 com.android.internal.R.dimen.notification_action_list_height);
Adrian Roos61254352016-04-25 15:45:04 -0700274 }
275
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700276 /**
277 * Set whether the list is in a mode where some actions are emphasized. This will trigger an
278 * equal measuring where all actions are full height and change a few parameters like
279 * the padding.
280 */
281 @RemotableViewMethod
282 public void setEmphasizedMode(boolean emphasizedMode) {
Selim Cinek396caca2018-04-10 17:46:46 -0700283 mEmphasizedMode = emphasizedMode;
284 int height;
285 if (emphasizedMode) {
286 int paddingTop = getResources().getDimensionPixelSize(
287 com.android.internal.R.dimen.notification_content_margin);
288 // same padding on bottom and at end
289 int paddingBottom = getResources().getDimensionPixelSize(
290 com.android.internal.R.dimen.notification_content_margin_end);
291 height = mEmphasizedHeight;
292 int buttonPaddingInternal = getResources().getDimensionPixelSize(
293 com.android.internal.R.dimen.button_inset_vertical_material);
294 setPaddingRelative(getPaddingStart(),
295 paddingTop - buttonPaddingInternal,
296 getPaddingEnd(),
297 paddingBottom - buttonPaddingInternal);
298 } else {
299 setPaddingRelative(getPaddingStart(),
300 mDefaultPaddingTop,
301 getPaddingEnd(),
302 mDefaultPaddingBottom);
303 height = mRegularHeight;
304 }
305 ViewGroup.LayoutParams layoutParams = getLayoutParams();
306 layoutParams.height = height;
307 setLayoutParams(layoutParams);
308 }
309
310 public int getExtraMeasureHeight() {
311 if (mEmphasizedMode) {
312 return mEmphasizedHeight - mRegularHeight;
313 }
314 return 0;
Adrian Roos61254352016-04-25 15:45:04 -0700315 }
316
317 public static final Comparator<Pair<Integer, TextView>> MEASURE_ORDER_COMPARATOR
318 = (a, b) -> a.first.compareTo(b.first);
319}