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