Home | History | Annotate | Download | only in widget
      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 
     99     AppCompatTextViewAutoSizeHelper(TextView textView) {
    100         mTextView = textView;
    101         mContext = mTextView.getContext();
    102     }
    103 
    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)
    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)
    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)
    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
    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)
    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)
    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)
    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)
    416     int[] getAutoSizeTextAvailableSizes() {
    417         return mAutoSizeTextSizesInPx;
    418     }
    419 
    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 
    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.
    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      */
    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 
    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)
    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 
    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)
    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 
    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      */
    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 
    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)
    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 
    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 
    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
    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)
    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      */
    818     private boolean supportsAutoSizeText() {
    819         // Auto-size only supports TextView and all siblings but EditText.
    820         return !(mTextView instanceof AppCompatEditText);
    821     }
    822 }
    823