1 /* 2 * Copyright 2021 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; 18 19 import static androidx.annotation.Dimension.DP; 20 import static androidx.wear.protolayout.DimensionBuilders.dp; 21 import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER; 22 import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_START; 23 import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_UNDEFINED; 24 import static androidx.wear.protolayout.material.ChipDefaults.DEFAULT_HEIGHT; 25 import static androidx.wear.protolayout.material.ChipDefaults.DEFAULT_MARGIN_PERCENT; 26 import static androidx.wear.protolayout.material.ChipDefaults.HORIZONTAL_PADDING; 27 import static androidx.wear.protolayout.material.ChipDefaults.ICON_SIZE; 28 import static androidx.wear.protolayout.material.ChipDefaults.ICON_SPACER_WIDTH; 29 import static androidx.wear.protolayout.material.ChipDefaults.PRIMARY_COLORS; 30 import static androidx.wear.protolayout.materialcore.Chip.METADATA_TAG_CUSTOM_CONTENT; 31 import static androidx.wear.protolayout.materialcore.Chip.METADATA_TAG_ICON; 32 import static androidx.wear.protolayout.materialcore.Helper.checkNotNull; 33 import static androidx.wear.protolayout.materialcore.Helper.staticString; 34 35 import android.content.Context; 36 37 import androidx.annotation.Dimension; 38 import androidx.annotation.RestrictTo; 39 import androidx.annotation.RestrictTo.Scope; 40 import androidx.wear.protolayout.ColorBuilders.ColorProp; 41 import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters; 42 import androidx.wear.protolayout.DimensionBuilders.ContainerDimension; 43 import androidx.wear.protolayout.DimensionBuilders.DpProp; 44 import androidx.wear.protolayout.LayoutElementBuilders; 45 import androidx.wear.protolayout.LayoutElementBuilders.ColorFilter; 46 import androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignment; 47 import androidx.wear.protolayout.LayoutElementBuilders.Image; 48 import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement; 49 import androidx.wear.protolayout.ModifiersBuilders.Clickable; 50 import androidx.wear.protolayout.TypeBuilders.StringProp; 51 import androidx.wear.protolayout.expression.Fingerprint; 52 import androidx.wear.protolayout.material.Typography.TypographyName; 53 import androidx.wear.protolayout.proto.LayoutElementProto; 54 55 import org.jspecify.annotations.NonNull; 56 import org.jspecify.annotations.Nullable; 57 58 /** 59 * ProtoLayout component {@link Chip} that represents clickable object with the text, optional label 60 * and optional icon or with custom content. 61 * 62 * <p>The Chip is Stadium shape that has a max height designed to take no more than two lines of 63 * text of {@link Typography#TYPOGRAPHY_BUTTON} style and with minimum tap target to meet 64 * accessibility requirements. The {@link Chip} can have an icon horizontally parallel to the two 65 * lines of text. Width of chip can very, and the recommended size is screen dependent with the 66 * recommended margin being applied. 67 * 68 * <p>The recommended set of {@link ChipColors} styles can be obtained from {@link ChipDefaults}., 69 * e.g. {@link ChipDefaults#PRIMARY_COLORS} to get a color scheme for a primary {@link Chip}. 70 * 71 * <p>When accessing the contents of a container for testing, note that this element can't be simply 72 * casted back to the original type, i.e.: 73 * 74 * <pre>{@code 75 * Chip chip = new Chip... 76 * Box box = new Box.Builder().addContent(chip).build(); 77 * 78 * Chip myChip = (Chip) box.getContents().get(0); 79 * }</pre> 80 * 81 * will fail. 82 * 83 * <p>To be able to get {@link Chip} object from any layout element, {@link #fromLayoutElement} 84 * method should be used, i.e.: 85 * 86 * <pre>{@code 87 * Chip myChip = Chip.fromLayoutElement(box.getContents().get(0)); 88 * }</pre> 89 * 90 * @see androidx.wear.protolayout.material.layouts.PrimaryLayout.Builder#setContent if this Chip is 91 * used inside of {@link androidx.wear.protolayout.material.layouts.PrimaryLayout}. 92 */ 93 public class Chip implements LayoutElement { 94 private final androidx.wear.protolayout.materialcore.@NonNull Chip mElement; 95 Chip(androidx.wear.protolayout.materialcore.@onNull Chip element)96 Chip(androidx.wear.protolayout.materialcore.@NonNull Chip element) { 97 mElement = element; 98 } 99 100 /** Builder class for {@link Chip}. */ 101 public static final class Builder implements LayoutElement.Builder { 102 private final @NonNull Context mContext; 103 private @Nullable LayoutElement mCustomContent; 104 private @Nullable String mImageResourceId = null; 105 private @Nullable String mPrimaryLabel = null; 106 private @Nullable String mSecondaryLabel = null; 107 private @Nullable StringProp mContentDescription = null; 108 private @NonNull ChipColors mChipColors = PRIMARY_COLORS; 109 private @NonNull DpProp mIconSize = ICON_SIZE; 110 @HorizontalAlignment private int mHorizontalAlign = HORIZONTAL_ALIGN_UNDEFINED; 111 @TypographyName private int mPrimaryLabelTypography; 112 private boolean mIsScalable = true; 113 private int mMaxLines = 0; // 0 indicates that is not set. 114 private final androidx.wear.protolayout.materialcore.Chip.@NonNull Builder mCoreBuilder; 115 116 /** 117 * Creates a builder for the {@link Chip} with associated action. It is required to add 118 * content later with setters. 119 * 120 * @param context The application's context. 121 * @param clickable Associated {@link Clickable} for click events. When the Chip is clicked 122 * it will fire the associated action. 123 * @param deviceParameters The device parameters used to derive defaults for this Chip. 124 */ Builder( @onNull Context context, @NonNull Clickable clickable, @NonNull DeviceParameters deviceParameters)125 public Builder( 126 @NonNull Context context, 127 @NonNull Clickable clickable, 128 @NonNull DeviceParameters deviceParameters) { 129 mContext = context; 130 float width = 131 (100 - 2 * DEFAULT_MARGIN_PERCENT) * deviceParameters.getScreenWidthDp() / 100; 132 mPrimaryLabelTypography = Typography.TYPOGRAPHY_BUTTON; 133 mCoreBuilder = new androidx.wear.protolayout.materialcore.Chip.Builder(clickable); 134 mCoreBuilder.setWidth(dp(width)); 135 mCoreBuilder.setHorizontalPadding(HORIZONTAL_PADDING); 136 mCoreBuilder.setHeight(DEFAULT_HEIGHT); 137 mCoreBuilder.setBackgroundColor(mChipColors.getBackgroundColor()); 138 mCoreBuilder.setMinimalTappableSquareLength(ChipDefaults.MIN_TAPPABLE_SQUARE_LENGTH); 139 mCoreBuilder.setIconSpacerWidth(ICON_SPACER_WIDTH); 140 } 141 142 /** 143 * Sets the width of {@link Chip}. If not set, default value will be set to fill the screen. 144 */ setWidth(@onNull ContainerDimension width)145 public @NonNull Builder setWidth(@NonNull ContainerDimension width) { 146 mCoreBuilder.setWidth(width); 147 return this; 148 } 149 150 /** 151 * Sets the width of {@link Chip}. If not set, default value will be set to fill the 152 * screen. 153 */ setWidth(@imensionunit = DP) float width)154 public @NonNull Builder setWidth(@Dimension(unit = DP) float width) { 155 return setWidth(dp(width)); 156 } 157 158 /** 159 * Sets the custom content for the {@link Chip}. Any previously added content will be 160 * overridden. 161 */ setCustomContent(@onNull LayoutElement content)162 public @NonNull Builder setCustomContent(@NonNull LayoutElement content) { 163 this.mCustomContent = content; 164 this.mPrimaryLabel = null; 165 this.mSecondaryLabel = null; 166 this.mImageResourceId = null; 167 return this; 168 } 169 170 /** 171 * Sets the static content description for the {@link Chip}. It is highly recommended to 172 * provide this for chip containing icon. 173 */ setContentDescription(@onNull CharSequence contentDescription)174 public @NonNull Builder setContentDescription(@NonNull CharSequence contentDescription) { 175 return setContentDescription(staticString(contentDescription.toString())); 176 } 177 178 /** 179 * Sets the content description for the {@link Chip}. It is highly recommended to provide 180 * this for chip containing icon. 181 * 182 * <p>While this field is statically accessible from 1.0, it's only bindable since version 183 * 1.2 and renderers supporting version 1.2 will use the dynamic value (if set). 184 */ setContentDescription(@onNull StringProp contentDescription)185 public @NonNull Builder setContentDescription(@NonNull StringProp contentDescription) { 186 this.mContentDescription = contentDescription; 187 return this; 188 } 189 190 /** 191 * Sets the primary label for the {@link Chip}. Any previously added custom content will be 192 * overridden. Primary label can be on 1 or 2 lines, depending on the length and existence 193 * of secondary label. 194 */ setPrimaryLabelContent(@onNull String primaryLabel)195 public @NonNull Builder setPrimaryLabelContent(@NonNull String primaryLabel) { 196 this.mPrimaryLabel = primaryLabel; 197 this.mCustomContent = null; 198 return this; 199 } 200 201 /** 202 * Used for creating {@code CompactChip} and {@code TitleChip}. 203 * 204 * <p>Sets the font for the primary label and should only be used internally. 205 */ setPrimaryLabelTypography(@ypographyName int typography)206 @NonNull Builder setPrimaryLabelTypography(@TypographyName int typography) { 207 this.mPrimaryLabelTypography = typography; 208 return this; 209 } 210 211 /** 212 * Used for creating {@code CompactChip} and {@code TitleChip}. 213 * 214 * <p>Sets the icon size and should only be used internally. 215 */ setIconSize(@onNull DpProp size)216 @NonNull Builder setIconSize(@NonNull DpProp size) { 217 this.mIconSize = size; 218 return this; 219 } 220 221 /** 222 * Used for creating {@code CompactChip} and {@code TitleChip}. 223 * 224 * <p>Sets whether the font for the primary label is scalable. 225 */ setIsPrimaryLabelScalable(boolean isScalable)226 @NonNull Builder setIsPrimaryLabelScalable(boolean isScalable) { 227 this.mIsScalable = isScalable; 228 return this; 229 } 230 231 /** 232 * Sets the secondary label for the {@link Chip}. Any previously added custom content will 233 * be overridden. If secondary label is set, primary label must be set too with {@link 234 * #setPrimaryLabelContent}. 235 */ setSecondaryLabelContent(@onNull String secondaryLabel)236 public @NonNull Builder setSecondaryLabelContent(@NonNull String secondaryLabel) { 237 this.mSecondaryLabel = secondaryLabel; 238 this.mCustomContent = null; 239 return this; 240 } 241 242 /** 243 * Sets the icon for the {@link Chip}. Any previously added custom content will be 244 * overridden. Provided icon will be tinted to the given content color from {@link 245 * ChipColors}. This icon should be image with chosen alpha channel and not an actual image. 246 * If icon is set, primary label must be set too with {@link #setPrimaryLabelContent}. 247 */ setIconContent(@onNull String imageResourceId)248 public @NonNull Builder setIconContent(@NonNull String imageResourceId) { 249 this.mImageResourceId = imageResourceId; 250 this.mCustomContent = null; 251 return this; 252 } 253 254 /** 255 * Sets the colors for the {@link Chip}. If set, {@link ChipColors#getBackgroundColor()} 256 * will be used for the background of the button, {@link ChipColors#getContentColor()} for 257 * main text, {@link ChipColors#getSecondaryContentColor()} for label text and {@link 258 * ChipColors#getIconColor()} will be used as color for the icon itself. If not set, {@link 259 * ChipDefaults#PRIMARY_COLORS} will be used. 260 */ setChipColors(@onNull ChipColors chipColors)261 public @NonNull Builder setChipColors(@NonNull ChipColors chipColors) { 262 mChipColors = chipColors; 263 mCoreBuilder.setBackgroundColor(chipColors.getBackgroundColor()); 264 return this; 265 } 266 267 /** 268 * Sets the horizontal alignment in the chip. It is strongly recommended that the content of 269 * the chip is start-aligned if there is more than primary text in it. By default, {@link 270 * HorizontalAlignment#HORIZONTAL_ALIGN_CENTER} will be used when only a primary label is 271 * present. Otherwise {@link HorizontalAlignment#HORIZONTAL_ALIGN_START} will be used. 272 */ setHorizontalAlignment( @orizontalAlignment int horizontalAlignment)273 public @NonNull Builder setHorizontalAlignment( 274 @HorizontalAlignment int horizontalAlignment) { 275 mHorizontalAlign = horizontalAlignment; 276 return this; 277 } 278 279 /** Used for creating {@code CompactChip} and {@code TitleChip}. */ setHorizontalPadding(@onNull DpProp horizontalPadding)280 @NonNull Builder setHorizontalPadding(@NonNull DpProp horizontalPadding) { 281 mCoreBuilder.setHorizontalPadding(horizontalPadding); 282 return this; 283 } 284 285 /** Used for creating {@code CompactChip} and {@code TitleChip}. */ setHeight(@onNull DpProp height)286 @NonNull Builder setHeight(@NonNull DpProp height) { 287 mCoreBuilder.setHeight(height); 288 return this; 289 } 290 291 /** Used for creating {@code CompactChip} and {@code TitleChip}. */ setMaxLines(int maxLines)292 @NonNull Builder setMaxLines(int maxLines) { 293 this.mMaxLines = maxLines; 294 return this; 295 } 296 297 /** Constructs and returns {@link Chip} with the provided content and look. */ 298 @Override build()299 public @NonNull Chip build() { 300 mCoreBuilder.setContentDescription(getCorrectContentDescription()); 301 mCoreBuilder.setHorizontalAlignment(getCorrectHorizontalAlignment()); 302 303 if (mCustomContent != null) { 304 mCoreBuilder.setCustomContent(mCustomContent); 305 } else { 306 setCorrectContent(); 307 } 308 309 return new Chip(mCoreBuilder.build()); 310 } 311 getCorrectContentDescription()312 private @NonNull StringProp getCorrectContentDescription() { 313 if (mContentDescription == null) { 314 String staticValue = ""; 315 if (mPrimaryLabel != null) { 316 staticValue += mPrimaryLabel; 317 } 318 if (mSecondaryLabel != null) { 319 staticValue += "\n" + mSecondaryLabel; 320 } 321 mContentDescription = new StringProp.Builder(staticValue).build(); 322 } 323 return checkNotNull(mContentDescription); 324 } 325 326 @HorizontalAlignment getCorrectHorizontalAlignment()327 private int getCorrectHorizontalAlignment() { 328 if (mHorizontalAlign != HORIZONTAL_ALIGN_UNDEFINED) { 329 return mHorizontalAlign; 330 } 331 if (mPrimaryLabel != null && mSecondaryLabel == null && mImageResourceId == null) { 332 return HORIZONTAL_ALIGN_CENTER; 333 } else { 334 return HORIZONTAL_ALIGN_START; 335 } 336 } 337 isIconOnly()338 private boolean isIconOnly() { 339 return mPrimaryLabel == null && mSecondaryLabel == null && mCustomContent == null; 340 } 341 342 @SuppressWarnings("deprecation") // TEXT_OVERFLOW_ELLIPSIZE_END as existing API setCorrectContent()343 private void setCorrectContent() { 344 if (mImageResourceId != null) { 345 Image icon = 346 new Image.Builder() 347 .setResourceId(mImageResourceId) 348 .setWidth(mIconSize) 349 .setHeight(mIconSize) 350 .setColorFilter( 351 new ColorFilter.Builder() 352 .setTint(mChipColors.getIconColor()) 353 .build()) 354 .build(); 355 mCoreBuilder.setIconContent(icon); 356 357 if (isIconOnly()) { 358 return; 359 } 360 } 361 362 Text mainTextElement = 363 new Text.Builder(mContext, checkNotNull(mPrimaryLabel)) 364 .setTypography(mPrimaryLabelTypography) 365 .setColor(mChipColors.getContentColor()) 366 .setMaxLines(getCorrectMaxLines()) 367 .setOverflow(LayoutElementBuilders.TEXT_OVERFLOW_ELLIPSIZE_END) 368 .setMultilineAlignment(LayoutElementBuilders.TEXT_ALIGN_START) 369 .setScalable(mIsScalable) 370 .build(); 371 372 mCoreBuilder.setPrimaryLabelContent(mainTextElement); 373 374 if (mSecondaryLabel != null) { 375 Text labelTextElement = 376 new Text.Builder(mContext, mSecondaryLabel) 377 .setTypography(Typography.TYPOGRAPHY_CAPTION2) 378 .setColor(mChipColors.getSecondaryContentColor()) 379 .setMaxLines(1) 380 .setOverflow(LayoutElementBuilders.TEXT_OVERFLOW_ELLIPSIZE_END) 381 .setMultilineAlignment(LayoutElementBuilders.TEXT_ALIGN_START) 382 .build(); 383 mCoreBuilder.setSecondaryLabelContent(labelTextElement); 384 } 385 } 386 getCorrectMaxLines()387 private int getCorrectMaxLines() { 388 if (mMaxLines > 0) { 389 return mMaxLines; 390 } 391 return mSecondaryLabel != null ? 1 : 2; 392 } 393 } 394 395 /** Returns the visible height of this Chip. */ getHeight()396 public @NonNull ContainerDimension getHeight() { 397 return mElement.getHeight(); 398 } 399 400 /** Returns width of this Chip. */ getWidth()401 public @NonNull ContainerDimension getWidth() { 402 return mElement.getWidth(); 403 } 404 405 /** Returns click event action associated with this Chip. */ getClickable()406 public @NonNull Clickable getClickable() { 407 return mElement.getClickable(); 408 } 409 410 /** Returns chip colors of this Chip. */ getChipColors()411 public @NonNull ChipColors getChipColors() { 412 ColorProp backgroundColor = mElement.getBackgroundColor(); 413 ColorProp contentColor = null; 414 ColorProp secondaryContentColor = null; 415 ColorProp iconTintColor = null; 416 417 if (!getMetadataTag().equals(METADATA_TAG_CUSTOM_CONTENT)) { 418 if (getMetadataTag().equals(METADATA_TAG_ICON)) { 419 Image icon = checkNotNull(getIconContentObject()); 420 iconTintColor = checkNotNull(checkNotNull(icon.getColorFilter()).getTint()); 421 } 422 423 424 Text maybePrimaryLabel = getPrimaryLabelContentObject(); 425 if (maybePrimaryLabel != null) { 426 contentColor = checkNotNull(maybePrimaryLabel).getColor(); 427 Text label = getSecondaryLabelContentObject(); 428 if (label != null) { 429 secondaryContentColor = label.getColor(); 430 } 431 } 432 } 433 434 // Populate other colors if they are not found. 435 if (contentColor == null) { 436 contentColor = new ColorProp.Builder(0).build(); 437 } 438 if (secondaryContentColor == null) { 439 secondaryContentColor = contentColor; 440 } 441 if (iconTintColor == null) { 442 iconTintColor = contentColor; 443 } 444 445 return new ChipColors(backgroundColor, iconTintColor, contentColor, secondaryContentColor); 446 } 447 448 /** Returns content description of this Chip. */ getContentDescription()449 public @Nullable StringProp getContentDescription() { 450 return mElement.getContentDescription(); 451 } 452 453 /** Returns custom content from this Chip if it has been added. Otherwise, it returns null. */ getCustomContent()454 public @Nullable LayoutElement getCustomContent() { 455 return mElement.getCustomContent(); 456 } 457 458 /** Returns primary label from this Chip if it has been added. Otherwise, it returns null. */ getPrimaryLabelContent()459 public @Nullable String getPrimaryLabelContent() { 460 Text primaryLabel = getPrimaryLabelContentObject(); 461 return primaryLabel != null ? primaryLabel.getText().getValue() : null; 462 } 463 464 /** Returns secondary label from this Chip if it has been added. Otherwise, it returns null. */ getSecondaryLabelContent()465 public @Nullable String getSecondaryLabelContent() { 466 Text secondaryLabel = getSecondaryLabelContentObject(); 467 return secondaryLabel != null ? secondaryLabel.getText().getValue() : null; 468 } 469 470 /** Returns icon id from this Chip if it has been added. Otherwise, it returns null. */ getIconContent()471 public @Nullable String getIconContent() { 472 Image icon = getIconContentObject(); 473 return icon != null ? checkNotNull(icon.getResourceId()).getValue() : null; 474 } 475 getPrimaryLabelContentObject()476 private @Nullable Text getPrimaryLabelContentObject() { 477 LayoutElement content = mElement.getPrimaryLabelContent(); 478 if (content != null) { 479 return Text.fromLayoutElement(content); 480 } 481 return null; 482 } 483 getSecondaryLabelContentObject()484 private @Nullable Text getSecondaryLabelContentObject() { 485 LayoutElement content = mElement.getSecondaryLabelContent(); 486 if (content != null) { 487 return Text.fromLayoutElement(content); 488 } 489 return null; 490 } 491 getIconContentObject()492 private @Nullable Image getIconContentObject() { 493 LayoutElement content = mElement.getIconContent(); 494 return content instanceof Image ? (Image) content : null; 495 } 496 497 /** Returns the horizontal alignment of the content in this Chip. */ 498 @HorizontalAlignment getHorizontalAlignment()499 public int getHorizontalAlignment() { 500 return mElement.getHorizontalAlignment(); 501 } 502 503 /** Returns metadata tag set to this Chip. */ getMetadataTag()504 @NonNull String getMetadataTag() { 505 return mElement.getMetadataTag(); 506 } 507 508 /** 509 * Returns Chip object from the given LayoutElement (e.g. one retrieved from a container's 510 * content with {@code container.getContents().get(index)}) if that element can be converted to 511 * Chip. Otherwise, it will return null. 512 */ fromLayoutElement(@onNull LayoutElement element)513 public static @Nullable Chip fromLayoutElement(@NonNull LayoutElement element) { 514 if (element instanceof Chip) { 515 return (Chip) element; 516 } 517 androidx.wear.protolayout.materialcore.Chip coreChip = 518 androidx.wear.protolayout.materialcore.Chip.fromLayoutElement(element); 519 return coreChip == null ? null : new Chip(coreChip); 520 } 521 522 @Override 523 @RestrictTo(Scope.LIBRARY_GROUP) toLayoutElementProto()524 public LayoutElementProto.@NonNull LayoutElement toLayoutElementProto() { 525 return mElement.toLayoutElementProto(); 526 } 527 528 @Override 529 @RestrictTo(Scope.LIBRARY_GROUP) getFingerprint()530 public @Nullable Fingerprint getFingerprint() { 531 return mElement.getFingerprint(); 532 } 533 } 534