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 
17 package android.widget;
18 
19 import android.annotation.NonNull;
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.util.AttributeSet;
23 import android.util.SparseIntArray;
24 import android.view.Gravity;
25 import android.view.View;
26 import android.view.ViewDebug;
27 import android.view.ViewGroup;
28 import android.view.ViewHierarchyEncoder;
29 import android.view.inspector.InspectableProperty;
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  */
47 public 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      */
TableRow(Context context)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      */
TableRow(Context context, AttributeSet attrs)72     public TableRow(Context context, AttributeSet attrs) {
73         super(context, attrs);
74         initTableRow();
75     }
76 
initTableRow()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
setOnHierarchyChangeListener(OnHierarchyChangeListener listener)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      */
setColumnCollapsed(int columnIndex, boolean collapsed)101     void setColumnCollapsed(int columnIndex, boolean collapsed) {
102         final View child = getVirtualChildAt(columnIndex);
103         if (child != null) {
104             child.setVisibility(collapsed ? GONE : VISIBLE);
105         }
106     }
107 
108     /**
109      * {@inheritDoc}
110      */
111     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)112     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
113         // enforce horizontal layout
114         measureHorizontal(widthMeasureSpec, heightMeasureSpec);
115     }
116 
117     /**
118      * {@inheritDoc}
119      */
120     @Override
onLayout(boolean changed, int l, int t, int r, int b)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
getVirtualChildAt(int i)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
getVirtualChildCount()147     public int getVirtualChildCount() {
148         if (mColumnToChildIndex == null) {
149             mapIndexAndColumns();
150         }
151         return mNumColumns;
152     }
153 
mapIndexAndColumns()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
measureNullChild(int childIndex)183     int measureNullChild(int childIndex) {
184         return mConstrainedColumnWidths[childIndex];
185     }
186 
187     /**
188      * {@inheritDoc}
189      */
190     @Override
measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight)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
getChildrenSkipCount(View child, int index)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
getLocationOffset(View child)267     int getLocationOffset(View child) {
268         return ((TableRow.LayoutParams) child.getLayoutParams()).mOffset[LayoutParams.LOCATION];
269     }
270 
271     /**
272      * {@inheritDoc}
273      */
274     @Override
getNextLocationOffset(View child)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      */
getColumnsWidths(int widthMeasureSpec, int heightMeasureSpec)288     int[] getColumnsWidths(int widthMeasureSpec, int heightMeasureSpec) {
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.makeSafeMeasureSpec(
308                                     MeasureSpec.getSize(heightMeasureSpec),
309                                     MeasureSpec.UNSPECIFIED);
310                             break;
311                         default:
312                             spec = MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY);
313                     }
314                     child.measure(spec, spec);
315 
316                     final int width = child.getMeasuredWidth() + layoutParams.leftMargin +
317                             layoutParams.rightMargin;
318                     columnWidths[i] = width;
319                 } else {
320                     columnWidths[i] = 0;
321                 }
322             } else {
323                 columnWidths[i] = 0;
324             }
325         }
326 
327         return columnWidths;
328     }
329 
330     /**
331      * <p>Sets the width of all of the columns in this row. At layout time,
332      * this row sets a fixed width, as defined by <code>columnWidths</code>,
333      * on each child (or cell, or column.)</p>
334      *
335      * @param columnWidths the fixed width of each column that this row must
336      *                     honor
337      * @throws IllegalArgumentException when columnWidths' length is smaller
338      *         than the number of children in this row
339      * {@hide}
340      */
setColumnsWidthConstraints(int[] columnWidths)341     void setColumnsWidthConstraints(int[] columnWidths) {
342         if (columnWidths == null || columnWidths.length < getVirtualChildCount()) {
343             throw new IllegalArgumentException(
344                     "columnWidths should be >= getVirtualChildCount()");
345         }
346 
347         mConstrainedColumnWidths = columnWidths;
348     }
349 
350     /**
351      * {@inheritDoc}
352      */
353     @Override
generateLayoutParams(AttributeSet attrs)354     public LayoutParams generateLayoutParams(AttributeSet attrs) {
355         return new TableRow.LayoutParams(getContext(), attrs);
356     }
357 
358     /**
359      * Returns a set of layout parameters with a width of
360      * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT},
361      * a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and no spanning.
362      */
363     @Override
generateDefaultLayoutParams()364     protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
365         return new LayoutParams();
366     }
367 
368     /**
369      * {@inheritDoc}
370      */
371     @Override
checkLayoutParams(ViewGroup.LayoutParams p)372     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
373         return p instanceof TableRow.LayoutParams;
374     }
375 
376     /**
377      * {@inheritDoc}
378      */
379     @Override
generateLayoutParams(ViewGroup.LayoutParams p)380     protected LinearLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
381         return new LayoutParams(p);
382     }
383 
384     @Override
getAccessibilityClassName()385     public CharSequence getAccessibilityClassName() {
386         return TableRow.class.getName();
387     }
388 
389     /**
390      * <p>Set of layout parameters used in table rows.</p>
391      *
392      * @see android.widget.TableLayout.LayoutParams
393      *
394      * @attr ref android.R.styleable#TableRow_Cell_layout_column
395      * @attr ref android.R.styleable#TableRow_Cell_layout_span
396      */
397     public static class LayoutParams extends LinearLayout.LayoutParams {
398         /**
399          * <p>The column index of the cell represented by the widget.</p>
400          */
401         @ViewDebug.ExportedProperty(category = "layout")
402         @InspectableProperty(name = "layout_column")
403         public int column;
404 
405         /**
406          * <p>The number of columns the widgets spans over.</p>
407          */
408         @ViewDebug.ExportedProperty(category = "layout")
409         @InspectableProperty(name = "layout_span")
410         public int span;
411 
412         private static final int LOCATION = 0;
413         private static final int LOCATION_NEXT = 1;
414 
415         private int[] mOffset = new int[2];
416 
417         /**
418          * {@inheritDoc}
419          */
LayoutParams(Context c, AttributeSet attrs)420         public LayoutParams(Context c, AttributeSet attrs) {
421             super(c, attrs);
422 
423             TypedArray a =
424                     c.obtainStyledAttributes(attrs,
425                             com.android.internal.R.styleable.TableRow_Cell);
426 
427             column = a.getInt(com.android.internal.R.styleable.TableRow_Cell_layout_column, -1);
428             span = a.getInt(com.android.internal.R.styleable.TableRow_Cell_layout_span, 1);
429             if (span <= 1) {
430                 span = 1;
431             }
432 
433             a.recycle();
434         }
435 
436         /**
437          * <p>Sets the child width and the child height.</p>
438          *
439          * @param w the desired width
440          * @param h the desired height
441          */
LayoutParams(int w, int h)442         public LayoutParams(int w, int h) {
443             super(w, h);
444             column = -1;
445             span = 1;
446         }
447 
448         /**
449          * <p>Sets the child width, height and weight.</p>
450          *
451          * @param w the desired width
452          * @param h the desired height
453          * @param initWeight the desired weight
454          */
LayoutParams(int w, int h, float initWeight)455         public LayoutParams(int w, int h, float initWeight) {
456             super(w, h, initWeight);
457             column = -1;
458             span = 1;
459         }
460 
461         /**
462          * <p>Sets the child width to {@link android.view.ViewGroup.LayoutParams}
463          * and the child height to
464          * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p>
465          */
LayoutParams()466         public LayoutParams() {
467             super(MATCH_PARENT, WRAP_CONTENT);
468             column = -1;
469             span = 1;
470         }
471 
472         /**
473          * <p>Puts the view in the specified column.</p>
474          *
475          * <p>Sets the child width to {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}
476          * and the child height to
477          * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p>
478          *
479          * @param column the column index for the view
480          */
LayoutParams(int column)481         public LayoutParams(int column) {
482             this();
483             this.column = column;
484         }
485 
486         /**
487          * {@inheritDoc}
488          */
LayoutParams(ViewGroup.LayoutParams p)489         public LayoutParams(ViewGroup.LayoutParams p) {
490             super(p);
491         }
492 
493         /**
494          * {@inheritDoc}
495          */
LayoutParams(MarginLayoutParams source)496         public LayoutParams(MarginLayoutParams source) {
497             super(source);
498         }
499 
500         @Override
setBaseAttributes(TypedArray a, int widthAttr, int heightAttr)501         protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
502             // We don't want to force users to specify a layout_width
503             if (a.hasValue(widthAttr)) {
504                 width = a.getLayoutDimension(widthAttr, "layout_width");
505             } else {
506                 width = MATCH_PARENT;
507             }
508 
509             // We don't want to force users to specify a layout_height
510             if (a.hasValue(heightAttr)) {
511                 height = a.getLayoutDimension(heightAttr, "layout_height");
512             } else {
513                 height = WRAP_CONTENT;
514             }
515         }
516 
517         /** @hide */
518         @Override
encodeProperties(@onNull ViewHierarchyEncoder encoder)519         protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
520             super.encodeProperties(encoder);
521             encoder.addProperty("layout:column", column);
522             encoder.addProperty("layout:span", span);
523         }
524     }
525 
526     // special transparent hierarchy change listener
527     private class ChildrenTracker implements OnHierarchyChangeListener {
528         private OnHierarchyChangeListener listener;
529 
setOnHierarchyChangeListener(OnHierarchyChangeListener listener)530         private void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
531             this.listener = listener;
532         }
533 
onChildViewAdded(View parent, View child)534         public void onChildViewAdded(View parent, View child) {
535             // dirties the index to column map
536             mColumnToChildIndex = null;
537 
538             if (this.listener != null) {
539                 this.listener.onChildViewAdded(parent, child);
540             }
541         }
542 
onChildViewRemoved(View parent, View child)543         public void onChildViewRemoved(View parent, View child) {
544             // dirties the index to column map
545             mColumnToChildIndex = null;
546 
547             if (this.listener != null) {
548                 this.listener.onChildViewRemoved(parent, child);
549             }
550         }
551     }
552 }
553