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.widget.flags.Flags.messagingChildRequestLayout; 20 21 import android.annotation.Nullable; 22 import android.annotation.Px; 23 import android.content.Context; 24 import android.content.res.TypedArray; 25 import android.graphics.Canvas; 26 import android.os.Build; 27 import android.os.Trace; 28 import android.util.AttributeSet; 29 import android.view.RemotableViewMethod; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.view.ViewParent; 33 import android.widget.RemoteViews; 34 35 import com.android.internal.R; 36 37 /** 38 * A custom-built layout for the Notification.MessagingStyle. 39 * 40 * Evicts children until they all fit. 41 */ 42 @RemoteViews.RemoteView 43 public class MessagingLinearLayout extends ViewGroup { 44 45 /** 46 * Spacing to be applied between views. 47 */ 48 private int mSpacing; 49 50 private int mMaxDisplayedLines = Integer.MAX_VALUE; 51 52 private static final boolean TRACE_ONMEASURE = Build.isDebuggable(); 53 MessagingLinearLayout(Context context, @Nullable AttributeSet attrs)54 public MessagingLinearLayout(Context context, @Nullable AttributeSet attrs) { 55 super(context, attrs); 56 57 final TypedArray a = context.obtainStyledAttributes(attrs, 58 R.styleable.MessagingLinearLayout, 0, 59 0); 60 61 final int N = a.getIndexCount(); 62 for (int i = 0; i < N; i++) { 63 int attr = a.getIndex(i); 64 switch (attr) { 65 case R.styleable.MessagingLinearLayout_spacing: 66 mSpacing = a.getDimensionPixelSize(i, 0); 67 break; 68 } 69 } 70 71 a.recycle(); 72 } 73 74 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)75 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 76 if (TRACE_ONMEASURE) { 77 Trace.beginSection("MessagingLinearLayout#onMeasure"); 78 trackMeasureSpecs(widthMeasureSpec, heightMeasureSpec); 79 } 80 // This is essentially a bottom-up linear layout that only adds children that fit entirely 81 // up to a maximum height. 82 int targetHeight = MeasureSpec.getSize(heightMeasureSpec); 83 switch (MeasureSpec.getMode(heightMeasureSpec)) { 84 case MeasureSpec.UNSPECIFIED: 85 targetHeight = Integer.MAX_VALUE; 86 break; 87 } 88 89 // Now that we know which views to take, fix up the indents and see what width we get. 90 int measuredWidth = mPaddingLeft + mPaddingRight; 91 final int count = getChildCount(); 92 int totalHeight; 93 for (int i = 0; i < count; ++i) { 94 final View child = getChildAt(i); 95 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 96 lp.hide = true; 97 // Child always needs to be measured to calculate hide property correctly in onMeasure. 98 if (messagingChildRequestLayout()) { 99 child.requestLayout(); 100 } 101 if (child instanceof MessagingChild) { 102 MessagingChild messagingChild = (MessagingChild) child; 103 // Whenever we encounter the message first, it's always first in the layout 104 messagingChild.setIsFirstInLayout(true); 105 } 106 } 107 108 totalHeight = mPaddingTop + mPaddingBottom; 109 boolean first = true; 110 int linesRemaining = mMaxDisplayedLines; 111 // Starting from the bottom: we measure every view as if it were the only one. If it still 112 // fits, we take it, otherwise we stop there. 113 MessagingChild previousChild = null; 114 View previousView = null; 115 int previousChildHeight = 0; 116 int previousTotalHeight = 0; 117 int previousLinesConsumed = 0; 118 for (int i = count - 1; i >= 0 && totalHeight < targetHeight; i--) { 119 if (getChildAt(i).getVisibility() == GONE) { 120 continue; 121 } 122 final View child = getChildAt(i); 123 LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); 124 MessagingChild messagingChild = null; 125 int spacing = mSpacing; 126 int previousChildIncrease = 0; 127 if (child instanceof MessagingChild) { 128 // We need to remeasure the previous child again if it's not the first anymore 129 if (previousChild != null && previousChild.hasDifferentHeightWhenFirst()) { 130 previousChild.setIsFirstInLayout(false); 131 measureChildWithMargins(previousView, widthMeasureSpec, 0, heightMeasureSpec, 132 previousTotalHeight - previousChildHeight); 133 previousChildIncrease = previousView.getMeasuredHeight() - previousChildHeight; 134 linesRemaining -= previousChild.getConsumedLines() - previousLinesConsumed; 135 } 136 messagingChild = (MessagingChild) child; 137 messagingChild.setMaxDisplayedLines(linesRemaining); 138 spacing += messagingChild.getExtraSpacing(); 139 } 140 spacing = first ? 0 : spacing; 141 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight 142 - mPaddingTop - mPaddingBottom + spacing); 143 144 final int childHeight = child.getMeasuredHeight(); 145 int newHeight = Math.max(totalHeight, totalHeight + childHeight + lp.topMargin + 146 lp.bottomMargin + spacing + previousChildIncrease); 147 int measureType = MessagingChild.MEASURED_NORMAL; 148 if (messagingChild != null) { 149 measureType = messagingChild.getMeasuredType(); 150 } 151 152 // We never measure the first item as too small, we want to at least show something. 153 boolean isTooSmall = measureType == MessagingChild.MEASURED_TOO_SMALL && !first; 154 boolean isShortened = measureType == MessagingChild.MEASURED_SHORTENED 155 || measureType == MessagingChild.MEASURED_TOO_SMALL && first; 156 boolean showView = newHeight <= targetHeight && !isTooSmall; 157 if (showView) { 158 if (messagingChild != null) { 159 previousLinesConsumed = messagingChild.getConsumedLines(); 160 linesRemaining -= previousLinesConsumed; 161 previousChild = messagingChild; 162 previousView = child; 163 previousChildHeight = childHeight; 164 previousTotalHeight = totalHeight; 165 } 166 totalHeight = newHeight; 167 measuredWidth = Math.max(measuredWidth, 168 child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin 169 + mPaddingLeft + mPaddingRight); 170 lp.hide = false; 171 if (isShortened || linesRemaining <= 0) { 172 break; 173 } 174 } else { 175 // We now became too short, let's make sure to reset any previous views to be first 176 // and remeasure it. 177 if (previousChild != null && previousChild.hasDifferentHeightWhenFirst()) { 178 previousChild.setIsFirstInLayout(true); 179 // We need to remeasure the previous child again since it became first 180 measureChildWithMargins(previousView, widthMeasureSpec, 0, heightMeasureSpec, 181 previousTotalHeight - previousChildHeight); 182 // The totalHeight is already correct here since we only set it during the 183 // first pass 184 } 185 break; 186 } 187 first = false; 188 } 189 190 setMeasuredDimension( 191 resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth), 192 widthMeasureSpec), 193 Math.max(getSuggestedMinimumHeight(), totalHeight)); 194 if (TRACE_ONMEASURE) { 195 Trace.endSection(); 196 } 197 } 198 199 @Override onLayout(boolean changed, int left, int top, int right, int bottom)200 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 201 final int paddingLeft = mPaddingLeft; 202 203 int childTop; 204 205 // Where right end of child should go 206 final int width = right - left; 207 final int childRight = width - mPaddingRight; 208 209 final int layoutDirection = getLayoutDirection(); 210 final int count = getChildCount(); 211 212 childTop = mPaddingTop; 213 214 boolean first = true; 215 final boolean shown = isShown(); 216 for (int i = 0; i < count; i++) { 217 final View child = getChildAt(i); 218 if (child.getVisibility() == GONE) { 219 continue; 220 } 221 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 222 MessagingChild messagingChild = (MessagingChild) child; 223 224 final int childWidth = child.getMeasuredWidth(); 225 final int childHeight = child.getMeasuredHeight(); 226 227 int childLeft; 228 if (layoutDirection == LAYOUT_DIRECTION_RTL) { 229 childLeft = childRight - childWidth - lp.rightMargin; 230 } else { 231 childLeft = paddingLeft + lp.leftMargin; 232 } 233 if (lp.hide) { 234 if (shown && lp.visibleBefore) { 235 // We still want to lay out the child to have great animations 236 child.layout(childLeft, childTop, childLeft + childWidth, 237 childTop + lp.lastVisibleHeight); 238 messagingChild.hideAnimated(); 239 } 240 lp.visibleBefore = false; 241 continue; 242 } else { 243 lp.visibleBefore = true; 244 lp.lastVisibleHeight = childHeight; 245 } 246 247 if (!first) { 248 childTop += mSpacing; 249 } 250 251 childTop += lp.topMargin; 252 child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); 253 254 childTop += childHeight + lp.bottomMargin; 255 256 first = false; 257 } 258 } 259 trackMeasureSpecs(int widthMeasureSpec, int heightMeasureSpec)260 private void trackMeasureSpecs(int widthMeasureSpec, int heightMeasureSpec) { 261 if (!TRACE_ONMEASURE) { 262 return; 263 } 264 265 final int availableWidth = MeasureSpec.getSize(widthMeasureSpec); 266 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 267 final int availableHeight = MeasureSpec.getSize(heightMeasureSpec); 268 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 269 Trace.setCounter("MessagingLinearLayout#onMeasure_widthMeasureSpecSize", 270 availableWidth); 271 Trace.setCounter("MessagingLinearLayout#onMeasure_widthMeasureSpecMode", 272 widthMode); 273 Trace.setCounter("MessagingLinearLayout#onMeasure_heightMeasureSpecSize", 274 availableHeight); 275 Trace.setCounter("MessagingLinearLayout#onMeasure_heightMeasureSpecMode", 276 heightMode); 277 } 278 279 @Override drawChild(Canvas canvas, View child, long drawingTime)280 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 281 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 282 if (lp.hide) { 283 MessagingChild messagingChild = (MessagingChild) child; 284 if (!messagingChild.isHidingAnimated()) { 285 return true; 286 } 287 } 288 return super.drawChild(canvas, child, drawingTime); 289 } 290 291 /** 292 * Set the spacing to be applied between views. 293 */ setSpacing(@x int spacing)294 public void setSpacing(@Px int spacing) { 295 if (mSpacing != spacing) { 296 mSpacing = spacing; 297 requestLayout(); 298 } 299 } 300 301 @Override generateLayoutParams(AttributeSet attrs)302 public LayoutParams generateLayoutParams(AttributeSet attrs) { 303 return new LayoutParams(mContext, attrs); 304 } 305 306 @Override generateDefaultLayoutParams()307 protected LayoutParams generateDefaultLayoutParams() { 308 return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 309 310 } 311 312 @Override generateLayoutParams(ViewGroup.LayoutParams lp)313 protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 314 LayoutParams copy = new LayoutParams(lp.width, lp.height); 315 if (lp instanceof MarginLayoutParams) { 316 copy.copyMarginsFrom((MarginLayoutParams) lp); 317 } 318 return copy; 319 } 320 isGone(View view)321 public static boolean isGone(View view) { 322 if (view.getVisibility() == View.GONE) { 323 return true; 324 } 325 final ViewGroup.LayoutParams lp = view.getLayoutParams(); 326 if (lp instanceof MessagingLinearLayout.LayoutParams 327 && ((MessagingLinearLayout.LayoutParams) lp).hide) { 328 return true; 329 } 330 return false; 331 } 332 333 /** 334 * Sets how many lines should be displayed at most 335 */ 336 @RemotableViewMethod setMaxDisplayedLines(int numberLines)337 public void setMaxDisplayedLines(int numberLines) { 338 mMaxDisplayedLines = numberLines; 339 } 340 getMessagingLayout()341 public IMessagingLayout getMessagingLayout() { 342 View view = this; 343 while (true) { 344 ViewParent p = view.getParent(); 345 if (p instanceof View) { 346 view = (View) p; 347 if (view instanceof IMessagingLayout) { 348 return (IMessagingLayout) view; 349 } 350 } else { 351 return null; 352 } 353 } 354 } 355 356 @Override getBaseline()357 public int getBaseline() { 358 // When placed in a horizontal linear layout (as is the case in a single-line MessageGroup), 359 // align with the last visible child (which is the one that will be displayed in the single- 360 // line group. 361 int childCount = getChildCount(); 362 for (int i = childCount - 1; i >= 0; i--) { 363 final View child = getChildAt(i); 364 if (isGone(child)) { 365 continue; 366 } 367 final int childBaseline = child.getBaseline(); 368 if (childBaseline == -1) { 369 return -1; 370 } 371 MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 372 return lp.topMargin + childBaseline; 373 } 374 return super.getBaseline(); 375 } 376 377 public interface MessagingChild { 378 int MEASURED_NORMAL = 0; 379 int MEASURED_SHORTENED = 1; 380 int MEASURED_TOO_SMALL = 2; 381 getMeasuredType()382 int getMeasuredType(); getConsumedLines()383 int getConsumedLines(); setMaxDisplayedLines(int lines)384 void setMaxDisplayedLines(int lines); hideAnimated()385 void hideAnimated(); isHidingAnimated()386 boolean isHidingAnimated(); 387 388 /** 389 * Set that this view is first in layout. Relevant and only set if 390 * {@link #hasDifferentHeightWhenFirst()}. 391 * @param first is this first? 392 */ setIsFirstInLayout(boolean first)393 default void setIsFirstInLayout(boolean first) {} 394 395 /** 396 * @return if this layout has different height it is first in the layout 397 */ hasDifferentHeightWhenFirst()398 default boolean hasDifferentHeightWhenFirst() { 399 return false; 400 } getExtraSpacing()401 default int getExtraSpacing() { 402 return 0; 403 } recycle()404 void recycle(); 405 } 406 407 public static class LayoutParams extends MarginLayoutParams { 408 409 public boolean hide = false; 410 public boolean visibleBefore = false; 411 public int lastVisibleHeight; 412 LayoutParams(Context c, AttributeSet attrs)413 public LayoutParams(Context c, AttributeSet attrs) { 414 super(c, attrs); 415 } 416 LayoutParams(int width, int height)417 public LayoutParams(int width, int height) { 418 super(width, height); 419 } 420 } 421 } 422