/*
* Copyright (C) 2007 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 android.widget;
import com.android.internal.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.SparseBooleanArray;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import java.util.regex.Pattern;
/**
* <p>按照行列来组织子视图的布局.表格布局包含一系列的 {@link android.widget.TableRow
* 表格行}对象,用于定义行(实际上你也可以使用其它子对象,将在后面进行解释).
* 表格布局不为它的行、列和单元格显示表格线.每个行可以包含0个以上(包括0)的单元格;
* 每个单元格可以设置一个{@link android.view.View 视图}对象.与行包含很多单元格一样,
* 表格包含很多列.表格的单元格可以为空.单元格可以象 HTML 那样跨列.</p>
*
* <p>列的宽度由该列所有行中最宽的一个单元格决定.不过表格布局可以通过
* {@link #setColumnShrinkable(int, boolean) setColumnShrinkable()} 方法或者
* {@link #setColumnStretchable(int, boolean) setColumnStretchable()}
* 方法来标记某些列可以收缩或可以拉伸.
* 如果标记为可以收缩,列宽可以收缩以使表格适合容器的大小.如果标记为可以拉伸,
* 列宽可以拉伸以占用多余的空间.表格的总宽度由其父容器决定.
* 记住列可以同时具有可拉伸和可收缩标记是很重要的.在列可以调整其宽度以占用可用空间,
* 但不能超过限度时是很有用的.最后,你可以通过调用
* {@link #setColumnCollapsed(int,boolean) setColumnCollapsed()} 方法来隐藏列.
* </p>
*
* <p>表格布局的子对象不能指定 <code>layout_width</code> 属性.宽度永远是
* <code>MATCH_PARENT</code>.不过子对象可以定义 <code>layout_height</code>
* 属性;其默认值是 {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}.
* 如果子对象是 {@link android.widget.TableRow 表格行},其高度永远是
* {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}.</p>
*
* <p>无论是在代码还是在 XML 布局文件中,单元格必须安装索引顺序加入表格行.
* 列号是从 0 开始的.如果你不为子单元格指定列号,其将自动增值,使用下一个可用列号.
* 如果你跳过某个列号,他在表格行中作为空可以改对待.参见 ApiDemos
* 中通过 XML 创建表格的布局示例.</p>
*
* @author translate by cnmahj
*/
public class TableLayout extends LinearLayout {
private int[] mMaxWidths;
private SparseBooleanArray mStretchableColumns;
private SparseBooleanArray mShrinkableColumns;
private SparseBooleanArray mCollapsedColumns;
private boolean mShrinkAllColumns;
private boolean mStretchAllColumns;
private TableLayout.PassThroughHierarchyChangeListener mPassThroughListener;
private boolean mInitialized;
/**
* <p>为给定的上下文创建表格布局.</p>
*
* @param context 应用程序上下文.
*/
public TableLayout(Context context) {
super(context);
initTableLayout();
}
/**
* <p>使用指定的属性集合为给定的上下文创建表格布局.</p>
*
* @param context 应用程序上下文.
* @param attrs 属性集合.
*/
public TableLayout(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TableLayout);
String stretchedColumns = a.getString(R.styleable.TableLayout_stretchColumns);
if (stretchedColumns != null) {
if (stretchedColumns.charAt(0) == '*') {
mStretchAllColumns = true;
} else {
mStretchableColumns = parseColumns(stretchedColumns);
}
}
String shrinkedColumns = a.getString(R.styleable.TableLayout_shrinkColumns);
if (shrinkedColumns != null) {
if (shrinkedColumns.charAt(0) == '*') {
mShrinkAllColumns = true;
} else {
mShrinkableColumns = parseColumns(shrinkedColumns);
}
}
String collapsedColumns = a.getString(R.styleable.TableLayout_collapseColumns);
if (collapsedColumns != null) {
mCollapsedColumns = parseColumns(collapsedColumns);
}
a.recycle();
initTableLayout();
}
/**
* <p>Parses a sequence of columns ids defined in a CharSequence with the
* following pattern (regex): \d+(\s*,\s*\d+)*</p>
*
* <p>Examples: "1" or "13, 7, 6" or "".</p>
*
* <p>The result of the parsing is stored in a sparse boolean array. The
* parsed column ids are used as the keys of the sparse array. The values
* are always true.</p>
*
* @param sequence a sequence of column ids, can be empty but not null
* @return a sparse array of boolean mapping column indexes to the columns
* collapse state
*/
private static SparseBooleanArray parseColumns(String sequence) {
SparseBooleanArray columns = new SparseBooleanArray();
Pattern pattern = Pattern.compile("\\s*,\\s*");
String[] columnDefs = pattern.split(sequence);
for (String columnIdentifier : columnDefs) {
try {
int columnIndex = Integer.parseInt(columnIdentifier);
// only valid, i.e. positive, columns indexes are handled
if (columnIndex >= 0) {
// putting true in this sparse array indicates that the
// column index was defined in the XML file
columns.put(columnIndex, true);
}
} catch (NumberFormatException e) {
// we just ignore columns that don't exist
}
}
return columns;
}
/**
* <p>Performs initialization common to prorgrammatic use and XML use of
* this widget.</p>
*/
private void initTableLayout() {
if (mCollapsedColumns == null) {
mCollapsedColumns = new SparseBooleanArray();
}
if (mStretchableColumns == null) {
mStretchableColumns = new SparseBooleanArray();
}
if (mShrinkableColumns == null) {
mShrinkableColumns = new SparseBooleanArray();
}
// TableLayouts are always in vertical orientation; keep this tracked
// for shared LinearLayout code.
setOrientation(VERTICAL);
mPassThroughListener = new PassThroughHierarchyChangeListener();
// make sure to call the parent class method to avoid potential
// infinite loops
super.setOnHierarchyChangeListener(mPassThroughListener);
mInitialized = true;
}
/**
* {@inheritDoc}
*/
@Override
public void setOnHierarchyChangeListener(
OnHierarchyChangeListener listener) {
// the user listener is delegated to our pass-through listener
mPassThroughListener.mOnHierarchyChangeListener = listener;
}
private void requestRowsLayout() {
if (mInitialized) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
getChildAt(i).requestLayout();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void requestLayout() {
if (mInitialized) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
getChildAt(i).forceLayout();
}
}
super.requestLayout();
}
/**
* <p>指示,是否所有的列都是可收缩的.</p>
*
* @return 如果所有列都可收缩,返回真;否则返回假.
*
* @attr ref android.R.styleable#TableLayout_shrinkColumns
*/
public boolean isShrinkAllColumns() {
return mShrinkAllColumns;
}
/**
* <p>标记所有列为可收缩的便利的方法.</p>
*
* @param shrinkAllColumns 如果标记所有列为可收缩时为真.
*
* @attr ref android.R.styleable#TableLayout_shrinkColumns
*/
public void setShrinkAllColumns(boolean shrinkAllColumns) {
mShrinkAllColumns = shrinkAllColumns;
}
/**
* <p>指示,是否所有的列都是可拉伸的.</p>
*
* @return 如果所有列都可拉伸,返回真;否则返回假.
*
* @attr ref android.R.styleable#TableLayout_stretchColumns
*/
public boolean isStretchAllColumns() {
return mStretchAllColumns;
}
/**
* <p>标记所有列为可拉伸的便利的方法.</p>
*
* @param stretchAllColumns 如果标记所有列为可拉伸时为真.
*
* @attr ref android.R.styleable#TableLayout_stretchColumns
*/
public void setStretchAllColumns(boolean stretchAllColumns) {
mStretchAllColumns = stretchAllColumns;
}
/**
* <p>折叠或回复给定列.折叠时,列从屏幕上消失,其空间由其它列占用.
* 当列属于 {@link android.widget.TableRow} 时才可以进行折叠/回复操作.</p>
*
* <p>调用该方法会请求布局操作.</p>
*
* @param columnIndex 列索引.
* @param isCollapsed 折叠时为真;否则为假.
*
* @attr ref android.R.styleable#TableLayout_collapseColumns
*/
public void setColumnCollapsed(int columnIndex, boolean isCollapsed) {
// update the collapse status of the column
mCollapsedColumns.put(columnIndex, isCollapsed);
int count = getChildCount();
for (int i = 0; i < count; i++) {
final View view = getChildAt(i);
if (view instanceof TableRow) {
((TableRow) view).setColumnCollapsed(columnIndex, isCollapsed);
}
}
requestRowsLayout();
}
/**
* <p>返回指定列的折叠状态.</p>
*
* @param columnIndex 列索引.
* @return 折叠时为真;否则为假.
*/
public boolean isColumnCollapsed(int columnIndex) {
return mCollapsedColumns.get(columnIndex);
}
/**
* <p>设置指定列是否可拉伸.可拉伸时,列会尽可能多的占用行中的可用空间.</p>
*
* <p>调用该方法会请求布局操作.</p>
*
* @param columnIndex 列的索引.
* @param isStretchable 如果列可以拉伸,设为真;否则设为假.默认是假.
*
* @attr ref android.R.styleable#TableLayout_stretchColumns
*/
public void setColumnStretchable(int columnIndex, boolean isStretchable) {
mStretchableColumns.put(columnIndex, isStretchable);
requestRowsLayout();
}
/**
* <p>返回指定的列是否可拉伸.</p>
*
* @param columnIndex 列的索引.
* @return 如果列可以拉伸,返回真;否则返回假.
*/
public boolean isColumnStretchable(int columnIndex) {
return mStretchAllColumns || mStretchableColumns.get(columnIndex);
}
/**
* <p>设置指定列是否可收缩.当行太宽时,表格可以收缩该列以提供更多空间.</p>
*
* <p>调用该方法会请求布局操作.</p>
*
* @param columnIndex 列的索引.
* @param isShrinkable 如果列可以拉伸,设为真;否则设为假.默认是假.
*
* @attr ref android.R.styleable#TableLayout_shrinkColumns
*/
public void setColumnShrinkable(int columnIndex, boolean isShrinkable) {
mShrinkableColumns.put(columnIndex, isShrinkable);
requestRowsLayout();
}
/**
* <p>返回指定的列是否可收缩.</p>
*
* @param columnIndex 列的索引.
* @return 如果列可以收缩,返回真;否则返回假.
*/
public boolean isColumnShrinkable(int columnIndex) {
return mShrinkAllColumns || mShrinkableColumns.get(columnIndex);
}
/**
* <p>Applies the columns collapse status to a new row added to this
* table. This method is invoked by PassThroughHierarchyChangeListener
* upon child insertion.</p>
*
* <p>This method only applies to {@link android.widget.TableRow}
* instances.</p>
*
* @param child the newly added child
*/
private void trackCollapsedColumns(View child) {
if (child instanceof TableRow) {
final TableRow row = (TableRow) child;
final SparseBooleanArray collapsedColumns = mCollapsedColumns;
final int count = collapsedColumns.size();
for (int i = 0; i < count; i++) {
int columnIndex = collapsedColumns.keyAt(i);
boolean isCollapsed = collapsedColumns.valueAt(i);
// the collapse status is set only when the column should be
// collapsed; otherwise, this might affect the default
// visibility of the row's children
if (isCollapsed) {
row.setColumnCollapsed(columnIndex, isCollapsed);
}
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void addView(View child) {
super.addView(child);
requestRowsLayout();
}
/**
* {@inheritDoc}
*/
@Override
public void addView(View child, int index) {
super.addView(child, index);
requestRowsLayout();
}
/**
* {@inheritDoc}
*/
@Override
public void addView(View child, ViewGroup.LayoutParams params) {
super.addView(child, params);
requestRowsLayout();
}
/**
* {@inheritDoc}
*/
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
super.addView(child, index, params);
requestRowsLayout();
}
/**
* {@inheritDoc}
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// enforce vertical layout
measureVertical(widthMeasureSpec, heightMeasureSpec);
}
/**
* {@inheritDoc}
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// enforce vertical layout
layoutVertical();
}
/**
* {@inheritDoc}
*/
@Override
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth,
int heightMeasureSpec, int totalHeight) {
// when the measured child is a table row, we force the width of its
// children with the widths computed in findLargestCells()
if (child instanceof TableRow) {
((TableRow) child).setColumnsWidthConstraints(mMaxWidths);
}
super.measureChildBeforeLayout(child, childIndex,
widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight);
}
/**
* {@inheritDoc}
*/
@Override
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
findLargestCells(widthMeasureSpec);
shrinkAndStretchColumns(widthMeasureSpec);
super.measureVertical(widthMeasureSpec, heightMeasureSpec);
}
/**
* <p>Finds the largest cell in each column. For each column, the width of
* the largest cell is applied to all the other cells.</p>
*
* @param widthMeasureSpec the measure constraint imposed by our parent
*/
private void findLargestCells(int widthMeasureSpec) {
boolean firstRow = true;
// find the maximum width for each column
// the total number of columns is dynamically changed if we find
// wider rows as we go through the children
// the array is reused for each layout operation; the array can grow
// but never shrinks. Unused extra cells in the array are just ignored
// this behavior avoids to unnecessary grow the array after the first
// layout operation
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
if (child instanceof TableRow) {
final TableRow row = (TableRow) child;
// forces the row's height
final ViewGroup.LayoutParams layoutParams = row.getLayoutParams();
layoutParams.height = LayoutParams.WRAP_CONTENT;
final int[] widths = row.getColumnsWidths(widthMeasureSpec);
final int newLength = widths.length;
// this is the first row, we just need to copy the values
if (firstRow) {
if (mMaxWidths == null || mMaxWidths.length != newLength) {
mMaxWidths = new int[newLength];
}
System.arraycopy(widths, 0, mMaxWidths, 0, newLength);
firstRow = false;
} else {
int length = mMaxWidths.length;
final int difference = newLength - length;
// the current row is wider than the previous rows, so
// we just grow the array and copy the values
if (difference > 0) {
final int[] oldMaxWidths = mMaxWidths;
mMaxWidths = new int[newLength];
System.arraycopy(oldMaxWidths, 0, mMaxWidths, 0,
oldMaxWidths.length);
System.arraycopy(widths, oldMaxWidths.length,
mMaxWidths, oldMaxWidths.length, difference);
}
// the row is narrower or of the same width as the previous
// rows, so we find the maximum width for each column
// if the row is narrower than the previous ones,
// difference will be negative
final int[] maxWidths = mMaxWidths;
length = Math.min(length, newLength);
for (int j = 0; j < length; j++) {
maxWidths[j] = Math.max(maxWidths[j], widths[j]);
}
}
}
}
}
/**
* <p>Shrinks the columns if their total width is greater than the
* width allocated by widthMeasureSpec. When the total width is less
* than the allocated width, this method attempts to stretch columns
* to fill the remaining space.</p>
*
* @param widthMeasureSpec the width measure specification as indicated
* by this widget's parent
*/
private void shrinkAndStretchColumns(int widthMeasureSpec) {
// when we have no row, mMaxWidths is not initialized and the loop
// below could cause a NPE
if (mMaxWidths == null) {
return;
}
// should we honor AT_MOST, EXACTLY and UNSPECIFIED?
int totalWidth = 0;
for (int width : mMaxWidths) {
totalWidth += width;
}
int size = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight;
if ((totalWidth > size) && (mShrinkAllColumns || mShrinkableColumns.size() > 0)) {
// oops, the largest columns are wider than the row itself
// fairly redistribute the row's width among the columns
mutateColumnsWidth(mShrinkableColumns, mShrinkAllColumns, size, totalWidth);
} else if ((totalWidth < size) && (mStretchAllColumns || mStretchableColumns.size() > 0)) {
// if we have some space left, we distribute it among the
// expandable columns
mutateColumnsWidth(mStretchableColumns, mStretchAllColumns, size, totalWidth);
}
}
private void mutateColumnsWidth(SparseBooleanArray columns,
boolean allColumns, int size, int totalWidth) {
int skipped = 0;
final int[] maxWidths = mMaxWidths;
final int length = maxWidths.length;
final int count = allColumns ? length : columns.size();
final int totalExtraSpace = size - totalWidth;
int extraSpace = totalExtraSpace / count;
// Column's widths are changed: force child table rows to re-measure.
// (done by super.measureVertical after shrinkAndStretchColumns.)
final int nbChildren = getChildCount();
for (int i = 0; i < nbChildren; i++) {
View child = getChildAt(i);
if (child instanceof TableRow) {
child.forceLayout();
}
}
if (!allColumns) {
for (int i = 0; i < count; i++) {
int column = columns.keyAt(i);
if (columns.valueAt(i)) {
if (column < length) {
maxWidths[column] += extraSpace;
} else {
skipped++;
}
}
}
} else {
for (int i = 0; i < count; i++) {
maxWidths[i] += extraSpace;
}
// we don't skip any column so we can return right away
return;
}
if (skipped > 0 && skipped < count) {
// reclaim any extra space we left to columns that don't exist
extraSpace = skipped * extraSpace / (count - skipped);
for (int i = 0; i < count; i++) {
int column = columns.keyAt(i);
if (columns.valueAt(i) && column < length) {
if (extraSpace > maxWidths[column]) {
maxWidths[column] = 0;
} else {
maxWidths[column] += extraSpace;
}
}
}
}
}
/**
* {@inheritDoc}
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new TableLayout.LayoutParams(getContext(), attrs);
}
/**
* 返回宽度为 {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT},
* 高度为 {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
* 的布局参数集合.
*/
@Override
protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams();
}
/**
* {@inheritDoc}
*/
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof TableLayout.LayoutParams;
}
/**
* {@inheritDoc}
*/
@Override
protected LinearLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
event.setClassName(TableLayout.class.getName());
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.setClassName(TableLayout.class.getName());
}
/**
* <p>该类强制将子视图的宽度设为 {@link #MATCH_PARENT},
* 将没有设置高度的子视图的高度设为 {@link #WRAP_CONTENT}.</p>
*/
@SuppressWarnings({"UnusedDeclaration"})
public static class LayoutParams extends LinearLayout.LayoutParams {
/**
* {@inheritDoc}
*/
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
/**
* {@inheritDoc}
*/
public LayoutParams(int w, int h) {
super(MATCH_PARENT, h);
}
/**
* {@inheritDoc}
*/
public LayoutParams(int w, int h, float initWeight) {
super(MATCH_PARENT, h, initWeight);
}
/**
* <p>将子视图的宽度设为 {@link android.view.ViewGroup.LayoutParams},
* 高度设为 {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p>
*/
public LayoutParams() {
super(MATCH_PARENT, WRAP_CONTENT);
}
/**
* {@inheritDoc}
*/
public LayoutParams(ViewGroup.LayoutParams p) {
super(p);
}
/**
* {@inheritDoc}
*/
public LayoutParams(MarginLayoutParams source) {
super(source);
}
/**
* <p>如果没有指定布局高度,则修改行的宽度为
* {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT};
* 高度为 {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p>
*
* @param a 特定风格的属性集合.
* @param widthAttr 查询宽度属性用的索引.
* @param heightAttr 查询高度属性用的索引.
*/
@Override
protected void setBaseAttributes(TypedArray a,
int widthAttr, int heightAttr) {
this.width = MATCH_PARENT;
if (a.hasValue(heightAttr)) {
this.height = a.getLayoutDimension(heightAttr, "layout_height");
} else {
this.height = WRAP_CONTENT;
}
}
}
/**
* <p>A pass-through listener acts upon the events and dispatches them
* to another listener. This allows the table layout to set its own internal
* hierarchy change listener without preventing the user to setup his.</p>
*/
private class PassThroughHierarchyChangeListener implements
OnHierarchyChangeListener {
private OnHierarchyChangeListener mOnHierarchyChangeListener;
/**
* {@inheritDoc}
*/
public void onChildViewAdded(View parent, View child) {
trackCollapsedColumns(child);
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewAdded(parent, child);
}
}
/**
* {@inheritDoc}
*/
public void onChildViewRemoved(View parent, View child) {
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
}
}
}
}