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