chromium/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/DualControlLayout.java

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.browser_ui.widget;

import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;

import androidx.annotation.IdRes;
import androidx.annotation.IntDef;
import androidx.annotation.StyleRes;

import org.chromium.ui.widget.ButtonCompat;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Automatically lays out one or two Views, placing them on the same row if possible and stacking
 * them otherwise.
 *
 * Use cases of this Layout include placement of infobar buttons and placement of TextViews inside
 * of spinner controls (http://goto.google.com/infobar-spec).
 *
 * Layout parameters (i.e. margins) are ignored to enforce consistency.  Alignment defines where the
 * controls are placed (for RTL, flip everything):
 *
 * ALIGN_START                      ALIGN_APART                      ALIGN_END
 * -----------------------------    -----------------------------    -----------------------------
 * | PRIMARY SECONDARY         |    | SECONDARY         PRIMARY |    |         SECONDARY PRIMARY |
 * -----------------------------    -----------------------------    -----------------------------
 *
 * Controls are stacked automatically when they don't fit on the same row, with each control taking
 * up the full available width and with the primary control sitting on top of the secondary.
 * -----------------------------
 * | PRIMARY------------------ |
 * | SECONDARY---------------- |
 * -----------------------------
 */
public final class DualControlLayout extends ViewGroup {
    // When changing these values, you need to update ui/android/java/res/values/attrs.xml
    @IntDef({
        DualControlLayoutAlignment.START,
        DualControlLayoutAlignment.END,
        DualControlLayoutAlignment.APART
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface DualControlLayoutAlignment {
        int START = 0;
        int END = 1;
        int APART = 2;
    }

    @IntDef({
        ButtonType.PRIMARY_FILLED,
        ButtonType.PRIMARY_TEXT,
        ButtonType.SECONDARY_TEXT,
        ButtonType.PRIMARY_OUTLINED,
        ButtonType.SECONDARY_OUTLINED
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ButtonType {
        // Buttons will have the primary button ID with a filled container.
        int PRIMARY_FILLED = 0;

        //  Buttons will have the primary button ID without an outlined border or a filled container
        int PRIMARY_TEXT = 1;

        // Buttons will have the primary button ID with an outlined border but without a filled
        // container.
        int PRIMARY_OUTLINED = 2;

        // Buttons will have the secondary button ID without an outlined border or a filled
        // container (same style as PRIMARY_TEXT)
        int SECONDARY_TEXT = 3;

        // Buttons will have the secondary button ID with an outlined border but without a filled
        // container (same style as PRIMARY_OUTLINED)
        int SECONDARY_OUTLINED = 4;
    }

    /**
     * Creates a standardized Button that can be used for DualControlLayouts showing buttons.
     *
     * @param buttonType Determines button's function and appearance.
     * @param text Text to display on the button.
     * @param listener Listener to alert when the button has been clicked.
     * @return Button that can be used in the view.
     */
    public static Button createButtonForLayout(
            Context context, @ButtonType int buttonType, String text, OnClickListener listener) {
        ButtonCompat button = new ButtonCompat(context, getButtonTheme(buttonType));
        button.setId(getButtonId(buttonType));
        button.setOnClickListener(listener);
        button.setText(text);

        if (buttonType == ButtonType.PRIMARY_OUTLINED
                || buttonType == ButtonType.SECONDARY_OUTLINED) {
            // TODO(crbug.com/346931122) By default R.style.OutlinedButton capitalizes the text
            // labels.
            button.setAllCaps(false);
        }

        return button;
    }

    private static @StyleRes int getButtonTheme(@ButtonType int buttonType) {
        switch (buttonType) {
            case ButtonType.PRIMARY_FILLED:
                return R.style.FilledButtonThemeOverlay;
            case ButtonType.PRIMARY_TEXT:
            case ButtonType.SECONDARY_TEXT:
                return R.style.TextButtonThemeOverlay;
            case ButtonType.PRIMARY_OUTLINED:
            case ButtonType.SECONDARY_OUTLINED:
                return R.style.OutlinedButton;
            default:
                throw new IllegalArgumentException("Unknown button type");
        }
    }

    private static @IdRes int getButtonId(@ButtonType int buttonType) {
        switch (buttonType) {
            case ButtonType.PRIMARY_FILLED:
            case ButtonType.PRIMARY_TEXT:
            case ButtonType.PRIMARY_OUTLINED:
                return R.id.button_primary;
            case ButtonType.SECONDARY_TEXT:
            case ButtonType.SECONDARY_OUTLINED:
                return R.id.button_secondary;
            default:
                throw new IllegalArgumentException("Unknown button type");
        }
    }

    private final int mHorizontalMarginBetweenViews;

    /** Define how the controls will be laid out. */
    @DualControlLayoutAlignment private int mAlignment = DualControlLayoutAlignment.START;

    /** Margin between the controls when they're stacked.  By default, there is no margin. */
    private int mStackedMargin;

    private boolean mIsStacked;
    private View mPrimaryView;
    private View mSecondaryView;

    /**
     * Construct a new DualControlLayout.
     *
     * See {@link ViewGroup} for parameter details.  attrs may be null if constructed dynamically.
     */
    public DualControlLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        // Cache dimensions.
        Resources resources = getContext().getResources();
        mHorizontalMarginBetweenViews =
                resources.getDimensionPixelSize(R.dimen.dual_control_margin_between_items);

        if (attrs != null) parseAttributes(attrs);
    }

    /**
     * Define how the controls will be laid out.
     *
     * @param alignment One of ALIGN_START, ALIGN_APART, ALIGN_END.
     */
    public void setAlignment(@DualControlLayoutAlignment int alignment) {
        mAlignment = alignment;
    }

    /** See {@link #mAlignment}. */
    @DualControlLayoutAlignment
    public int getAlignment() {
        return mAlignment;
    }

    /** See {@link #mStackedMargin}. */
    public void setStackedMargin(int stackedMargin) {
        mStackedMargin = stackedMargin;
    }

    /** See {@link #mStackedMargin}. */
    public int getStackedMargin() {
        return mStackedMargin;
    }

    @Override
    public void onViewAdded(View child) {
        super.onViewAdded(child);

        if (mPrimaryView == null) {
            mPrimaryView = child;
        } else if (mSecondaryView == null) {
            mSecondaryView = child;
        } else {
            throw new IllegalStateException("Too many children added to DualControlLayout");
        }
    }

    @Override
    public void removeAllViews() {
        mPrimaryView = null;
        mSecondaryView = null;
        super.removeAllViews();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mIsStacked = false;
        int sidePadding = getPaddingLeft() + getPaddingRight();
        int verticalPadding = getPaddingTop() + getPaddingBottom();

        // Measure the primary View, allowing it to be as wide as the Layout.
        int maxWidth =
                MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED
                        ? Integer.MAX_VALUE
                        : (MeasureSpec.getSize(widthMeasureSpec) - sidePadding);
        int unspecifiedSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        measureChild(mPrimaryView, unspecifiedSpec, unspecifiedSpec);

        int layoutWidth = mPrimaryView.getMeasuredWidth();
        int layoutHeight = mPrimaryView.getMeasuredHeight();

        if (mSecondaryView != null) {
            // Measure the secondary View, allowing it to be as wide as the layout.
            measureChild(mSecondaryView, unspecifiedSpec, unspecifiedSpec);
            int combinedWidth = mPrimaryView.getMeasuredWidth() + mSecondaryView.getMeasuredWidth();
            if (mPrimaryView.getMeasuredWidth() > 0 && mSecondaryView.getMeasuredWidth() > 0) {
                combinedWidth += mHorizontalMarginBetweenViews;
            }

            if (combinedWidth > maxWidth) {
                // Stack the Views on top of each other.
                mIsStacked = true;

                int widthSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY);
                mPrimaryView.measure(widthSpec, unspecifiedSpec);
                mSecondaryView.measure(widthSpec, unspecifiedSpec);

                layoutWidth = maxWidth;
                layoutHeight =
                        mPrimaryView.getMeasuredHeight()
                                + mStackedMargin
                                + mSecondaryView.getMeasuredHeight();
            } else {
                // The Views fit side by side.  Check which is taller to find the layout height.
                layoutWidth = combinedWidth;
                layoutHeight = Math.max(layoutHeight, mSecondaryView.getMeasuredHeight());
            }
        }
        layoutWidth += sidePadding;
        layoutHeight += verticalPadding;

        setMeasuredDimension(
                resolveSize(layoutWidth, widthMeasureSpec),
                resolveSize(layoutHeight, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int leftPadding = getPaddingLeft();
        int rightPadding = getPaddingRight();

        int width = right - left;
        boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
        boolean isPrimaryOnRight =
                (isRtl && mAlignment == DualControlLayoutAlignment.START)
                        || (!isRtl
                                && (mAlignment == DualControlLayoutAlignment.APART
                                        || mAlignment == DualControlLayoutAlignment.END));

        // If primary view visibility is GONE, do not take into account the space it would occupy.
        int primaryViewMeasuredWidth =
                mPrimaryView.getVisibility() != View.GONE ? mPrimaryView.getMeasuredWidth() : 0;
        int primaryRight =
                isPrimaryOnRight
                        ? (width - rightPadding)
                        : (primaryViewMeasuredWidth + leftPadding);
        int primaryLeft = primaryRight - primaryViewMeasuredWidth;
        int primaryTop = getPaddingTop();
        int primaryBottom = primaryTop + mPrimaryView.getMeasuredHeight();
        mPrimaryView.layout(primaryLeft, primaryTop, primaryRight, primaryBottom);

        if (mIsStacked) {
            // Fill out the row.  onMeasure() should have already applied the correct width.
            int secondaryTop = primaryBottom + mStackedMargin;
            int secondaryBottom = secondaryTop + mSecondaryView.getMeasuredHeight();
            mSecondaryView.layout(
                    leftPadding,
                    secondaryTop,
                    leftPadding + mSecondaryView.getMeasuredWidth(),
                    secondaryBottom);
        } else if (mSecondaryView != null) {
            // Center the secondary View vertically with the primary View.
            int secondaryHeight = mSecondaryView.getMeasuredHeight();
            int primaryCenter = (primaryTop + primaryBottom) / 2;
            int secondaryTop = primaryCenter - (secondaryHeight / 2);
            int secondaryBottom = secondaryTop + secondaryHeight;

            // Determine where to place the secondary View.
            int secondaryLeft;
            int secondaryRight;
            if (mAlignment == DualControlLayoutAlignment.APART) {
                // Put the second View on the other side of the Layout from the primary View.
                secondaryLeft =
                        isPrimaryOnRight
                                ? leftPadding
                                : width - rightPadding - mSecondaryView.getMeasuredWidth();
                secondaryRight = secondaryLeft + mSecondaryView.getMeasuredWidth();
            } else if (isPrimaryOnRight) {
                // Sit to the left of the primary View.
                secondaryRight = primaryLeft;
                if (primaryViewMeasuredWidth > 0) {
                    secondaryRight -= mHorizontalMarginBetweenViews;
                }
                secondaryLeft = secondaryRight - mSecondaryView.getMeasuredWidth();
            } else {
                // Sit to the right of the primary View.
                secondaryLeft = primaryRight;
                if (primaryViewMeasuredWidth > 0) {
                    secondaryLeft += mHorizontalMarginBetweenViews;
                }
                secondaryRight = secondaryLeft + mSecondaryView.getMeasuredWidth();
            }

            mSecondaryView.layout(secondaryLeft, secondaryTop, secondaryRight, secondaryBottom);
        }
    }

    private void parseAttributes(AttributeSet attrs) {
        TypedArray a =
                getContext().obtainStyledAttributes(attrs, R.styleable.DualControlLayout, 0, 0);

        // Set the stacked margin.
        if (a.hasValue(R.styleable.DualControlLayout_stackedMargin)) {
            setStackedMargin(
                    a.getDimensionPixelSize(R.styleable.DualControlLayout_stackedMargin, 0));
        }

        // Create the primary button, if necessary.
        String primaryButtonText = null;
        if (a.hasValue(R.styleable.DualControlLayout_primaryButtonText)) {
            primaryButtonText = a.getString(R.styleable.DualControlLayout_primaryButtonText);
        }
        if (!TextUtils.isEmpty(primaryButtonText)) {
            addView(
                    createButtonForLayout(
                            getContext(), ButtonType.PRIMARY_FILLED, primaryButtonText, null));
        }

        // Build the secondary button, but only if there's a primary button set.
        String secondaryButtonText = null;
        if (a.hasValue(R.styleable.DualControlLayout_secondaryButtonText)) {
            secondaryButtonText = a.getString(R.styleable.DualControlLayout_secondaryButtonText);
        }
        if (!TextUtils.isEmpty(primaryButtonText) && !TextUtils.isEmpty(secondaryButtonText)) {
            addView(
                    createButtonForLayout(
                            getContext(), ButtonType.SECONDARY_TEXT, secondaryButtonText, null));
        }

        // Set the alignment.
        if (a.hasValue(R.styleable.DualControlLayout_buttonAlignment)) {
            setAlignment(
                    a.getInt(
                            R.styleable.DualControlLayout_buttonAlignment,
                            DualControlLayoutAlignment.START));
        }

        a.recycle();
    }
}