From 5af3fa9e7939b0552093b41cb73e7a6349d821b6 Mon Sep 17 00:00:00 2001 From: Mis012 Date: Thu, 21 Mar 2024 16:19:19 +0100 Subject: [PATCH] api-impl: add TableLayout from AOSP --- src/api-impl/android/view/ViewGroup.java | 8 +- src/api-impl/android/widget/TableLayout.java | 781 +++++++++++++++++++ src/api-impl/android/widget/TableRow.java | 547 +++++++++++++ src/api-impl/meson.build | 2 + 4 files changed, 1336 insertions(+), 2 deletions(-) create mode 100644 src/api-impl/android/widget/TableLayout.java create mode 100644 src/api-impl/android/widget/TableRow.java diff --git a/src/api-impl/android/view/ViewGroup.java b/src/api-impl/android/view/ViewGroup.java index b58fda43..63f34680 100644 --- a/src/api-impl/android/view/ViewGroup.java +++ b/src/api-impl/android/view/ViewGroup.java @@ -360,12 +360,16 @@ public class ViewGroup extends View implements ViewParent, ViewManager { public LayoutParams(Context context, AttributeSet attrs) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout); - width = a.getLayoutDimension(R.styleable.ViewGroup_Layout_layout_width, "layout_width"); - height = a.getLayoutDimension(R.styleable.ViewGroup_Layout_layout_height, "layout_height"); + setBaseAttributes(a, R.styleable.ViewGroup_Layout_layout_width, R.styleable.ViewGroup_Layout_layout_height); this.gravity = attrs.getAttributeIntValue("http://schemas.android.com/apk/res/android", "layout_gravity", -1); a.recycle(); } + protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) { + width = a.getLayoutDimension(widthAttr, "layout_width"); + height = a.getLayoutDimension(heightAttr, "layout_height"); + } + public void resolveLayoutDirection(int layoutDirection) {} /** diff --git a/src/api-impl/android/widget/TableLayout.java b/src/api-impl/android/widget/TableLayout.java new file mode 100644 index 00000000..811e14ad --- /dev/null +++ b/src/api-impl/android/widget/TableLayout.java @@ -0,0 +1,781 @@ +/* + * 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.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 com.android.internal.R; +import java.util.regex.Pattern; + +/** + *

A layout that arranges its children into rows and columns. + * A TableLayout consists of a number of {@link android.widget.TableRow} objects, + * each defining a row (actually, you can have other children, which will be + * explained below). TableLayout containers do not display border lines for + * their rows, columns, or cells. Each row has zero or more cells; each cell can + * hold one {@link android.view.View View} object. The table has as many columns + * as the row with the most cells. A table can leave cells empty. Cells can span + * columns, as they can in HTML.

+ * + *

The width of a column is defined by the row with the widest cell in that + * column. However, a TableLayout can specify certain columns as shrinkable or + * stretchable by calling + * {@link #setColumnShrinkable(int, boolean) setColumnShrinkable()} + * or {@link #setColumnStretchable(int, boolean) setColumnStretchable()}. If + * marked as shrinkable, the column width can be shrunk to fit the table into + * its parent object. If marked as stretchable, it can expand in width to fit + * any extra space. The total width of the table is defined by its parent + * container. It is important to remember that a column can be both shrinkable + * and stretchable. In such a situation, the column will change its size to + * always use up the available space, but never more. Finally, you can hide a + * column by calling + * {@link #setColumnCollapsed(int,boolean) setColumnCollapsed()}.

+ * + *

The children of a TableLayout cannot specify the layout_width + * attribute. Width is always MATCH_PARENT. However, the + * layout_height attribute can be defined by a child; default value + * is {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}. If the child + * is a {@link android.widget.TableRow}, then the height is always + * {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}.

+ * + *

Cells must be added to a row in increasing column order, both in code and + * XML. Column numbers are zero-based. If you don't specify a column number for + * a child cell, it will autoincrement to the next available column. If you skip + * a column number, it will be considered an empty cell in that row. See the + * TableLayout examples in ApiDemos for examples of creating tables in XML.

+ * + *

Although the typical child of a TableLayout is a TableRow, you can + * actually use any View subclass as a direct child of TableLayout. The View + * will be displayed as a single row that spans all the table columns.

+ * + */ +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; + + /** + *

Creates a new TableLayout for the given context.

+ * + * @param context the application environment + */ + public TableLayout(Context context) { + super(context); + initTableLayout(); + } + + /** + *

Creates a new TableLayout for the given context and with the + * specified set attributes.

+ * + * @param context the application environment + * @param attrs a collection of attributes + */ + 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(); + } + + /** + *

Parses a sequence of columns ids defined in a CharSequence with the + * following pattern (regex): \d+(\s*,\s*\d+)*

+ * + *

Examples: "1" or "13, 7, 6" or "".

+ * + *

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.

+ * + * @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; + } + + /** + *

Performs initialization common to prorgrammatic use and XML use of + * this widget.

+ */ + 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(); + } + + /** + *

Indicates whether all columns are shrinkable or not.

+ * + * @return true if all columns are shrinkable, false otherwise + * + * @attr ref android.R.styleable#TableLayout_shrinkColumns + */ + public boolean isShrinkAllColumns() { + return mShrinkAllColumns; + } + + /** + *

Convenience method to mark all columns as shrinkable.

+ * + * @param shrinkAllColumns true to mark all columns shrinkable + * + * @attr ref android.R.styleable#TableLayout_shrinkColumns + */ + public void setShrinkAllColumns(boolean shrinkAllColumns) { + mShrinkAllColumns = shrinkAllColumns; + } + + /** + *

Indicates whether all columns are stretchable or not.

+ * + * @return true if all columns are stretchable, false otherwise + * + * @attr ref android.R.styleable#TableLayout_stretchColumns + */ + public boolean isStretchAllColumns() { + return mStretchAllColumns; + } + + /** + *

Convenience method to mark all columns as stretchable.

+ * + * @param stretchAllColumns true to mark all columns stretchable + * + * @attr ref android.R.styleable#TableLayout_stretchColumns + */ + public void setStretchAllColumns(boolean stretchAllColumns) { + mStretchAllColumns = stretchAllColumns; + } + + /** + *

Collapses or restores a given column. When collapsed, a column + * does not appear on screen and the extra space is reclaimed by the + * other columns. A column is collapsed/restored only when it belongs to + * a {@link android.widget.TableRow}.

+ * + *

Calling this method requests a layout operation.

+ * + * @param columnIndex the index of the column + * @param isCollapsed true if the column must be collapsed, false otherwise + * + * @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(); + } + + /** + *

Returns the collapsed state of the specified column.

+ * + * @param columnIndex the index of the column + * @return true if the column is collapsed, false otherwise + */ + public boolean isColumnCollapsed(int columnIndex) { + return mCollapsedColumns.get(columnIndex); + } + + /** + *

Makes the given column stretchable or not. When stretchable, a column + * takes up as much as available space as possible in its row.

+ * + *

Calling this method requests a layout operation.

+ * + * @param columnIndex the index of the column + * @param isStretchable true if the column must be stretchable, + * false otherwise. Default is false. + * + * @attr ref android.R.styleable#TableLayout_stretchColumns + */ + public void setColumnStretchable(int columnIndex, boolean isStretchable) { + mStretchableColumns.put(columnIndex, isStretchable); + requestRowsLayout(); + } + + /** + *

Returns whether the specified column is stretchable or not.

+ * + * @param columnIndex the index of the column + * @return true if the column is stretchable, false otherwise + */ + public boolean isColumnStretchable(int columnIndex) { + return mStretchAllColumns || mStretchableColumns.get(columnIndex); + } + + /** + *

Makes the given column shrinkable or not. When a row is too wide, the + * table can reclaim extra space from shrinkable columns.

+ * + *

Calling this method requests a layout operation.

+ * + * @param columnIndex the index of the column + * @param isShrinkable true if the column must be shrinkable, + * false otherwise. Default is false. + * + * @attr ref android.R.styleable#TableLayout_shrinkColumns + */ + public void setColumnShrinkable(int columnIndex, boolean isShrinkable) { + mShrinkableColumns.put(columnIndex, isShrinkable); + requestRowsLayout(); + } + + /** + *

Returns whether the specified column is shrinkable or not.

+ * + * @param columnIndex the index of the column + * @return true if the column is shrinkable, false otherwise. Default is false. + */ + public boolean isColumnShrinkable(int columnIndex) { + return mShrinkAllColumns || mShrinkableColumns.get(columnIndex); + } + + /** + *

Applies the columns collapse status to a new row added to this + * table. This method is invoked by PassThroughHierarchyChangeListener + * upon child insertion.

+ * + *

This method only applies to {@link android.widget.TableRow} + * instances.

+ * + * @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(l, t, r, b); + } + + /** + * {@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); + } + + /** + *

Finds the largest cell in each column. For each column, the width of + * the largest cell is applied to all the other cells.

+ * + * @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]); + } + } + } + } + } + + /** + *

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.

+ * + * @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); + } + + /** + * Returns a set of layout parameters with a width of + * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}, + * and a height of {@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()); + } +*/ + /** + *

This set of layout parameters enforces the width of each child to be + * {@link #MATCH_PARENT} and the height of each child to be + * {@link #WRAP_CONTENT}, but only if the height is not specified.

+ */ + @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); + } + + /** + *

Sets the child width to + * {@link android.view.ViewGroup.LayoutParams} and the child height to + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.

+ */ + public LayoutParams() { + super(MATCH_PARENT, WRAP_CONTENT); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(ViewGroup.LayoutParams p) { + super(p); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + /** + *

Fixes the row's width to + * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}; the row's + * height is fixed to + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} if no layout + * height is specified.

+ * + * @param a the styled attributes set + * @param widthAttr the width attribute to fetch + * @param heightAttr the height attribute to fetch + */ + @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; + } + } + } + + /** + *

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.

+ */ + 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); + } + } + } +} diff --git a/src/api-impl/android/widget/TableRow.java b/src/api-impl/android/widget/TableRow.java new file mode 100644 index 00000000..69e4bfa0 --- /dev/null +++ b/src/api-impl/android/widget/TableRow.java @@ -0,0 +1,547 @@ +/* + * 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.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.accessibility.AccessibilityEvent; +//import android.view.accessibility.AccessibilityNodeInfo; + +/** + *

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}.

+ * + *

The children of a TableRow do not need to specify the + * layout_width and layout_height 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}.

+ * + *

+ * Also see {@link TableRow.LayoutParams android.widget.TableRow.LayoutParams} + * for layout attributes

+ */ +public class TableRow extends LinearLayout { + private int mNumColumns = 0; + private int[] mColumnWidths; + private int[] mConstrainedColumnWidths; + private SparseIntArray mColumnToChildIndex; + + private ChildrenTracker mChildrenTracker; + + /** + *

Creates a new TableRow for the given context.

+ * + * @param context the application environment + */ + public TableRow(Context context) { + super(context); + initTableRow(); + } + + /** + *

Creates a new TableRow for the given context and with the + * specified set attributes.

+ * + * @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); + } + + /** + *

Collapses or restores a given column.

+ * + * @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]; + } + + /** + *

Measures the preferred width of each child, including its margins.

+ * + * @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) { + 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.makeMeasureSpec(0, 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; + } + + /** + *

Sets the width of all of the columns in this row. At layout time, + * this row sets a fixed width, as defined by columnWidths, + * on each child (or cell, or column.)

+ * + * @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 void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(TableRow.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(TableRow.class.getName()); + } +*/ + /** + *

Set of layout parameters used in table rows.

+ * + * @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 { + /** + *

The column index of the cell represented by the widget.

+ */ +// @ViewDebug.ExportedProperty(category = "layout") + public int column; + + /** + *

The number of columns the widgets spans over.

+ */ +// @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(); + } + + /** + *

Sets the child width and the child height.

+ * + * @param w the desired width + * @param h the desired height + */ + public LayoutParams(int w, int h) { + super(w, h); + column = -1; + span = 1; + } + + /** + *

Sets the child width, height and weight.

+ * + * @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; + } + + /** + *

Sets the child width to {@link android.view.ViewGroup.LayoutParams} + * and the child height to + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.

+ */ + public LayoutParams() { + super(MATCH_PARENT, WRAP_CONTENT); + column = -1; + span = 1; + } + + /** + *

Puts the view in the specified column.

+ * + *

Sets the child width to {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} + * and the child height to + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.

+ * + * @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; + } + } + } + + // 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); + } + } + } +} diff --git a/src/api-impl/meson.build b/src/api-impl/meson.build index 3eeed32b..c2f00999 100644 --- a/src/api-impl/meson.build +++ b/src/api-impl/meson.build @@ -441,6 +441,8 @@ hax_jar = jar('hax', [ 'android/widget/Spinner.java', 'android/widget/SpinnerAdapter.java', 'android/widget/Switch.java', + 'android/widget/TableLayout.java', + 'android/widget/TableRow.java', 'android/widget/TextView.java', 'android/widget/Toast.java', 'android/widget/Toolbar.java',