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