ResizingTextView.java revision afb8ae27a2d6f7d7088a39b9eb4a43369df91270
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 * @hide
29 */
30class ResizingTextView extends TextView {
31
32    /**
33     * Trigger text resize when text flows into the last line of a multi-line text view.
34     */
35    public static final int TRIGGER_MAX_LINES = 0x01;
36
37    private int mTriggerConditions; // Union of trigger conditions
38    private int mResizedTextSize;
39    private boolean mMaintainLineSpacing;
40    private int mResizedPaddingAdjustmentTop;
41    private int mResizedPaddingAdjustmentBottom;
42
43    private boolean mIsResized = false;
44    // Remember default properties in case we need to restore them
45    private boolean mDefaultsInitialized = false;
46    private int mDefaultTextSize;
47    private float mDefaultLineSpacingExtra;
48    private int mDefaultPaddingTop;
49    private int mDefaultPaddingBottom;
50
51    public ResizingTextView(Context ctx, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
52        super(ctx, attrs, defStyleAttr);
53        TypedArray a = ctx.obtainStyledAttributes(attrs, R.styleable.lbResizingTextView,
54                defStyleAttr, defStyleRes);
55
56        try {
57            mTriggerConditions = a.getInt(
58                    R.styleable.lbResizingTextView_resizeTrigger, TRIGGER_MAX_LINES);
59            mResizedTextSize = a.getDimensionPixelSize(
60                    R.styleable.lbResizingTextView_resizedTextSize, -1);
61            mMaintainLineSpacing = a.getBoolean(
62                    R.styleable.lbResizingTextView_maintainLineSpacing, true);
63            mResizedPaddingAdjustmentTop = a.getDimensionPixelOffset(
64                    R.styleable.lbResizingTextView_resizedPaddingAdjustmentTop, 0);
65            mResizedPaddingAdjustmentBottom = a.getDimensionPixelOffset(
66                    R.styleable.lbResizingTextView_resizedPaddingAdjustmentBottom, 0);
67        } finally {
68            a.recycle();
69        }
70    }
71
72    public ResizingTextView(Context ctx, AttributeSet attrs, int defStyleAttr) {
73        this(ctx, attrs, defStyleAttr, 0);
74    }
75
76    public ResizingTextView(Context ctx, AttributeSet attrs) {
77        // TODO We should define our own style that inherits from TextViewStyle, to set defaults
78        // for new styleables,  We then pass the appropriate R.attr up the constructor chain here.
79        this(ctx, attrs, android.R.attr.textViewStyle);
80    }
81
82    public ResizingTextView(Context ctx) {
83        this(ctx, null);
84    }
85
86    /**
87     * @return the trigger conditions used to determine whether resize occurs
88     */
89    public int getTriggerConditions() {
90        return mTriggerConditions;
91    }
92
93    /**
94     * Set the trigger conditions used to determine whether resize occurs. Pass
95     * a union of trigger condition constants, such as {@link ResizingTextView#TRIGGER_MAX_LINES}.
96     *
97     * @param conditions A union of trigger condition constants
98     */
99    public void setTriggerConditions(int conditions) {
100        if (mTriggerConditions != conditions) {
101            mTriggerConditions = conditions;
102            // Always request a layout when trigger conditions change
103            requestLayout();
104        }
105    }
106
107    /**
108     * @return the resized text size
109     */
110    public int getResizedTextSize() {
111        return mResizedTextSize;
112    }
113
114    /**
115     * Set the text size for resized text.
116     *
117     * @param size The text size for resized text
118     */
119    public void setResizedTextSize(int size) {
120        if (mResizedTextSize != size) {
121            mResizedTextSize = size;
122            resizeParamsChanged();
123        }
124    }
125
126    /**
127     * @return whether or not to maintain line spacing when resizing text.
128     * The default is true.
129     */
130    public boolean getMaintainLineSpacing() {
131        return mMaintainLineSpacing;
132    }
133
134    /**
135     * Set whether or not to maintain line spacing when resizing text.
136     * The default is true.
137     *
138     * @param maintain Whether or not to maintain line spacing
139     */
140    public void setMaintainLineSpacing(boolean maintain) {
141        if (mMaintainLineSpacing != maintain) {
142            mMaintainLineSpacing = maintain;
143            resizeParamsChanged();
144        }
145    }
146
147    /**
148     * @return desired adjustment to top padding for resized text
149     */
150    public int getResizedPaddingAdjustmentTop() {
151        return mResizedPaddingAdjustmentTop;
152    }
153
154    /**
155     * Set the desired adjustment to top padding for resized text.
156     *
157     * @param adjustment The adjustment to top padding, in pixels
158     */
159    public void setResizedPaddingAdjustmentTop(int adjustment) {
160        if (mResizedPaddingAdjustmentTop != adjustment) {
161            mResizedPaddingAdjustmentTop = adjustment;
162            resizeParamsChanged();
163        }
164    }
165
166    /**
167     * @return desired adjustment to bottom padding for resized text
168     */
169    public int getResizedPaddingAdjustmentBottom() {
170        return mResizedPaddingAdjustmentBottom;
171    }
172
173    /**
174     * Set the desired adjustment to bottom padding for resized text.
175     *
176     * @param adjustment The adjustment to bottom padding, in pixels
177     */
178    public void setResizedPaddingAdjustmentBottom(int adjustment) {
179        if (mResizedPaddingAdjustmentBottom != adjustment) {
180            mResizedPaddingAdjustmentBottom = adjustment;
181            resizeParamsChanged();
182        }
183    }
184
185    private void resizeParamsChanged() {
186        // If we're not resized, then changing resize parameters doesn't
187        // affect layout, so don't bother requesting.
188        if (mIsResized) {
189            requestLayout();
190        }
191    }
192
193    @Override
194    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
195        if (!mDefaultsInitialized) {
196            mDefaultTextSize = (int) getTextSize();
197            mDefaultLineSpacingExtra = getLineSpacingExtra();
198            mDefaultPaddingTop = getPaddingTop();
199            mDefaultPaddingBottom = getPaddingBottom();
200            mDefaultsInitialized = true;
201        }
202
203        // Always try first to measure with defaults. Otherwise, we may think we can get away
204        // with larger text sizes later when we actually can't.
205        setTextSize(TypedValue.COMPLEX_UNIT_PX, mDefaultTextSize);
206        setLineSpacing(mDefaultLineSpacingExtra, getLineSpacingMultiplier());
207        setPaddingTopAndBottom(mDefaultPaddingTop, mDefaultPaddingBottom);
208
209        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
210
211        boolean resizeText = false;
212
213        final Layout layout = getLayout();
214        if (layout != null) {
215            if ((mTriggerConditions & TRIGGER_MAX_LINES) > 0) {
216                final int lineCount = layout.getLineCount();
217                final int maxLines = getMaxLines();
218                if (maxLines > 1) {
219                    resizeText = lineCount == maxLines;
220                }
221            }
222        }
223
224        final int currentSizePx = (int) getTextSize();
225        boolean remeasure = false;
226        if (resizeText) {
227            if (mResizedTextSize != -1 && currentSizePx != mResizedTextSize) {
228                setTextSize(TypedValue.COMPLEX_UNIT_PX, mResizedTextSize);
229                remeasure = true;
230            }
231            // Check for other desired adjustments in addition to the text size
232            final float targetLineSpacingExtra = mDefaultLineSpacingExtra +
233                    mDefaultTextSize - mResizedTextSize;
234            if (mMaintainLineSpacing && getLineSpacingExtra() != targetLineSpacingExtra) {
235                setLineSpacing(targetLineSpacingExtra, getLineSpacingMultiplier());
236                remeasure = true;
237            }
238            final int paddingTop = mDefaultPaddingTop + mResizedPaddingAdjustmentTop;
239            final int paddingBottom = mDefaultPaddingBottom + mResizedPaddingAdjustmentBottom;
240            if (getPaddingTop() != paddingTop || getPaddingBottom() != paddingBottom) {
241                setPaddingTopAndBottom(paddingTop, paddingBottom);
242                remeasure = true;
243            }
244        } else {
245            // Use default size, line spacing, and padding
246            if (mResizedTextSize != -1 && currentSizePx != mDefaultTextSize) {
247                setTextSize(TypedValue.COMPLEX_UNIT_PX, mDefaultTextSize);
248                remeasure = true;
249            }
250            if (mMaintainLineSpacing && getLineSpacingExtra() != mDefaultLineSpacingExtra) {
251                setLineSpacing(mDefaultLineSpacingExtra, getLineSpacingMultiplier());
252                remeasure = true;
253            }
254            if (getPaddingTop() != mDefaultPaddingTop ||
255                    getPaddingBottom() != mDefaultPaddingBottom) {
256                setPaddingTopAndBottom(mDefaultPaddingTop, mDefaultPaddingBottom);
257                remeasure = true;
258            }
259        }
260        mIsResized = resizeText;
261        if (remeasure) {
262            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
263        }
264    }
265
266    private void setPaddingTopAndBottom(int paddingTop, int paddingBottom) {
267        if (isPaddingRelative()) {
268            setPaddingRelative(getPaddingStart(), paddingTop, getPaddingEnd(), paddingBottom);
269        } else {
270            setPadding(getPaddingLeft(), paddingTop, getPaddingRight(), paddingBottom);
271        }
272    }
273}
274