MultiSlotLayout.java

/*
 * Copyright 2022 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.layouts;

import static androidx.annotation.Dimension.DP;
import static androidx.wear.tiles.DimensionBuilders.dp;
import static androidx.wear.tiles.DimensionBuilders.expand;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.DEFAULT_VERTICAL_SPACER_HEIGHT;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.MULTI_SLOT_LAYOUT_HORIZONTAL_SPACER_WIDTH;

import android.annotation.SuppressLint;

import androidx.annotation.Dimension;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters;
import androidx.wear.tiles.DimensionBuilders.DpProp;
import androidx.wear.tiles.LayoutElementBuilders;
import androidx.wear.tiles.LayoutElementBuilders.Box;
import androidx.wear.tiles.LayoutElementBuilders.Layout;
import androidx.wear.tiles.LayoutElementBuilders.LayoutElement;
import androidx.wear.tiles.LayoutElementBuilders.Row;
import androidx.wear.tiles.LayoutElementBuilders.Spacer;
import androidx.wear.tiles.TimelineBuilders.Timeline;
import androidx.wear.tiles.TimelineBuilders.TimelineEntry;
import androidx.wear.tiles.proto.LayoutElementProto;

import java.util.ArrayList;
import java.util.List;

/**
 * Opinionated Tiles layout style with optional primary and secondary Labels on rows 1 and 3, row 2
 * is a row of horizontally aligned and spaced slots (for icons or other small content). Followed by
 * a 4th row that contains a primary (compact) chip.
 *
 * <p>Recommended number of added slots is 1 to 3. Their width will be scaled to fit and have the
 * same value, with the {@link LayoutDefaults#MULTI_SLOT_LAYOUT_HORIZONTAL_SPACER_WIDTH} space
 * between.
 */
// TODO(b/215323986): Link visuals.
public class MultiSlotLayout implements LayoutElement {
    @NonNull private final PrimaryLayout mElement;

    MultiSlotLayout(@NonNull PrimaryLayout mElement) {
        this.mElement = mElement;
    }

    /** Builder class for {@link MultiSlotLayout}. */
    public static final class Builder implements LayoutElement.Builder {
        @NonNull private final DeviceParameters mDeviceParameters;
        @Nullable private LayoutElement mPrimaryChip = null;
        @Nullable private LayoutElement mPrimaryLabelText = null;
        @Nullable private LayoutElement mSecondaryLabelText = null;
        @NonNull private final List<LayoutElement> mSlotsContent = new ArrayList<>();
        @NonNull private DpProp mHorizontalSpacerWidth = MULTI_SLOT_LAYOUT_HORIZONTAL_SPACER_WIDTH;
        @NonNull private DpProp mVerticalSpacerHeight = DEFAULT_VERTICAL_SPACER_HEIGHT;

        /**
         * Creates a builder for the {@link MultiSlotLayout}. Content inside of it can later be set
         * with {@link #addSlotContent}, {@link #setPrimaryChipContent}, {@link
         * #setPrimaryLabelTextContent} and {@link #setSecondaryLabelTextContent}.
         */
        public Builder(@NonNull DeviceParameters deviceParameters) {
            this.mDeviceParameters = deviceParameters;
        }

        /** Sets the primary compact chip which will be at the bottom. */
        @NonNull
        @SuppressWarnings("MissingGetterMatchingBuilder")
        // There is no direct matching getter for this setter as the serialized format of the
        // ProtoLayouts do not allow for a direct reconstruction of the arguments. Instead there are
        // methods to get the contents a whole for rendering.
        public Builder setPrimaryChipContent(@NonNull LayoutElement primaryChip) {
            this.mPrimaryChip = primaryChip;
            return this;
        }

        /** Sets the primary label text which will be above the slots. */
        @NonNull
        @SuppressWarnings("MissingGetterMatchingBuilder")
        // There is no direct matching getter for this setter as the serialized format of the
        // ProtoLayouts do not allow for a direct reconstruction of the arguments. Instead there are
        // methods to get the contents a whole for rendering.
        public Builder setPrimaryLabelTextContent(@NonNull LayoutElement primaryLabelText) {
            this.mPrimaryLabelText = primaryLabelText;
            return this;
        }

        /** Sets the secondary label text which will be below the slots. */
        @NonNull
        @SuppressWarnings("MissingGetterMatchingBuilder")
        // There is no direct matching getter for this setter as the serialized format of the
        // ProtoLayouts do not allow for a direct reconstruction of the arguments. Instead there are
        // methods to get the contents a whole for rendering.
        public Builder setSecondaryLabelTextContent(@NonNull LayoutElement secondaryLabelText) {
            this.mSecondaryLabelText = secondaryLabelText;
            return this;
        }

        /** Add one new slot to the layout with the given content inside. */
        @NonNull
        @SuppressWarnings("MissingGetterMatchingBuilder")
        // There is no direct matching getter for this setter as the serialized format of the
        // ProtoLayouts do not allow for a direct reconstruction of the arguments. b/221427609
        public Builder addSlotContent(@NonNull LayoutElement slotContent) {
            mSlotsContent.add(slotContent);
            return this;
        }

