1 /* 2 * Copyright 2022 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 androidx.wear.protolayout.material.layouts; 18 19 import static androidx.annotation.Dimension.DP; 20 import static androidx.wear.protolayout.DimensionBuilders.dp; 21 import static androidx.wear.protolayout.DimensionBuilders.expand; 22 import static androidx.wear.protolayout.DimensionBuilders.wrap; 23 import static androidx.wear.protolayout.material.ChipDefaults.MIN_TAPPABLE_SQUARE_LENGTH; 24 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.DEFAULT_VERTICAL_SPACER_HEIGHT; 25 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.LAYOUTS_LABEL_PADDING_PERCENT; 26 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_PERCENT; 27 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_ROUND_DP; 28 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_SQUARE_DP; 29 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_BOTTOM_ROUND_PERCENT; 30 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_BOTTOM_SQUARE_PERCENT; 31 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_HORIZONTAL_ROUND_PERCENT; 32 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_HORIZONTAL_SQUARE_PERCENT; 33 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_TOP_ROUND_PERCENT; 34 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_TOP_SQUARE_PERCENT; 35 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_ROUND_DP; 36 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_SQUARE_DP; 37 import static androidx.wear.protolayout.material.layouts.LayoutDefaults.insetElementWithPadding; 38 import static androidx.wear.protolayout.materialcore.Helper.checkNotNull; 39 import static androidx.wear.protolayout.materialcore.Helper.checkTag; 40 import static androidx.wear.protolayout.materialcore.Helper.getMetadataTagBytes; 41 import static androidx.wear.protolayout.materialcore.Helper.getTagBytes; 42 import static androidx.wear.protolayout.materialcore.Helper.isRoundDevice; 43 44 import android.annotation.SuppressLint; 45 46 import androidx.annotation.Dimension; 47 import androidx.annotation.IntDef; 48 import androidx.annotation.RestrictTo; 49 import androidx.annotation.RestrictTo.Scope; 50 import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters; 51 import androidx.wear.protolayout.DimensionBuilders.DpProp; 52 import androidx.wear.protolayout.DimensionBuilders.SpacerDimension; 53 import androidx.wear.protolayout.LayoutElementBuilders; 54 import androidx.wear.protolayout.LayoutElementBuilders.Box; 55 import androidx.wear.protolayout.LayoutElementBuilders.Column; 56 import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement; 57 import androidx.wear.protolayout.LayoutElementBuilders.Spacer; 58 import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata; 59 import androidx.wear.protolayout.ModifiersBuilders.Modifiers; 60 import androidx.wear.protolayout.ModifiersBuilders.Padding; 61 import androidx.wear.protolayout.expression.Fingerprint; 62 import androidx.wear.protolayout.material.CompactChip; 63 import androidx.wear.protolayout.proto.LayoutElementProto; 64 65 import org.jspecify.annotations.NonNull; 66 import org.jspecify.annotations.Nullable; 67 68 import java.lang.annotation.Retention; 69 import java.lang.annotation.RetentionPolicy; 70 import java.util.Arrays; 71 import java.util.List; 72 73 /** 74 * ProtoLayout layout that represents a suggested layout style for Material ProtoLayout with the 75 * primary (compact) chip at the bottom with the given content in the center and the recommended 76 * margin and padding applied. There is a fixed slot for an optional primary label above or optional 77 * secondary label below the main content area. Visuals and design samples can be found 78 * <a href="https://developer.android.com/design/ui/wear/guides/surfaces/tiles-layouts#layout-templates">here</a>. 79 * 80 * <p>It is highly recommended that main content has max lines between 2 and 4 (dependant on labels 81 * present), i.e.: * No labels are present: content with max 4 lines, * 1 label is present: content 82 * with max 3 lines, * 2 labels are present: content with max 2 lines. 83 * 84 * <p>When accessing the contents of a container for testing, note that this element can't be simply 85 * casted back to the original type, i.e.: 86 * 87 * <pre>{@code 88 * PrimaryLayout pl = new PrimaryLayout... 89 * Box box = new Box.Builder().addContent(pl).build(); 90 * 91 * PrimaryLayout myPl = (PrimaryLayout) box.getContents().get(0); 92 * }</pre> 93 * 94 * will fail. 95 * 96 * <p>To be able to get {@link PrimaryLayout} object from any layout element, {@link 97 * #fromLayoutElement} method should be used, i.e.: 98 * 99 * <pre>{@code 100 * PrimaryLayout myPl = PrimaryLayout.fromLayoutElement(box.getContents().get(0)); 101 * }</pre> 102 */ 103 public class PrimaryLayout implements LayoutElement { 104 /** 105 * Prefix tool tag for Metadata in Modifiers, so we know that Box is actually a PrimaryLayout. 106 */ 107 static final String METADATA_TAG_PREFIX = "PL_"; 108 109 /** Index for byte array that contains bits to check whether the contents are present or not. */ 110 static final int FLAG_INDEX = METADATA_TAG_PREFIX.length(); 111 112 /** 113 * Base tool tag for Metadata in Modifiers, so we know that Box is actually a PrimaryLayout and 114 * what optional content is added. 115 */ 116 static final byte[] METADATA_TAG_BASE = 117 Arrays.copyOf(getTagBytes(METADATA_TAG_PREFIX), FLAG_INDEX + 1); 118 119 /** 120 * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether 121 * the primary chip is present or not. 122 */ 123 static final int CHIP_PRESENT = 0x1; 124 125 /** 126 * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether 127 * the primary label is present or not. 128 */ 129 static final int PRIMARY_LABEL_PRESENT = 1 << 1; 130 131 /** 132 * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether 133 * the secondary label is present or not. 134 */ 135 static final int SECONDARY_LABEL_PRESENT = 1 << 2; 136 137 /** 138 * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether 139 * the content is present or not. 140 */ 141 static final int CONTENT_PRESENT = 1 << 3; 142 143 /** 144 * Bit position in a byte on {@link #FLAG_INDEX} index in metadata byte array to check whether 145 * the responsive content inset is used or not. 146 */ 147 static final int CONTENT_INSET_USED = 1 << 4; 148 149 /** Position of the primary label in its own inner column if exists. */ 150 static final int PRIMARY_LABEL_POSITION = 1; 151 /** Position of the content in its own inner column. */ 152 static final int CONTENT_ONLY_POSITION = 0; 153 /** Position of the primary chip in main layout column. */ 154 static final int PRIMARY_CHIP_POSITION = 1; 155 156 private final boolean mIsResponsiveInsetEnabled; 157 158 @RestrictTo(Scope.LIBRARY) 159 @Retention(RetentionPolicy.SOURCE) 160 @IntDef( 161 flag = true, 162 value = { 163 CHIP_PRESENT, 164 PRIMARY_LABEL_PRESENT, 165 SECONDARY_LABEL_PRESENT, 166 CONTENT_PRESENT, 167 CONTENT_INSET_USED 168 }) 169 @interface ContentBits {} 170 171 private final @NonNull Box mImpl; 172 173 // This contains inner columns and primary chip. 174 private final @NonNull List<LayoutElement> mAllContent; 175 // This contains primary label content. 176 private final @NonNull List<LayoutElement> mPrimaryLabel; 177 // This contains optional labels, spacers and main content. 178 private final @NonNull List<LayoutElement> mContentAndSecondaryLabel; 179 PrimaryLayout(@onNull Box layoutElement)180 PrimaryLayout(@NonNull Box layoutElement) { 181 this.mImpl = layoutElement; 182 this.mAllContent = ((Column) layoutElement.getContents().get(0)).getContents(); 183 List<LayoutElement> innerContent = ((Column) mAllContent.get(0)).getContents(); 184 this.mPrimaryLabel = ((Column) innerContent.get(0)).getContents(); 185 this.mContentAndSecondaryLabel = 186 ((Column) ((Box) innerContent.get(1)).getContents().get(0)).getContents(); 187 this.mIsResponsiveInsetEnabled = areElementsPresent(CONTENT_INSET_USED); 188 } 189 190 /** Builder class for {@link PrimaryLayout}. */ 191 public static final class Builder implements LayoutElement.Builder { 192 private final @NonNull DeviceParameters mDeviceParameters; 193 private @Nullable LayoutElement mPrimaryChip = null; 194 private boolean mIsResponsiveInsetEnabled = false; 195 private @Nullable LayoutElement mPrimaryLabelText = null; 196 private @Nullable LayoutElement mSecondaryLabelText = null; 197 private @NonNull LayoutElement mContent = new Box.Builder().build(); 198 private @NonNull DpProp mVerticalSpacerHeight = DEFAULT_VERTICAL_SPACER_HEIGHT; 199 private byte mMetadataContentByte = 0; 200 201 /** 202 * Creates a builder for the {@link PrimaryLayout} from the given content. Content inside of 203 * it can later be set with {@link #setContent}, {@link #setPrimaryChipContent}, {@link 204 * #setPrimaryLabelTextContent} and {@link #setSecondaryLabelTextContent}. 205 * 206 * <p>For optimal layouts across different screen sizes, it is highly recommended to call 207 * {@link #setResponsiveContentInsetEnabled}. 208 */ Builder(@onNull DeviceParameters deviceParameters)209 public Builder(@NonNull DeviceParameters deviceParameters) { 210 this.mDeviceParameters = deviceParameters; 211 } 212 213 /** 214 * Changes this {@link PrimaryLayout} to use responsive insets for its content (primary 215 * label, secondary label and primary bottom chip) by adding an additional space on 216 * the side of this element to avoid that content going off the screen edge. 217 * 218 * <p>It is highly recommended to call this method with {@code true} when using this layout 219 * to optimize it for different screen sizes. 220 */ setResponsiveContentInsetEnabled(boolean enabled)221 public @NonNull Builder setResponsiveContentInsetEnabled(boolean enabled) { 222 this.mIsResponsiveInsetEnabled = enabled; 223 if (enabled) { 224 mMetadataContentByte = (byte) (mMetadataContentByte | CONTENT_INSET_USED); 225 } else { 226 mMetadataContentByte = (byte) (mMetadataContentByte & ~CONTENT_INSET_USED); 227 } 228 return this; 229 } 230 231 /** 232 * Sets the element which is in the slot at the bottom of the layout. Note that it is 233 * accepted to pass in any {@link LayoutElement}, but it is strongly recommended to add a 234 * {@link CompactChip} as the layout is optimized for it. 235 */ setPrimaryChipContent(@onNull LayoutElement compactChip)236 public @NonNull Builder setPrimaryChipContent(@NonNull LayoutElement compactChip) { 237 this.mPrimaryChip = compactChip; 238 mMetadataContentByte = (byte) (mMetadataContentByte | CHIP_PRESENT); 239 return this; 240 } 241 242 /** Sets the content in the primary label slot which will be above the main content. */ setPrimaryLabelTextContent( @onNull LayoutElement primaryLabelText)243 public @NonNull Builder setPrimaryLabelTextContent( 244 @NonNull LayoutElement primaryLabelText) { 245 this.mPrimaryLabelText = primaryLabelText; 246 mMetadataContentByte = (byte) (mMetadataContentByte | PRIMARY_LABEL_PRESENT); 247 return this; 248 } 249 250 /** 251 * Sets the content in the primary label slot which will be below the main content. It is 252 * highly recommended to have primary label set when having secondary label. 253 */ setSecondaryLabelTextContent( @onNull LayoutElement secondaryLabelText)254 public @NonNull Builder setSecondaryLabelTextContent( 255 @NonNull LayoutElement secondaryLabelText) { 256 this.mSecondaryLabelText = secondaryLabelText; 257 mMetadataContentByte = (byte) (mMetadataContentByte | SECONDARY_LABEL_PRESENT); 258 return this; 259 } 260 261 /** 262 * Sets the additional content to this layout, above the primary chip. 263 * 264 * <p>The content slot will wrap the elements' height, so the height of the given content 265 * must be fixed or set to wrap ({@code expand} can't be used). 266 * 267 * <p>This layout has built-in horizontal margins, so the given content should have width 268 * set to {@code expand} to use all the available space, rather than an explicit width which 269 * may lead to clipping. 270 */ setContent(@onNull LayoutElement content)271 public @NonNull Builder setContent(@NonNull LayoutElement content) { 272 this.mContent = content; 273 mMetadataContentByte = (byte) (mMetadataContentByte | CONTENT_PRESENT); 274 return this; 275 } 276 277 /** 278 * Sets the vertical spacer height which is used as a space between main content and 279 * secondary label if there is any. If not set, {@link 280 * LayoutDefaults#DEFAULT_VERTICAL_SPACER_HEIGHT} will be used. 281 */ 282 // The @Dimension(unit = DP) on dp() is seemingly being ignored, so lint complains that 283 // we're passing PX to something expecting DP. Just suppress the warning for now. 284 @SuppressLint("ResourceType") setVerticalSpacerHeight(@imensionunit = DP) float height)285 public @NonNull Builder setVerticalSpacerHeight(@Dimension(unit = DP) float height) { 286 this.mVerticalSpacerHeight = dp(height); 287 return this; 288 } 289 290 /** Constructs and returns {@link PrimaryLayout} with the provided content and look. */ 291 // The @Dimension(unit = DP) on dp() is seemingly being ignored, so lint complains that 292 // we're passing DP to something expecting PX. Just suppress the warning for now. 293 @SuppressLint("ResourceType") 294 @Override build()295 public @NonNull PrimaryLayout build() { 296 float topPadding = getTopPadding(); 297 float bottomPadding = getBottomPadding(); 298 float horizontalPadding = getHorizontalPadding(); 299 300 float primaryChipHeight = 301 mPrimaryChip != null ? MIN_TAPPABLE_SQUARE_LENGTH.getValue() : 0; 302 303 DpProp mainContentHeight = 304 dp( 305 mDeviceParameters.getScreenHeightDp() 306 - primaryChipHeight 307 - bottomPadding 308 - topPadding); 309 310 // Layout organization: column(column(primary label + spacer + (box(column(content + 311 // secondary label))) + chip) 312 313 // First column that has all other content and chip. 314 Column.Builder layoutBuilder = new Column.Builder(); 315 316 // Contains primary label, main content and secondary label. Primary label will be 317 // wrapped, while other content will be expanded so it can be centered in the remaining 318 // space. 319 Column.Builder contentAreaBuilder = 320 new Column.Builder() 321 .setWidth(expand()) 322 .setHeight(mainContentHeight) 323 .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER); 324 325 // Contains main content and secondary label with wrapped height so it can be put inside 326 // of the Box to be centered. 327 Column.Builder contentSecondaryLabelBuilder = 328 new Column.Builder() 329 .setWidth(expand()) 330 .setHeight(wrap()) 331 .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER); 332 333 // Needs to be in column because of the spacers. 334 Column.Builder primaryLabelBuilder = 335 new Column.Builder().setWidth(expand()).setHeight(wrap()); 336 337 if (mPrimaryLabelText != null) { 338 primaryLabelBuilder.addContent( 339 new Spacer.Builder().setHeight(getPrimaryLabelTopSpacerHeight()).build()); 340 primaryLabelBuilder.addContent(maybeInsetLabel(mPrimaryLabelText)); 341 } 342 343 contentAreaBuilder.addContent(primaryLabelBuilder.build()); 344 345 contentSecondaryLabelBuilder.addContent( 346 new Box.Builder() 347 .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER) 348 .setWidth(expand()) 349 .setHeight(wrap()) 350 .addContent(mContent) 351 .build()); 352 353 if (mSecondaryLabelText != null) { 354 contentSecondaryLabelBuilder.addContent( 355 new Spacer.Builder().setHeight(mVerticalSpacerHeight).build()); 356 contentSecondaryLabelBuilder.addContent( 357 // We only need to add extra padding to the secondary label if there's no 358 // bottom chip. 359 mPrimaryChip == null 360 ? maybeInsetLabel(mSecondaryLabelText) : mSecondaryLabelText); 361 } 362 363 contentAreaBuilder.addContent( 364 new Box.Builder() 365 .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER) 366 .setWidth(expand()) 367 .setHeight(expand()) 368 .addContent(contentSecondaryLabelBuilder.build()) 369 .build()); 370 371 layoutBuilder 372 .setModifiers( 373 new Modifiers.Builder() 374 .setPadding( 375 new Padding.Builder() 376 .setRtlAware(true) 377 .setStart(dp(horizontalPadding)) 378 .setEnd(dp(horizontalPadding)) 379 .setTop(dp(topPadding)) 380 .setBottom(dp(bottomPadding)) 381 .build()) 382 .build()) 383 .setWidth(expand()) 384 .setHeight(expand()) 385 .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER); 386 387 layoutBuilder.addContent(contentAreaBuilder.build()); 388 389 if (mPrimaryChip != null) { 390 // Depending on setter for responsive inset, this method will return either fixed or 391 // percentage based padding. 392 float horizontalChipPadding = getChipHorizontalPadding(); 393 layoutBuilder.addContent( 394 new Box.Builder() 395 .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM) 396 .setWidth(expand()) 397 .setHeight(wrap()) 398 .setModifiers( 399 new Modifiers.Builder() 400 .setPadding( 401 new Padding.Builder() 402 .setRtlAware(true) 403 .setStart(dp(horizontalChipPadding)) 404 .setEnd(dp(horizontalChipPadding)) 405 .build()) 406 .build()) 407 .addContent(mPrimaryChip) 408 .build()); 409 } 410 411 byte[] metadata = METADATA_TAG_BASE.clone(); 412 metadata[FLAG_INDEX] = mMetadataContentByte; 413 414 Box.Builder element = 415 new Box.Builder() 416 .setWidth(expand()) 417 .setHeight(expand()) 418 .setModifiers( 419 new Modifiers.Builder() 420 .setMetadata( 421 new ElementMetadata.Builder() 422 .setTagData(metadata) 423 .build()) 424 .build()) 425 .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM) 426 .addContent(layoutBuilder.build()); 427 428 return new PrimaryLayout(element.build()); 429 } 430 431 /** 432 * Wraps the given label (for primary or secondary label content) if responsive inset is 433 * set. 434 */ maybeInsetLabel(@onNull LayoutElement label)435 private LayoutElement maybeInsetLabel(@NonNull LayoutElement label) { 436 if (!mIsResponsiveInsetEnabled) { 437 return label; 438 } 439 440 return insetElementWithPadding( 441 label, mDeviceParameters.getScreenWidthDp() * LAYOUTS_LABEL_PADDING_PERCENT); 442 } 443 444 /** 445 * Returns the recommended bottom padding, based on percentage values in {@link 446 * LayoutDefaults}. 447 */ getBottomPadding()448 private float getBottomPadding() { 449 return mPrimaryChip != null 450 ? (mDeviceParameters.getScreenHeightDp() 451 * (isRoundDevice(mDeviceParameters) 452 ? PRIMARY_LAYOUT_MARGIN_BOTTOM_ROUND_PERCENT 453 : PRIMARY_LAYOUT_MARGIN_BOTTOM_SQUARE_PERCENT)) 454 : getTopPadding(); 455 } 456 457 /** 458 * Returns the recommended top padding, based on percentage values in {@link 459 * LayoutDefaults}. 460 */ 461 @Dimension(unit = DP) getTopPadding()462 private float getTopPadding() { 463 return mDeviceParameters.getScreenHeightDp() 464 * (isRoundDevice(mDeviceParameters) 465 ? PRIMARY_LAYOUT_MARGIN_TOP_ROUND_PERCENT 466 : PRIMARY_LAYOUT_MARGIN_TOP_SQUARE_PERCENT); 467 } 468 469 /** 470 * Returns the recommended horizontal padding, based on percentage values in {@link 471 * LayoutDefaults}. 472 */ 473 @Dimension(unit = DP) getHorizontalPadding()474 private float getHorizontalPadding() { 475 return mDeviceParameters.getScreenWidthDp() 476 * (isRoundDevice(mDeviceParameters) 477 ? PRIMARY_LAYOUT_MARGIN_HORIZONTAL_ROUND_PERCENT 478 : PRIMARY_LAYOUT_MARGIN_HORIZONTAL_SQUARE_PERCENT); 479 } 480 481 /** 482 * Returns the recommended horizontal padding for primary chip, based on percentage values 483 * and DP values in {@link LayoutDefaults}. 484 */ 485 @Dimension(unit = DP) getChipHorizontalPadding()486 private float getChipHorizontalPadding() { 487 return mIsResponsiveInsetEnabled 488 ? PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_PERCENT 489 * mDeviceParameters.getScreenWidthDp() 490 : isRoundDevice(mDeviceParameters) 491 ? PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_ROUND_DP 492 : PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_SQUARE_DP; 493 } 494 495 /** Returns the spacer height to be placed above primary label to accommodate Tile icon. */ getPrimaryLabelTopSpacerHeight()496 private @NonNull DpProp getPrimaryLabelTopSpacerHeight() { 497 return isRoundDevice(mDeviceParameters) 498 ? PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_ROUND_DP 499 : PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_SQUARE_DP; 500 } 501 } 502 503 /** Get the primary label content from this layout. */ getPrimaryLabelTextContent()504 public @Nullable LayoutElement getPrimaryLabelTextContent() { 505 if (!areElementsPresent(PRIMARY_LABEL_PRESENT)) { 506 return null; 507 } 508 LayoutElement primaryLabelWrapper = mPrimaryLabel.get(PRIMARY_LABEL_POSITION); 509 // If responsive inset are used, label passed in is wrapped in a Box. 510 return mIsResponsiveInsetEnabled 511 ? ((Box) primaryLabelWrapper).getContents().get(0) 512 : primaryLabelWrapper; 513 } 514 515 /** Get the secondary label content from this layout. */ getSecondaryLabelTextContent()516 public @Nullable LayoutElement getSecondaryLabelTextContent() { 517 if (!areElementsPresent(SECONDARY_LABEL_PRESENT)) { 518 return null; 519 } 520 // By tag we know that secondary label exists. It will always be at last position. 521 LayoutElement secondaryLabelWrapper = 522 mContentAndSecondaryLabel.get(mContentAndSecondaryLabel.size() - 1); 523 return mIsResponsiveInsetEnabled && !areElementsPresent(CHIP_PRESENT) 524 ? ((Box) secondaryLabelWrapper).getContents().get(0) 525 : secondaryLabelWrapper; 526 } 527 528 /** Get the inner content from this layout. */ getContent()529 public @Nullable LayoutElement getContent() { 530 if (!areElementsPresent(CONTENT_PRESENT)) { 531 return null; 532 } 533 return ((Box) mContentAndSecondaryLabel.get(CONTENT_ONLY_POSITION)).getContents().get(0); 534 } 535 536 /** Get the primary chip content from this layout. */ getPrimaryChipContent()537 public @Nullable LayoutElement getPrimaryChipContent() { 538 if (areElementsPresent(CHIP_PRESENT)) { 539 return ((Box) mAllContent.get(PRIMARY_CHIP_POSITION)).getContents().get(0); 540 } 541 return null; 542 } 543 544 /** Returns whether the contents from this layout are using responsive inset. */ isResponsiveContentInsetEnabled()545 public boolean isResponsiveContentInsetEnabled() { 546 return mIsResponsiveInsetEnabled; 547 } 548 549 /** Get the vertical spacer height from this layout. */ 550 // The @Dimension(unit = DP) on getValue() is seemingly being ignored, so lint complains that 551 // we're passing PX to something expecting DP. Just suppress the warning for now. 552 @SuppressLint("ResourceType") 553 @Dimension(unit = DP) getVerticalSpacerHeight()554 public float getVerticalSpacerHeight() { 555 if (areElementsPresent(SECONDARY_LABEL_PRESENT)) { 556 LayoutElement element = mContentAndSecondaryLabel.get(CONTENT_ONLY_POSITION + 1); 557 if (element instanceof Spacer) { 558 SpacerDimension height = ((Spacer) element).getHeight(); 559 if (height instanceof DpProp) { 560 return ((DpProp) height).getValue(); 561 } 562 } 563 } 564 return DEFAULT_VERTICAL_SPACER_HEIGHT.getValue(); 565 } 566 areElementsPresent(@ontentBits int elementFlag)567 private boolean areElementsPresent(@ContentBits int elementFlag) { 568 return (getMetadataTag()[FLAG_INDEX] & elementFlag) == elementFlag; 569 } 570 571 /** Returns metadata tag set to this PrimaryLayout. */ getMetadataTag()572 byte @NonNull [] getMetadataTag() { 573 return getMetadataTagBytes(checkNotNull(checkNotNull(mImpl.getModifiers()).getMetadata())); 574 } 575 576 /** 577 * Returns PrimaryLayout object from the given LayoutElement (e.g. one retrieved from a 578 * container's content with {@code container.getContents().get(index)}) if that element can be 579 * converted to PrimaryLayout. Otherwise, it will return null. 580 */ fromLayoutElement(@onNull LayoutElement element)581 public static @Nullable PrimaryLayout fromLayoutElement(@NonNull LayoutElement element) { 582 if (element instanceof PrimaryLayout) { 583 return (PrimaryLayout) element; 584 } 585 if (!(element instanceof Box)) { 586 return null; 587 } 588 Box boxElement = (Box) element; 589 if (!checkTag(boxElement.getModifiers(), METADATA_TAG_PREFIX, METADATA_TAG_BASE)) { 590 return null; 591 } 592 // Now we are sure that this element is a PrimaryLayout. 593 return new PrimaryLayout(boxElement); 594 } 595 596 @Override 597 @RestrictTo(Scope.LIBRARY_GROUP) toLayoutElementProto()598 public LayoutElementProto.@NonNull LayoutElement toLayoutElementProto() { 599 return mImpl.toLayoutElementProto(); 600 } 601 602 @Override 603 @RestrictTo(Scope.LIBRARY_GROUP) getFingerprint()604 public @Nullable Fingerprint getFingerprint() { 605 return mImpl.getFingerprint(); 606 } 607 } 608