ResizingTextView.java revision c39d9c75590eca86a5e7e32a8824ba04a0d42e9b
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.widget;
15
16import android.content.Context;
17import android.content.res.TypedArray;
18import android.text.Layout;
19import android.util.AttributeSet;
20import android.util.TypedValue;
21import android.widget.TextView;
22
23import android.support.v17.leanback.R;
24
25/**
26 * <p>A {@link android.widget.TextView} that adjusts text size automatically in response
27 * to certain trigger conditions, such as text that wraps over multiple lines.</p>
28 */
29class ResizingTextView extends TextView {
30
31    /**
32     * Trigger text resize when text flows into the last line of a multi-line text view.
33     */
34    public static final int TRIGGER_MAX_LINES = 0x01;
35
36    private int mTriggerConditions; // Union of trigger conditions
37    private int mResizedTextSize;
38    // Note: Maintaining line spacing turned out not to be useful, and will be removed in
39    // the next round of design for this class (b/18736630). For now it simply defaults to false.
40    private boolean mMaintainLineSpacing;
41    private int mResizedPaddingAdjustmentTop;
42    private int mResizedPaddingAdjustmentBottom;
43
44    private boolean mIsResized = false;
45    // Remember default properties in case we need to restore them
46    private boolean mDefaultsInitialized = false;
47    private int mDefaultTextSize;
48    private float mDefaultLineSpacingExtra;
49    private int mDefaultPaddingTop;
50    private int mDefaultPaddingBottom;
51
52    public ResizingTextView(Context ctx, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
53        super(ctx, attrs, defStyleAttr);
54        TypedArray a = ctx.obtainStyledAttributes(attrs, R.styleable.lbResizingTextView,
55                defStyleAttr, defStyleRes);
56
57        try {
58            mTriggerConditions = a.getInt(
59                    R.styleable.lbResizingTextView_resizeTrigger, TRIGGER_MAX_LINES);
60            mResizedTextSize = a.getDimensionPixelSize(
61                    R.styleable.lbResizingTextView_resizedTextSize, -1);
62            mMaintainLineSpacing = a.getBoolean(
63                    R.styleable.lbResizingTextView_maintainLineSpacing, false);
64            mResizedPaddingAdjustmentTop = a.getDimensionPixelOffset(
65                    R.styleable.lbResizingTextView_resizedPaddingAdjustmentTop, 0);
66            mResizedPaddingAdjustmentBottom = a.getDimensionPixelOffset(
67                    R.styleable.lbResizingTextView_resizedPaddingAdjustmentBottom, 0);
68        } finally {
69            a.recycle();
70        }
71    }
72
73    public ResizingTextView(Context ctx, AttributeSet attrs, int defStyleAttr) {
74        this(ctx, attrs, defStyleAttr, 0);
75    }
76
77    public ResizingTextView(Context ctx, AttributeSet attrs) {
78        // TODO We should define our own style that inherits from TextViewStyle, to set defaults
79        // for new styleables,  We then pass the appropriate R.attr up the constructor chain here.
80        this(ctx, attrs, android.R.attr.textViewStyle);
81    }
82
83    public ResizingTextView(Context ctx) {
84        this(ctx, null);
85    }
86
87    /**
88     * @return the trigger conditions used to determine whether resize occurs
89     */
90    public int getTriggerConditions() {
91        return mTriggerConditions;
92    }
93
94    /**
95     * Set the trigger conditions used to determine whether resize occurs. Pass
96     * a union of trigger condition constants, such as {@link ResizingTextView#TRIGGER_MAX_LINES}.
97     *
98     * @param conditions A union of trigger condition constants
99     */
100    public void setTriggerConditions(int conditions) {
101        if (mTriggerConditions != conditions) {
102            mTriggerConditions = conditions;
103            // Always request a layout when trigger conditions change
104            requestLayout();
105        }
106    }
107
108    /**
109     * @return the resized text size
110     */
111    public int getResizedTextSize() {
112        return mResizedTextSize;
113    }
114
115    /**
116     * Set the text size for resized text.
117     *
118     * @param size The text size for resized text
119     */
120    public void setResizedTextSize(int size) {
121        if (mResizedTextSize != size) {
122            mResizedTextSize = size;
123            resizeParamsChanged();
124        }
125    }
126
127    /**
128     * @return whether or not to maintain line spacing when resizing text.
129     * The default is true.
130     */
131    public boolean getMaintainLineSpacing() {
132        return mMaintainLineSpacing;
133    }
134
135    /**
136     * Set whether or not to maintain line spacing when resizing text.
137     * The default is true.
138     *
139     * @param maintain Whether or not to maintain line spacing
140     */
141    public void setMaintainLineSpacing(boolean maintain) {
142        if (mMaintainLineSpacing != maintain) {
143            mMaintainLineSpacing = maintain;
144            resizeParamsChanged();
145        }
146    }
147
148    /**
149     * @return desired adjustment to top padding for resized text
150     */
151    public int getResizedPaddingAdjustmentTop() {
152        return mResizedPaddingAdjustmentTop;
153    }
154
155    /**
156     * Set the desired adjustment to top padding for resized text.
157     *
158     * @param adjustment The adjustment to top padding, in pixels
159     */
160    public void setResizedPaddingAdjustmentTop(int adjustment) {
161        if (mResizedPaddingAdjustmentTop != adjustment) {
162            mResizedPaddingAdjustmentTop = adjustment;
163            resizeParamsChanged();
164        }
165    }
166
167    /**
168     * @return desired adjustment to bottom padding for resized text
169     */
170    public int getResizedPaddingAdjustmentBottom() {
171        return mResizedPaddingAdjustmentBottom;
172    }
173
174    /**
175     * Set the desired adjustment to bottom padding for resized text.
176     *
177     * @param adjustment The adjustment to bottom padding, in pixels
178     */
179    public void setResizedPaddingAdjustmentBottom(int adjustment) {
180        if (mResizedPaddingAdjustmentBottom != adjustment) {
181            mResizedPaddingAdjustmentBottom = adjustment;
182            resizeParamsChanged();
183        }
184    }
185
186    private void resizeParamsChanged() {
187        // If we're not resized, then changing resize parameters doesn't
188        // affect layout, so don't bother requesting.
189        if (mIsResized) {
190            requestLayout();
191        }
192    }
193
194    @Override
195    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
196        if (!mDefaultsInitialized) {
197            mDefaultTextSize = (int) getTextSize();
198            mDefaultLineSpacingExtra = getLineSpacingExtra();
199            mDefaultPaddingTop = getPaddingTop();
200            mDefaultPaddingBottom = getPaddingBottom();
201            mDefaultsInitialized = true;
202        }
203
204        // Always try first to measure with defaults. Otherwise, we may think we can get away
205        // with larger text sizes later when we actually can't.
206        setTextSize(TypedValue.COMPLEX_UNIT_PX, mDefaultTextSize);
207        setLineSpacing(mDefaultLineSpacingExtra, getLineSpacingMultiplier());
208        setPaddingTopAndBottom(mDefaultPaddingTop, mDefaultPaddingBottom);
209
210        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
211
212        boolean resizeText = false;
213
214        final Layout layout = getLayout();
215        if (layout != null) {
216            if ((mTriggerConditions & TRIGGER_MAX_LINES) > 0) {
217                final int lineCount = layout.getLineCount();
218                final int maxLines = getMaxLines();
219                if (maxLines > 1) {
220                    resizeText = lineCount == maxLines;
221                }
222            }
223        }
224
225        final int currentSizePx = (int) getTextSize();
226        boolean remeasure = false;
227        if (resizeText) {
228            if (mResizedTextSize != -1 && currentSizePx != mResizedTextSize) {
229                setTextSize(TypedValue.COMPLEX_UNIT_PX, mResizedTextSize);
230                remeasure = true;
231            }
232            // Check for other desired adjustments in addition to the text size
233            final float targetLineSpacingExtra = mDefaultLineSpacingExtra +
234                    mDefaultTextSize - mResizedTextSize;
235            if (mMaintainLineSpacing && getLineSpacingExtra() != targetLineSpacingExtra) {
236                setLineSpacing(targetLineSpacingExtra, getLineSpacingMultiplier());
237                remeasure = true;
238            }
239            final int paddingTop = mDefaultPaddingTop + mResizedPaddingAdjustmentTop;
240            final int paddingBottom = mDefaultPaddingBottom + mResizedPaddingAdjustmentBottom;
241            if (getPaddingTop() != paddingTop || getPaddingBottom() != paddingBottom) {
242                setPaddingTopAndBottom(paddingTop, paddingBottom);
243                remeasure = true;
244            }
245        } else {
246            // Use default size, line spacing, and padding
247            if (mResizedTextSize != -1 && currentSizePx != mDefaultTextSize) {
248                setTextSize(TypedValue.COMPLEX_UNIT_PX, mDefaultTextSize);
249                remeasure = true;
250            }
251            if (mMaintainLineSpacing && getLineSpacingExtra() != mDefaultLineSpacingExtra) {
252                setLineSpacing(mDefaultLineSpacingExtra, getLineSpacingMultiplier());
253                remeasure = true;
254            }
255            if (getPaddingTop() != mDefaultPaddingTop ||
256                    getPaddingBottom() != mDefaultPaddingBottom) {
257                setPaddingTopAndBottom(mDefaultPaddingTop, mDefaultPaddingBottom);
258                remeasure = true;
259            }
260        }
261        mIsResized = resizeText;
262        if (remeasure) {
263            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
264        }
265    }
266
267    private void setPaddingTopAndBottom(int paddingTop, int paddingBottom) {
268        if (isPaddingRelative()) {
269            setPaddingRelative(getPaddingStart(), paddingTop, getPaddingEnd(), paddingBottom);
270        } else {
271            setPadding(getPaddingLeft(), paddingTop, getPaddingRight(), paddingBottom);
272        }
273    }
274}
275