| // 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.text.TextUtils; |
| import android.text.method.LinkMovementMethod; |
| import android.view.LayoutInflater; |
| 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.chrome.R; |
| import org.chromium.ui.base.LocalizationUtils; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * Layout that can be used to arrange an InfoBar's View. |
| * All InfoBars consist of at least: |
| * - An icon representing the InfoBar's purpose on the left side. |
| * - A message describing the action that the user can take. |
| * - A close button on the right side. |
| * |
| * Views should never be added with anything but a call to addGroup() to ensure that groups are not |
| * broken apart. |
| * |
| * Widths and heights defined in the LayoutParams will be overwritten due to the nature of the |
| * layout algorithm. However, setting a minimum width in another way, like TextView.getMinWidth(), |
| * should still be obeyed. |
| * |
| * Logic for what happens when things are clicked should be implemented by the InfoBarView. |
| */ |
| public class InfoBarLayout extends ViewGroup implements View.OnClickListener { |
| private static final String TAG = "InfoBarLayout"; |
| |
| /** |
| * Parameters used for laying out children. |
| */ |
| public static class LayoutParams extends ViewGroup.LayoutParams { |
| /** Alignment parameters that determine where in the main row an item will float. */ |
| public static final int ALIGN_START = 0; |
| public static final int ALIGN_END = 1; |
| |
| /** Whether the View is meant for the main row. */ |
| public boolean isInMainRow; |
| |
| /** Views grouped together are laid out together immediately adjacent to each other. */ |
| public boolean isGroupedWithNextView; |
| |
| /** When on the main row, indicates whether the control floats on the left or the right. */ |
| public int align; |
| |
| /** If the control is a button, ID of the resource that was last used as its background. */ |
| public int background; |
| |
| public LayoutParams() { |
| super(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); |
| align = ALIGN_END; |
| isInMainRow = true; |
| } |
| |
| public LayoutParams(LayoutParams other) { |
| super(other); |
| isGroupedWithNextView = other.isGroupedWithNextView; |
| align = other.align; |
| isInMainRow = other.isInMainRow; |
| } |
| } |
| |
| private static class GroupInfo { |
| public int numViews; |
| public int width; |
| public int greatestMemberWidth; |
| public int endIndex; |
| public boolean hasButton; |
| }; |
| |
| private final int mDimensionMinSize; |
| private final int mDimensionMargin; |
| private final int mDimensionIconSize; |
| private final boolean mLayoutRTL; |
| private final InfoBarView mInfoBarView; |
| |
| private final ImageView mIconView; |
| private final TextView mMessageView; |
| private final ImageButton mCloseButton; |
| |
| /** Background resource IDs to use for the buttons. */ |
| private final int mBackgroundFloating; |
| private final int mBackgroundFullLeft; |
| private final int mBackgroundFullRight; |
| |
| /** |
| * Indices of child Views that start new layout rows. |
| * The last entry is the number of child Views, allowing calculation of the size of each row by |
| * taking the difference between subsequent indices. |
| */ |
| private final ArrayList<Integer> mIndicesOfRows; |
| |
| /** |
| * Constructs the layout for the specified InfoBar. |
| * @param context The context used to render. |
| * @param infoBarView InfoBarView that listens to events. |
| * @param backgroundType Type of InfoBar background being shown. |
| * @param iconResourceId ID of the icon to use for the InfoBar. |
| */ |
| public InfoBarLayout(Context context, InfoBarView infoBarView, int backgroundType, |
| int iconResourceId) { |
| super(context); |
| mIndicesOfRows = new ArrayList<Integer>(); |
| mLayoutRTL = LocalizationUtils.isSystemLayoutDirectionRtl(); |
| mInfoBarView = infoBarView; |
| |
| // Determine what backgrounds we'll be needing for the buttons. |
| if (backgroundType == InfoBar.BACKGROUND_TYPE_INFO) { |
| mBackgroundFloating = R.drawable.infobar_button_normal_floating; |
| mBackgroundFullLeft = R.drawable.infobar_button_normal_full_left; |
| mBackgroundFullRight = R.drawable.infobar_button_normal_full_right; |
| } else { |
| mBackgroundFloating = R.drawable.infobar_button_warning_floating; |
| mBackgroundFullLeft = R.drawable.infobar_button_warning_full_left; |
| mBackgroundFullRight = R.drawable.infobar_button_warning_full_right; |
| } |
| |
| // Grab the dimensions. |
| mDimensionMinSize = |
| context.getResources().getDimensionPixelSize(R.dimen.infobar_min_size); |
| mDimensionMargin = |
| context.getResources().getDimensionPixelSize(R.dimen.infobar_margin); |
| mDimensionIconSize = |
| context.getResources().getDimensionPixelSize(R.dimen.infobar_icon_size); |
| |
| // Create the main controls. |
| mCloseButton = new ImageButton(context); |
| mIconView = new ImageView(context); |
| mMessageView = (TextView) LayoutInflater.from(context).inflate(R.layout.infobar_text, null); |
| addGroup(mCloseButton, mIconView, mMessageView); |
| |
| // Set up the close button. |
| mCloseButton.setId(R.id.infobar_close_button); |
| mCloseButton.setImageResource(R.drawable.infobar_dismiss); |
| mCloseButton.setBackgroundResource(R.drawable.infobar_close_bg); |
| mCloseButton.setOnClickListener(this); |
| |
| mCloseButton.setContentDescription(getResources().getString(R.string.infobar_close)); |
| |
| // Set up the icon. |
| mIconView.setFocusable(false); |
| if (iconResourceId != 0) { |
| mIconView.setImageResource(iconResourceId); |
| } else { |
| mIconView.setVisibility(View.INVISIBLE); |
| } |
| |
| // Set up the TextView. |
| mMessageView.setMovementMethod(LinkMovementMethod.getInstance()); |
| mMessageView.setText(infoBarView.getMessageText(context), TextView.BufferType.SPANNABLE); |
| |
| // Only the close button floats to the right; the icon and the message both float left. |
| ((LayoutParams) mIconView.getLayoutParams()).align = LayoutParams.ALIGN_START; |
| ((LayoutParams) mMessageView.getLayoutParams()).align = LayoutParams.ALIGN_START; |
| |
| // Vertically center the icon and close buttons of an unstretched InfoBar. If the InfoBar |
| // is stretched, they both stay in place. |
| mIconView.getLayoutParams().width = mDimensionIconSize; |
| mIconView.getLayoutParams().height = mDimensionIconSize; |
| |
| // We apply padding to the close button so that it has a big touch target. |
| int closeButtonHeight = mCloseButton.getDrawable().getIntrinsicHeight(); |
| int closePadding = (mDimensionMinSize - closeButtonHeight) / 2; |
| if (closePadding >= 0) { |
| mCloseButton.setPadding(closePadding, closePadding, closePadding, closePadding); |
| } else { |
| assert closePadding >= 0 : "Assets are too large for this layout."; |
| } |
| |
| // Add all of the other InfoBar specific controls. |
| infoBarView.createContent(this); |
| } |
| |
| @Override |
| protected LayoutParams generateDefaultLayoutParams() { |
| return new LayoutParams(); |
| } |
| |
| /** |
| * Add a view to the Layout. |
| * This function must never be called with an index that isn't -1 to ensure that groups aren't |
| * broken apart. |
| */ |
| @Override |
| public void addView(View child, int index, ViewGroup.LayoutParams params) { |
| if (index == -1) { |
| super.addView(child, index, params); |
| } else { |
| assert false : "Adding children at random places can break group structure."; |
| super.addView(child, -1, params); |
| } |
| } |
| |
| /** |
| * Add a group of Views that are measured and laid out together. |
| */ |
| public void addGroup(View... group) { |
| for (int i = 0; i < group.length; i++) { |
| final View member = group[i]; |
| addView(member); |
| |
| LayoutParams params = (LayoutParams) member.getLayoutParams(); |
| params.isGroupedWithNextView = (i != group.length - 1); |
| } |
| } |
| |
| /** |
| * Add up to two buttons to the layout. |
| * |
| * Buttons with null text are hidden from view. The secondary button may only exist if the |
| * primary button does. |
| * |
| * @param primaryText Text for the primary button. |
| * @param secondaryText Text for the secondary button. |
| */ |
| public void addButtons(String primaryText, String secondaryText) { |
| Button primaryButton = null; |
| Button secondaryButton = null; |
| |
| if (!TextUtils.isEmpty(secondaryText)) { |
| secondaryButton = (Button) LayoutInflater.from(getContext()).inflate( |
| R.layout.infobar_button, null); |
| secondaryButton.setId(R.id.button_secondary); |
| secondaryButton.setOnClickListener(this); |
| secondaryButton.setText(secondaryText); |
| } |
| |
| if (!TextUtils.isEmpty(primaryText)) { |
| primaryButton = (Button) LayoutInflater.from(getContext()).inflate( |
| R.layout.infobar_button, null); |
| primaryButton.setId(R.id.button_primary); |
| primaryButton.setOnClickListener(this); |
| primaryButton.setText(primaryText); |
| } |
| |
| // Group the buttons together so that they are laid out next to each other. |
| if (primaryButton == null && secondaryButton != null) { |
| assert false : "When using only one button, make it the primary button."; |
| } else if (primaryButton != null && secondaryButton != null) { |
| addGroup(secondaryButton, primaryButton); |
| } else if (primaryButton != null) { |
| addGroup(primaryButton); |
| } |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
| final int rowWidth = right - left; |
| int rowTop = layoutMainRow(rowWidth); |
| for (int row = 1; row < mIndicesOfRows.size() - 1; row++) { |
| rowTop = layoutRow(row, rowTop, rowWidth); |
| } |
| } |
| |
| /** |
| * Lays out the controls in the main row. |
| * |
| * This method is complicated mainly because of the arbitrariness for when a control can |
| * float either left or right, and whether we're doing an RTL layout. |
| * |
| * Layout proceeds in three phases: |
| * - Laying out of the icon and close button are done separately from the rest of the controls |
| * because they are locked into their respective corners. These two controls bound the rest |
| * of the controls in the main row. |
| * |
| * - Items floating to the left are then laid out, traversing the children array in a forwards |
| * manner. This includes the InfoBar message. |
| * |
| * - A final pass lays out items aligned to the end of the bar, traversing the children array |
| * backwards so that the correct ordering of the children is preserved. Going forwards would |
| * cause buttons to flip (e.g.). |
| * |
| * @param width Maximum width of the row. |
| * @return How tall the main row is. |
| */ |
| private int layoutMainRow(int width) { |
| final int rowStart = mIndicesOfRows.get(0); |
| final int rowEnd = mIndicesOfRows.get(1); |
| final int rowHeight = computeMainRowHeight(rowStart, rowEnd); |
| |
| // Lay out the icon and the close button. |
| int closeLeft; |
| int iconPadding = (mDimensionMinSize - mDimensionIconSize) / 2; |
| int iconLeft = iconPadding; |
| if (mLayoutRTL) { |
| iconLeft += width - mDimensionMinSize; |
| closeLeft = 0; |
| } else { |
| closeLeft = width - mCloseButton.getMeasuredWidth(); |
| } |
| mIconView.layout(iconLeft, iconPadding, iconLeft + mDimensionIconSize, |
| iconPadding + mDimensionIconSize); |
| mCloseButton.layout(closeLeft, 0, closeLeft + mDimensionMinSize, mDimensionMinSize); |
| |
| // Go from left to right to catch all items aligned with the start of the InfoBar. |
| int rowLeft = mDimensionMinSize; |
| int rowRight = width - mDimensionMinSize; |
| for (int i = rowStart; i < rowEnd; i++) { |
| final View child = getChildAt(i); |
| LayoutParams params = (LayoutParams) child.getLayoutParams(); |
| if (params.align != LayoutParams.ALIGN_START || child.getVisibility() == View.GONE |
| || child == mCloseButton || child == mIconView) { |
| continue; |
| } |
| |
| // Everything is vertically centered. |
| int childTop = (rowHeight - child.getMeasuredHeight()) / 2; |
| int childLeft; |
| |
| if (mLayoutRTL) { |
| if (!isMainControl(child)) rowRight -= mDimensionMargin; |
| childLeft = rowRight - child.getMeasuredWidth(); |
| rowRight -= child.getMeasuredWidth(); |
| } else { |
| if (!isMainControl(child)) rowLeft += mDimensionMargin; |
| childLeft = rowLeft; |
| rowLeft += child.getMeasuredWidth(); |
| } |
| |
| child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), |
| childTop + child.getMeasuredHeight()); |
| } |
| |
| // Go from right to left to catch all items aligned with the end of the InfoBar. |
| for (int i = rowEnd - 1; i >= rowStart; i--) { |
| final View child = getChildAt(i); |
| LayoutParams params = (LayoutParams) child.getLayoutParams(); |
| if (params.align != LayoutParams.ALIGN_END || child.getVisibility() == View.GONE |
| || child == mCloseButton || child == mIconView) { |
| continue; |
| } |
| |
| // Everything is vertically centered. |
| int childTop = (rowHeight - child.getMeasuredHeight()) / 2; |
| int childLeft; |
| |
| if (!mLayoutRTL) { |
| childLeft = rowRight - child.getMeasuredWidth(); |
| rowRight -= child.getMeasuredWidth(); |
| if (!isMainControl(child)) rowRight -= mDimensionMargin; |
| } else { |
| childLeft = rowLeft; |
| rowLeft += child.getMeasuredWidth(); |
| if (!isMainControl(child)) rowLeft += mDimensionMargin; |
| } |
| |
| child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), |
| childTop + child.getMeasuredHeight()); |
| } |
| |
| return rowHeight; |
| } |
| |
| /** |
| * Lays out the controls in the row other than the main one. |
| * |
| * This case is much simpler than the main row since the items are all equally sized and simply |
| * entails moving through the children and laying them down from the start of the InfoBar to the |
| * end. |
| * |
| * @param row Index of the row |
| * @param rowTop Y-coordinate of the layout the controls should be aligned to. |
| * @param width Maximum width of the row. |
| * @return How tall the row is. |
| */ |
| private int layoutRow(int row, int rowTop, int width) { |
| final int rowStart = mIndicesOfRows.get(row); |
| final int rowEnd = mIndicesOfRows.get(row + 1); |
| final boolean hasButton = isButton(getChildAt(rowStart)); |
| |
| int rowLeft = hasButton ? 0 : mDimensionMargin; |
| int rowRight = width - (hasButton ? 0 : mDimensionMargin); |
| |
| for (int i = rowStart; i < rowEnd; i++) { |
| final View child = getChildAt(i); |
| if (child.getVisibility() == View.GONE) continue; |
| |
| int childLeft; |
| if (mLayoutRTL) { |
| childLeft = rowRight - child.getMeasuredWidth(); |
| rowRight -= child.getMeasuredWidth() + (hasButton ? 0 : mDimensionMargin); |
| } else { |
| childLeft = rowLeft; |
| rowLeft += child.getMeasuredWidth() + (hasButton ? 0 : mDimensionMargin); |
| } |
| |
| child.layout(childLeft, rowTop, childLeft + child.getMeasuredWidth(), |
| rowTop + child.getMeasuredHeight()); |
| } |
| |
| return rowTop + computeRowHeight(rowStart, rowEnd); |
| } |
| |
| /** |
| * Checks if the child is one of the main InfoBar controls. |
| * @param child View to check. |
| * @return True if the child is one of the main controls. |
| */ |
| private boolean isMainControl(View child) { |
| return child == mIconView || child == mMessageView || child == mCloseButton; |
| } |
| |
| /** |
| * Marks that the given index is the start of its own row. |
| * @param rowStartIndex Index of the child view at the start of the next row. |
| */ |
| private void addRowStartIndex(int rowStartIndex) { |
| if (mIndicesOfRows.size() == 0 |
| || rowStartIndex != mIndicesOfRows.get(mIndicesOfRows.size() - 1)) { |
| mIndicesOfRows.add(rowStartIndex); |
| } |
| } |
| |
| /** |
| * Computes properties of the next group of Views to assign to rows. |
| * @param startIndex Index of the first child in the group. |
| * @return GroupInfo containing information about the current group. |
| */ |
| private GroupInfo getNextGroup(int startIndex) { |
| GroupInfo groupInfo = new GroupInfo(); |
| groupInfo.endIndex = startIndex; |
| |
| final int childCount = getChildCount(); |
| int currentChildIndex = startIndex; |
| while (groupInfo.endIndex < childCount) { |
| final View groupChild = getChildAt(groupInfo.endIndex); |
| if (groupChild.getVisibility() != View.GONE) { |
| groupInfo.hasButton |= isButton(groupChild); |
| groupInfo.width += groupChild.getMeasuredWidth(); |
| groupInfo.greatestMemberWidth = |
| Math.max(groupInfo.greatestMemberWidth, groupChild.getMeasuredWidth()); |
| groupInfo.numViews++; |
| } |
| groupInfo.endIndex++; |
| |
| LayoutParams params = (LayoutParams) groupChild.getLayoutParams(); |
| if (!params.isGroupedWithNextView) break; |
| } |
| |
| return groupInfo; |
| } |
| |
| @Override |
| protected void measureChild(View child, int widthSpec, int heightSpec) { |
| // If a control is on the main row, then it should be only as large as it wants to be. |
| // Otherwise, it must occupy the same amount of space as everything else on its row. |
| LayoutParams params = (LayoutParams) child.getLayoutParams(); |
| params.width = params.isInMainRow ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT; |
| super.measureChild(child, widthSpec, heightSpec); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| assert getLayoutParams().height == LayoutParams.WRAP_CONTENT |
| : "InfoBar heights cannot be constrained."; |
| |
| final int maxWidth = MeasureSpec.getSize(widthMeasureSpec); |
| mIndicesOfRows.clear(); |
| |
| // Measure all children with the assumption that they may take up the full size of the |
| // parent. This determines how big each child wants to be. |
| final int childCount = getChildCount(); |
| for (int numChild = 0; numChild < childCount; numChild++) { |
| final View child = getChildAt(numChild); |
| if (child.getVisibility() == View.GONE) continue; |
| ((LayoutParams) child.getLayoutParams()).isInMainRow = true; |
| measureChild(child, widthMeasureSpec, heightMeasureSpec); |
| } |
| |
| // Allocate as many Views as possible to the main row, then place everything else on the |
| // following rows. |
| int currentChildIndex = measureMainRow(maxWidth); |
| measureRemainingRows(maxWidth, currentChildIndex); |
| |
| // Buttons must have their backgrounds manually changed to give the illusion of having a |
| // single pixel boundary between them. |
| updateBackgroundsForButtons(); |
| |
| // Determine how tall the container should be by measuring all the children in their rows. |
| int layoutHeight = computeHeight(); |
| setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), |
| resolveSize(layoutHeight, heightMeasureSpec)); |
| } |
| |
| /** |
| * Assign as many Views as can fit onto the main row. |
| * |
| * The main row consists of at least the icon, the close button, and the message. Groups of |
| * controls are added to the main row as long as they can fit within the width of the InfoBar. |
| * |
| * @param maxWidth The maximum width of the main row. |
| * @return The index of the last child that couldn't fit on the main row. |
| */ |
| private int measureMainRow(int maxWidth) { |
| final int childCount = getChildCount(); |
| |
| // The main row has the icon and the close button taking the upper left and upper right |
| // corners of the InfoBar, each of which occupies a square of |
| // mDimensionMinSize x mDimensionMinSize pixels. |
| GroupInfo mainControlInfo = getNextGroup(0); |
| int remainingWidth = maxWidth - (mDimensionMinSize * 2) - mMessageView.getMeasuredWidth(); |
| addRowStartIndex(0); |
| |
| // Go through the rest of the Views and keep adding them until they can't fit. |
| int currentChildIndex = mainControlInfo.endIndex; |
| while (currentChildIndex < childCount && remainingWidth > 0) { |
| GroupInfo groupInfo = getNextGroup(currentChildIndex); |
| int widthWithMargins = groupInfo.width + mDimensionMargin * groupInfo.numViews; |
| |
| if (widthWithMargins <= remainingWidth) { |
| // If the group fits on the main row, add it. |
| currentChildIndex = groupInfo.endIndex; |
| remainingWidth -= widthWithMargins; |
| } else { |
| // We can't fit the current group on the main row. |
| break; |
| } |
| } |
| addRowStartIndex(currentChildIndex); |
| |
| // The icon and the close button are set to be squares occupying the upper left and |
| // upper right corners of the InfoBar. |
| int specWidth = MeasureSpec.makeMeasureSpec(mDimensionMinSize, MeasureSpec.EXACTLY); |
| int specHeight = MeasureSpec.makeMeasureSpec(mDimensionMinSize, MeasureSpec.EXACTLY); |
| measureChild(mIconView, specWidth, specHeight); |
| measureChild(mCloseButton, specWidth, specHeight); |
| |
| // Measure out everything else except the message. |
| remainingWidth = maxWidth - (mDimensionMinSize * 2); |
| for (int i = 0; i < currentChildIndex; i++) { |
| final View child = getChildAt(i); |
| if (child.getVisibility() == View.GONE || isMainControl(child)) continue; |
| |
| specWidth = MeasureSpec.makeMeasureSpec(remainingWidth, MeasureSpec.AT_MOST); |
| specHeight = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); |
| measureChild(child, specWidth, specHeight); |
| remainingWidth -= child.getMeasuredWidth() + mDimensionMargin; |
| } |
| |
| // The message sucks up the remaining width on the line after all other controls |
| // have gotten all the space they requested. |
| specWidth = MeasureSpec.makeMeasureSpec(remainingWidth, MeasureSpec.AT_MOST); |
| specHeight = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); |
| measureChild(mMessageView, specWidth, specHeight); |
| |
| return currentChildIndex; |
| } |
| |
| /** |
| * Assign children to rows in the layout. |
| * |
| * We first try to assign children in the same group to the same row, but only if they fit when |
| * they are of equal width. Otherwise, we split the group onto multiple rows. |
| * |
| * @param maxWidth Maximum width that the row can take. |
| * @param currentChildIndex Start index of the current group. |
| */ |
| private void measureRemainingRows(int maxWidth, int currentChildIndex) { |
| final int childCount = getChildCount(); |
| final int specHeight = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); |
| |
| while (currentChildIndex < childCount) { |
| GroupInfo groupInfo = getNextGroup(currentChildIndex); |
| |
| int availableWidth; |
| int boundaryMargins; |
| if (groupInfo.hasButton) { |
| // Buttons take up the full width of the InfoBar. |
| availableWidth = maxWidth; |
| boundaryMargins = 0; |
| } else { |
| // Other controls obey the side boundaries, and have boundaries between them. |
| availableWidth = maxWidth - mDimensionMargin * 2; |
| boundaryMargins = (groupInfo.numViews - 1) * mDimensionMargin; |
| } |
| |
| // Determine how wide each item would be on the same row, including boundaries. |
| int evenWidth = (availableWidth - boundaryMargins) / groupInfo.numViews; |
| |
| if (groupInfo.greatestMemberWidth <= evenWidth) { |
| // Fit everything on the same row. |
| int specWidth = MeasureSpec.makeMeasureSpec(evenWidth, MeasureSpec.EXACTLY); |
| for (int i = currentChildIndex; i < groupInfo.endIndex; i++) { |
| final View child = getChildAt(i); |
| if (child.getVisibility() == View.GONE) continue; |
| ((LayoutParams) child.getLayoutParams()).isInMainRow = false; |
| measureChild(child, specWidth, specHeight); |
| } |
| addRowStartIndex(currentChildIndex); |
| } else { |
| // Add each member of the group to its own row. |
| int specWidth = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.EXACTLY); |
| for (int i = currentChildIndex; i < groupInfo.endIndex; i++) { |
| final View child = getChildAt(i); |
| if (child.getVisibility() == View.GONE) continue; |
| ((LayoutParams) child.getLayoutParams()).isInMainRow = false; |
| measureChild(child, specWidth, specHeight); |
| addRowStartIndex(i); |
| } |
| } |
| |
| currentChildIndex = groupInfo.endIndex; |
| } |
| |
| addRowStartIndex(childCount); |
| } |
| |
| /** |
| * Calculate how tall the layout is, accounting for margins and children. |
| * @return How big the layout should be. |
| */ |
| private int computeHeight() { |
| int cumulativeHeight = 0; |
| |
| // Calculate how big each row is. |
| final int numRows = mIndicesOfRows.size() - 1; |
| for (int row = 0; row < numRows; row++) { |
| final int rowStart = mIndicesOfRows.get(row); |
| final int rowEnd = mIndicesOfRows.get(row + 1); |
| |
| if (row == 0) { |
| cumulativeHeight += computeMainRowHeight(rowStart, rowEnd); |
| } else { |
| cumulativeHeight += computeRowHeight(rowStart, rowEnd); |
| } |
| } |
| |
| return cumulativeHeight; |
| } |
| |
| /** |
| * Computes how tall the main row is. |
| * @param rowStart Index of the first child. |
| * @param rowEnd One past the index of the last child. |
| */ |
| private int computeMainRowHeight(int rowStart, int rowEnd) { |
| // The icon and close button already have their margins baked into their padding values, |
| // but the other Views have a margin above and below. |
| final int verticalMargins = mDimensionMargin * 2; |
| int rowHeight = mDimensionMinSize; |
| for (int i = rowStart; i < rowEnd; i++) { |
| View child = getChildAt(i); |
| if (child == mCloseButton || child == mIconView || child.getVisibility() == View.GONE) { |
| continue; |
| } |
| rowHeight = Math.max(rowHeight, child.getMeasuredHeight() + verticalMargins); |
| } |
| return rowHeight; |
| } |
| |
| /** |
| * Computes how tall a row below the main row is. |
| * |
| * Margins are only applied downward since the rows above are handling the margin on their side. |
| * Buttons ignore margins since they have to be right against the boundary. |
| * |
| * @param rowStart Index of the first child. |
| * @param rowEnd One past the index of the last child. |
| */ |
| private int computeRowHeight(int rowStart, int rowEnd) { |
| boolean isButtonRow = isButton(getChildAt(rowStart)); |
| final int verticalMargins = isButtonRow ? 0 : mDimensionMargin; |
| int rowHeight = 0; |
| for (int i = rowStart; i < rowEnd; i++) { |
| final View child = getChildAt(i); |
| if (child.getVisibility() == View.GONE) continue; |
| rowHeight = Math.max(rowHeight, child.getMeasuredHeight() + verticalMargins); |
| } |
| return rowHeight; |
| } |
| |
| /** |
| * Determines if the given View is either the primary or secondary button. |
| * @param child View to check. |
| * @return Whether the child is the primary or secondary button. |
| */ |
| private boolean isButton(View child) { |
| return child.getId() == R.id.button_secondary || child.getId() == R.id.button_primary; |
| } |
| |
| /** |
| * Update the backgrounds for the buttons to account for their current positioning. |
| * The primary and secondary buttons are special-cased in that their backgrounds change to |
| * create the illusion of a single-stroke boundary between them. |
| */ |
| private void updateBackgroundsForButtons() { |
| boolean bothButtonsExist = findViewById(R.id.button_primary) != null |
| && findViewById(R.id.button_secondary) != null; |
| |
| for (int row = 0; row < mIndicesOfRows.size() - 1; row++) { |
| final int rowStart = mIndicesOfRows.get(row); |
| final int rowEnd = mIndicesOfRows.get(row + 1); |
| final int rowSize = rowEnd - rowStart; |
| |
| for (int i = rowStart; i < rowEnd; i++) { |
| final View child = getChildAt(i); |
| if (child.getVisibility() == View.GONE || !isButton(child)) continue; |
| |
| // Determine which background we need to show. |
| int background; |
| if (row == 0) { |
| // Button will be floating. |
| background = mBackgroundFloating; |
| } else if (rowSize == 1 || !bothButtonsExist) { |
| // Button takes up the full width of the screen. |
| background = mBackgroundFullRight; |
| } else if (mLayoutRTL) { |
| // Primary button will be to the left of the secondary. |
| background = child.getId() == R.id.button_primary |
| ? mBackgroundFullLeft : mBackgroundFullRight; |
| } else { |
| // Primary button will be to the right of the secondary. |
| background = child.getId() == R.id.button_primary |
| ? mBackgroundFullRight : mBackgroundFullLeft; |
| } |
| |
| // Update the background. |
| LayoutParams params = (LayoutParams) child.getLayoutParams(); |
| if (params.background != background) { |
| params.background = background; |
| |
| // Save the padding; Android decides to overwrite it on some builds. |
| int paddingLeft = child.getPaddingLeft(); |
| int paddingTop = child.getPaddingTop(); |
| int paddingRight = child.getPaddingRight(); |
| int paddingBottom = child.getPaddingBottom(); |
| int buttonWidth = child.getMeasuredWidth(); |
| int buttonHeight = child.getMeasuredHeight(); |
| |
| // Set the background, then restore the padding. |
| child.setBackgroundResource(background); |
| child.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); |
| |
| // Re-measuring is necessary to correct the text gravity. |
| int specWidth = MeasureSpec.makeMeasureSpec(buttonWidth, MeasureSpec.EXACTLY); |
| int specHeight = MeasureSpec.makeMeasureSpec(buttonHeight, MeasureSpec.EXACTLY); |
| measureChild(child, specWidth, specHeight); |
| } |
| } |
| } |
| } |
| |
| /** |
| * 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); |
| } |
| } |
| |
| } |