// Copyright 2013 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.infobar;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ClickableSpan;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.widget.DualControlLayout;
import org.chromium.ui.widget.ButtonCompat;
import java.util.ArrayList;
import java.util.List;
/**
* Layout that arranges an infobar's views.
*
* An InfoBarLayout consists of:
* - A message describing why the infobar is being displayed.
* - A close button in the top right corner.
* - (optional) An icon representing the infobar's purpose in the top left corner.
* - (optional) Additional {@link InfoBarControlLayouts} for specialized controls (e.g. spinners).
* - (optional) One or two buttons with text at the bottom, or a button paired with an ImageView.
*
* When adding custom views, widths and heights defined in the LayoutParams will be ignored.
* Setting a minimum width using {@link View#setMininumWidth()} will be obeyed.
*
* Logic for what happens when things are clicked should be implemented by the InfoBarView.
*/
public final class InfoBarLayout extends ViewGroup implements View.OnClickListener {
/**
* Parameters used for laying out children.
*/
private static class LayoutParams extends ViewGroup.LayoutParams {
public int startMargin;
public int endMargin;
public int topMargin;
public int bottomMargin;
// Where this view will be laid out. Calculated in onMeasure() and used in onLayout().
public int start;
public int top;
LayoutParams(int startMargin, int topMargin, int endMargin, int bottomMargin) {
super(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
this.startMargin = startMargin;
this.topMargin = topMargin;
this.endMargin = endMargin;
this.bottomMargin = bottomMargin;
}
}
private final int mSmallIconSize;
private final int mSmallIconMargin;
private final int mBigIconSize;
private final int mBigIconMargin;
private final int mMarginAboveButtonGroup;
private final int mMarginAboveControlGroups;
private final int mPadding;
private final int mMinWidth;
private final int mAccentColor;
private final InfoBarView mInfoBarView;
private final ImageButton mCloseButton;
private final InfoBarControlLayout mMessageLayout;
private final List<InfoBarControlLayout> mControlLayouts;
private TextView mMessageTextView;
private ImageView mIconView;
private DualControlLayout mButtonRowLayout;
private CharSequence mMessageMainText;
private String mMessageLinkText;
/**
* Constructs a layout for the specified infobar. After calling this, be sure to set the
* message, the buttons, and/or the custom content using setMessage(), setButtons(), and
* setCustomContent().
*
* @param context The context used to render.
* @param infoBarView InfoBarView that listens to events.
* @param iconResourceId ID of the icon to use for the infobar.
* @param iconBitmap Bitmap for the icon to use, if the resource ID wasn't passed through.
* @param message The message to show in the infobar.
*/
public InfoBarLayout(Context context, InfoBarView infoBarView, int iconResourceId,
Bitmap iconBitmap, CharSequence message) {
super(context);
mControlLayouts = new ArrayList<InfoBarControlLayout>();
mInfoBarView = infoBarView;
// Cache resource values.
Resources res = getResources();
mSmallIconSize = res.getDimensionPixelSize(R.dimen.infobar_small_icon_size);
mSmallIconMargin = res.getDimensionPixelSize(R.dimen.infobar_small_icon_margin);
mBigIconSize = res.getDimensionPixelSize(R.dimen.infobar_big_icon_size);
mBigIconMargin = res.getDimensionPixelSize(R.dimen.infobar_big_icon_margin);
mMarginAboveButtonGroup =
res.getDimensionPixelSize(R.dimen.infobar_margin_above_button_row);
mMarginAboveControlGroups =
res.getDimensionPixelSize(R.dimen.infobar_margin_above_control_groups);
mPadding = res.getDimensionPixelOffset(R.dimen.infobar_padding);
mMinWidth = res.getDimensionPixelSize(R.dimen.infobar_min_width);
mAccentColor = ApiCompatibilityUtils.getColor(res, R.color.infobar_accent_blue);
// Set up the close button. Apply padding so it has a big touch target.
mCloseButton = new ImageButton(context);
mCloseButton.setId(R.id.infobar_close_button);
mCloseButton.setImageResource(R.drawable.btn_close);
TypedArray a = getContext().obtainStyledAttributes(
new int [] {R.attr.selectableItemBackground});
Drawable closeButtonBackground = a.getDrawable(0);
a.recycle();
mCloseButton.setBackground(closeButtonBackground);
mCloseButton.setPadding(mPadding, mPadding, mPadding, mPadding);
mCloseButton.setOnClickListener(this);
mCloseButton.setContentDescription(res.getString(R.string.infobar_close));
mCloseButton.setLayoutParams(new LayoutParams(0, -mPadding, -mPadding, -mPadding));
// Set up the icon.
if (iconResourceId != 0 || iconBitmap != null) {
mIconView = new ImageView(context);
if (iconResourceId != 0) {
mIconView.setImageResource(iconResourceId);
} else if (iconBitmap != null) {
mIconView.setImageBitmap(iconBitmap);
}
mIconView.setLayoutParams(new LayoutParams(0, 0, mSmallIconMargin, 0));
mIconView.getLayoutParams().width = mSmallIconSize;
mIconView.getLayoutParams().height = mSmallIconSize;
mIconView.setFocusable(false);
}
// Set up the message view.
mMessageMainText = message;
mMessageLayout = new InfoBarControlLayout(context);
mMessageTextView = mMessageLayout.addMainMessage(prepareMainMessageString());
}
/**
* Returns the {@link TextView} corresponding to the main infobar message.
*/
TextView getMessageTextView() {
return mMessageTextView;
}
/**
* Returns the {@link InfoBarControlLayout} containing the TextView showing the main infobar
* message and associated controls, which is sandwiched between its icon and close button.
*/
InfoBarControlLayout getMessageLayout() {
return mMessageLayout;
}
/**
* Sets the message to show on the infobar.
* TODO(dfalcantara): Do some magic here to determine if TextViews need to have line spacing
* manually added. Android changed when these values were applied between
* KK and L: https://crbug.com/543205
*/
public void setMessage(CharSequence message) {
mMessageMainText = message;
mMessageTextView.setText(prepareMainMessageString());
}
/**
* Sets the message to show for a link in the message, if an infobar requires a link
* (e.g. "Learn more").
*/
public void setMessageLinkText(String linkText) {
mMessageLinkText = linkText;
mMessageTextView.setText(prepareMainMessageString());
}
/**
* Adds an {@link InfoBarControlLayout} to house additional infobar controls, like toggles and
* spinners.
*/
public InfoBarControlLayout addControlLayout() {
InfoBarControlLayout controlLayout = new InfoBarControlLayout(getContext());
mControlLayouts.add(controlLayout);
return controlLayout;
}
/**
* Adds one or two buttons to the layout.
*
* @param primaryText Text for the primary button. If empty, no buttons are added at all.
* @param secondaryText Text for the secondary button, or null if there isn't a second button.
*/
public void setButtons(String primaryText, String secondaryText) {
if (TextUtils.isEmpty(primaryText)) {
assert TextUtils.isEmpty(secondaryText);
return;
}
Button secondaryButton = null;
if (!TextUtils.isEmpty(secondaryText)) {
secondaryButton = DualControlLayout.createButtonForLayout(
getContext(), false, secondaryText, this);
}
setBottomViews(primaryText, secondaryButton, DualControlLayout.ALIGN_END);
}
/**
* Sets up the bottom-most part of the infobar with a primary button (e.g. OK) and a secondary
* View of your choice. Subclasses should be calling {@link #setButtons(String, String)}
* instead of this function in nearly all cases (that function calls this one).
*
* @param primaryText Text to display on the primary button. If empty, the bottom layout is not
* created.
* @param secondaryView View that is aligned with the primary button. May be null.
* @param alignment One of ALIGN_START, ALIGN_APART, or ALIGN_END from
* {@link DualControlLayout}.
*/
public void setBottomViews(String primaryText, View secondaryView, int alignment) {
assert !TextUtils.isEmpty(primaryText);
Button primaryButton = DualControlLayout.createButtonForLayout(
getContext(), true, primaryText, this);
assert mButtonRowLayout == null;
mButtonRowLayout = new DualControlLayout(getContext(), null);
mButtonRowLayout.setAlignment(alignment);
mButtonRowLayout.setStackedMargin(getResources().getDimensionPixelSize(
R.dimen.infobar_margin_between_stacked_buttons));
mButtonRowLayout.addView(primaryButton);
if (secondaryView != null) mButtonRowLayout.addView(secondaryView);
}
/**
* Adjusts styling to account for the big icon layout.
*/
public void setIsUsingBigIcon() {
LayoutParams lp = (LayoutParams) mIconView.getLayoutParams();
lp.width = mBigIconSize;
lp.height = mBigIconSize;
lp.endMargin = mBigIconMargin;
Resources res = getContext().getResources();
String typeface = res.getString(R.string.roboto_medium_typeface);
int textStyle = res.getInteger(R.integer.roboto_medium_textstyle);
float textSize = res.getDimension(R.dimen.infobar_big_icon_message_size);
mMessageTextView.setTypeface(Typeface.create(typeface, textStyle));
mMessageTextView.setMaxLines(1);
mMessageTextView.setEllipsize(TextUtils.TruncateAt.END);
mMessageTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
}
/**
* Returns the primary button, or null if it doesn't exist.
*/
public ButtonCompat getPrimaryButton() {
return mButtonRowLayout == null ? null
: (ButtonCompat) mButtonRowLayout.findViewById(R.id.button_primary);
}
/**
* Returns the icon, or null if it doesn't exist.
*/
public ImageView getIcon() {
return mIconView;
}
/**
* Must be called after the message, buttons, and custom content have been set, and before the
* first call to onMeasure().
*/
void onContentCreated() {
// Add the child views in the desired focus order.
if (mIconView != null) addView(mIconView);
addView(mMessageLayout);
for (View v : mControlLayouts) addView(v);
if (mButtonRowLayout != null) addView(mButtonRowLayout);
addView(mCloseButton);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(0, 0, 0, 0);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// Place all the views in the positions already determined during onMeasure().
int width = right - left;
boolean isRtl = ApiCompatibilityUtils.isLayoutRtl(this);
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childLeft = lp.start;
int childRight = lp.start + child.getMeasuredWidth();
if (isRtl) {
int tmp = width - childRight;
childRight = width - childLeft;
childLeft = tmp;
}
child.layout(childLeft, lp.top, childRight, lp.top + child.getMeasuredHeight());
}
}
/**
* Measures and determines where children should go.
*
* For current specs, see https://goto.google.com/infobar-spec
*
* All controls are padded from the infobar boundary by the same amount, but different types of
* control groups are bound by different widths and have different margins:
* --------------------------------------------------------------------------------
* | PADDING |
* | -------------------------------------------------------------------------- |
* | | ICON | MESSAGE LAYOUT | X | |
* | |------+ +---| |
* | | | | | |
* | | ------------------------------------------------------------------| |
* | | | CONTROL LAYOUT #1 | |
* | | ------------------------------------------------------------------| |
* | | | CONTROL LAYOUT #X | |
* | |------------------------------------------------------------------------| |
* | | BOTTOM ROW LAYOUT | |
* | -------------------------------------------------------------------------| |
* | |
* --------------------------------------------------------------------------------
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
assert getLayoutParams().height == LayoutParams.WRAP_CONTENT
: "InfoBar heights cannot be constrained.";
// Apply the padding that surrounds all the infobar controls.
final int layoutWidth = Math.max(MeasureSpec.getSize(widthMeasureSpec), mMinWidth);
final int paddedStart = mPadding;
final int paddedEnd = layoutWidth - mPadding;
int layoutBottom = mPadding;
// Measure and place the icon in the top-left corner.
int unspecifiedSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
if (mIconView != null) {
LayoutParams iconParams = getChildLayoutParams(mIconView);
measureChild(mIconView, unspecifiedSpec, unspecifiedSpec);
iconParams.start = paddedStart + iconParams.startMargin;
iconParams.top = layoutBottom + iconParams.topMargin;
}
final int iconWidth = getChildWidthWithMargins(mIconView);
// Measure and place the close button in the top-right corner of the layout.
LayoutParams closeParams = getChildLayoutParams(mCloseButton);
measureChild(mCloseButton, unspecifiedSpec, unspecifiedSpec);
closeParams.start = paddedEnd - closeParams.endMargin - mCloseButton.getMeasuredWidth();
closeParams.top = layoutBottom + closeParams.topMargin;
// Determine how much width is available for all the different control layouts; see the
// function JavaDoc above for details.
final int paddedWidth = paddedEnd - paddedStart;
final int controlLayoutWidth = paddedWidth - iconWidth;
final int messageWidth = controlLayoutWidth - getChildWidthWithMargins(mCloseButton);
// The message layout is sandwiched between the icon and the close button.
LayoutParams messageParams = getChildLayoutParams(mMessageLayout);
measureChildWithFixedWidth(mMessageLayout, messageWidth);
messageParams.start = paddedStart + iconWidth;
messageParams.top = layoutBottom;
// Control layouts are placed below the message layout and the close button. The icon is
// ignored for this particular calculation because the icon enforces a left margin on all of
// the control layouts and won't be overlapped.
layoutBottom += Math.max(getChildHeightWithMargins(mMessageLayout),
getChildHeightWithMargins(mCloseButton));
// The other control layouts are constrained only by the icon's width.
final int controlPaddedStart = paddedStart + iconWidth;
for (int i = 0; i < mControlLayouts.size(); i++) {
View child = mControlLayouts.get(i);
measureChildWithFixedWidth(child, controlLayoutWidth);
layoutBottom += mMarginAboveControlGroups;
getChildLayoutParams(child).start = controlPaddedStart;
getChildLayoutParams(child).top = layoutBottom;
layoutBottom += child.getMeasuredHeight();
}
// The button layout takes up the full width of the infobar and sits below everything else,
// including the icon.
layoutBottom = Math.max(layoutBottom, getChildHeightWithMargins(mIconView));
if (mButtonRowLayout != null) {
measureChildWithFixedWidth(mButtonRowLayout, paddedWidth);
layoutBottom += mMarginAboveButtonGroup;
getChildLayoutParams(mButtonRowLayout).start = paddedStart;
getChildLayoutParams(mButtonRowLayout).top = layoutBottom;
layoutBottom += mButtonRowLayout.getMeasuredHeight();
}
// Apply padding to the bottom of the infobar.
layoutBottom += mPadding;
setMeasuredDimension(resolveSize(layoutWidth, widthMeasureSpec),
resolveSize(layoutBottom, heightMeasureSpec));
}
private static int getChildWidthWithMargins(View view) {
if (view == null) return 0;
return view.getMeasuredWidth() + getChildLayoutParams(view).startMargin
+ getChildLayoutParams(view).endMargin;
}
private static int getChildHeightWithMargins(View view) {
if (view == null) return 0;
return view.getMeasuredHeight() + getChildLayoutParams(view).topMargin
+ getChildLayoutParams(view).bottomMargin;
}
private static LayoutParams getChildLayoutParams(View view) {
return (LayoutParams) view.getLayoutParams();
}
/**
* Measures a child for the given space, taking into account its margins.
*/
private void measureChildWithFixedWidth(View child, int width) {
LayoutParams lp = getChildLayoutParams(child);
int availableWidth = width - lp.startMargin - lp.endMargin;
int widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.EXACTLY);
int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
child.measure(widthSpec, heightSpec);
}
/**
* Listens for View clicks.
* Classes that override this function MUST call this one.
* @param view View that was clicked on.
*/
@Override
public void onClick(View view) {
mInfoBarView.setControlsEnabled(false);
if (view.getId() == R.id.infobar_close_button) {
mInfoBarView.onCloseButtonClicked();
} else if (view.getId() == R.id.button_primary) {
mInfoBarView.onButtonClicked(true);
} else if (view.getId() == R.id.button_secondary) {
mInfoBarView.onButtonClicked(false);
}
}
/**
* Prepares text to be displayed as the infobar's main message, including setting up a
* clickable link if the infobar requires it.
*/
private CharSequence prepareMainMessageString() {
SpannableStringBuilder fullString = new SpannableStringBuilder();
if (mMessageMainText != null) fullString.append(mMessageMainText);
// Concatenate the text to display for the link and make it clickable.
if (!TextUtils.isEmpty(mMessageLinkText)) {
if (fullString.length() > 0) fullString.append(" ");
int spanStart = fullString.length();
fullString.append(mMessageLinkText);
fullString.setSpan(new ClickableSpan() {
@Override
public void onClick(View view) {
mInfoBarView.onLinkClicked();
}
}, spanStart, fullString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return fullString;
}
}