        /**
         * Sets the horizontal spacer width which is used as a space between slots if there is more
         * than one slot. If not set, {@link
         * LayoutDefaults#MULTI_SLOT_LAYOUT_HORIZONTAL_SPACER_WIDTH} will be used.
         */
        @NonNull
        @SuppressWarnings("MissingGetterMatchingBuilder")
        // There is no direct matching getter for this setter as the serialized format of the
        // ProtoLayouts do not allow for a direct reconstruction of the arguments. Instead there are
        // methods to get the contents a whole for rendering.
        public Builder setHorizontalSpacerWidth(@Dimension(unit = DP) float width) {
            this.mHorizontalSpacerWidth = dp(width);
            return this;
        }

        /**
         * Sets the vertical spacer height which is used as a space between all slots and primary or
         * secondary label if there is any. If not set, {@link
         * LayoutDefaults#DEFAULT_VERTICAL_SPACER_HEIGHT} will be used.
         */
        @NonNull
        @SuppressWarnings("MissingGetterMatchingBuilder")
        // There is no direct matching getter for this setter as the serialized format of the
        // ProtoLayouts do not allow for a direct reconstruction of the arguments. Instead there are
        // methods to get the contents a whole for rendering.
        public Builder setVerticalSpacerHeight(@Dimension(unit = DP) float height) {
            this.mVerticalSpacerHeight = dp(height);
            return this;
        }

        @NonNull
        @Override
        // The @Dimension(unit = DP) on mVerticalSpacerHeight.getValue() is seemingly being ignored,
        // so lint complains that we're passing PX to something expecting DP. Just suppress the
        // warning for now.
        @SuppressLint("ResourceType")
        public MultiSlotLayout build() {
            PrimaryLayout.Builder layoutBuilder = new PrimaryLayout.Builder(mDeviceParameters);
            layoutBuilder.setVerticalSpacerHeight(mVerticalSpacerHeight.getValue());

            if (mPrimaryChip != null) {
                layoutBuilder.setPrimaryChipContent(mPrimaryChip);
            }

            if (mPrimaryLabelText != null) {
                layoutBuilder.setPrimaryLabelTextContent(mPrimaryLabelText);
            }

            if (mSecondaryLabelText != null) {
                layoutBuilder.setSecondaryLabelTextContent(mSecondaryLabelText);
            }

            if (mSlotsContent.size() > 0) {
                float horizontalPadding = layoutBuilder.getHorizontalPadding();
                DpProp rowWidth = dp(mDeviceParameters.getScreenWidthDp() - horizontalPadding * 2);
                Row.Builder rowBuilder =
                        new Row.Builder()
                                .setHeight(expand())
                                .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER)
                                .setWidth(rowWidth);

                boolean isFirst = true;
                for (LayoutElement column : mSlotsContent) {
                    if (!isFirst) {
                        rowBuilder.addContent(
                                new Spacer.Builder().setWidth(mHorizontalSpacerWidth).build());
                    } else {
                        isFirst = false;
                    }
                    rowBuilder.addContent(
                            new Box.Builder()
                                    .setWidth(expand())
                                    .setHeight(expand())
                                    .addContent(column)
                                    .build());
                }

                layoutBuilder.setContent(rowBuilder.build());
            }

            return new MultiSlotLayout(layoutBuilder.build());
        }
    }

    /** Returns the {@link Layout} object containing this layout template. */
    @NonNull
    public Layout toLayout() {
        return toLayoutBuilder().build();
    }

    /** Returns the {@link Layout.Builder} object containing this layout template. */
    @NonNull
    public Layout.Builder toLayoutBuilder() {
        return new Layout.Builder().setRoot(mElement);
    }

    /** Returns the {@link TimelineEntry.Builder} object containing this layout template. */
    @NonNull
    public TimelineEntry.Builder toTimelineEntryBuilder() {
        return new TimelineEntry.Builder().setLayout(toLayout());
    }

    /** Returns the {@link TimelineEntry} object containing this layout template. */
    @NonNull
    public TimelineEntry toTimelineEntry() {
        return toTimelineEntryBuilder().build();
    }

    /** Returns the {@link Timeline.Builder} object containing this layout template. */
    @NonNull
    public Timeline.Builder toTimelineBuilder() {
        return new Timeline.Builder().addTimelineEntry(toTimelineEntry());
    }

    /** Returns the {@link Timeline} object containing this layout template. */
    @NonNull
    public Timeline toTimeline() {
        return toTimelineBuilder().build();
    }

    /** @hide */
    @NonNull
    @Override
    @RestrictTo(Scope.LIBRARY_GROUP)
    public LayoutElementProto.LayoutElement toLayoutElementProto() {
        return mElement.toLayoutElementProto();
    }
}