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 android.content.Context; 20 import android.content.res.TypedArray; 21 import android.graphics.drawable.RippleDrawable; 22 import android.util.AttributeSet; 23 import android.util.Pair; 24 import android.view.Gravity; 25 import android.view.RemotableViewMethod; 26 import android.view.View; 27 import android.view.ViewGroup; 28 import android.widget.LinearLayout; 29 import android.widget.RemoteViews; 30 import android.widget.TextView; 31 32 import java.util.ArrayList; 33 import 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 40 public class NotificationActionListLayout extends LinearLayout { 41 42 private final int mGravity; 43 private int mTotalWidth = 0; 44 private ArrayList<Pair<Integer, TextView>> mMeasureOrderTextViews = new ArrayList<>(); 45 private ArrayList<View> mMeasureOrderOther = new ArrayList<>(); 46 private boolean mEmphasizedMode; 47 private int mDefaultPaddingBottom; 48 private int mDefaultPaddingTop; 49 private int mEmphasizedHeight; 50 private int mRegularHeight; 51 52 public NotificationActionListLayout(Context context, AttributeSet attrs) { 53 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(); 67 } 68 69 @Override 70 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 71 if (mEmphasizedMode) { 72 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 73 return; 74 } 75 final int N = getChildCount(); 76 int textViews = 0; 77 int otherViews = 0; 78 int notGoneChildren = 0; 79 80 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++; 89 } 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 } 108 109 if (needRebuild) { 110 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 120 int measuredChildren = 0; 121 for (int i = 0; i < N; i++) { 122 // 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 154 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(); 185 // 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 } 190 } 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) { 200 if (mEmphasizedMode) { 201 super.onLayout(changed, left, top, right, bottom); 202 return; 203 } 204 final boolean isLayoutRtl = isLayoutRtl(); 205 final int paddingTop = mPaddingTop; 206 final boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0; 207 208 int childTop; 209 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 220 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 229 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 257 protected void onFinishInflate() { 258 super.onFinishInflate(); 259 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); 274 } 275 276 /** 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) { 283 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; 315 } 316 317 public static final Comparator<Pair<Integer, TextView>> MEASURE_ORDER_COMPARATOR 318 = (a, b) -> a.first.compareTo(b.first); 319 } 320