TableLayout.java revision f013e1afd1e68af5e3b868c26a653bbfb39538f8
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 com.android.internal.R;
20
21import android.content.Context;
22import android.content.res.TypedArray;
23import android.util.AttributeSet;
24import android.util.SparseBooleanArray;
25import android.view.View;
26import android.view.ViewGroup;
27
28import java.util.regex.Pattern;
29
30/**
31 * <p>A layout that arranges its children into rows and columns.
32 * A TableLayout consists of a number of {@link android.widget.TableRow} objects,
33 * each defining a row (actually, you can have other children, which will be
34 * explained below). TableLayout containers do not display border lines for
35 * their rows, columns, or cells. Each row has zero or more cells; each cell can
36 * hold one {@link android.view.View View} object. The table has as many columns
37 * as the row with the most cells. A table can leave cells empty. Cells can span
38 * columns, as they can in HTML.</p>
39 *
40 * <p>The width of a column is defined by the row with the widest cell in that
41 * column. However, a TableLayout can specify certain columns as shrinkable or
42 * stretchable by calling
43 * {@link #setColumnShrinkable(int, boolean) setColumnShrinkable()}
44 * or {@link #setColumnStretchable(int, boolean) setColumnStretchable()}. If
45 * marked as shrinkable, the column width can be shrunk to fit the table into
46 * its parent object. If marked as stretchable, it can expand in width to fit
47 * any extra space. The total width of the table is defined by its parent
48 * container. It is important to remember that a column can be both shrinkable
49 * and stretchable. In such a situation, the column will change its size to
50 * always use up the available space, but never more. Finally, you can hide a
51 * column by calling
52 * {@link #setColumnCollapsed(int,boolean) setColumnCollapsed()}.</p>
53 *
54 * <p>The children of a TableLayout cannot specify the <code>layout_width</code>
55 * attribute. Width is always <code>FILL_PARENT</code>. However, the
56 * <code>layout_height</code> attribute can be defined by a child; default value
57 * is {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}. If the child
58 * is a {@link android.widget.TableRow}, then the height is always
59 * {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}.</p>
60 *
61 * <p> Cells must be added to a row in increasing column order, both in code and
62 * XML. Column numbers are zero-based. If you don't specify a column number for
63 * a child cell, it will autoincrement to the next available column. If you skip
64 * a column number, it will be considered an empty cell in that row. See the
65 * TableLayout examples in ApiDemos for examples of creating tables in XML.</p>
66 *
67 * <p>Although the typical child of a TableLayout is a TableRow, you can
68 * actually use any View subclass as a direct child of TableLayout. The View
69 * will be displayed as a single row that spans all the table columns.</p>
70 */
71public class TableLayout extends LinearLayout {
72    private int[] mMaxWidths;
73    private SparseBooleanArray mStretchableColumns;
74    private SparseBooleanArray mShrinkableColumns;
75    private SparseBooleanArray mCollapsedColumns;
76
77    private boolean mShrinkAllColumns;
78    private boolean mStretchAllColumns;
79
80    private TableLayout.PassThroughHierarchyChangeListener mPassThroughListener;
81
82    private boolean mInitialized;
83
84    /**
85     * <p>Creates a new TableLayout for the given context.</p>
86     *
87     * @param context the application environment
88     */
89    public TableLayout(Context context) {
90        super(context);
91        initTableLayout();
92    }
93
94    /**
95     * <p>Creates a new TableLayout for the given context and with the
96     * specified set attributes.</p>
97     *
98     * @param context the application environment
99     * @param attrs a collection of attributes
100     */
101    public TableLayout(Context context, AttributeSet attrs) {
102        super(context, attrs);
103
104        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TableLayout);
105
106        String stretchedColumns = a.getString(R.styleable.TableLayout_stretchColumns);
107        if (stretchedColumns != null) {
108            if (stretchedColumns.charAt(0) == '*') {
109                mStretchAllColumns = true;
110            } else {
111                mStretchableColumns = parseColumns(stretchedColumns);
112            }
113        }
114
115        String shrinkedColumns = a.getString(R.styleable.TableLayout_shrinkColumns);
116        if (shrinkedColumns != null) {
117            if (shrinkedColumns.charAt(0) == '*') {
118                mShrinkAllColumns = true;
119            } else {
120                mShrinkableColumns = parseColumns(shrinkedColumns);
121            }
122        }
123
124        String collapsedColumns = a.getString(R.styleable.TableLayout_collapseColumns);
125        if (collapsedColumns != null) {
126            mCollapsedColumns = parseColumns(collapsedColumns);
127        }
128
129        a.recycle();
130        initTableLayout();
131    }
132
133    /**
134     * <p>Parses a sequence of columns ids defined in a CharSequence with the
135     * following pattern (regex): \d+(\s*,\s*\d+)*</p>
136     *
137     * <p>Examples: "1" or "13, 7, 6" or "".</p>
138     *
139     * <p>The result of the parsing is stored in a sparse boolean array. The
140     * parsed column ids are used as the keys of the sparse array. The values
141     * are always true.</p>
142     *
143     * @param sequence a sequence of column ids, can be empty but not null
144     * @return a sparse array of boolean mapping column indexes to the columns
145     *         collapse state
146     */
147    private static SparseBooleanArray parseColumns(String sequence) {
148        SparseBooleanArray columns = new SparseBooleanArray();
149        Pattern pattern = Pattern.compile("\\s*,\\s*");
150        String[] columnDefs = pattern.split(sequence);
151
152        for (String columnIdentifier : columnDefs) {
153            try {
154                int columnIndex = Integer.parseInt(columnIdentifier);
155                // only valid, i.e. positive, columns indexes are handled
156                if (columnIndex >= 0) {
157                    // putting true in this sparse array indicates that the
158                    // column index was defined in the XML file
159                    columns.put(columnIndex, true);
160                }
161            } catch (NumberFormatException e) {
162                // we just ignore columns that don't exist
163            }
164        }
165
166        return columns;
167    }
168
169    /**
170     * <p>Performs initialization common to prorgrammatic use and XML use of
171     * this widget.</p>
172     */
173    private void initTableLayout() {
174        if (mCollapsedColumns == null) {
175            mCollapsedColumns = new SparseBooleanArray();
176        }
177        if (mStretchableColumns == null) {
178            mStretchableColumns = new SparseBooleanArray();
179        }
180        if (mShrinkableColumns == null) {
181            mShrinkableColumns = new SparseBooleanArray();
182        }
183
184        mPassThroughListener = new PassThroughHierarchyChangeListener();
185        // make sure to call the parent class method to avoid potential
186        // infinite loops
187        super.setOnHierarchyChangeListener(mPassThroughListener);
188
189        mInitialized = true;
190    }
191
192    /**
193     * {@inheritDoc}
194     */
195    @Override
196    public void setOnHierarchyChangeListener(
197            OnHierarchyChangeListener listener) {
198        // the user listener is delegated to our pass-through listener
199        mPassThroughListener.mOnHierarchyChangeListener = listener;
200    }
201
202    private void requestRowsLayout() {
203        if (mInitialized) {
204            final int count = getChildCount();
205            for (int i = 0; i < count; i++) {
206                getChildAt(i).requestLayout();
207            }
208        }
209    }
210
211    /**
212     * {@inheritDoc}
213     */
214    @Override
215    public void requestLayout() {
216        if (mInitialized) {
217            int count = getChildCount();
218            for (int i = 0; i < count; i++) {
219                getChildAt(i).forceLayout();
220            }
221        }
222
223        super.requestLayout();
224    }
225
226    /**
227     * <p>Indicates whether all columns are shrinkable or not.</p>
228     *
229     * @return true if all columns are shrinkable, false otherwise
230     */
231    public boolean isShrinkAllColumns() {
232        return mShrinkAllColumns;
233    }
234
235    /**
236     * <p>Convenience method to mark all columns as shrinkable.</p>
237     *
238     * @param shrinkAllColumns true to mark all columns shrinkable
239     *
240     * @attr ref android.R.styleable#TableLayout_shrinkColumns
241     */
242    public void setShrinkAllColumns(boolean shrinkAllColumns) {
243        mShrinkAllColumns = shrinkAllColumns;
244    }
245
246    /**
247     * <p>Indicates whether all columns are stretchable or not.</p>
248     *
249     * @return true if all columns are stretchable, false otherwise
250     */
251    public boolean isStretchAllColumns() {
252        return mStretchAllColumns;
253    }
254
255    /**
256     * <p>Convenience method to mark all columns as stretchable.</p>
257     *
258     * @param stretchAllColumns true to mark all columns stretchable
259     *
260     * @attr ref android.R.styleable#TableLayout_stretchColumns
261     */
262    public void setStretchAllColumns(boolean stretchAllColumns) {
263        mStretchAllColumns = stretchAllColumns;
264    }
265
266    /**
267     * <p>Collapses or restores a given column. When collapsed, a column
268     * does not appear on screen and the extra space is reclaimed by the
269     * other columns. A column is collapsed/restored only when it belongs to
270     * a {@link android.widget.TableRow}.</p>
271     *
272     * <p>Calling this method requests a layout operation.</p>
273     *
274     * @param columnIndex the index of the column
275     * @param isCollapsed true if the column must be collapsed, false otherwise
276     *
277     * @attr ref android.R.styleable#TableLayout_collapseColumns
278     */
279    public void setColumnCollapsed(int columnIndex, boolean isCollapsed) {
280        // update the collapse status of the column
281        mCollapsedColumns.put(columnIndex, isCollapsed);
282
283        int count = getChildCount();
284        for (int i = 0; i < count; i++) {
285            final View view = getChildAt(i);
286            if (view instanceof TableRow) {
287                ((TableRow) view).setColumnCollapsed(columnIndex, isCollapsed);
288            }
289        }
290
291        requestRowsLayout();
292    }
293
294    /**
295     * <p>Returns the collapsed state of the specified column.</p>
296     *
297     * @param columnIndex the index of the column
298     * @return true if the column is collapsed, false otherwise
299     */
300    public boolean isColumnCollapsed(int columnIndex) {
301        return mCollapsedColumns.get(columnIndex);
302    }
303
304    /**
305     * <p>Makes the given column stretchable or not. When stretchable, a column
306     * takes up as much as available space as possible in its row.</p>
307     *
308     * <p>Calling this method requests a layout operation.</p>
309     *
310     * @param columnIndex the index of the column
311     * @param isStretchable true if the column must be stretchable,
312     *                      false otherwise. Default is false.
313     *
314     * @attr ref android.R.styleable#TableLayout_stretchColumns
315     */
316    public void setColumnStretchable(int columnIndex, boolean isStretchable) {
317        mStretchableColumns.put(columnIndex, isStretchable);
318        requestRowsLayout();
319    }
320
321    /**
322     * <p>Returns whether the specified column is stretchable or not.</p>
323     *
324     * @param columnIndex the index of the column
325     * @return true if the column is stretchable, false otherwise
326     */
327    public boolean isColumnStretchable(int columnIndex) {
328        return mStretchAllColumns || mStretchableColumns.get(columnIndex);
329    }
330
331    /**
332     * <p>Makes the given column shrinkable or not. When a row is too wide, the
333     * table can reclaim extra space from shrinkable columns.</p>
334     *
335     * <p>Calling this method requests a layout operation.</p>
336     *
337     * @param columnIndex the index of the column
338     * @param isShrinkable true if the column must be shrinkable,
339     *                     false otherwise. Default is false.
340     *
341     * @attr ref android.R.styleable#TableLayout_shrinkColumns
342     */
343    public void setColumnShrinkable(int columnIndex, boolean isShrinkable) {
344        mShrinkableColumns.put(columnIndex, isShrinkable);
345        requestRowsLayout();
346    }
347
348    /**
349     * <p>Returns whether the specified column is shrinkable or not.</p>
350     *
351     * @param columnIndex the index of the column
352     * @return true if the column is shrinkable, false otherwise. Default is false.
353     */
354    public boolean isColumnShrinkable(int columnIndex) {
355        return mShrinkAllColumns || mShrinkableColumns.get(columnIndex);
356    }
357
358    /**
359     * <p>Applies the columns collapse status to a new row added to this
360     * table. This method is invoked by PassThroughHierarchyChangeListener
361     * upon child insertion.</p>
362     *
363     * <p>This method only applies to {@link android.widget.TableRow}
364     * instances.</p>
365     *
366     * @param child the newly added child
367     */
368    private void trackCollapsedColumns(View child) {
369        if (child instanceof TableRow) {
370            final TableRow row = (TableRow) child;
371            final SparseBooleanArray collapsedColumns = mCollapsedColumns;
372            final int count = collapsedColumns.size();
373            for (int i = 0; i < count; i++) {
374                int columnIndex = collapsedColumns.keyAt(i);
375                boolean isCollapsed = collapsedColumns.valueAt(i);
376                // the collapse status is set only when the column should be
377                // collapsed; otherwise, this might affect the default
378                // visibility of the row's children
379                if (isCollapsed) {
380                    row.setColumnCollapsed(columnIndex, isCollapsed);
381                }
382            }
383        }
384    }
385
386    /**
387     * {@inheritDoc}
388     */
389    @Override
390    public void addView(View child) {
391        super.addView(child);
392        requestRowsLayout();
393    }
394
395    /**
396     * {@inheritDoc}
397     */
398    @Override
399    public void addView(View child, int index) {
400        super.addView(child, index);
401        requestRowsLayout();
402    }
403
404    /**
405     * {@inheritDoc}
406     */
407    @Override
408    public void addView(View child, ViewGroup.LayoutParams params) {
409        super.addView(child, params);
410        requestRowsLayout();
411    }
412
413    /**
414     * {@inheritDoc}
415     */
416    @Override
417    public void addView(View child, int index, ViewGroup.LayoutParams params) {
418        super.addView(child, index, params);
419        requestRowsLayout();
420    }
421
422    /**
423     * {@inheritDoc}
424     */
425    @Override
426    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
427        // enforce vertical layout
428        measureVertical(widthMeasureSpec, heightMeasureSpec);
429    }
430
431    /**
432     * {@inheritDoc}
433     */
434    @Override
435    protected void onLayout(boolean changed, int l, int t, int r, int b) {
436        // enforce vertical layout
437        layoutVertical();
438    }
439
440    /**
441     * {@inheritDoc}
442     */
443    @Override
444    void measureChildBeforeLayout(View child, int childIndex,
445            int widthMeasureSpec, int totalWidth,
446            int heightMeasureSpec, int totalHeight) {
447        // when the measured child is a table row, we force the width of its
448        // children with the widths computed in findLargestCells()
449        if (child instanceof TableRow) {
450            ((TableRow) child).setColumnsWidthConstraints(mMaxWidths);
451        }
452
453        super.measureChildBeforeLayout(child, childIndex,
454                widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight);
455    }
456
457    /**
458     * {@inheritDoc}
459     */
460    @Override
461    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
462        findLargestCells(widthMeasureSpec);
463        shrinkAndStretchColumns(widthMeasureSpec);
464
465        super.measureVertical(widthMeasureSpec, heightMeasureSpec);
466    }
467
468    /**
469     * <p>Finds the largest cell in each column. For each column, the width of
470     * the largest cell is applied to all the other cells.</p>
471     *
472     * @param widthMeasureSpec the measure constraint imposed by our parent
473     */
474    private void findLargestCells(int widthMeasureSpec) {
475        boolean firstRow = true;
476
477        // find the maximum width for each column
478        // the total number of columns is dynamically changed if we find
479        // wider rows as we go through the children
480        // the array is reused for each layout operation; the array can grow
481        // but never shrinks. Unused extra cells in the array are just ignored
482        // this behavior avoids to unnecessary grow the array after the first
483        // layout operation
484        final int count = getChildCount();
485        for (int i = 0; i < count; i++) {
486            final View child = getChildAt(i);
487            if (child.getVisibility() == GONE) {
488                continue;
489            }
490
491            if (child instanceof TableRow) {
492                final TableRow row = (TableRow) child;
493                // forces the row's height
494                final ViewGroup.LayoutParams layoutParams = row.getLayoutParams();
495                layoutParams.height = LayoutParams.WRAP_CONTENT;
496
497                final int[] widths = row.getColumnsWidths(widthMeasureSpec);
498                final int newLength = widths.length;
499                // this is the first row, we just need to copy the values
500                if (firstRow) {
501                    if (mMaxWidths == null || mMaxWidths.length != newLength) {
502                        mMaxWidths = new int[newLength];
503                    }
504                    System.arraycopy(widths, 0, mMaxWidths, 0, newLength);
505                    firstRow = false;
506                } else {
507                    int length = mMaxWidths.length;
508                    final int difference = newLength - length;
509                    // the current row is wider than the previous rows, so
510                    // we just grow the array and copy the values
511                    if (difference > 0) {
512                        final int[] oldMaxWidths = mMaxWidths;
513                        mMaxWidths = new int[newLength];
514                        System.arraycopy(oldMaxWidths, 0, mMaxWidths, 0,
515                                oldMaxWidths.length);
516                        System.arraycopy(widths, oldMaxWidths.length,
517                                mMaxWidths, oldMaxWidths.length, difference);
518                    }
519
520                    // the row is narrower or of the same width as the previous
521                    // rows, so we find the maximum width for each column
522                    // if the row is narrower than the previous ones,
523                    // difference will be negative
524                    final int[] maxWidths = mMaxWidths;
525                    length = Math.min(length, newLength);
526                    for (int j = 0; j < length; j++) {
527                        maxWidths[j] = Math.max(maxWidths[j], widths[j]);
528                    }
529                }
530            }
531        }
532    }
533
534    /**
535     * <p>Shrinks the columns if their total width is greater than the
536     * width allocated by widthMeasureSpec. When the total width is less
537     * than the allocated width, this method attempts to stretch columns
538     * to fill the remaining space.</p>
539     *
540     * @param widthMeasureSpec the width measure specification as indicated
541     *                         by this widget's parent
542     */
543    private void shrinkAndStretchColumns(int widthMeasureSpec) {
544        // when we have no row, mMaxWidths is not initialized and the loop
545        // below could cause a NPE
546        if (mMaxWidths == null) {
547            return;
548        }
549
550        // should we honor AT_MOST, EXACTLY and UNSPECIFIED?
551        int totalWidth = 0;
552        for (int width : mMaxWidths) {
553            totalWidth += width;
554        }
555
556        int size = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight;
557
558        if ((totalWidth > size) && (mShrinkAllColumns || mShrinkableColumns.size() > 0)) {
559            // oops, the largest columns are wider than the row itself
560            // fairly redistribute the row's widh among the columns
561            mutateColumnsWidth(mShrinkableColumns, mShrinkAllColumns, size, totalWidth);
562        } else if ((totalWidth < size) && (mStretchAllColumns || mStretchableColumns.size() > 0)) {
563            // if we have some space left, we distribute it among the
564            // expandable columns
565            mutateColumnsWidth(mStretchableColumns, mStretchAllColumns, size, totalWidth);
566        }
567    }
568
569    private void mutateColumnsWidth(SparseBooleanArray columns,
570            boolean allColumns, int size, int totalWidth) {
571        int skipped = 0;
572        final int[] maxWidths = mMaxWidths;
573        final int length = maxWidths.length;
574        final int count = allColumns ? length : columns.size();
575        final int totalExtraSpace = size - totalWidth;
576        int extraSpace = totalExtraSpace / count;
577
578        if (!allColumns) {
579            for (int i = 0; i < count; i++) {
580                int column = columns.keyAt(i);
581                if (columns.valueAt(i)) {
582                    if (column < length) {
583                        maxWidths[column] += extraSpace;
584                    } else {
585                        skipped++;
586                    }
587                }
588            }
589        } else {
590            for (int i = 0; i < count; i++) {
591                maxWidths[i] += extraSpace;
592            }
593
594            // we don't skip any column so we can return right away
595            return;
596        }
597
598        if (skipped > 0 && skipped < count) {
599            // reclaim any extra space we left to columns that don't exist
600            extraSpace = skipped * extraSpace / (count - skipped);
601            for (int i = 0; i < count; i++) {
602                int column = columns.keyAt(i);
603                if (columns.valueAt(i) && column < length) {
604                    if (extraSpace > maxWidths[column]) {
605                        maxWidths[column] = 0;
606                    } else {
607                        maxWidths[column] += extraSpace;
608                    }
609                }
610            }
611        }
612    }
613
614    /**
615     * {@inheritDoc}
616     */
617    @Override
618    public LayoutParams generateLayoutParams(AttributeSet attrs) {
619        return new TableLayout.LayoutParams(getContext(), attrs);
620    }
621
622    /**
623     * Returns a set of layout parameters with a width of
624     * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT},
625     * and a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.
626     */
627    @Override
628    protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
629        return new LayoutParams();
630    }
631
632    /**
633     * {@inheritDoc}
634     */
635    @Override
636    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
637        return p instanceof TableLayout.LayoutParams;
638    }
639
640    /**
641     * {@inheritDoc}
642     */
643    @Override
644    protected LinearLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
645        return new LayoutParams(p);
646    }
647
648    /**
649     * <p>This set of layout parameters enforces the width of each child to be
650     * {@link #FILL_PARENT} and the height of each child to be
651     * {@link #WRAP_CONTENT}, but only if the height is not specified.</p>
652     */
653    @SuppressWarnings({"UnusedDeclaration"})
654    public static class LayoutParams extends LinearLayout.LayoutParams {
655        /**
656         * {@inheritDoc}
657         */
658        public LayoutParams(Context c, AttributeSet attrs) {
659            super(c, attrs);
660        }
661
662        /**
663         * {@inheritDoc}
664         */
665        public LayoutParams(int w, int h) {
666            super(FILL_PARENT, h);
667        }
668
669        /**
670         * {@inheritDoc}
671         */
672        public LayoutParams(int w, int h, float initWeight) {
673            super(FILL_PARENT, h, initWeight);
674        }
675
676        /**
677         * <p>Sets the child width to
678         * {@link android.view.ViewGroup.LayoutParams} and the child height to
679         * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p>
680         */
681        public LayoutParams() {
682            super(FILL_PARENT, WRAP_CONTENT);
683        }
684
685        /**
686         * {@inheritDoc}
687         */
688        public LayoutParams(ViewGroup.LayoutParams p) {
689            super(p);
690        }
691
692        /**
693         * {@inheritDoc}
694         */
695        public LayoutParams(MarginLayoutParams source) {
696            super(source);
697        }
698
699        /**
700         * <p>Fixes the row's width to
701         * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT}; the row's
702         * height is fixed to
703         * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} if no layout
704         * height is specified.</p>
705         *
706         * @param a the styled attributes set
707         * @param widthAttr the width attribute to fetch
708         * @param heightAttr the height attribute to fetch
709         */
710        @Override
711        protected void setBaseAttributes(TypedArray a,
712                int widthAttr, int heightAttr) {
713            this.width = FILL_PARENT;
714            if (a.hasValue(heightAttr)) {
715                this.height = a.getLayoutDimension(heightAttr, "layout_height");
716            } else {
717                this.height = WRAP_CONTENT;
718            }
719        }
720    }
721
722    /**
723     * <p>A pass-through listener acts upon the events and dispatches them
724     * to another listener. This allows the table layout to set its own internal
725     * hierarchy change listener without preventing the user to setup his.</p>
726     */
727    private class PassThroughHierarchyChangeListener implements
728            OnHierarchyChangeListener {
729        private OnHierarchyChangeListener mOnHierarchyChangeListener;
730
731        /**
732         * {@inheritDoc}
733         */
734        public void onChildViewAdded(View parent, View child) {
735            trackCollapsedColumns(child);
736
737            if (mOnHierarchyChangeListener != null) {
738                mOnHierarchyChangeListener.onChildViewAdded(parent, child);
739            }
740        }
741
742        /**
743         * {@inheritDoc}
744         */
745        public void onChildViewRemoved(View parent, View child) {
746            if (mOnHierarchyChangeListener != null) {
747                mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
748            }
749        }
750    }
751}
752