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