// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.widget; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import org.chromium.base.ApiCompatibilityUtils; import org.chromium.chrome.R; import org.chromium.ui.widget.ButtonCompat; /** * 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 { public static final int ALIGN_START = 0; public static final int ALIGN_END = 1; public static final int ALIGN_APART = 2; /** * Creates a standardized Button that can be used for DualControlLayouts showing buttons. * * @param isPrimary Whether or not the button is meant to act as a "Confirm" button. * @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, boolean isPrimary, String text, OnClickListener listener) { int lightActiveColor = ApiCompatibilityUtils.getColor(context.getResources(), R.color.light_active_color); if (isPrimary) { ButtonCompat primaryButton = new ButtonCompat(context, lightActiveColor, false); primaryButton.setId(R.id.button_primary); primaryButton.setOnClickListener(listener); primaryButton.setText(text); primaryButton.setTextColor(Color.WHITE); return primaryButton; } else { Button secondaryButton = ButtonCompat.createBorderlessButton(context); secondaryButton.setId(R.id.button_secondary); secondaryButton.setOnClickListener(listener); secondaryButton.setText(text); secondaryButton.setTextColor(lightActiveColor); return secondaryButton; } } private final int mHorizontalMarginBetweenViews; private int mAlignment = ALIGN_START; 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.infobar_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(int alignment) { mAlignment = alignment; } /** * Sets the margin between the controls when they're stacked. By default, there is no margin. */ public void setStackedMargin(int stackedMargin) { mStackedMargin = stackedMargin; } @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 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 = ApiCompatibilityUtils.isLayoutRtl(this); boolean isPrimaryOnRight = (isRtl && mAlignment == ALIGN_START) || (!isRtl && (mAlignment == ALIGN_APART || mAlignment == ALIGN_END)); int primaryRight = isPrimaryOnRight ? (width - rightPadding) : (mPrimaryView.getMeasuredWidth() + leftPadding); int primaryLeft = primaryRight - mPrimaryView.getMeasuredWidth(); 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 == ALIGN_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 (mPrimaryView.getMeasuredWidth() > 0) { secondaryRight -= mHorizontalMarginBetweenViews; } secondaryLeft = secondaryRight - mSecondaryView.getMeasuredWidth(); } else { // Sit to the right of the primary View. secondaryLeft = primaryRight; if (mPrimaryView.getMeasuredWidth() > 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(), true, 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(), false, secondaryButtonText, null)); } } }