1 /* 2 * Copyright (C) 2024 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.notifLinearlayoutOptimized; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.graphics.drawable.Drawable; 26 import android.os.Build; 27 import android.os.Trace; 28 import android.util.AttributeSet; 29 import android.util.Log; 30 import android.view.Gravity; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.LinearLayout; 34 import android.widget.RemoteViews; 35 36 import java.util.ArrayList; 37 import java.util.List; 38 39 /** 40 * This LinearLayout customizes the measurement behavior of LinearLayout for Notification layouts. 41 * When there is exactly 42 * one child View with <code>layout_weight</code>. onMeasure methods of this LinearLayout will: 43 * 1. Measure all other children. 44 * 2. Calculate the remaining space for the View with <code>layout_weight</code> 45 * 3. Measure the weighted View using the calculated remaining width or height (based on 46 * Orientation). 47 * This ensures that the weighted View fills the remaining space in LinearLayout with only single 48 * measure. 49 * 50 * **Assumptions:** 51 * - There is *exactly one* child view with non-zero <code>layout_weight</code>. 52 * - Other views should not have weight. 53 * - LinearLayout doesn't have <code>weightSum</code>. 54 * - Horizontal LinearLayout's width should be measured EXACTLY. 55 * - Horizontal LinearLayout shouldn't need baseLineAlignment. 56 * - Horizontal LinearLayout shouldn't have any child that has negative left or right margin. 57 * - Vertical LinearLayout shouldn't have MATCH_PARENT children when it is not measured EXACTLY. 58 * 59 * @hide 60 */ 61 @RemoteViews.RemoteView 62 public class NotificationOptimizedLinearLayout extends LinearLayout { 63 private static final boolean DEBUG_LAYOUT = false; 64 private static final boolean TRACE_ONMEASURE = Build.isDebuggable(); 65 private static final String TAG = "NotifOptimizedLinearLayout"; 66 67 private boolean mShouldUseOptimizedLayout = false; 68 NotificationOptimizedLinearLayout(Context context)69 public NotificationOptimizedLinearLayout(Context context) { 70 super(context); 71 } 72 NotificationOptimizedLinearLayout(Context context, @Nullable AttributeSet attrs)73 public NotificationOptimizedLinearLayout(Context context, @Nullable AttributeSet attrs) { 74 super(context, attrs); 75 } 76 NotificationOptimizedLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr)77 public NotificationOptimizedLinearLayout(Context context, @Nullable AttributeSet attrs, 78 int defStyleAttr) { 79 super(context, attrs, defStyleAttr); 80 } 81 NotificationOptimizedLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)82 public NotificationOptimizedLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, 83 int defStyleRes) { 84 super(context, attrs, defStyleAttr, defStyleRes); 85 } 86 87 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)88 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 89 final View weightedChildView = getSingleWeightedChild(); 90 mShouldUseOptimizedLayout = 91 isUseOptimizedLinearLayoutFlagEnabled() && weightedChildView != null 92 && isOptimizationPossible(widthMeasureSpec, heightMeasureSpec); 93 94 if (mShouldUseOptimizedLayout) { 95 onMeasureOptimized(weightedChildView, widthMeasureSpec, heightMeasureSpec); 96 } else { 97 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 98 } 99 } 100 isUseOptimizedLinearLayoutFlagEnabled()101 private boolean isUseOptimizedLinearLayoutFlagEnabled() { 102 final boolean enabled = notifLinearlayoutOptimized(); 103 if (!enabled) { 104 logSkipOptimizedOnMeasure("enableNotifLinearlayoutOptimized flag is off."); 105 } 106 return enabled; 107 } 108 109 /** 110 * Checks if optimizations can be safely applied to this LinearLayout during layout 111 * calculations. Optimizations might be disabled in the following cases: 112 * 113 * **weightSum**: When LinearLayout has weightSum 114 * ** MATCH_PARENT children in non EXACT dimension** 115 * **Horizontal LinearLayout with non-EXACT width** 116 * **Baseline Alignment:** If views need to align their baselines in Horizontal LinearLayout 117 * 118 * @param widthMeasureSpec The width measurement specification. 119 * @param heightMeasureSpec The height measurement specification. 120 * @return `true` if optimization is possible, `false` otherwise. 121 */ isOptimizationPossible(int widthMeasureSpec, int heightMeasureSpec)122 private boolean isOptimizationPossible(int widthMeasureSpec, int heightMeasureSpec) { 123 final boolean hasWeightSum = getWeightSum() > 0.0f; 124 if (hasWeightSum) { 125 logSkipOptimizedOnMeasure("Has weightSum."); 126 return false; 127 } 128 129 if (requiresMatchParentRemeasureForVerticalLinearLayout(widthMeasureSpec)) { 130 logSkipOptimizedOnMeasure( 131 "Vertical LinearLayout requires children width MATCH_PARENT remeasure "); 132 return false; 133 } 134 135 final boolean isHorizontal = getOrientation() == HORIZONTAL; 136 if (isHorizontal && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) { 137 logSkipOptimizedOnMeasure("Horizontal LinearLayout's width should be " 138 + "measured EXACTLY"); 139 return false; 140 } 141 142 if (requiresBaselineAlignmentForHorizontalLinearLayout()) { 143 logSkipOptimizedOnMeasure("Need to apply baseline."); 144 return false; 145 } 146 147 if (requiresNegativeMarginHandlingForHorizontalLinearLayout()) { 148 logSkipOptimizedOnMeasure("Need to handle negative margins."); 149 return false; 150 } 151 return true; 152 } 153 154 /** 155 * @return if the horizontal linearlayout requires to handle negative margins in its children. 156 * In that case, we can't use excessSpace because LinearLayout negative margin handling for 157 * excess space and WRAP_CONTENT is different. 158 */ requiresNegativeMarginHandlingForHorizontalLinearLayout()159 private boolean requiresNegativeMarginHandlingForHorizontalLinearLayout() { 160 if (getOrientation() == VERTICAL) { 161 return false; 162 } 163 164 final List<View> activeChildren = getActiveChildren(); 165 for (int i = 0; i < activeChildren.size(); i++) { 166 final View child = activeChildren.get(i); 167 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 168 if (lp.leftMargin < 0 || lp.rightMargin < 0) { 169 return true; 170 } 171 } 172 return false; 173 } 174 175 /** 176 * @return if the vertical linearlayout requires match_parent children remeasure 177 */ requiresMatchParentRemeasureForVerticalLinearLayout(int widthMeasureSpec)178 private boolean requiresMatchParentRemeasureForVerticalLinearLayout(int widthMeasureSpec) { 179 // HORIZONTAL measuring is handled by LinearLayout. That's why we don't need to check it 180 // here. 181 if (getOrientation() == HORIZONTAL) { 182 return false; 183 } 184 185 // When the width is not EXACT, children with MATCH_PARENT width need to be double measured. 186 // This needs to be handled in LinearLayout because NotificationOptimizedLinearLayout 187 final boolean nonExactWidth = 188 MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY; 189 final List<View> activeChildren = getActiveChildren(); 190 for (int i = 0; i < activeChildren.size(); i++) { 191 final View child = activeChildren.get(i); 192 final ViewGroup.LayoutParams lp = child.getLayoutParams(); 193 if (nonExactWidth && lp.width == ViewGroup.LayoutParams.MATCH_PARENT) { 194 return true; 195 } 196 } 197 return false; 198 } 199 200 /** 201 * @return if this layout needs to apply baseLineAlignment. 202 */ requiresBaselineAlignmentForHorizontalLinearLayout()203 private boolean requiresBaselineAlignmentForHorizontalLinearLayout() { 204 // baseLineAlignment is not important for Vertical LinearLayout. 205 if (getOrientation() == VERTICAL) { 206 return false; 207 } 208 // Early return, if it is already disabled 209 if (!isBaselineAligned()) { 210 return false; 211 } 212 213 final List<View> activeChildren = getActiveChildren(); 214 final int minorGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; 215 216 for (int i = 0; i < activeChildren.size(); i++) { 217 final View child = activeChildren.get(i); 218 if (child.getLayoutParams() instanceof LayoutParams) { 219 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 220 int childBaseline = -1; 221 222 if (lp.height != LayoutParams.MATCH_PARENT) { 223 childBaseline = child.getBaseline(); 224 } 225 if (childBaseline == -1) { 226 // This child doesn't have a baseline. 227 continue; 228 } 229 int gravity = lp.gravity; 230 if (gravity < 0) { 231 gravity = minorGravity; 232 } 233 234 final int result = gravity & Gravity.VERTICAL_GRAVITY_MASK; 235 if (result == Gravity.TOP || result == Gravity.BOTTOM) { 236 return true; 237 } 238 } 239 } 240 return false; 241 } 242 243 /** 244 * Finds the single child view within this layout that has a non-zero weight assigned to its 245 * LayoutParams. 246 * 247 * @return The weighted child view, or null if multiple weighted children exist or no weighted 248 * children are found. 249 */ 250 @Nullable getSingleWeightedChild()251 private View getSingleWeightedChild() { 252 final boolean isVertical = getOrientation() == VERTICAL; 253 final List<View> activeChildren = getActiveChildren(); 254 View singleWeightedChild = null; 255 for (int i = 0; i < activeChildren.size(); i++) { 256 final View child = activeChildren.get(i); 257 if (child.getLayoutParams() instanceof LayoutParams) { 258 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 259 if ((!isVertical && lp.width == ViewGroup.LayoutParams.MATCH_PARENT) 260 || (isVertical && lp.height == ViewGroup.LayoutParams.MATCH_PARENT)) { 261 logSkipOptimizedOnMeasure( 262 "There is a match parent child in the related orientation."); 263 return null; 264 } 265 if (lp.weight != 0) { 266 if (singleWeightedChild == null) { 267 singleWeightedChild = child; 268 } else { 269 logSkipOptimizedOnMeasure("There is more than one weighted child."); 270 return null; 271 } 272 } 273 } 274 } 275 if (singleWeightedChild == null) { 276 logSkipOptimizedOnMeasure("There is no weighted child in this layout."); 277 } else { 278 final LayoutParams lp = (LayoutParams) singleWeightedChild.getLayoutParams(); 279 boolean isHeightWrapContentOrZero = 280 lp.height == ViewGroup.LayoutParams.WRAP_CONTENT || lp.height == 0; 281 boolean isWidthWrapContentOrZero = 282 lp.width == ViewGroup.LayoutParams.WRAP_CONTENT || lp.width == 0; 283 if ((isVertical && !isHeightWrapContentOrZero) 284 || (!isVertical && !isWidthWrapContentOrZero)) { 285 logSkipOptimizedOnMeasure( 286 "Single weighted child should be either WRAP_CONTENT or 0" 287 + " in the related orientation"); 288 singleWeightedChild = null; 289 } 290 } 291 292 return singleWeightedChild; 293 } 294 295 /** 296 * Optimized measurement for the single weighted child in this LinearLayout. 297 * Measures other children, calculates remaining space, then measures the weighted 298 * child using the remaining width (or height). 299 * 300 * Note: Horizontal LinearLayout doesn't need to apply baseline in optimized case @see 301 * {@link #requiresBaselineAlignmentForHorizontalLinearLayout}. 302 * 303 * @param weightedChildView The weighted child view(with `layout_weight!=0`) 304 * @param widthMeasureSpec The width MeasureSpec to use for measurement 305 * @param heightMeasureSpec The height MeasureSpec to use for measurement. 306 */ onMeasureOptimized(@onNull View weightedChildView, int widthMeasureSpec, int heightMeasureSpec)307 private void onMeasureOptimized(@NonNull View weightedChildView, int widthMeasureSpec, 308 int heightMeasureSpec) { 309 try { 310 if (TRACE_ONMEASURE) { 311 Trace.beginSection("NotifOptimizedLinearLayout#onMeasure"); 312 } 313 314 if (getOrientation() == LinearLayout.HORIZONTAL) { 315 final ViewGroup.LayoutParams lp = weightedChildView.getLayoutParams(); 316 final int childWidth = lp.width; 317 final boolean isBaselineAligned = isBaselineAligned(); 318 // It should be marked 0 so that it use excessSpace in LinearLayout's onMeasure 319 lp.width = 0; 320 321 // It doesn't need to apply baseline. So disable it. 322 setBaselineAligned(false); 323 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 324 325 // restore values. 326 lp.width = childWidth; 327 setBaselineAligned(isBaselineAligned); 328 } else { 329 measureVerticalOptimized(weightedChildView, widthMeasureSpec, heightMeasureSpec); 330 } 331 } finally { 332 if (TRACE_ONMEASURE) { 333 trackShouldUseOptimizedLayout(); 334 Trace.endSection(); 335 } 336 } 337 } 338 339 @Override onLayout(boolean changed, int l, int t, int r, int b)340 protected void onLayout(boolean changed, int l, int t, int r, int b) { 341 if (mShouldUseOptimizedLayout) { 342 onLayoutOptimized(changed, l, t, r, b); 343 } else { 344 super.onLayout(changed, l, t, r, b); 345 } 346 } 347 onLayoutOptimized(boolean changed, int l, int t, int r, int b)348 private void onLayoutOptimized(boolean changed, int l, int t, int r, int b) { 349 if (getOrientation() == LinearLayout.HORIZONTAL) { 350 super.onLayout(changed, l, t, r, b); 351 } else { 352 layoutVerticalOptimized(l, t, r, b); 353 } 354 } 355 356 /** 357 * Optimized measurement for the single weighted child in this LinearLayout. 358 * Measures other children, calculates remaining space, then measures the weighted 359 * child using the exact remaining height. 360 * 361 * @param weightedChildView The weighted child view(with `layout_weight=1` 362 * @param widthMeasureSpec The width MeasureSpec to use for measurement 363 * @param heightMeasureSpec The height MeasureSpec to use for measurement. 364 */ measureVerticalOptimized(@onNull View weightedChildView, int widthMeasureSpec, int heightMeasureSpec)365 private void measureVerticalOptimized(@NonNull View weightedChildView, int widthMeasureSpec, 366 int heightMeasureSpec) { 367 int totalLength = 0; 368 int maxWidth = 0; 369 final int availableHeight = MeasureSpec.getSize(heightMeasureSpec); 370 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 371 372 // 1. Measure all unweighted children 373 for (int i = 0; i < getChildCount(); i++) { 374 final View child = getChildAt(i); 375 if (child == null || child.getVisibility() == GONE) { 376 continue; 377 } 378 379 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 380 381 if (child == weightedChildView) { 382 // In excessMode, LinearLayout add weighted child top and bottom margins to 383 // totalLength when their sum is positive. 384 if (lp.height == 0 && heightMode == MeasureSpec.EXACTLY) { 385 totalLength = Math.max(totalLength, totalLength + lp.topMargin 386 + lp.bottomMargin); 387 } 388 continue; 389 } 390 391 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); 392 // LinearLayout only adds measured children heights and its top and bottom margins 393 // to totalLength when their sum is positive. 394 totalLength = Math.max(totalLength, 395 totalLength + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); 396 maxWidth = Math.max(maxWidth, 397 child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); 398 } 399 400 // Add padding to totalLength that we are going to use for remaining space. 401 totalLength += mPaddingTop + mPaddingBottom; 402 403 // 2. generate measure spec for weightedChildView. 404 final MarginLayoutParams lp = (MarginLayoutParams) weightedChildView.getLayoutParams(); 405 // height should be AT_MOST for non EXACT cases. 406 final int childHeightMeasureMode = 407 heightMode == MeasureSpec.EXACTLY ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST; 408 final int childHeightMeasureSpec; 409 410 // In excess mode, LinearLayout measures weighted children with remaining space. Otherwise, 411 // it is measured with remaining space just like other children. 412 if (lp.height == 0 && heightMode == MeasureSpec.EXACTLY) { 413 childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( 414 Math.max(0, availableHeight - totalLength), childHeightMeasureMode); 415 } else { 416 final int usedHeight = lp.topMargin + lp.bottomMargin + totalLength; 417 childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( 418 Math.max(0, availableHeight - usedHeight), childHeightMeasureMode); 419 } 420 final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 421 mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin, lp.width); 422 423 // 3. Measure weightedChildView with the remaining space. 424 weightedChildView.measure(childWidthMeasureSpec, childHeightMeasureSpec); 425 426 totalLength = Math.max(totalLength, 427 totalLength + weightedChildView.getMeasuredHeight() + lp.topMargin 428 + lp.bottomMargin); 429 430 maxWidth = Math.max(maxWidth, 431 weightedChildView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); 432 433 // Add padding to width 434 maxWidth += getPaddingLeft() + getPaddingRight(); 435 436 // Resolve final dimensions 437 final int finalWidth = resolveSizeAndState(Math.max(maxWidth, getSuggestedMinimumWidth()), 438 widthMeasureSpec, 0); 439 final int finalHeight = resolveSizeAndState( 440 Math.max(totalLength, getSuggestedMinimumHeight()), heightMeasureSpec, 0); 441 setMeasuredDimension(finalWidth, finalHeight); 442 } 443 444 @NonNull getActiveChildren()445 private List<View> getActiveChildren() { 446 final int childCount = getChildCount(); 447 final List<View> activeChildren = new ArrayList<>(); 448 for (int i = 0; i < childCount; i++) { 449 final View child = getChildAt(i); 450 if (child == null || child.getVisibility() == View.GONE) { 451 continue; 452 } 453 activeChildren.add(child); 454 } 455 return activeChildren; 456 } 457 458 //region LinearLayout copy methods 459 460 /** 461 * layoutVerticalOptimized is a version of LinearLayout's layoutVertical method that 462 * excludes 463 * TableRow-related functionalities. 464 * 465 * @see LinearLayout#onLayout(boolean, int, int, int, int) 466 */ layoutVerticalOptimized(int left, int top, int right, int bottom)467 private void layoutVerticalOptimized(int left, int top, int right, 468 int bottom) { 469 final int paddingLeft = mPaddingLeft; 470 final int mTotalLength = getMeasuredHeight(); 471 int childTop; 472 int childLeft; 473 474 // Where right end of child should go 475 final int width = right - left; 476 int childRight = width - mPaddingRight; 477 478 // Space available for child 479 int childSpace = width - paddingLeft - mPaddingRight; 480 481 final int count = getChildCount(); 482 483 final int majorGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; 484 final int minorGravity = getGravity() & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; 485 486 switch (majorGravity) { 487 case Gravity.BOTTOM: 488 // mTotalLength contains the padding already 489 childTop = mPaddingTop + bottom - top - mTotalLength; 490 break; 491 492 // mTotalLength contains the padding already 493 case Gravity.CENTER_VERTICAL: 494 childTop = mPaddingTop + (bottom - top - mTotalLength) / 2; 495 break; 496 497 case Gravity.TOP: 498 default: 499 childTop = mPaddingTop; 500 break; 501 } 502 final int dividerHeight = getDividerHeight(); 503 for (int i = 0; i < count; i++) { 504 final View child = getChildAt(i); 505 if (child != null && child.getVisibility() != GONE) { 506 final int childWidth = child.getMeasuredWidth(); 507 final int childHeight = child.getMeasuredHeight(); 508 509 final LinearLayout.LayoutParams lp = 510 (LinearLayout.LayoutParams) child.getLayoutParams(); 511 512 int gravity = lp.gravity; 513 if (gravity < 0) { 514 gravity = minorGravity; 515 } 516 final int layoutDirection = getLayoutDirection(); 517 final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, 518 layoutDirection); 519 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 520 case Gravity.CENTER_HORIZONTAL: 521 childLeft = 522 paddingLeft + ((childSpace - childWidth) / 2) + lp.leftMargin 523 - lp.rightMargin; 524 break; 525 526 case Gravity.RIGHT: 527 childLeft = childRight - childWidth - lp.rightMargin; 528 break; 529 530 case Gravity.LEFT: 531 default: 532 childLeft = paddingLeft + lp.leftMargin; 533 break; 534 } 535 536 if (hasDividerBeforeChildAt(i)) { 537 childTop += dividerHeight; 538 } 539 540 childTop += lp.topMargin; 541 child.layout(childLeft, childTop, childLeft + childWidth, 542 childTop + childHeight); 543 childTop += childHeight + lp.bottomMargin; 544 545 } 546 } 547 } 548 549 /** 550 * Used in laying out views vertically. 551 * 552 * @see #layoutVerticalOptimized 553 * @see LinearLayout#onLayout(boolean, int, int, int, int) 554 */ getDividerHeight()555 private int getDividerHeight() { 556 final Drawable dividerDrawable = getDividerDrawable(); 557 if (dividerDrawable == null) { 558 return 0; 559 } else { 560 return dividerDrawable.getIntrinsicHeight(); 561 } 562 } 563 //endregion 564 565 //region Logging&Tracing trackShouldUseOptimizedLayout()566 private void trackShouldUseOptimizedLayout() { 567 if (TRACE_ONMEASURE) { 568 Trace.setCounter("NotifOptimizedLinearLayout#shouldUseOptimizedLayout", 569 mShouldUseOptimizedLayout ? 1 : 0); 570 } 571 } 572 logSkipOptimizedOnMeasure(String reason)573 private void logSkipOptimizedOnMeasure(String reason) { 574 if (DEBUG_LAYOUT) { 575 final StringBuilder logMessage = new StringBuilder(); 576 int layoutId = getId(); 577 if (layoutId != NO_ID) { 578 final Resources resources = getResources(); 579 if (resources != null) { 580 logMessage.append("["); 581 logMessage.append(resources.getResourceName(layoutId)); 582 logMessage.append("] "); 583 } 584 } 585 logMessage.append("Going to skip onMeasureOptimized reason:"); 586 logMessage.append(reason); 587 588 Log.d(TAG, logMessage.toString()); 589 } 590 } 591 //endregion 592 } 593