ProgressIndicatorLayout.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.wear.tiles.DimensionBuilders.dp;
import static androidx.wear.tiles.DimensionBuilders.expand;
import static androidx.wear.tiles.material.Helper.checkNotNull;
import static androidx.wear.tiles.material.Helper.isRoundDevice;
import static androidx.wear.tiles.material.ProgressIndicatorDefaults.DEFAULT_PADDING;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.PROGRESS_INDICATOR_LAYOUT_MARGIN_HORIZONTAL_ROUND_DP;
import static androidx.wear.tiles.material.layouts.LayoutDefaults.PROGRESS_INDICATOR_LAYOUT_MARGIN_HORIZONTAL_SQUARE_DP;

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.LayoutElement;
import androidx.wear.tiles.ModifiersBuilders.Modifiers;
import androidx.wear.tiles.ModifiersBuilders.Padding;
import androidx.wear.tiles.material.CircularProgressIndicator;
import androidx.wear.tiles.proto.LayoutElementProto;

/**
 * Tiles layout that represents the suggested layout style for Material Tiles with the progress
 * indicator around the edges of the screen and the given content inside of it and the recommended
 * margin and padding applied.
 */
// TODO(b/215323986): Link visuals.
public class ProgressIndicatorLayout implements LayoutElement {
    @NonNull private final LayoutElement mElement;

    ProgressIndicatorLayout(@NonNull LayoutElement layoutElement) {
        this.mElement = layoutElement;
    }

    /** Builder class for {@link ProgressIndicatorLayout}. */
    public static final class Builder implements LayoutElement.Builder {
        @NonNull private final DeviceParameters mDeviceParameters;
        @Nullable private LayoutElement mProgressIndicator = null;
        @Nullable private LayoutElement mContent = null;

        /**
         * Creates a builder for the {@link ProgressIndicatorLayout}t. Custom content inside of it
         * can later be set with ({@link #setContent}.
         */
        public Builder(@NonNull DeviceParameters deviceParameters) {
            this.mDeviceParameters = deviceParameters;
        }

        /** Sets the progress indicator which will be around the edges. */
        @NonNull
        // There is no direct matching getter for this setter as the serialized format of the
        // Tiles do not allow for a direct reconstruction of the arguments. Instead there are
        // methods to get the contents a whole for rendering.
        @SuppressWarnings("MissingGetterMatchingBuilder")
        public Builder setProgressIndicatorContent(@NonNull LayoutElement progressIndicator) {
            this.mProgressIndicator = progressIndicator;
            return this;
        }

        /** Sets the additional content to this layout, inside of the screen. */
        @NonNull
        public Builder setContent(@NonNull LayoutElement content) {
            this.mContent = content;
            return this;
        }

        /**
         * Constructs and returns {@link ProgressIndicatorLayout} with the provided content and
         * look.
         */
        @NonNull
        @Override
        public ProgressIndicatorLayout build() {
            float thicknessDp =
                    mProgressIndicator instanceof CircularProgressIndicator
                            ? ((CircularProgressIndicator) mProgressIndicator)
                                    .getStrokeWidth()
                                    .getValue()
                            : 0;
            float horizontalPaddingDp =
                    isRoundDevice(mDeviceParameters)
                            ? PROGRESS_INDICATOR_LAYOUT_MARGIN_HORIZONTAL_ROUND_DP
                            : PROGRESS_INDICATOR_LAYOUT_MARGIN_HORIZONTAL_SQUARE_DP;
            float indicatorWidth = 2 * (thicknessDp + DEFAULT_PADDING.getValue());
            float mainContentHeightDp = mDeviceParameters.getScreenHeightDp() - indicatorWidth;
            float mainContentWidthDp = mDeviceParameters.getScreenWidthDp() - indicatorWidth;

            DpProp mainContentHeight = dp(Math.min(mainContentHeightDp, mainContentWidthDp));
            DpProp mainContentWidth = dp(Math.min(mainContentHeightDp, mainContentWidthDp));

            Modifiers modifiers =
                    new Modifiers.Builder()
                            .setPadding(
                                    new Padding.Builder()
                                            .setStart(dp(horizontalPaddingDp))
                                            .setEnd(dp(horizontalPaddingDp))
                                            .build())
                            .build();

            Box.Builder boxBuilder =
                    new Box.Builder()
                            .setWidth(expand())
                            .setHeight(expand())
                            .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER)
                            .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER);

            if (mContent != null) {
                boxBuilder.addContent(
                        new Box.Builder()
                                .setModifiers(modifiers)
                                .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER)
                                .setHorizontalAlignment(
                                        LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER)
                                .setHeight(mainContentHeight)
                                .setWidth(mainContentWidth)
                                .addContent(mContent)
                                .build());
            }

            if (mProgressIndicator != null) {
                boxBuilder.addContent(mProgressIndicator);
            }

            return new ProgressIndicatorLayout(boxBuilder.build());
        }
    }

    /** Get the inner content from this layout. */
    @NonNull
    public LayoutElement getContent() {
        return checkNotNull(((Box) ((Box) mElement).getContents().get(0)).getContents().get(0));
    }

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