blob: e013553ec046da04283a65ef6b808786729692af [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
Lucas Dupinbd9798f2017-10-24 18:04:51 -070019import android.annotation.Nullable;
Adrian Roos61254352016-04-25 15:45:04 -070020import android.content.Context;
Lucas Dupinbd9798f2017-10-24 18:04:51 -070021import android.content.res.Resources;
22import android.content.res.TypedArray;
Selim Cinek06e9e1f2016-07-08 17:14:16 -070023import android.graphics.drawable.Drawable;
John Reck4aef5262017-12-07 14:55:26 -080024import android.graphics.drawable.RippleDrawable;
Adrian Roos61254352016-04-25 15:45:04 -070025import android.util.AttributeSet;
26import android.util.Pair;
27import android.view.Gravity;
Selim Cinek06e9e1f2016-07-08 17:14:16 -070028import android.view.RemotableViewMethod;
Adrian Roos61254352016-04-25 15:45:04 -070029import android.view.View;
Selim Cinek06e9e1f2016-07-08 17:14:16 -070030import android.widget.LinearLayout;
Adrian Roos61254352016-04-25 15:45:04 -070031import android.widget.RemoteViews;
32import android.widget.TextView;
33
34import java.util.ArrayList;
35import java.util.Comparator;
36
37/**
38 * Layout for notification actions that ensures that no action consumes more than their share of
39 * the remaining available width, and the last action consumes the remaining space.
40 */
41@RemoteViews.RemoteView
Selim Cinek06e9e1f2016-07-08 17:14:16 -070042public class NotificationActionListLayout extends LinearLayout {
Adrian Roos61254352016-04-25 15:45:04 -070043
Lucas Dupinbd9798f2017-10-24 18:04:51 -070044 private final int mGravity;
Adrian Roos61254352016-04-25 15:45:04 -070045 private int mTotalWidth = 0;
46 private ArrayList<Pair<Integer, TextView>> mMeasureOrderTextViews = new ArrayList<>();
47 private ArrayList<View> mMeasureOrderOther = new ArrayList<>();
Selim Cinek06e9e1f2016-07-08 17:14:16 -070048 private boolean mMeasureLinearly;
49 private int mDefaultPaddingEnd;
50 private Drawable mDefaultBackground;
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 Cinek06e9e1f2016-07-08 17:14:16 -070071 if (mMeasureLinearly) {
72 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
80 View lastNotGoneChild = null;
81 for (int i = 0; i < N; i++) {
82 View c = getChildAt(i);
83 if (c instanceof TextView) {
84 textViews++;
85 } else {
86 otherViews++;
87 }
88 if (c.getVisibility() != GONE) {
89 notGoneChildren++;
90 lastNotGoneChild = c;
91 }
92 }
93
94 // Rebuild the measure order if the number of children changed or the text length of
95 // any of the children changed.
96 boolean needRebuild = false;
97 if (textViews != mMeasureOrderTextViews.size()
98 || otherViews != mMeasureOrderOther.size()) {
99 needRebuild = true;
100 }
101 if (!needRebuild) {
102 final int size = mMeasureOrderTextViews.size();
103 for (int i = 0; i < size; i++) {
104 Pair<Integer, TextView> pair = mMeasureOrderTextViews.get(i);
105 if (pair.first != pair.second.getText().length()) {
106 needRebuild = true;
107 }
108 }
109 }
110 if (notGoneChildren > 1 && needRebuild) {
111 rebuildMeasureOrder(textViews, otherViews);
112 }
113
114 final boolean constrained =
115 MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED;
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700116 final boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0;
Adrian Roos61254352016-04-25 15:45:04 -0700117
118 final int innerWidth = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight;
119 final int otherSize = mMeasureOrderOther.size();
120 int usedWidth = 0;
121
122 // Optimization: Don't do this if there's only one child.
123 int measuredChildren = 0;
124 for (int i = 0; i < N && notGoneChildren > 1; i++) {
125 // Measure shortest children first. To avoid measuring twice, we approximate by looking
126 // at the text length.
127 View c;
128 if (i < otherSize) {
129 c = mMeasureOrderOther.get(i);
130 } else {
131 c = mMeasureOrderTextViews.get(i - otherSize).second;
132 }
133 if (c.getVisibility() == GONE) {
134 continue;
135 }
136 MarginLayoutParams lp = (MarginLayoutParams) c.getLayoutParams();
137
138 int usedWidthForChild = usedWidth;
139 if (constrained) {
140 // Make sure that this child doesn't consume more than its share of the remaining
141 // total available space. Not used space will benefit subsequent views. Since we
142 // measure in the order of (approx.) size, a large view can still take more than its
143 // share if the others are small.
144 int availableWidth = innerWidth - usedWidth;
145 int maxWidthForChild = availableWidth / (notGoneChildren - measuredChildren);
146
147 usedWidthForChild = innerWidth - maxWidthForChild;
148 }
149
150 measureChildWithMargins(c, widthMeasureSpec, usedWidthForChild,
151 heightMeasureSpec, 0 /* usedHeight */);
152
153 usedWidth += c.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
154 measuredChildren++;
155 }
156
157 // Make sure to measure the last child full-width if we didn't use up the entire width,
158 // or we didn't measure yet because there's just one child.
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700159 if (lastNotGoneChild != null && !centerAligned && (constrained && usedWidth < innerWidth
Adrian Roos61254352016-04-25 15:45:04 -0700160 || notGoneChildren == 1)) {
161 MarginLayoutParams lp = (MarginLayoutParams) lastNotGoneChild.getLayoutParams();
162 if (notGoneChildren > 1) {
163 // Need to make room, since we already measured this once.
164 usedWidth -= lastNotGoneChild.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
165 }
166
167 int originalWidth = lp.width;
168 lp.width = LayoutParams.MATCH_PARENT;
169 measureChildWithMargins(lastNotGoneChild, widthMeasureSpec, usedWidth,
170 heightMeasureSpec, 0 /* usedHeight */);
171 lp.width = originalWidth;
172
173 usedWidth += lastNotGoneChild.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
174 }
175
176 mTotalWidth = usedWidth + mPaddingRight + mPaddingLeft;
177 setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec),
178 resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec));
179 }
180
181 private void rebuildMeasureOrder(int capacityText, int capacityOther) {
182 clearMeasureOrder();
183 mMeasureOrderTextViews.ensureCapacity(capacityText);
184 mMeasureOrderOther.ensureCapacity(capacityOther);
185 final int childCount = getChildCount();
186 for (int i = 0; i < childCount; i++) {
187 View c = getChildAt(i);
188 if (c instanceof TextView && ((TextView) c).getText().length() > 0) {
189 mMeasureOrderTextViews.add(Pair.create(((TextView) c).getText().length(),
190 (TextView)c));
191 } else {
192 mMeasureOrderOther.add(c);
193 }
194 }
195 mMeasureOrderTextViews.sort(MEASURE_ORDER_COMPARATOR);
196 }
197
198 private void clearMeasureOrder() {
199 mMeasureOrderOther.clear();
200 mMeasureOrderTextViews.clear();
201 }
202
203 @Override
204 public void onViewAdded(View child) {
205 super.onViewAdded(child);
206 clearMeasureOrder();
John Reck4aef5262017-12-07 14:55:26 -0800207 // For some reason ripples + notification actions seem to be an unhappy combination
208 // b/69474443 so just turn them off for now.
209 if (child.getBackground() instanceof RippleDrawable) {
210 ((RippleDrawable)child.getBackground()).setForceSoftware(true);
211 }
Adrian Roos61254352016-04-25 15:45:04 -0700212 }
213
214 @Override
215 public void onViewRemoved(View child) {
216 super.onViewRemoved(child);
217 clearMeasureOrder();
218 }
219
220 @Override
221 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700222 if (mMeasureLinearly) {
223 super.onLayout(changed, left, top, right, bottom);
224 return;
225 }
Adrian Roos61254352016-04-25 15:45:04 -0700226 final boolean isLayoutRtl = isLayoutRtl();
227 final int paddingTop = mPaddingTop;
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700228 final boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0;
Adrian Roos61254352016-04-25 15:45:04 -0700229
230 int childTop;
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700231 int childLeft = centerAligned ? left + (right - left) / 2 - mTotalWidth / 2 : 0;
Adrian Roos61254352016-04-25 15:45:04 -0700232
233 // Where bottom of child should go
234 final int height = bottom - top;
235
236 // Space available for child
237 int innerHeight = height - paddingTop - mPaddingBottom;
238
239 final int count = getChildCount();
240
241 final int layoutDirection = getLayoutDirection();
242 switch (Gravity.getAbsoluteGravity(Gravity.START, layoutDirection)) {
243 case Gravity.RIGHT:
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700244 childLeft += mPaddingLeft + right - left - mTotalWidth;
Adrian Roos61254352016-04-25 15:45:04 -0700245 break;
246
247 case Gravity.LEFT:
248 default:
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700249 childLeft += mPaddingLeft;
Adrian Roos61254352016-04-25 15:45:04 -0700250 break;
251 }
252
253 int start = 0;
254 int dir = 1;
255 //In case of RTL, start drawing from the last child.
256 if (isLayoutRtl) {
257 start = count - 1;
258 dir = -1;
259 }
260
261 for (int i = 0; i < count; i++) {
262 final int childIndex = start + dir * i;
263 final View child = getChildAt(childIndex);
264 if (child.getVisibility() != GONE) {
265 final int childWidth = child.getMeasuredWidth();
266 final int childHeight = child.getMeasuredHeight();
267
268 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
269
270 childTop = paddingTop + ((innerHeight - childHeight) / 2)
271 + lp.topMargin - lp.bottomMargin;
272
273 childLeft += lp.leftMargin;
274 child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
275 childLeft += childWidth + lp.rightMargin;
276 }
277 }
278 }
279
280 @Override
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700281 protected void onFinishInflate() {
282 super.onFinishInflate();
283 mDefaultPaddingEnd = getPaddingEnd();
284 mDefaultBackground = getBackground();
Adrian Roos61254352016-04-25 15:45:04 -0700285 }
286
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700287 /**
288 * Set whether the list is in a mode where some actions are emphasized. This will trigger an
289 * equal measuring where all actions are full height and change a few parameters like
290 * the padding.
291 */
292 @RemotableViewMethod
293 public void setEmphasizedMode(boolean emphasizedMode) {
294 mMeasureLinearly = emphasizedMode;
295 setPaddingRelative(getPaddingStart(), getPaddingTop(),
296 emphasizedMode ? 0 : mDefaultPaddingEnd, getPaddingBottom());
297 setBackground(emphasizedMode ? null : mDefaultBackground);
298 requestLayout();
Adrian Roos61254352016-04-25 15:45:04 -0700299 }
300
301 public static final Comparator<Pair<Integer, TextView>> MEASURE_ORDER_COMPARATOR
302 = (a, b) -> a.first.compareTo(b.first);
303}