[go: nahoru, domu]

blob: 6108c030ba6a3d6dacde5110e6c204972cfe00f4 [file] [log] [blame]
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.wear.tiles.material;
import static androidx.annotation.Dimension.DP;
import static androidx.wear.tiles.DimensionBuilders.dp;
import static androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_START;
import static androidx.wear.tiles.material.ChipDefaults.DEFAULT_HEIGHT;
import static androidx.wear.tiles.material.ChipDefaults.DEFAULT_MARGIN_PERCENT;
import static androidx.wear.tiles.material.ChipDefaults.HORIZONTAL_PADDING;
import static androidx.wear.tiles.material.ChipDefaults.ICON_SIZE;
import static androidx.wear.tiles.material.ChipDefaults.PRIMARY;
import static androidx.wear.tiles.material.ChipDefaults.VERTICAL_PADDING;
import static androidx.wear.tiles.material.Helper.checkNotNull;
import static androidx.wear.tiles.material.Helper.radiusOf;
import androidx.annotation.Dimension;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.wear.tiles.ActionBuilders.Action;
import androidx.wear.tiles.ColorBuilders.ColorProp;
import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters;
import androidx.wear.tiles.DimensionBuilders.ContainerDimension;
import androidx.wear.tiles.DimensionBuilders.DpProp;
import androidx.wear.tiles.LayoutElementBuilders;
import androidx.wear.tiles.LayoutElementBuilders.Box;
import androidx.wear.tiles.LayoutElementBuilders.ColorFilter;
import androidx.wear.tiles.LayoutElementBuilders.Column;
import androidx.wear.tiles.LayoutElementBuilders.FontStyle;
import androidx.wear.tiles.LayoutElementBuilders.FontStyles;
import androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignment;
import androidx.wear.tiles.LayoutElementBuilders.Image;
import androidx.wear.tiles.LayoutElementBuilders.LayoutElement;
import androidx.wear.tiles.LayoutElementBuilders.Row;
import androidx.wear.tiles.LayoutElementBuilders.Spacer;
import androidx.wear.tiles.LayoutElementBuilders.Text;
import androidx.wear.tiles.ModifiersBuilders;
import androidx.wear.tiles.ModifiersBuilders.Background;
import androidx.wear.tiles.ModifiersBuilders.Clickable;
import androidx.wear.tiles.ModifiersBuilders.Corner;
import androidx.wear.tiles.ModifiersBuilders.Modifiers;
import androidx.wear.tiles.ModifiersBuilders.Padding;
import androidx.wear.tiles.proto.LayoutElementProto;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
* Tiles component {@link Chip} that represents clickable object with the text, optional label and
* optional icon or with custom content.
*
* <p>The Chip is Stadium shape and has a max height designed to take no more than two lines of text
* of {@link FontStyles#button} style. The {@link Chip} can have an icon horizontally parallel to
* the two lines of text. Width of chip can very, and the recommended size is screen dependent with
* the recommended margin being defined in {@link ChipDefaults#DEFAULT_MARGIN_PERCENT} which is set
* by default.
*
* <p>The recommended set of {@link ChipColors} styles can be obtained from {@link ChipDefaults}.,
* e.g. {@link ChipDefaults#PRIMARY} to get a color scheme for a primary {@link Chip} which by
* default will have a solid background of {@link Colors#PRIMARY} and content color of {@link
* Colors#ON_PRIMARY}.
*/
public class Chip implements LayoutElement {
@NonNull private final Box mElement;
Chip(@NonNull Box element) {
mElement = element;
}
/** Builder class for {@link androidx.wear.tiles.material.Chip}. */
public static final class Builder implements LayoutElement.Builder {
private static final int NOT_SET = 0;
private static final int TEXT = 1;
private static final int ICON = 2;
private static final int CUSTOM_CONTENT = 3;
private int mMaxLines = 0; // 0 indicates that is not set.
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({NOT_SET, TEXT, ICON, CUSTOM_CONTENT})
@interface ChipType {}
@Nullable private LayoutElement mCustomContent;
@NonNull private String mResourceId = "";
@NonNull private String mPrimaryText = "";
@Nullable private String mLabelText = null;
@NonNull private final Action mAction;
@NonNull private final String mClickableId;
@NonNull private final DeviceParameters mDeviceParameters;
@NonNull private String mContentDescription = "";
@NonNull private ContainerDimension mWidth;
@NonNull private DpProp mHeight = DEFAULT_HEIGHT;
@NonNull private ChipColors mChipColors = PRIMARY;
private @ChipType int mType = NOT_SET;
private @HorizontalAlignment int mHorizontalAlign = HORIZONTAL_ALIGN_START;
@NonNull private FontStyle mPrimaryTextFont;
@NonNull private DpProp mHorizontalPadding = HORIZONTAL_PADDING;
@NonNull private DpProp mVerticalPadding = VERTICAL_PADDING;
/**
* Creates a builder for the {@link Chip} with associated action. It is required to add
* content later with setters.
*
* @param action Associated Actions for click events. When the Chip is clicked it will fire
* the associated action.
* @param clickableId The ID associated with the given action's clickable.
* @param deviceParameters The device parameters used for styling text.
*/
@SuppressWarnings("LambdaLast")
public Builder(
@NonNull Action action,
@NonNull String clickableId,
@NonNull DeviceParameters deviceParameters) {
mAction = action;
mClickableId = clickableId;
mDeviceParameters = deviceParameters;
mWidth =
dp(
(100 - 2 * DEFAULT_MARGIN_PERCENT)
* deviceParameters.getScreenWidthDp()
/ 100);
mPrimaryTextFont = FontStyles.button(deviceParameters).build();
}
/**
* Sets the width of {@link Chip}. If not set, default value will be screen width decreased
* by {@link ChipDefaults#DEFAULT_MARGIN_PERCENT}.
*/
@NonNull
public Builder setWidth(@NonNull DpProp width) {
mWidth = width;
return this;
}
/**
* Sets the width of {@link Chip}. If not set, default value will be screen width decreased
* by {@link ChipDefaults#DEFAULT_MARGIN_PERCENT}.
*/
@NonNull
public Builder setWidth(@Dimension(unit = DP) float width) {
mWidth = dp(width);
return this;
}
/**
* Sets the custom content for the {@link Chip}. Any previously added content will be
* overridden.
*/
@NonNull
public Builder setContent(@NonNull LayoutElement content) {
this.mCustomContent = content;
this.mType = CUSTOM_CONTENT;
return this;
}
/**
* Sets the content description for the {@link Chip}. It is highly recommended to provide
* this for chip containing icon.
*/
@NonNull
public Builder setContentDescription(@NonNull String contentDescription) {
this.mContentDescription = contentDescription;
return this;
}
/**
* Sets the content of the {@link Chip} to be the given primary text. Any previously added
* content will be overridden. Primary text can be on 1 or 2 lines, depending on the length.
*/
// There are multiple methods to set different type of content, but there is general getter
// getContent that will return LayoutElement set by any of it. b/217197259
@NonNull
@SuppressWarnings("MissingGetterMatchingBuilder") // There's getContent() method.
public Builder setPrimaryTextContent(@NonNull String primaryText) {
this.mPrimaryText = primaryText;
this.mLabelText = null;
this.mType = TEXT;
return this;
}
/**
* Used for creating CompactChip and TitleChip.
*
* <p>Sets the font for the primary text and should only be used internally.
*/
@NonNull
Builder setPrimaryTextFontStyle(@NonNull FontStyle fontStyle) {
this.mPrimaryTextFont = fontStyle;
return this;
}
/**
* Sets the content of the {@link Chip} to be the given primary text and secondary label.
* Any previously added content will be overridden. Primary text can be shown on 1 line
* only.
*/
@NonNull
@SuppressWarnings("MissingGetterMatchingBuilder") // There's getContent() method.
public Builder setPrimaryTextLabelContent(
@NonNull String primaryText, @NonNull String label) {
this.mPrimaryText = primaryText;
this.mLabelText = label;
this.mType = TEXT;
return this;
}
/**
* Sets the content of the {@link Chip} to be the given primary text with an icon and
* secondary label. Any previously added content will be overridden. Provided icon will be
* tinted to the given content color from {@link ChipColors}. This icon should be image with
* chosen alpha channel and not an actual image.
*/
@NonNull
@SuppressWarnings("MissingGetterMatchingBuilder") // There's getContent() method.
public Builder setPrimaryTextIconContent(
@NonNull String primaryText, @NonNull String resourceId) {
this.mPrimaryText = primaryText;
this.mResourceId = resourceId;
this.mLabelText = null;
this.mType = ICON;
return this;
}
/**
* Sets the content of the {@link Chip} to be the given primary text with an icon. Any
* previously added content will be overridden. Provided icon will be tinted to the given
* content color from {@link ChipColors}. This icon should be image with chosen alpha
* channel and not an actual image. Primary text can be shown on 1 line only.
*/
@NonNull
@SuppressWarnings("MissingGetterMatchingBuilder") // There's getContent() method.
public Builder setPrimaryTextLabelIconContent(
@NonNull String primaryText, @NonNull String label, @NonNull String resourceId) {
this.mPrimaryText = primaryText;
this.mLabelText = label;
this.mResourceId = resourceId;
this.mType = ICON;
return this;
}
/**
* Sets the colors for the {@link Chip}. If set, {@link ChipColors#getBackgroundColor()}
* will be used for the background of the button, {@link ChipColors#getContentColor()} for
* main text, {@link ChipColors#getSecondaryContentColor()} for label text and {@link
* ChipColors#getIconTintColor()} will be used as tint color for the icon itself. If not
* set, {@link ChipDefaults#PRIMARY} will be used.
*/
@NonNull
public Builder setChipColors(@NonNull ChipColors chipColors) {
mChipColors = chipColors;
return this;
}
// TODO(b/207350548): In RTL mode, should icon still be on the left.
/**
* Sets the horizontal alignment in the chip. It is strongly recommended that the content of
* the chip is start-aligned if there is more than primary text in it. If not set, {@link
* HorizontalAlignment#HORIZONTAL_ALIGN_START} will be used.
*/
@NonNull
public Builder setHorizontalAlignment(@HorizontalAlignment int horizontalAlignment) {
mHorizontalAlign = horizontalAlignment;
return this;
}
/** Used for creating CompactChip and TitleChip. */
@NonNull
Builder setHorizontalPadding(@NonNull DpProp horizontalPadding) {
this.mHorizontalPadding = horizontalPadding;
return this;
}
/** Used for creating CompactChip and TitleChip. */
@NonNull
Builder setVerticalPadding(@NonNull DpProp verticalPadding) {
this.mVerticalPadding = verticalPadding;
return this;
}
/** Used for creating CompactChip and TitleChip. */
@NonNull
Builder setHeight(@NonNull DpProp height) {
this.mHeight = height;
return this;
}
/** Used for creating CompactChip and TitleChip. */
@NonNull
Builder setWidth(@NonNull ContainerDimension width) {
this.mWidth = width;
return this;
}
/** Used for creating CompactChip and TitleChip. */
@NonNull
Builder setMaxLines(int maxLines) {
this.mMaxLines = maxLines;
return this;
}
/** Constructs and returns {@link Chip} with the provided content and look. */
@NonNull
@Override
public Chip build() {
Modifiers.Builder modifiers =
new Modifiers.Builder()
.setClickable(
new Clickable.Builder()
.setId(mClickableId)
.setOnClick(mAction)
.build())
.setPadding(
new Padding.Builder()
.setStart(mHorizontalPadding)
.setEnd(mHorizontalPadding)
.setBottom(mVerticalPadding)
.setTop(mVerticalPadding)
.build())
.setBackground(
new Background.Builder()
.setColor(mChipColors.getBackgroundColor())
.setCorner(
new Corner.Builder()
.setRadius(radiusOf(mHeight))
.build())
.build());
if (!mContentDescription.isEmpty()) {
modifiers.setSemantics(
new ModifiersBuilders.Semantics.Builder()
.setContentDescription(mContentDescription)
.build());
}
Box.Builder element =
new Box.Builder()
.setWidth(mWidth)
.setHeight(mHeight)
.setHorizontalAlignment(mHorizontalAlign)
.addContent(getCorrectContent())
.setModifiers(modifiers.build());
return new Chip(element.build());
}
@NonNull
private LayoutElement getCorrectContent() {
if (mType == NOT_SET) {
throw new IllegalStateException(
"No content set. Use setPrimaryTextContent or similar method to add"
+ " content");
}
if (mType == CUSTOM_CONTENT) {
return checkNotNull(mCustomContent);
}
Text mainTextElement =
new Text.Builder()
.setText(mPrimaryText)
.setFontStyle(customizeFontStyle())
.setMaxLines(getCorrectMaxLines())
.setOverflow(LayoutElementBuilders.TEXT_OVERFLOW_TRUNCATE)
.setMultilineAlignment(LayoutElementBuilders.TEXT_ALIGN_START)
.build();
// Placeholder for text.
Column.Builder column =
new Column.Builder()
.setHorizontalAlignment(HORIZONTAL_ALIGN_START)
.addContent(mainTextElement);
if (mLabelText != null) {
Text labelTextElement =
new Text.Builder()
.setText(mLabelText)
.setFontStyle(
FontStyles.caption2(mDeviceParameters)
.setColor(mChipColors.getSecondaryContentColor())
.build())
.setMaxLines(1)
.setOverflow(LayoutElementBuilders.TEXT_OVERFLOW_TRUNCATE)
.setMultilineAlignment(LayoutElementBuilders.TEXT_ALIGN_START)
.build();
column.addContent(labelTextElement);
}
if (mType == TEXT) {
return column.build();
} else {
return new Row.Builder()
.addContent(
new Image.Builder()
.setResourceId(mResourceId)
.setWidth(ICON_SIZE)
.setHeight(ICON_SIZE)
.setColorFilter(
new ColorFilter.Builder()
.setTint(mChipColors.getIconTintColor())
.build())
.build())
.addContent(
new Spacer.Builder()
.setHeight(mHeight)
.setWidth(VERTICAL_PADDING)
.build())
.addContent(column.build())
.setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER)
.build();
}
}
private FontStyle customizeFontStyle() {
return FontStyle.fromProto(
mPrimaryTextFont.toProto().toBuilder()
.setColor(mChipColors.getContentColor().toProto())
.build());
}
private int getCorrectMaxLines() {
if (mMaxLines > 0) {
return mMaxLines;
}
return mLabelText != null ? 1 : 2;
}
}
/** Returns height of this Chip. */
@NonNull
public ContainerDimension getHeight() {
return checkNotNull(mElement.getHeight());
}
/** Returns width of this Chip. */
@NonNull
public ContainerDimension getWidth() {
return checkNotNull(mElement.getWidth());
}
/** Returns click event action associated with this Chip. */
@NonNull
public Action getAction() {
return checkNotNull(
checkNotNull(checkNotNull(mElement.getModifiers()).getClickable()).getOnClick());
}
/** Returns background color of this Chip. */
@NonNull
private ColorProp getBackgroundColor() {
return checkNotNull(
checkNotNull(checkNotNull(mElement.getModifiers()).getBackground()).getColor());
}
/** Returns chip colors of this Chip. */
@NonNull
public ChipColors getChipColors() {
ColorProp backgroundColor = getBackgroundColor();
ColorProp contentColor = null;
ColorProp secondaryContentColor = null;
ColorProp iconTintColor = null;
LayoutElement content = getContent();
if (content instanceof Row) {
Row rowContent = (Row) content;
List<LayoutElement> contents = rowContent.getContents();
if (contents.size() == 3) {
// This is potentially our built chip with icon and column that has text. Column
// part will be extracted in the next condition.
LayoutElement element = contents.get(0);
if (element instanceof Image) {
// Extract icon tint color and prepare content for the second condition. If it
// is our chip, it will be a Column.
iconTintColor = getIconTintColorFromContent((Image) element);
content = contents.get(2);
}
}
}
if (content instanceof Column) {
Column columnContent = (Column) content;
List<LayoutElement> contents = columnContent.getContents();
if (contents.size() == 1 || contents.size() == 2) {
// This is potentially our chip and this part contains 1 or 2 lines of text.
LayoutElement element = contents.get(0);
if (element instanceof Text) {
contentColor = getTextColorFromContent((Text) element);
if (contents.size() == 2) {
element = contents.get(1);
if (element instanceof Text) {
secondaryContentColor = getTextColorFromContent((Text) element);
}
}
}
}
}
// Populate other colors if they are not found.
if (contentColor == null) {
contentColor = new ColorProp.Builder().build();
}
if (secondaryContentColor == null) {
secondaryContentColor = contentColor;
}
if (iconTintColor == null) {
iconTintColor = contentColor;
}
return new ChipColors(backgroundColor, iconTintColor, contentColor, secondaryContentColor);
}
private ColorProp getTextColorFromContent(Text text) {
ColorProp color = new ColorProp.Builder().build();
if (text.getFontStyle() != null && text.getFontStyle().getColor() != null) {
color = checkNotNull(checkNotNull(text.getFontStyle()).getColor());
}
return color;
}
private ColorProp getIconTintColorFromContent(Image image) {
ColorProp color = new ColorProp.Builder().build();
if (image.getColorFilter() != null && image.getColorFilter().getTint() != null) {
color = checkNotNull(checkNotNull(image.getColorFilter()).getTint());
}
return color;
}
/** Returns content description of this Chip. */
@NonNull
public String getContentDescription() {
return checkNotNull(
checkNotNull(checkNotNull(mElement.getModifiers()).getSemantics())
.getContentDescription());
}
/** Returns content of this Chip. */
@NonNull
public LayoutElement getContent() {
return checkNotNull(checkNotNull(mElement.getContents()).get(0));
}
/** Returns the horizontal alignment of the content in this Chip. */
public @HorizontalAlignment int getHorizontalAlignment() {
return checkNotNull(mElement.getHorizontalAlignment()).getValue();
}
/** @hide */
@NonNull
@Override
@RestrictTo(Scope.LIBRARY_GROUP)
public LayoutElementProto.LayoutElement toLayoutElementProto() {
return mElement.toLayoutElementProto();
}
}