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 
17 package android.support.v7.widget;
18 
19 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20 
21 import android.annotation.TargetApi;
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.content.res.TypedArray;
25 import android.graphics.RectF;
26 import android.os.Build;
27 import android.support.annotation.NonNull;
28 import android.support.annotation.RestrictTo;
29 import android.support.v4.os.BuildCompat;
30 import android.support.v4.widget.TextViewCompat;
31 import android.support.v7.appcompat.R;
32 import android.text.Layout;
33 import android.text.StaticLayout;
34 import android.text.TextDirectionHeuristic;
35 import android.text.TextDirectionHeuristics;
36 import android.text.TextPaint;
37 import android.util.AttributeSet;
38 import android.util.DisplayMetrics;
39 import android.util.TypedValue;
40 import android.widget.TextView;
41 
42 import java.lang.reflect.Method;
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.Collections;
46 import java.util.Hashtable;
47 import 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  */
56 class 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 
AppCompatTextViewAutoSizeHelper(TextView textView)93     AppCompatTextViewAutoSizeHelper(TextView textView) {
94         mTextView = textView;
95         mContext = mTextView.getContext();
96     }
97 
loadFromAttributes(AttributeSet attrs, int defStyleAttr)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)
setAutoSizeTextTypeWithDefaults(@extViewCompat.AutoSizeTextType int autoSizeTextType)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)
setAutoSizeTextTypeUniformWithConfiguration( int autoSizeMinTextSize, int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit)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)
setAutoSizeTextTypeUniformWithPresetSizes(@onNull int[] presetSizes, int unit)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
getAutoSizeTextType()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)
getAutoSizeStepGranularity()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)
getAutoSizeMinTextSize()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)
getAutoSizeMaxTextSize()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)
getAutoSizeTextAvailableSizes()403     int[] getAutoSizeTextAvailableSizes() {
404         return mAutoSizeTextSizesInPx;
405     }
406 
setupAutoSizeUniformPresetSizes(TypedArray textSizes)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 
setupAutoSizeUniformPresetSizesConfiguration()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.
cleanupAutoSizePresetSizes(int[] presetValues)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      */
validateAndSetAutoSizeTextTypeUniformConfiguration( float autoSizeMinTextSizeInPx, float autoSizeMaxTextSizeInPx, float autoSizeStepGranularityInPx)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 
setupAutoSizeText()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)
autoSizeText()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 
clearAutoSizeConfiguration()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)
setTextSizeInternal(int unit, float size)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 
setRawTextSize(float size)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      */
findLargestTextSizeWhichFits(RectF availableSpace)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 
suggestedSizeFitsInSpace(int suggestedSizeInPx, RectF availableSpace)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)
createStaticLayoutForMeasuring(CharSequence text, Layout.Alignment alignment, int availableWidth, int maxLines)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)
createStaticLayoutForMeasuringPre23(CharSequence text, Layout.Alignment alignment, int availableWidth)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 
invokeAndReturnWithDefault(@onNull Object object, @NonNull String methodName, @NonNull T defaultValue)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)
isAutoSizeEnabled()777     boolean isAutoSizeEnabled() {
778         return supportsAutoSizeText()
779                 && mAutoSizeTextType != TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE;
780     }
781 
782     /** @hide */
783     @RestrictTo(LIBRARY_GROUP)
getNeedsAutoSizeText()784     boolean getNeedsAutoSizeText() {
785         return mNeedsAutoSizeText;
786     }
787 
788     /** @hide */
789     @RestrictTo(LIBRARY_GROUP)
setNeedsAutoSizeText(boolean needsAutoSizeText)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      */
supportsAutoSizeText()797     private boolean supportsAutoSizeText() {
798         // Auto-size only supports TextView and all siblings but EditText.
799         return !(mTextView instanceof AppCompatEditText);
800     }
801 }
802