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