1/*
2 * Copyright (C) 2017 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.support.v7.widget;
18
19import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import android.annotation.TargetApi;
22import android.content.Context;
23import android.content.res.Resources;
24import android.content.res.TypedArray;
25import android.graphics.RectF;
26import android.os.Build;
27import android.support.annotation.NonNull;
28import android.support.annotation.Nullable;
29import android.support.annotation.RestrictTo;
30import android.support.v4.widget.TextViewCompat;
31import android.support.v7.appcompat.R;
32import android.text.Layout;
33import android.text.StaticLayout;
34import android.text.TextDirectionHeuristic;
35import android.text.TextDirectionHeuristics;
36import android.text.TextPaint;
37import android.text.method.TransformationMethod;
38import android.util.AttributeSet;
39import android.util.DisplayMetrics;
40import android.util.Log;
41import android.util.TypedValue;
42import android.widget.TextView;
43
44import java.lang.reflect.Method;
45import java.util.ArrayList;
46import java.util.Arrays;
47import java.util.Collections;
48import java.util.Hashtable;
49import java.util.List;
50
51/**
52 * Utility class which encapsulates the logic for the TextView auto-size text feature added to
53 * the Android Framework in {@link android.os.Build.VERSION_CODES#O}.
54 *
55 * <p>A TextView can be instructed to let the size of the text expand or contract automatically to
56 * fill its layout based on the TextView's characteristics and boundaries.
57 */
58class AppCompatTextViewAutoSizeHelper {
59    private static final String TAG = "ACTVAutoSizeHelper";
60    private static final RectF TEMP_RECTF = new RectF();
61    // Default minimum size for auto-sizing text in scaled pixels.
62    private static final int DEFAULT_AUTO_SIZE_MIN_TEXT_SIZE_IN_SP = 12;
63    // Default maximum size for auto-sizing text in scaled pixels.
64    private static final int DEFAULT_AUTO_SIZE_MAX_TEXT_SIZE_IN_SP = 112;
65    // Default value for the step size in pixels.
66    private static final int DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX = 1;
67    // Cache of TextView methods used via reflection; the key is the method name and the value is
68    // the method itself or null if it can not be found.
69    private static Hashtable<String, Method> sTextViewMethodByNameCache = new Hashtable<>();
70    // Use this to specify that any of the auto-size configuration int values have not been set.
71    static final float UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE = -1f;
72    // Ported from TextView#VERY_WIDE. Represents a maximum width in pixels the TextView takes when
73    // horizontal scrolling is activated.
74    private static final int VERY_WIDE = 1024 * 1024;
75    // Auto-size text type.
76    private int mAutoSizeTextType = TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE;
77    // Specify if auto-size text is needed.
78    private boolean mNeedsAutoSizeText = false;
79    // Step size for auto-sizing in pixels.
80    private float mAutoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
81    // Minimum text size for auto-sizing in pixels.
82    private float mAutoSizeMinTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
83    // Maximum text size for auto-sizing in pixels.
84    private float mAutoSizeMaxTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
85    // Contains a (specified or computed) distinct sorted set of text sizes in pixels to pick from
86    // when auto-sizing text.
87    private int[] mAutoSizeTextSizesInPx = new int[0];
88    // Specifies whether auto-size should use the provided auto size steps set or if it should
89    // build the steps set using mAutoSizeMinTextSizeInPx, mAutoSizeMaxTextSizeInPx and
90    // mAutoSizeStepGranularityInPx.
91    private boolean mHasPresetAutoSizeValues = false;
92    private TextPaint mTempTextPaint;
93
94    private final TextView mTextView;
95    private final Context mContext;
96
97    AppCompatTextViewAutoSizeHelper(TextView textView) {
98        mTextView = textView;
99        mContext = mTextView.getContext();
100    }
101
102    void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
103        float autoSizeMinTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
104        float autoSizeMaxTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
105        float autoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
106
107        TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.AppCompatTextView,
108                defStyleAttr, 0);
109        if (a.hasValue(R.styleable.AppCompatTextView_autoSizeTextType)) {
110            mAutoSizeTextType = a.getInt(R.styleable.AppCompatTextView_autoSizeTextType,
111                    TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE);
112        }
113        if (a.hasValue(R.styleable.AppCompatTextView_autoSizeStepGranularity)) {
114            autoSizeStepGranularityInPx = a.getDimension(
115                    R.styleable.AppCompatTextView_autoSizeStepGranularity,
116                    UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE);
117        }
118        if (a.hasValue(R.styleable.AppCompatTextView_autoSizeMinTextSize)) {
119            autoSizeMinTextSizeInPx = a.getDimension(
120                    R.styleable.AppCompatTextView_autoSizeMinTextSize,
121                    UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE);
122        }
123        if (a.hasValue(R.styleable.AppCompatTextView_autoSizeMaxTextSize)) {
124            autoSizeMaxTextSizeInPx = a.getDimension(
125                    R.styleable.AppCompatTextView_autoSizeMaxTextSize,
126                    UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE);
127        }
128        if (a.hasValue(R.styleable.AppCompatTextView_autoSizePresetSizes)) {
129            final int autoSizeStepSizeArrayResId = a.getResourceId(
130                    R.styleable.AppCompatTextView_autoSizePresetSizes, 0);
131            if (autoSizeStepSizeArrayResId > 0) {
132                final TypedArray autoSizePreDefTextSizes = a.getResources()
133                        .obtainTypedArray(autoSizeStepSizeArrayResId);
134                setupAutoSizeUniformPresetSizes(autoSizePreDefTextSizes);
135                autoSizePreDefTextSizes.recycle();
136            }
137        }
138        a.recycle();
139
140        if (supportsAutoSizeText()) {
141            if (mAutoSizeTextType == TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM) {
142                // If uniform auto-size has been specified but preset values have not been set then
143                // replace the auto-size configuration values that have not been specified with the
144                // defaults.
145                if (!mHasPresetAutoSizeValues) {
146                    final DisplayMetrics displayMetrics =
147                            mContext.getResources().getDisplayMetrics();
148
149                    if (autoSizeMinTextSizeInPx == UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {
150                        autoSizeMinTextSizeInPx = TypedValue.applyDimension(
151                                TypedValue.COMPLEX_UNIT_SP,
152                                DEFAULT_AUTO_SIZE_MIN_TEXT_SIZE_IN_SP,
153                                displayMetrics);
154                    }
155
156                    if (autoSizeMaxTextSizeInPx == UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {
157                        autoSizeMaxTextSizeInPx = TypedValue.applyDimension(
158                                TypedValue.COMPLEX_UNIT_SP,
159                                DEFAULT_AUTO_SIZE_MAX_TEXT_SIZE_IN_SP,
160                                displayMetrics);
161                    }
162
163                    if (autoSizeStepGranularityInPx
164                            == UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {
165                        autoSizeStepGranularityInPx = DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX;
166                    }
167
168                    validateAndSetAutoSizeTextTypeUniformConfiguration(autoSizeMinTextSizeInPx,
169                            autoSizeMaxTextSizeInPx,
170                            autoSizeStepGranularityInPx);
171                }
172
173                setupAutoSizeText();
174            }
175        } else {
176            mAutoSizeTextType = TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE;
177        }
178    }
179
180    /**
181     * Specify whether this widget should automatically scale the text to try to perfectly fit
182     * within the layout bounds by using the default auto-size configuration.
183     *
184     * @param autoSizeTextType the type of auto-size. Must be one of
185     *        {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_NONE} or
186     *        {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}
187     *
188     * @attr ref R.styleable#AppCompatTextView_autoSizeTextType
189     *
190     * @see #getAutoSizeTextType()
191     *
192     * @hide
193     */
194    @RestrictTo(LIBRARY_GROUP)
195    void setAutoSizeTextTypeWithDefaults(@TextViewCompat.AutoSizeTextType int autoSizeTextType) {
196        if (supportsAutoSizeText()) {
197            switch (autoSizeTextType) {
198                case TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE:
199                    clearAutoSizeConfiguration();
200                    break;
201                case TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM:
202                    final DisplayMetrics displayMetrics =
203                            mContext.getResources().getDisplayMetrics();
204                    final float autoSizeMinTextSizeInPx = TypedValue.applyDimension(
205                            TypedValue.COMPLEX_UNIT_SP,
206                            DEFAULT_AUTO_SIZE_MIN_TEXT_SIZE_IN_SP,
207                            displayMetrics);
208                    final float autoSizeMaxTextSizeInPx = TypedValue.applyDimension(
209                            TypedValue.COMPLEX_UNIT_SP,
210                            DEFAULT_AUTO_SIZE_MAX_TEXT_SIZE_IN_SP,
211                            displayMetrics);
212
213                    validateAndSetAutoSizeTextTypeUniformConfiguration(
214                            autoSizeMinTextSizeInPx,
215                            autoSizeMaxTextSizeInPx,
216                            DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX);
217                    if (setupAutoSizeText()) {
218                        autoSizeText();
219                    }
220                    break;
221                default:
222                    throw new IllegalArgumentException(
223                            "Unknown auto-size text type: " + autoSizeTextType);
224            }
225        }
226    }
227
228    /**
229     * Specify whether this widget should automatically scale the text to try to perfectly fit
230     * within the layout bounds. If all the configuration params are valid the type of auto-size is
231     * set to {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}.
232     *
233     * @param autoSizeMinTextSize the minimum text size available for auto-size
234     * @param autoSizeMaxTextSize the maximum text size available for auto-size
235     * @param autoSizeStepGranularity the auto-size step granularity. It is used in conjunction with
236     *                                the minimum and maximum text size in order to build the set of
237     *                                text sizes the system uses to choose from when auto-sizing
238     * @param unit the desired dimension unit for all sizes above. See {@link TypedValue} for the
239     *             possible dimension units
240     *
241     * @throws IllegalArgumentException if any of the configuration params are invalid.
242     *
243     * @attr ref R.styleable#AppCompatTextView_autoSizeTextType
244     * @attr ref R.styleable#AppCompatTextView_autoSizeMinTextSize
245     * @attr ref R.styleable#AppCompatTextView_autoSizeMaxTextSize
246     * @attr ref R.styleable#AppCompatTextView_autoSizeStepGranularity
247     *
248     * @see #setAutoSizeTextTypeWithDefaults(int)
249     * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
250     * @see #getAutoSizeMinTextSize()
251     * @see #getAutoSizeMaxTextSize()
252     * @see #getAutoSizeStepGranularity()
253     * @see #getAutoSizeTextAvailableSizes()
254     *
255     * @hide
256     */
257    @RestrictTo(LIBRARY_GROUP)
258    void setAutoSizeTextTypeUniformWithConfiguration(
259            int autoSizeMinTextSize,
260            int autoSizeMaxTextSize,
261            int autoSizeStepGranularity,
262            int unit) throws IllegalArgumentException {
263        if (supportsAutoSizeText()) {
264            final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
265            final float autoSizeMinTextSizeInPx = TypedValue.applyDimension(
266                    unit, autoSizeMinTextSize, displayMetrics);
267            final float autoSizeMaxTextSizeInPx = TypedValue.applyDimension(
268                    unit, autoSizeMaxTextSize, displayMetrics);
269            final float autoSizeStepGranularityInPx = TypedValue.applyDimension(
270                    unit, autoSizeStepGranularity, displayMetrics);
271
272            validateAndSetAutoSizeTextTypeUniformConfiguration(autoSizeMinTextSizeInPx,
273                    autoSizeMaxTextSizeInPx,
274                    autoSizeStepGranularityInPx);
275            if (setupAutoSizeText()) {
276                autoSizeText();
277            }
278        }
279    }
280
281    /**
282     * Specify whether this widget should automatically scale the text to try to perfectly fit
283     * within the layout bounds. If at least one value from the <code>presetSizes</code> is valid
284     * then the type of auto-size is set to {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}.
285     *
286     * @param presetSizes an {@code int} array of sizes in pixels
287     * @param unit the desired dimension unit for the preset sizes above. See {@link TypedValue} for
288     *             the possible dimension units
289     *
290     * @throws IllegalArgumentException if all of the <code>presetSizes</code> are invalid.
291     *_
292     * @attr ref R.styleable#AppCompatTextView_autoSizeTextType
293     * @attr ref R.styleable#AppCompatTextView_autoSizePresetSizes
294     *
295     * @see #setAutoSizeTextTypeWithDefaults(int)
296     * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
297     * @see #getAutoSizeMinTextSize()
298     * @see #getAutoSizeMaxTextSize()
299     * @see #getAutoSizeTextAvailableSizes()
300     *
301     * @hide
302     */
303    @RestrictTo(LIBRARY_GROUP)
304    void setAutoSizeTextTypeUniformWithPresetSizes(@NonNull int[] presetSizes, int unit)
305            throws IllegalArgumentException {
306        if (supportsAutoSizeText()) {
307            final int presetSizesLength = presetSizes.length;
308            if (presetSizesLength > 0) {
309                int[] presetSizesInPx = new int[presetSizesLength];
310
311                if (unit == TypedValue.COMPLEX_UNIT_PX) {
312                    presetSizesInPx = Arrays.copyOf(presetSizes, presetSizesLength);
313                } else {
314                    final DisplayMetrics displayMetrics =
315                            mContext.getResources().getDisplayMetrics();
316                    // Convert all to sizes to pixels.
317                    for (int i = 0; i < presetSizesLength; i++) {
318                        presetSizesInPx[i] = Math.round(TypedValue.applyDimension(unit,
319                                presetSizes[i], displayMetrics));
320                    }
321                }
322
323                mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(presetSizesInPx);
324                if (!setupAutoSizeUniformPresetSizesConfiguration()) {
325                    throw new IllegalArgumentException("None of the preset sizes is valid: "
326                            + Arrays.toString(presetSizes));
327                }
328            } else {
329                mHasPresetAutoSizeValues = false;
330            }
331
332            if (setupAutoSizeText()) {
333                autoSizeText();
334            }
335        }
336    }
337
338    /**
339     * Returns the type of auto-size set for this widget.
340     *
341     * @return an {@code int} corresponding to one of the auto-size types:
342     *         {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_NONE} or
343     *         {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}
344     *
345     * @attr ref R.styleable#AppCompatTextView_autoSizeTextType
346     *
347     * @see #setAutoSizeTextTypeWithDefaults(int)
348     * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
349     * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
350     *
351     * @hide
352     */
353    @RestrictTo(LIBRARY_GROUP)
354    @TextViewCompat.AutoSizeTextType
355    int getAutoSizeTextType() {
356        return mAutoSizeTextType;
357    }
358
359    /**
360     * @return the current auto-size step granularity in pixels.
361     *
362     * @attr ref R.styleable#AppCompatTextView_autoSizeStepGranularity
363     *
364     * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
365     *
366     * @hide
367     */
368    @RestrictTo(LIBRARY_GROUP)
369    int getAutoSizeStepGranularity() {
370        return Math.round(mAutoSizeStepGranularityInPx);
371    }
372
373    /**
374     * @return the current auto-size minimum text size in pixels (the default is 12sp). Note that
375     *         if auto-size has not been configured this function returns {@code -1}.
376     *
377     * @attr ref R.styleable#AppCompatTextView_autoSizeMinTextSize
378     *
379     * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
380     * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
381     *
382     * @hide
383     */
384    @RestrictTo(LIBRARY_GROUP)
385    int getAutoSizeMinTextSize() {
386        return Math.round(mAutoSizeMinTextSizeInPx);
387    }
388
389    /**
390     * @return the current auto-size maximum text size in pixels (the default is 112sp). Note that
391     *         if auto-size has not been configured this function returns {@code -1}.
392     *
393     * @attr ref R.styleable#AppCompatTextView_autoSizeMaxTextSize
394     *
395     * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
396     * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
397     *
398     * @hide
399     */
400    @RestrictTo(LIBRARY_GROUP)
401    int getAutoSizeMaxTextSize() {
402        return Math.round(mAutoSizeMaxTextSizeInPx);
403    }
404
405    /**
406     * @return the current auto-size {@code int} sizes array (in pixels).
407     *
408     * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
409     * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
410     *
411     * @hide
412     */
413    @RestrictTo(LIBRARY_GROUP)
414    int[] getAutoSizeTextAvailableSizes() {
415        return mAutoSizeTextSizesInPx;
416    }
417
418    private void setupAutoSizeUniformPresetSizes(TypedArray textSizes) {
419        final int textSizesLength = textSizes.length();
420        final int[] parsedSizes = new int[textSizesLength];
421
422        if (textSizesLength > 0) {
423            for (int i = 0; i < textSizesLength; i++) {
424                parsedSizes[i] = textSizes.getDimensionPixelSize(i, -1);
425            }
426            mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(parsedSizes);
427            setupAutoSizeUniformPresetSizesConfiguration();
428        }
429    }
430
431    private boolean setupAutoSizeUniformPresetSizesConfiguration() {
432        final int sizesLength = mAutoSizeTextSizesInPx.length;
433        mHasPresetAutoSizeValues = sizesLength > 0;
434        if (mHasPresetAutoSizeValues) {
435            mAutoSizeTextType = TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM;
436            mAutoSizeMinTextSizeInPx = mAutoSizeTextSizesInPx[0];
437            mAutoSizeMaxTextSizeInPx = mAutoSizeTextSizesInPx[sizesLength - 1];
438            mAutoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
439        }
440        return mHasPresetAutoSizeValues;
441    }
442
443    // Returns distinct sorted positive values.
444    private int[] cleanupAutoSizePresetSizes(int[] presetValues) {
445        final int presetValuesLength = presetValues.length;
446        if (presetValuesLength == 0) {
447            return presetValues;
448        }
449        Arrays.sort(presetValues);
450
451        final List<Integer> uniqueValidSizes = new ArrayList<>();
452        for (int i = 0; i < presetValuesLength; i++) {
453            final int currentPresetValue = presetValues[i];
454
455            if (currentPresetValue > 0
456                    && Collections.binarySearch(uniqueValidSizes, currentPresetValue) < 0) {
457                uniqueValidSizes.add(currentPresetValue);
458            }
459        }
460
461        if (presetValuesLength == uniqueValidSizes.size()) {
462            return presetValues;
463        } else {
464            final int uniqueValidSizesLength = uniqueValidSizes.size();
465            final int[] cleanedUpSizes = new int[uniqueValidSizesLength];
466            for (int i = 0; i < uniqueValidSizesLength; i++) {
467                cleanedUpSizes[i] = uniqueValidSizes.get(i);
468            }
469            return cleanedUpSizes;
470        }
471    }
472
473    /**
474     * If all params are valid then save the auto-size configuration.
475     *
476     * @throws IllegalArgumentException if any of the params are invalid
477     */
478    private void validateAndSetAutoSizeTextTypeUniformConfiguration(
479            float autoSizeMinTextSizeInPx,
480            float autoSizeMaxTextSizeInPx,
481            float autoSizeStepGranularityInPx) throws IllegalArgumentException {
482        // First validate.
483        if (autoSizeMinTextSizeInPx <= 0) {
484            throw new IllegalArgumentException("Minimum auto-size text size ("
485                    + autoSizeMinTextSizeInPx  + "px) is less or equal to (0px)");
486        }
487
488        if (autoSizeMaxTextSizeInPx <= autoSizeMinTextSizeInPx) {
489            throw new IllegalArgumentException("Maximum auto-size text size ("
490                    + autoSizeMaxTextSizeInPx + "px) is less or equal to minimum auto-size "
491                    + "text size (" + autoSizeMinTextSizeInPx + "px)");
492        }
493
494        if (autoSizeStepGranularityInPx <= 0) {
495            throw new IllegalArgumentException("The auto-size step granularity ("
496                    + autoSizeStepGranularityInPx + "px) is less or equal to (0px)");
497        }
498
499        // All good, persist the configuration.
500        mAutoSizeTextType = TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM;
501        mAutoSizeMinTextSizeInPx = autoSizeMinTextSizeInPx;
502        mAutoSizeMaxTextSizeInPx = autoSizeMaxTextSizeInPx;
503        mAutoSizeStepGranularityInPx = autoSizeStepGranularityInPx;
504        mHasPresetAutoSizeValues = false;
505    }
506
507    private boolean setupAutoSizeText() {
508        if (supportsAutoSizeText()
509                && mAutoSizeTextType == TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM) {
510            // Calculate the sizes set based on minimum size, maximum size and step size if we do
511            // not have a predefined set of sizes or if the current sizes array is empty.
512            if (!mHasPresetAutoSizeValues || mAutoSizeTextSizesInPx.length == 0) {
513                // Calculate sizes to choose from based on the current auto-size configuration.
514                int autoSizeValuesLength = 1;
515                float currentSize = Math.round(mAutoSizeMinTextSizeInPx);
516                while (Math.round(currentSize + mAutoSizeStepGranularityInPx)
517                        <= Math.round(mAutoSizeMaxTextSizeInPx)) {
518                    autoSizeValuesLength++;
519                    currentSize += mAutoSizeStepGranularityInPx;
520                }
521                int[] autoSizeTextSizesInPx = new int[autoSizeValuesLength];
522                float sizeToAdd = mAutoSizeMinTextSizeInPx;
523                for (int i = 0; i < autoSizeValuesLength; i++) {
524                    autoSizeTextSizesInPx[i] = Math.round(sizeToAdd);
525                    sizeToAdd += mAutoSizeStepGranularityInPx;
526                }
527                mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(autoSizeTextSizesInPx);
528            }
529
530            mNeedsAutoSizeText = true;
531        } else {
532            mNeedsAutoSizeText = false;
533        }
534
535        return mNeedsAutoSizeText;
536    }
537
538    /**
539     * Automatically computes and sets the text size.
540     *
541     * @hide
542     */
543    @RestrictTo(LIBRARY_GROUP)
544    void autoSizeText() {
545        if (!isAutoSizeEnabled()) {
546            return;
547        }
548
549        if (mNeedsAutoSizeText) {
550            if (mTextView.getMeasuredHeight() <= 0 || mTextView.getMeasuredWidth() <= 0) {
551                return;
552            }
553
554            final boolean horizontallyScrolling = invokeAndReturnWithDefault(
555                    mTextView, "getHorizontallyScrolling", false);
556            final int availableWidth = horizontallyScrolling
557                    ? VERY_WIDE
558                    : mTextView.getMeasuredWidth() - mTextView.getTotalPaddingLeft()
559                            - mTextView.getTotalPaddingRight();
560            final int availableHeight = mTextView.getHeight() - mTextView.getCompoundPaddingBottom()
561                    - mTextView.getCompoundPaddingTop();
562
563            if (availableWidth <= 0 || availableHeight <= 0) {
564                return;
565            }
566
567            synchronized (TEMP_RECTF) {
568                TEMP_RECTF.setEmpty();
569                TEMP_RECTF.right = availableWidth;
570                TEMP_RECTF.bottom = availableHeight;
571                final float optimalTextSize = findLargestTextSizeWhichFits(TEMP_RECTF);
572                if (optimalTextSize != mTextView.getTextSize()) {
573                    setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, optimalTextSize);
574                }
575            }
576        }
577        // Always try to auto-size if enabled. Functions that do not want to trigger auto-sizing
578        // after the next layout pass should set this to false.
579        mNeedsAutoSizeText = true;
580    }
581
582    private void clearAutoSizeConfiguration() {
583        mAutoSizeTextType = TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE;
584        mAutoSizeMinTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
585        mAutoSizeMaxTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
586        mAutoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
587        mAutoSizeTextSizesInPx = new int[0];
588        mNeedsAutoSizeText = false;
589    }
590
591    /** @hide */
592    @RestrictTo(LIBRARY_GROUP)
593    void setTextSizeInternal(int unit, float size) {
594        Resources res = mContext == null
595                ? Resources.getSystem()
596                : mContext.getResources();
597
598        setRawTextSize(TypedValue.applyDimension(unit, size, res.getDisplayMetrics()));
599    }
600
601    private void setRawTextSize(float size) {
602        if (size != mTextView.getPaint().getTextSize()) {
603            mTextView.getPaint().setTextSize(size);
604
605            boolean isInLayout = false;
606            if (Build.VERSION.SDK_INT >= 18) {
607                isInLayout = mTextView.isInLayout();
608            }
609
610            if (mTextView.getLayout() != null) {
611                // Do not auto-size right after setting the text size.
612                mNeedsAutoSizeText = false;
613
614                final String methodName = "nullLayouts";
615                try {
616                    Method method = getTextViewMethod(methodName);
617                    if (method != null) {
618                        method.invoke(mTextView);
619                    }
620                } catch (Exception ex) {
621                    Log.w(TAG, "Failed to invoke TextView#" + methodName + "() method", ex);
622                }
623
624                if (!isInLayout) {
625                    mTextView.requestLayout();
626                } else {
627                    mTextView.forceLayout();
628                }
629
630                mTextView.invalidate();
631            }
632        }
633    }
634
635    /**
636     * Performs a binary search to find the largest text size that will still fit within the size
637     * available to this view.
638     */
639    private int findLargestTextSizeWhichFits(RectF availableSpace) {
640        final int sizesCount = mAutoSizeTextSizesInPx.length;
641        if (sizesCount == 0) {
642            throw new IllegalStateException("No available text sizes to choose from.");
643        }
644
645        int bestSizeIndex = 0;
646        int lowIndex = bestSizeIndex + 1;
647        int highIndex = sizesCount - 1;
648        int sizeToTryIndex;
649        while (lowIndex <= highIndex) {
650            sizeToTryIndex = (lowIndex + highIndex) / 2;
651            if (suggestedSizeFitsInSpace(mAutoSizeTextSizesInPx[sizeToTryIndex], availableSpace)) {
652                bestSizeIndex = lowIndex;
653                lowIndex = sizeToTryIndex + 1;
654            } else {
655                highIndex = sizeToTryIndex - 1;
656                bestSizeIndex = highIndex;
657            }
658        }
659
660        return mAutoSizeTextSizesInPx[bestSizeIndex];
661    }
662
663    private boolean suggestedSizeFitsInSpace(int suggestedSizeInPx, RectF availableSpace) {
664        CharSequence text = mTextView.getText();
665        TransformationMethod transformationMethod = mTextView.getTransformationMethod();
666        if (transformationMethod != null) {
667            CharSequence transformedText = transformationMethod.getTransformation(text, mTextView);
668            if (transformedText != null) {
669                text = transformedText;
670            }
671        }
672
673        final int maxLines = Build.VERSION.SDK_INT >= 16 ? mTextView.getMaxLines() : -1;
674        if (mTempTextPaint == null) {
675            mTempTextPaint = new TextPaint();
676        } else {
677            mTempTextPaint.reset();
678        }
679        mTempTextPaint.set(mTextView.getPaint());
680        mTempTextPaint.setTextSize(suggestedSizeInPx);
681
682        // Needs reflection call due to being private.
683        Layout.Alignment alignment = invokeAndReturnWithDefault(
684                mTextView, "getLayoutAlignment", Layout.Alignment.ALIGN_NORMAL);
685        final StaticLayout layout = Build.VERSION.SDK_INT >= 23
686                ? createStaticLayoutForMeasuring(
687                        text, alignment, Math.round(availableSpace.right), maxLines)
688                : createStaticLayoutForMeasuringPre23(
689                        text, alignment, Math.round(availableSpace.right));
690        // Lines overflow.
691        if (maxLines != -1 && (layout.getLineCount() > maxLines
692                || (layout.getLineEnd(layout.getLineCount() - 1)) != text.length())) {
693            return false;
694        }
695
696        // Height overflow.
697        if (layout.getHeight() > availableSpace.bottom) {
698            return false;
699        }
700
701        return true;
702    }
703
704    @TargetApi(23)
705    private StaticLayout createStaticLayoutForMeasuring(CharSequence text,
706            Layout.Alignment alignment, int availableWidth, int maxLines) {
707        // Can use the StaticLayout.Builder (along with TextView params added in or after
708        // API 23) to construct the layout.
709        final TextDirectionHeuristic textDirectionHeuristic = invokeAndReturnWithDefault(
710                mTextView, "getTextDirectionHeuristic",
711                TextDirectionHeuristics.FIRSTSTRONG_LTR);
712
713        final StaticLayout.Builder layoutBuilder = StaticLayout.Builder.obtain(
714                text, 0, text.length(),  mTempTextPaint, availableWidth);
715
716        return layoutBuilder.setAlignment(alignment)
717                .setLineSpacing(
718                        mTextView.getLineSpacingExtra(),
719                        mTextView.getLineSpacingMultiplier())
720                .setIncludePad(mTextView.getIncludeFontPadding())
721                .setBreakStrategy(mTextView.getBreakStrategy())
722                .setHyphenationFrequency(mTextView.getHyphenationFrequency())
723                .setMaxLines(maxLines == -1 ? Integer.MAX_VALUE : maxLines)
724                .setTextDirection(textDirectionHeuristic)
725                .build();
726    }
727
728    @TargetApi(14)
729    private StaticLayout createStaticLayoutForMeasuringPre23(CharSequence text,
730            Layout.Alignment alignment, int availableWidth) {
731        // Setup defaults.
732        float lineSpacingMultiplier = 1.0f;
733        float lineSpacingAdd = 0.0f;
734        boolean includePad = true;
735
736        if (Build.VERSION.SDK_INT >= 16) {
737            // Call public methods.
738            lineSpacingMultiplier = mTextView.getLineSpacingMultiplier();
739            lineSpacingAdd = mTextView.getLineSpacingExtra();
740            includePad = mTextView.getIncludeFontPadding();
741        } else {
742            // Call private methods and make sure to provide fallback defaults in case something
743            // goes wrong. The default values have been inlined with the StaticLayout defaults.
744            lineSpacingMultiplier = invokeAndReturnWithDefault(mTextView,
745                    "getLineSpacingMultiplier", lineSpacingMultiplier);
746            lineSpacingAdd = invokeAndReturnWithDefault(mTextView,
747                    "getLineSpacingExtra", lineSpacingAdd);
748            includePad = invokeAndReturnWithDefault(mTextView,
749                    "getIncludeFontPadding", includePad);
750        }
751
752        // The layout could not be constructed using the builder so fall back to the
753        // most broad constructor.
754        return new StaticLayout(text, mTempTextPaint, availableWidth,
755                alignment,
756                lineSpacingMultiplier,
757                lineSpacingAdd,
758                includePad);
759    }
760
761    private <T> T invokeAndReturnWithDefault(@NonNull Object object,
762            @NonNull final String methodName, @NonNull final T defaultValue) {
763        T result = null;
764        boolean exceptionThrown = false;
765
766        try {
767            // Cache lookup.
768            Method method = getTextViewMethod(methodName);
769            result = (T) method.invoke(object);
770        } catch (Exception ex) {
771            exceptionThrown = true;
772            Log.w(TAG, "Failed to invoke TextView#" + methodName + "() method", ex);
773        } finally {
774            if (result == null && exceptionThrown) {
775                result = defaultValue;
776            }
777        }
778
779        return result;
780    }
781
782    @Nullable
783    private Method getTextViewMethod(@NonNull final String methodName) {
784        try {
785            Method method = sTextViewMethodByNameCache.get(methodName);
786            if (method == null) {
787                method = TextView.class.getDeclaredMethod(methodName);
788                if (method != null) {
789                    method.setAccessible(true);
790                    // Cache update.
791                    sTextViewMethodByNameCache.put(methodName, method);
792                }
793            }
794
795            return method;
796        } catch (Exception ex) {
797            Log.w(TAG, "Failed to retrieve TextView#" + methodName + "() method", ex);
798            return null;
799        }
800    }
801
802    /**
803     * @return {@code true} if this widget supports auto-sizing text and has been configured to
804     * auto-size.
805     *
806     * @hide
807     */
808    @RestrictTo(LIBRARY_GROUP)
809    boolean isAutoSizeEnabled() {
810        return supportsAutoSizeText()
811                && mAutoSizeTextType != TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE;
812    }
813
814    /**
815     * @return {@code true} if this TextView supports auto-sizing text to fit within its container.
816     */
817    private boolean supportsAutoSizeText() {
818        // Auto-size only supports TextView and all siblings but EditText.
819        return !(mTextView instanceof AppCompatEditText);
820    }
821}
822