/*
* 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 android.annotation.NonNull;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.SparseIntArray;
import android.view.Gravity;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
import android.view.ViewHierarchyEncoder;
/**
* <p>A layout that arranges its children horizontally. A TableRow should
* always be used as a child of a {@link android.widget.TableLayout}. If a
* TableRow's parent is not a TableLayout, the TableRow will behave as
* an horizontal {@link android.widget.LinearLayout}.</p>
*
* <p>The children of a TableRow do not need to specify the
* <code>layout_width</code> and <code>layout_height</code> attributes in the
* XML file. TableRow always enforces those values to be respectively
* {@link android.widget.TableLayout.LayoutParams#MATCH_PARENT} and
* {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}.</p>
*
* <p>
* Also see {@link TableRow.LayoutParams android.widget.TableRow.LayoutParams}
* for layout attributes </p>
*/
public class TableRow extends LinearLayout {
private int mNumColumns = 0;
private int[] mColumnWidths;
private int[] mConstrainedColumnWidths;
private SparseIntArray mColumnToChildIndex;
private ChildrenTracker mChildrenTracker;
/**
* <p>Creates a new TableRow for the given context.</p>
*
* @param context the application environment
*/
public TableRow(Context context) {
super(context);
initTableRow();
}
/**
* <p>Creates a new TableRow for the given context and with the
* specified set attributes.</p>
*
* @param context the application environment
* @param attrs a collection of attributes
*/
public TableRow(Context context, AttributeSet attrs) {
super(context, attrs);
initTableRow();
}
private void initTableRow() {
OnHierarchyChangeListener oldListener = mOnHierarchyChangeListener;
mChildrenTracker = new ChildrenTracker();
if (oldListener != null) {
mChildrenTracker.setOnHierarchyChangeListener(oldListener);
}
super.setOnHierarchyChangeListener(mChildrenTracker);
}
/**
* {@inheritDoc}
*/
@Override
public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
mChildrenTracker.setOnHierarchyChangeListener(listener);
}
/**
* <p>Collapses or restores a given column.</p>
*
* @param columnIndex the index of the column
* @param collapsed true if the column must be collapsed, false otherwise
* {@hide}
*/
void setColumnCollapsed(int columnIndex, boolean collapsed) {
View child = getVirtualChildAt(columnIndex);
if (child != null) {
child.setVisibility(collapsed ? GONE : VISIBLE);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// enforce horizontal layout
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
/**
* {@inheritDoc}
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// enforce horizontal layout
layoutHorizontal(l, t, r, b);
}
/**
* {@inheritDoc}
*/
@Override
public View getVirtualChildAt(int i) {
if (mColumnToChildIndex == null) {
mapIndexAndColumns();
}
final int deflectedIndex = mColumnToChildIndex.get(i, -1);
if (deflectedIndex != -1) {
return getChildAt(deflectedIndex);
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public int getVirtualChildCount() {
if (mColumnToChildIndex == null) {
mapIndexAndColumns();
}
return mNumColumns;
}
private void mapIndexAndColumns() {
if (mColumnToChildIndex == null) {
int virtualCount = 0;
final int count = getChildCount();
mColumnToChildIndex = new SparseIntArray();
final SparseIntArray columnToChild = mColumnToChildIndex;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
final LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
if (layoutParams.column >= virtualCount) {
virtualCount = layoutParams.column;
}
for (int j = 0; j < layoutParams.span; j++) {
columnToChild.put(virtualCount++, i);
}
}
mNumColumns = virtualCount;
}
}
/**
* {@inheritDoc}
*/
@Override
int measureNullChild(int childIndex) {
return mConstrainedColumnWidths[childIndex];
}
/**
* {@inheritDoc}
*/
@Override
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth,
int heightMeasureSpec, int totalHeight) {
if (mConstrainedColumnWidths != null) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
int measureMode = MeasureSpec.EXACTLY;
int columnWidth = 0;
final int span = lp.span;
final int[] constrainedColumnWidths = mConstrainedColumnWidths;
for (int i = 0; i < span; i++) {
columnWidth += constrainedColumnWidths[childIndex + i];
}
final int gravity = lp.gravity;
final boolean isHorizontalGravity = Gravity.isHorizontal(gravity);
if (isHorizontalGravity) {
measureMode = MeasureSpec.AT_MOST;
}
// no need to care about padding here,
// ViewGroup.getChildMeasureSpec() would get rid of it anyway
// because of the EXACTLY measure spec we use
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.max(0, columnWidth - lp.leftMargin - lp.rightMargin), measureMode
);
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin +
lp .bottomMargin + totalHeight, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
if (isHorizontalGravity) {
final int childWidth = child.getMeasuredWidth();
lp.mOffset[LayoutParams.LOCATION_NEXT] = columnWidth - childWidth;
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.LEFT:
// don't offset on X axis
break;
case Gravity.RIGHT:
lp.mOffset[LayoutParams.LOCATION] = lp.mOffset[LayoutParams.LOCATION_NEXT];
break;
case Gravity.CENTER_HORIZONTAL:
lp.mOffset[LayoutParams.LOCATION] = lp.mOffset[LayoutParams.LOCATION_NEXT] / 2;
break;
}
} else {
lp.mOffset[LayoutParams.LOCATION] = lp.mOffset[LayoutParams.LOCATION_NEXT] = 0;
}
} else {
// fail silently when column widths are not available
super.measureChildBeforeLayout(child, childIndex, widthMeasureSpec,
totalWidth, heightMeasureSpec, totalHeight);
}
}
/**
* {@inheritDoc}
*/
@Override
int getChildrenSkipCount(View child, int index) {
LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
// when the span is 1 (default), we need to skip 0 child
return layoutParams.span - 1;
}
/**
* {@inheritDoc}
*/
@Override
int getLocationOffset(View child) {
return ((TableRow.LayoutParams) child.getLayoutParams()).mOffset[LayoutParams.LOCATION];
}
/**
* {@inheritDoc}
*/
@Override
int getNextLocationOffset(View child) {
return ((TableRow.LayoutParams) child.getLayoutParams()).mOffset[LayoutParams.LOCATION_NEXT];
}
/**
* <p>Measures the preferred width of each child, including its margins.</p>
*
* @param widthMeasureSpec the width constraint imposed by our parent
*
* @return an array of integers corresponding to the width of each cell, or
* column, in this row
* {@hide}
*/
int[] getColumnsWidths(int widthMeasureSpec, int heightMeasureSpec) {
final int numColumns = getVirtualChildCount();
if (mColumnWidths == null || numColumns != mColumnWidths.length) {
mColumnWidths = new int[numColumns];
}
final int[] columnWidths = mColumnWidths;
for (int i = 0; i < numColumns; i++) {
final View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
final LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
if (layoutParams.span == 1) {
int spec;
switch (layoutParams.width) {
case LayoutParams.WRAP_CONTENT:
spec = getChildMeasureSpec(widthMeasureSpec, 0, LayoutParams.WRAP_CONTENT);
break;
case LayoutParams.MATCH_PARENT:
spec = MeasureSpec.makeSafeMeasureSpec(
MeasureSpec.getSize(heightMeasureSpec),
MeasureSpec.UNSPECIFIED);
break;
default:
spec = MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY);
}
child.measure(spec, spec);
final int width = child.getMeasuredWidth() + layoutParams.leftMargin +
layoutParams.rightMargin;
columnWidths[i] = width;
} else {
columnWidths[i] = 0;
}
} else {
columnWidths[i] = 0;
}
}
return columnWidths;
}
/**
* <p>Sets the width of all of the columns in this row. At layout time,
* this row sets a fixed width, as defined by <code>columnWidths</code>,
* on each child (or cell, or column.)</p>
*
* @param columnWidths the fixed width of each column that this row must
* honor
* @throws IllegalArgumentException when columnWidths' length is smaller
* than the number of children in this row
* {@hide}
*/
void setColumnsWidthConstraints(int[] columnWidths) {
if (columnWidths == null || columnWidths.length < getVirtualChildCount()) {
throw new IllegalArgumentException(
"columnWidths should be >= getVirtualChildCount()");
}
mConstrainedColumnWidths = columnWidths;
}
/**
* {@inheritDoc}
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new TableRow.LayoutParams(getContext(), attrs);
}
/**
* Returns a set of layout parameters with a width of
* {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT},
* a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and no spanning.
*/
@Override
protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams();
}
/**
* {@inheritDoc}
*/
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof TableRow.LayoutParams;
}
/**
* {@inheritDoc}
*/
@Override
protected LinearLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
@Override
public CharSequence getAccessibilityClassName() {
return TableRow.class.getName();
}
/**
* <p>Set of layout parameters used in table rows.</p>
*
* @see android.widget.TableLayout.LayoutParams
*
* @attr ref android.R.styleable#TableRow_Cell_layout_column
* @attr ref android.R.styleable#TableRow_Cell_layout_span
*/
public static class LayoutParams extends LinearLayout.LayoutParams {
/**
* <p>The column index of the cell represented by the widget.</p>
*/
@ViewDebug.ExportedProperty(category = "layout")
public int column;
/**
* <p>The number of columns the widgets spans over.</p>
*/
@ViewDebug.ExportedProperty(category = "layout")
public int span;
private static final int LOCATION = 0;
private static final int LOCATION_NEXT = 1;
private int[] mOffset = new int[2];
/**
* {@inheritDoc}
*/
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a =
c.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.TableRow_Cell);
column = a.getInt(com.android.internal.R.styleable.TableRow_Cell_layout_column, -1);
span = a.getInt(com.android.internal.R.styleable.TableRow_Cell_layout_span, 1);
if (span <= 1) {
span = 1;
}
a.recycle();
}
/**
* <p>Sets the child width and the child height.</p>
*
* @param w the desired width
* @param h the desired height
*/
public LayoutParams(int w, int h) {
super(w, h);
column = -1;
span = 1;
}
/**
* <p>Sets the child width, height and weight.</p>
*
* @param w the desired width
* @param h the desired height
* @param initWeight the desired weight
*/
public LayoutParams(int w, int h, float initWeight) {
super(w, h, initWeight);
column = -1;
span = 1;
}
/**
* <p>Sets the child width to {@link android.view.ViewGroup.LayoutParams}
* and the child height to
* {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p>
*/
public LayoutParams() {
super(MATCH_PARENT, WRAP_CONTENT);
column = -1;
span = 1;
}
/**
* <p>Puts the view in the specified column.</p>
*
* <p>Sets the child width to {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}
* and the child height to
* {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p>
*
* @param column the column index for the view
*/
public LayoutParams(int column) {
this();
this.column = column;
}
/**
* {@inheritDoc}
*/
public LayoutParams(ViewGroup.LayoutParams p) {
super(p);
}
/**
* {@inheritDoc}
*/
public LayoutParams(MarginLayoutParams source) {
super(source);
}
@Override
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
// We don't want to force users to specify a layout_width
if (a.hasValue(widthAttr)) {
width = a.getLayoutDimension(widthAttr, "layout_width");
} else {
width = MATCH_PARENT;
}
// We don't want to force users to specify a layout_height
if (a.hasValue(heightAttr)) {
height = a.getLayoutDimension(heightAttr, "layout_height");
} else {
height = WRAP_CONTENT;
}
}
/** @hide */
@Override
protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
super.encodeProperties(encoder);
encoder.addProperty("layout:column", column);
encoder.addProperty("layout:span", span);
}
}
// special transparent hierarchy change listener
private class ChildrenTracker implements OnHierarchyChangeListener {
private OnHierarchyChangeListener listener;
private void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
this.listener = listener;
}
public void onChildViewAdded(View parent, View child) {
// dirties the index to column map
mColumnToChildIndex = null;
if (this.listener != null) {
this.listener.onChildViewAdded(parent, child);
}
}
public void onChildViewRemoved(View parent, View child) {
// dirties the index to column map
mColumnToChildIndex = null;
if (this.listener != null) {
this.listener.onChildViewRemoved(parent, child);
}
}
}
}