1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import android.annotation.NonNull;
20import android.content.Context;
21import android.content.res.TypedArray;
22import android.util.AttributeSet;
23import android.util.SparseIntArray;
24import android.view.Gravity;
25import android.view.View;
26import android.view.ViewDebug;
27import android.view.ViewGroup;
28import android.view.ViewHierarchyEncoder;
29
30/**
31 * <p>A layout that arranges its children horizontally. A TableRow should
32 * always be used as a child of a {@link android.widget.TableLayout}. If a
33 * TableRow's parent is not a TableLayout, the TableRow will behave as
34 * an horizontal {@link android.widget.LinearLayout}.</p>
35 *
36 * <p>The children of a TableRow do not need to specify the
37 * <code>layout_width</code> and <code>layout_height</code> attributes in the
38 * XML file. TableRow always enforces those values to be respectively
39 * {@link android.widget.TableLayout.LayoutParams#MATCH_PARENT} and
40 * {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}.</p>
41 *
42 * <p>
43 * Also see {@link TableRow.LayoutParams android.widget.TableRow.LayoutParams}
44 * for layout attributes </p>
45 */
46public class TableRow extends LinearLayout {
47    private int mNumColumns = 0;
48    private int[] mColumnWidths;
49    private int[] mConstrainedColumnWidths;
50    private SparseIntArray mColumnToChildIndex;
51
52    private ChildrenTracker mChildrenTracker;
53
54    /**
55     * <p>Creates a new TableRow for the given context.</p>
56     *
57     * @param context the application environment
58     */
59    public TableRow(Context context) {
60        super(context);
61        initTableRow();
62    }
63
64    /**
65     * <p>Creates a new TableRow for the given context and with the
66     * specified set attributes.</p>
67     *
68     * @param context the application environment
69     * @param attrs a collection of attributes
70     */
71    public TableRow(Context context, AttributeSet attrs) {
72        super(context, attrs);
73        initTableRow();
74    }
75
76    private void initTableRow() {
77        OnHierarchyChangeListener oldListener = mOnHierarchyChangeListener;
78        mChildrenTracker = new ChildrenTracker();
79        if (oldListener != null) {
80            mChildrenTracker.setOnHierarchyChangeListener(oldListener);
81        }
82        super.setOnHierarchyChangeListener(mChildrenTracker);
83    }
84
85    /**
86     * {@inheritDoc}
87     */
88    @Override
89    public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
90        mChildrenTracker.setOnHierarchyChangeListener(listener);
91    }
92
93    /**
94     * <p>Collapses or restores a given column.</p>
95     *
96     * @param columnIndex the index of the column
97     * @param collapsed true if the column must be collapsed, false otherwise
98     * {@hide}
99     */
100    void setColumnCollapsed(int columnIndex, boolean collapsed) {
101        final View child = getVirtualChildAt(columnIndex);
102        if (child != null) {
103            child.setVisibility(collapsed ? GONE : VISIBLE);
104        }
105    }
106
107    /**
108     * {@inheritDoc}
109     */
110    @Override
111    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
112        // enforce horizontal layout
113        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
114    }
115
116    /**
117     * {@inheritDoc}
118     */
119    @Override
120    protected void onLayout(boolean changed, int l, int t, int r, int b) {
121        // enforce horizontal layout
122        layoutHorizontal(l, t, r, b);
123    }
124
125    /**
126     * {@inheritDoc}
127     */
128    @Override
129    public View getVirtualChildAt(int i) {
130        if (mColumnToChildIndex == null) {
131            mapIndexAndColumns();
132        }
133
134        final int deflectedIndex = mColumnToChildIndex.get(i, -1);
135        if (deflectedIndex != -1) {
136            return getChildAt(deflectedIndex);
137        }
138
139        return null;
140    }
141
142    /**
143     * {@inheritDoc}
144     */
145    @Override
146    public int getVirtualChildCount() {
147        if (mColumnToChildIndex == null) {
148            mapIndexAndColumns();
149        }
150        return mNumColumns;
151    }
152
153    private void mapIndexAndColumns() {
154        if (mColumnToChildIndex == null) {
155            int virtualCount = 0;
156            final int count = getChildCount();
157
158            mColumnToChildIndex = new SparseIntArray();
159            final SparseIntArray columnToChild = mColumnToChildIndex;
160
161            for (int i = 0; i < count; i++) {
162                final View child = getChildAt(i);
163                final LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
164
165                if (layoutParams.column >= virtualCount) {
166                    virtualCount = layoutParams.column;
167                }
168
169                for (int j = 0; j < layoutParams.span; j++) {
170                    columnToChild.put(virtualCount++, i);
171                }
172            }
173
174            mNumColumns = virtualCount;
175        }
176    }
177
178    /**
179     * {@inheritDoc}
180     */
181    @Override
182    int measureNullChild(int childIndex) {
183        return mConstrainedColumnWidths[childIndex];
184    }
185
186    /**
187     * {@inheritDoc}
188     */
189    @Override
190    void measureChildBeforeLayout(View child, int childIndex,
191            int widthMeasureSpec, int totalWidth,
192            int heightMeasureSpec, int totalHeight) {
193        if (mConstrainedColumnWidths != null) {
194            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
195
196            int measureMode = MeasureSpec.EXACTLY;
197            int columnWidth = 0;
198
199            final int span = lp.span;
200            final int[] constrainedColumnWidths = mConstrainedColumnWidths;
201            for (int i = 0; i < span; i++) {
202                columnWidth += constrainedColumnWidths[childIndex + i];
203            }
204
205            final int gravity = lp.gravity;
206            final boolean isHorizontalGravity = Gravity.isHorizontal(gravity);
207
208            if (isHorizontalGravity) {
209                measureMode = MeasureSpec.AT_MOST;
210            }
211
212            // no need to care about padding here,
213            // ViewGroup.getChildMeasureSpec() would get rid of it anyway
214            // because of the EXACTLY measure spec we use
215            int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
216                    Math.max(0, columnWidth - lp.leftMargin - lp.rightMargin), measureMode
217            );
218            int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
219                    mPaddingTop + mPaddingBottom + lp.topMargin +
220                    lp .bottomMargin + totalHeight, lp.height);
221
222            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
223
224            if (isHorizontalGravity) {
225                final int childWidth = child.getMeasuredWidth();
226                lp.mOffset[LayoutParams.LOCATION_NEXT] = columnWidth - childWidth;
227
228                final int layoutDirection = getLayoutDirection();
229                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
230                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
231                    case Gravity.LEFT:
232                        // don't offset on X axis
233                        break;
234                    case Gravity.RIGHT:
235                        lp.mOffset[LayoutParams.LOCATION] = lp.mOffset[LayoutParams.LOCATION_NEXT];
236                        break;
237                    case Gravity.CENTER_HORIZONTAL:
238                        lp.mOffset[LayoutParams.LOCATION] = lp.mOffset[LayoutParams.LOCATION_NEXT] / 2;
239                        break;
240                }
241            } else {
242                lp.mOffset[LayoutParams.LOCATION] = lp.mOffset[LayoutParams.LOCATION_NEXT] = 0;
243            }
244        } else {
245            // fail silently when column widths are not available
246            super.measureChildBeforeLayout(child, childIndex, widthMeasureSpec,
247                    totalWidth, heightMeasureSpec, totalHeight);
248        }
249    }
250
251    /**
252     * {@inheritDoc}
253     */
254    @Override
255    int getChildrenSkipCount(View child, int index) {
256        LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
257
258        // when the span is 1 (default), we need to skip 0 child
259        return layoutParams.span - 1;
260    }
261
262    /**
263     * {@inheritDoc}
264     */
265    @Override
266    int getLocationOffset(View child) {
267        return ((TableRow.LayoutParams) child.getLayoutParams()).mOffset[LayoutParams.LOCATION];
268    }
269
270    /**
271     * {@inheritDoc}
272     */
273    @Override
274    int getNextLocationOffset(View child) {
275        return ((TableRow.LayoutParams) child.getLayoutParams()).mOffset[LayoutParams.LOCATION_NEXT];
276    }
277
278    /**
279     * <p>Measures the preferred width of each child, including its margins.</p>
280     *
281     * @param widthMeasureSpec the width constraint imposed by our parent
282     *
283     * @return an array of integers corresponding to the width of each cell, or
284     *         column, in this row
285     * {@hide}
286     */
287    int[] getColumnsWidths(int widthMeasureSpec, int heightMeasureSpec) {
288        final int numColumns = getVirtualChildCount();
289        if (mColumnWidths == null || numColumns != mColumnWidths.length) {
290            mColumnWidths = new int[numColumns];
291        }
292
293        final int[] columnWidths = mColumnWidths;
294
295        for (int i = 0; i < numColumns; i++) {
296            final View child = getVirtualChildAt(i);
297            if (child != null && child.getVisibility() != GONE) {
298                final LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
299                if (layoutParams.span == 1) {
300                    int spec;
301                    switch (layoutParams.width) {
302                        case LayoutParams.WRAP_CONTENT:
303                            spec = getChildMeasureSpec(widthMeasureSpec, 0, LayoutParams.WRAP_CONTENT);
304                            break;
305                        case LayoutParams.MATCH_PARENT:
306                            spec = MeasureSpec.makeSafeMeasureSpec(
307                                    MeasureSpec.getSize(heightMeasureSpec),
308                                    MeasureSpec.UNSPECIFIED);
309                            break;
310                        default:
311                            spec = MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY);
312                    }
313                    child.measure(spec, spec);
314
315                    final int width = child.getMeasuredWidth() + layoutParams.leftMargin +
316                            layoutParams.rightMargin;
317                    columnWidths[i] = width;
318                } else {
319                    columnWidths[i] = 0;
320                }
321            } else {
322                columnWidths[i] = 0;
323            }
324        }
325
326        return columnWidths;
327    }
328
329    /**
330     * <p>Sets the width of all of the columns in this row. At layout time,
331     * this row sets a fixed width, as defined by <code>columnWidths</code>,
332     * on each child (or cell, or column.)</p>
333     *
334     * @param columnWidths the fixed width of each column that this row must
335     *                     honor
336     * @throws IllegalArgumentException when columnWidths' length is smaller
337     *         than the number of children in this row
338     * {@hide}
339     */
340    void setColumnsWidthConstraints(int[] columnWidths) {
341        if (columnWidths == null || columnWidths.length < getVirtualChildCount()) {
342            throw new IllegalArgumentException(
343                    "columnWidths should be >= getVirtualChildCount()");
344        }
345
346        mConstrainedColumnWidths = columnWidths;
347    }
348
349    /**
350     * {@inheritDoc}
351     */
352    @Override
353    public LayoutParams generateLayoutParams(AttributeSet attrs) {
354        return new TableRow.LayoutParams(getContext(), attrs);
355    }
356
357    /**
358     * Returns a set of layout parameters with a width of
359     * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT},
360     * a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and no spanning.
361     */
362    @Override
363    protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
364        return new LayoutParams();
365    }
366
367    /**
368     * {@inheritDoc}
369     */
370    @Override
371    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
372        return p instanceof TableRow.LayoutParams;
373    }
374
375    /**
376     * {@inheritDoc}
377     */
378    @Override
379    protected LinearLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
380        return new LayoutParams(p);
381    }
382
383    @Override
384    public CharSequence getAccessibilityClassName() {
385        return TableRow.class.getName();
386    }
387
388    /**
389     * <p>Set of layout parameters used in table rows.</p>
390     *
391     * @see android.widget.TableLayout.LayoutParams
392     *
393     * @attr ref android.R.styleable#TableRow_Cell_layout_column
394     * @attr ref android.R.styleable#TableRow_Cell_layout_span
395     */
396    public static class LayoutParams extends LinearLayout.LayoutParams {
397        /**
398         * <p>The column index of the cell represented by the widget.</p>
399         */
400        @ViewDebug.ExportedProperty(category = "layout")
401        public int column;
402
403        /**
404         * <p>The number of columns the widgets spans over.</p>
405         */
406        @ViewDebug.ExportedProperty(category = "layout")
407        public int span;
408
409        private static final int LOCATION = 0;
410        private static final int LOCATION_NEXT = 1;
411
412        private int[] mOffset = new int[2];
413
414        /**
415         * {@inheritDoc}
416         */
417        public LayoutParams(Context c, AttributeSet attrs) {
418            super(c, attrs);
419
420            TypedArray a =
421                    c.obtainStyledAttributes(attrs,
422                            com.android.internal.R.styleable.TableRow_Cell);
423
424            column = a.getInt(com.android.internal.R.styleable.TableRow_Cell_layout_column, -1);
425            span = a.getInt(com.android.internal.R.styleable.TableRow_Cell_layout_span, 1);
426            if (span <= 1) {
427                span = 1;
428            }
429
430            a.recycle();
431        }
432
433        /**
434         * <p>Sets the child width and the child height.</p>
435         *
436         * @param w the desired width
437         * @param h the desired height
438         */
439        public LayoutParams(int w, int h) {
440            super(w, h);
441            column = -1;
442            span = 1;
443        }
444
445        /**
446         * <p>Sets the child width, height and weight.</p>
447         *
448         * @param w the desired width
449         * @param h the desired height
450         * @param initWeight the desired weight
451         */
452        public LayoutParams(int w, int h, float initWeight) {
453            super(w, h, initWeight);
454            column = -1;
455            span = 1;
456        }
457
458        /**
459         * <p>Sets the child width to {@link android.view.ViewGroup.LayoutParams}
460         * and the child height to
461         * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p>
462         */
463        public LayoutParams() {
464            super(MATCH_PARENT, WRAP_CONTENT);
465            column = -1;
466            span = 1;
467        }
468
469        /**
470         * <p>Puts the view in the specified column.</p>
471         *
472         * <p>Sets the child width to {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}
473         * and the child height to
474         * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p>
475         *
476         * @param column the column index for the view
477         */
478        public LayoutParams(int column) {
479            this();
480            this.column = column;
481        }
482
483        /**
484         * {@inheritDoc}
485         */
486        public LayoutParams(ViewGroup.LayoutParams p) {
487            super(p);
488        }
489
490        /**
491         * {@inheritDoc}
492         */
493        public LayoutParams(MarginLayoutParams source) {
494            super(source);
495        }
496
497        @Override
498        protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
499            // We don't want to force users to specify a layout_width
500            if (a.hasValue(widthAttr)) {
501                width = a.getLayoutDimension(widthAttr, "layout_width");
502            } else {
503                width = MATCH_PARENT;
504            }
505
506            // We don't want to force users to specify a layout_height
507            if (a.hasValue(heightAttr)) {
508                height = a.getLayoutDimension(heightAttr, "layout_height");
509            } else {
510                height = WRAP_CONTENT;
511            }
512        }
513
514        /** @hide */
515        @Override
516        protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
517            super.encodeProperties(encoder);
518            encoder.addProperty("layout:column", column);
519            encoder.addProperty("layout:span", span);
520        }
521    }
522
523    // special transparent hierarchy change listener
524    private class ChildrenTracker implements OnHierarchyChangeListener {
525        private OnHierarchyChangeListener listener;
526
527        private void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
528            this.listener = listener;
529        }
530
531        public void onChildViewAdded(View parent, View child) {
532            // dirties the index to column map
533            mColumnToChildIndex = null;
534
535            if (this.listener != null) {
536                this.listener.onChildViewAdded(parent, child);
537            }
538        }
539
540        public void onChildViewRemoved(View parent, View child) {
541            // dirties the index to column map
542            mColumnToChildIndex = null;
543
544            if (this.listener != null) {
545                this.listener.onChildViewRemoved(parent, child);
546            }
547        }
548    }
549}
550