1package com.android.deskclock.widget;
2
3import android.content.Context;
4import android.util.AttributeSet;
5import android.view.View;
6import android.widget.LinearLayout;
7import android.widget.TextView;
8
9/**
10 * When this layout is in the Horizontal orientation and one and only one child is a TextView with a
11 * non-null android:ellipsize, this layout will reduce android:maxWidth of that TextView to ensure
12 * the siblings are not truncated. This class is useful when that ellipsize-text-view "starts"
13 * before other children of this view group. This layout has no effect if:
14 * <ul>
15 *     <li>the orientation is not horizontal</li>
16 *     <li>any child has weights.</li>
17 *     <li>more than one child has a non-null android:ellipsize.</li>
18 * </ul>
19 *
20 * <p>The purpose of this horizontal-linear-layout is to ensure that when the sum of widths of the
21 * children are greater than this parent, the maximum width of the ellipsize-text-view, is reduced
22 * so that no siblings are truncated.</p>
23 *
24 * <p>For example: Given Text1 has android:ellipsize="end" and Text2 has android:ellipsize="none",
25 * as Text1 and/or Text2 grow in width, both will consume more width until Text2 hits the end
26 * margin, then Text1 will cease to grow and instead shrink to accommodate any further growth in
27 * Text2.</p>
28 * <ul>
29 * <li>|[text1]|[text2]              |</li>
30 * <li>|[text1 text1]|[text2 text2]  |</li>
31 * <li>|[text...]|[text2 text2 text2]|</li>
32 * </ul>
33 */
34public class EllipsizeLayout extends LinearLayout {
35
36    @SuppressWarnings("unused")
37    public EllipsizeLayout(Context context) {
38        this(context, null);
39    }
40
41    public EllipsizeLayout(Context context, AttributeSet attrs) {
42        super(context, attrs);
43    }
44
45    /**
46     * This override only acts when the LinearLayout is in the Horizontal orientation and is in it's
47     * final measurement pass(MeasureSpec.EXACTLY). In this case only, this class
48     * <ul>
49     *     <li>Identifies the one TextView child with the non-null android:ellipsize.</li>
50     *     <li>Re-measures the needed width of all children (by calling measureChildWithMargins with
51     *     the width measure specification to MeasureSpec.UNSPECIFIED.)</li>
52     *     <li>Sums the children's widths.</li>
53     *     <li>Whenever the sum of the children's widths is greater than this parent was allocated,
54     *     the maximum width of the one TextView child with the non-null android:ellipsize is
55     *     reduced.</li>
56     * </ul>
57     *
58     * @param widthMeasureSpec horizontal space requirements as imposed by the parent
59     * @param heightMeasureSpec vertical space requirements as imposed by the parent
60     */
61    @Override
62    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
63        if (getOrientation() == HORIZONTAL
64                && (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY)) {
65            int totalLength = 0;
66            // If any of the constraints of this class are exceeded, outOfSpec becomes true
67            // and the no alterations are made to the ellipsize-text-view.
68            boolean outOfSpec = false;
69            TextView ellipsizeView = null;
70            final int count = getChildCount();
71            final int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
72            final int queryWidthMeasureSpec = MeasureSpec.
73                    makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.UNSPECIFIED);
74
75            for (int ii = 0; ii < count && !outOfSpec; ++ii) {
76                final View child = getChildAt(ii);
77                if (child != null && child.getVisibility() != GONE) {
78                    // Identify the ellipsize view
79                    if (child instanceof TextView) {
80                        final TextView tv = (TextView) child;
81                        if (tv.getEllipsize() != null) {
82                            if (ellipsizeView == null) {
83                                ellipsizeView = tv;
84                                // Clear the maximum width on ellipsizeView before measurement
85                                ellipsizeView.setMaxWidth(Integer.MAX_VALUE);
86                            } else {
87                                // TODO: support multiple android:ellipsize
88                                outOfSpec = true;
89                            }
90                        }
91                    }
92                    // Ask the child to measure itself
93                    measureChildWithMargins(child, queryWidthMeasureSpec, 0, heightMeasureSpec, 0);
94
95                    // Get the layout parameters to check for a weighted width and to add the
96                    // child's margins to the total length.
97                    final LinearLayout.LayoutParams layoutParams =
98                            (LinearLayout.LayoutParams) child.getLayoutParams();
99                    if (layoutParams != null) {
100                        outOfSpec |= (layoutParams.weight > 0f);
101                        totalLength += child.getMeasuredWidth()
102                                + layoutParams.leftMargin + layoutParams.rightMargin;
103                    } else {
104                        outOfSpec = true;
105                    }
106                }
107            }
108            // Last constraint test
109            outOfSpec |= (ellipsizeView == null) || (totalLength == 0);
110
111            if (!outOfSpec && totalLength > parentWidth) {
112                int maxWidth = ellipsizeView.getMeasuredWidth() - (totalLength - parentWidth);
113                // TODO: Respect android:minWidth (easy with @TargetApi(16))
114                final int minWidth = 0;
115                if (maxWidth < minWidth) {
116                    maxWidth = minWidth;
117                }
118                ellipsizeView.setMaxWidth(maxWidth);
119            }
120        }
121
122        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
123    }
124}
125