1 /* 2 * Copyright (C) 2016 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 com.android.incallui.autoresizetext; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.graphics.RectF; 22 import android.os.Build.VERSION; 23 import android.os.Build.VERSION_CODES; 24 import android.text.Layout.Alignment; 25 import android.text.StaticLayout; 26 import android.text.TextPaint; 27 import android.util.AttributeSet; 28 import android.util.DisplayMetrics; 29 import android.util.SparseIntArray; 30 import android.util.TypedValue; 31 import android.widget.TextView; 32 import javax.annotation.Nullable; 33 34 /** 35 * A TextView that automatically scales its text to completely fill its allotted width. 36 * 37 * <p>Note: In some edge cases, the binary search algorithm to find the best fit may slightly 38 * overshoot / undershoot its constraints. See b/26704434. No minimal repro case has been 39 * found yet. A known workaround is the solution provided on StackOverflow: 40 * http://stackoverflow.com/a/5535672 41 */ 42 public class AutoResizeTextView extends TextView { 43 private static final int NO_LINE_LIMIT = -1; 44 private static final float DEFAULT_MIN_TEXT_SIZE = 16.0f; 45 private static final int DEFAULT_RESIZE_STEP_UNIT = TypedValue.COMPLEX_UNIT_PX; 46 47 private final DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); 48 private final RectF availableSpaceRect = new RectF(); 49 private final SparseIntArray textSizesCache = new SparseIntArray(); 50 private final TextPaint textPaint = new TextPaint(); 51 private int resizeStepUnit = DEFAULT_RESIZE_STEP_UNIT; 52 private float minTextSize = DEFAULT_MIN_TEXT_SIZE; 53 private float maxTextSize; 54 private int maxWidth; 55 private int maxLines; 56 private float lineSpacingMultiplier = 1.0f; 57 private float lineSpacingExtra = 0.0f; 58 AutoResizeTextView(Context context)59 public AutoResizeTextView(Context context) { 60 super(context, null, 0); 61 initialize(context, null, 0, 0); 62 } 63 AutoResizeTextView(Context context, AttributeSet attrs)64 public AutoResizeTextView(Context context, AttributeSet attrs) { 65 super(context, attrs, 0); 66 initialize(context, attrs, 0, 0); 67 } 68 AutoResizeTextView(Context context, AttributeSet attrs, int defStyleAttr)69 public AutoResizeTextView(Context context, AttributeSet attrs, int defStyleAttr) { 70 super(context, attrs, defStyleAttr); 71 initialize(context, attrs, defStyleAttr, 0); 72 } 73 AutoResizeTextView( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)74 public AutoResizeTextView( 75 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 76 super(context, attrs, defStyleAttr, defStyleRes); 77 initialize(context, attrs, defStyleAttr, defStyleRes); 78 } 79 initialize( Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)80 private void initialize( 81 Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { 82 TypedArray typedArray = context.getTheme().obtainStyledAttributes( 83 attrs, R.styleable.AutoResizeTextView, defStyleAttr, defStyleRes); 84 readAttrs(typedArray); 85 textPaint.set(getPaint()); 86 } 87 88 /** Overridden because getMaxLines is only defined in JB+. */ 89 @Override getMaxLines()90 public final int getMaxLines() { 91 if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { 92 return super.getMaxLines(); 93 } else { 94 return maxLines; 95 } 96 } 97 98 /** Overridden because getMaxLines is only defined in JB+. */ 99 @Override setMaxLines(int maxLines)100 public final void setMaxLines(int maxLines) { 101 super.setMaxLines(maxLines); 102 this.maxLines = maxLines; 103 } 104 105 /** Overridden because getLineSpacingMultiplier is only defined in JB+. */ 106 @Override getLineSpacingMultiplier()107 public final float getLineSpacingMultiplier() { 108 if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { 109 return super.getLineSpacingMultiplier(); 110 } else { 111 return lineSpacingMultiplier; 112 } 113 } 114 115 /** Overridden because getLineSpacingExtra is only defined in JB+. */ 116 @Override getLineSpacingExtra()117 public final float getLineSpacingExtra() { 118 if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { 119 return super.getLineSpacingExtra(); 120 } else { 121 return lineSpacingExtra; 122 } 123 } 124 125 /** 126 * Overridden because getLineSpacingMultiplier and getLineSpacingExtra are only defined in JB+. 127 */ 128 @Override setLineSpacing(float add, float mult)129 public final void setLineSpacing(float add, float mult) { 130 super.setLineSpacing(add, mult); 131 lineSpacingMultiplier = mult; 132 lineSpacingExtra = add; 133 } 134 135 /** 136 * Although this overrides the setTextSize method from the TextView base class, it changes the 137 * semantics a bit: Calling setTextSize now specifies the maximum text size to be used by this 138 * view. If the text can't fit with that text size, the text size will be scaled down, up to the 139 * minimum text size specified in {@link #setMinTextSize}. 140 * 141 * <p>Note that the final size unit will be truncated to the nearest integer value of the 142 * specified unit. 143 */ 144 @Override setTextSize(int unit, float size)145 public final void setTextSize(int unit, float size) { 146 float maxTextSize = TypedValue.applyDimension(unit, size, displayMetrics); 147 if (this.maxTextSize != maxTextSize) { 148 this.maxTextSize = maxTextSize; 149 // TODO: It's not actually necessary to clear the whole cache here. To optimize cache 150 // deletion we'd have to delete all entries in the cache with a value equal or larger than 151 // MIN(old_max_size, new_max_size) when changing maxTextSize; and all entries with a value 152 // equal or smaller than MAX(old_min_size, new_min_size) when changing minTextSize. 153 textSizesCache.clear(); 154 requestLayout(); 155 } 156 } 157 158 /** 159 * Sets the lower text size limit and invalidate the view. 160 * 161 * <p>The parameters follow the same behavior as they do in {@link #setTextSize}. 162 * 163 * <p>Note that the final size unit will be truncated to the nearest integer value of the 164 * specified unit. 165 */ setMinTextSize(int unit, float size)166 public final void setMinTextSize(int unit, float size) { 167 float minTextSize = TypedValue.applyDimension(unit, size, displayMetrics); 168 if (this.minTextSize != minTextSize) { 169 this.minTextSize = minTextSize; 170 textSizesCache.clear(); 171 requestLayout(); 172 } 173 } 174 175 /** 176 * Sets the unit to use as step units when computing the resized font size. This view's text 177 * contents will always be rendered as a whole integer value in the unit specified here. For 178 * example, if the unit is {@link TypedValue#COMPLEX_UNIT_SP}, then the text size may end up 179 * being 13sp or 14sp, but never 13.5sp. 180 * 181 * <p>By default, the AutoResizeTextView uses the unit {@link TypedValue#COMPLEX_UNIT_PX}. 182 * 183 * @param unit the unit type to use; must be a known unit type from {@link TypedValue}. 184 */ setResizeStepUnit(int unit)185 public final void setResizeStepUnit(int unit) { 186 if (resizeStepUnit != unit) { 187 resizeStepUnit = unit; 188 requestLayout(); 189 } 190 } 191 readAttrs(TypedArray typedArray)192 private void readAttrs(TypedArray typedArray) { 193 resizeStepUnit = typedArray.getInt( 194 R.styleable.AutoResizeTextView_autoResizeText_resizeStepUnit, DEFAULT_RESIZE_STEP_UNIT); 195 minTextSize = (int) typedArray.getDimension( 196 R.styleable.AutoResizeTextView_autoResizeText_minTextSize, DEFAULT_MIN_TEXT_SIZE); 197 maxTextSize = (int) getTextSize(); 198 } 199 adjustTextSize()200 private void adjustTextSize() { 201 int maxWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); 202 int maxHeight = getMeasuredHeight() - getPaddingBottom() - getPaddingTop(); 203 204 if (maxWidth <= 0 || maxHeight <= 0) { 205 return; 206 } 207 208 this.maxWidth = maxWidth; 209 availableSpaceRect.right = maxWidth; 210 availableSpaceRect.bottom = maxHeight; 211 int minSizeInStepSizeUnits = (int) Math.ceil(convertToResizeStepUnits(minTextSize)); 212 int maxSizeInStepSizeUnits = (int) Math.floor(convertToResizeStepUnits(maxTextSize)); 213 float textSize = computeTextSize( 214 minSizeInStepSizeUnits, maxSizeInStepSizeUnits, availableSpaceRect); 215 super.setTextSize(resizeStepUnit, textSize); 216 } 217 suggestedSizeFitsInSpace(float suggestedSizeInPx, RectF availableSpace)218 private boolean suggestedSizeFitsInSpace(float suggestedSizeInPx, RectF availableSpace) { 219 textPaint.setTextSize(suggestedSizeInPx); 220 String text = getText().toString(); 221 int maxLines = getMaxLines(); 222 if (maxLines == 1) { 223 // If single line, check the line's height and width. 224 return textPaint.getFontSpacing() <= availableSpace.bottom 225 && textPaint.measureText(text) <= availableSpace.right; 226 } else { 227 // If multiline, lay the text out, then check the number of lines, the layout's height, 228 // and each line's width. 229 StaticLayout layout = new StaticLayout(text, 230 textPaint, 231 maxWidth, 232 Alignment.ALIGN_NORMAL, 233 getLineSpacingMultiplier(), 234 getLineSpacingExtra(), 235 true); 236 237 // Return false if we need more than maxLines. The text is obviously too big in this case. 238 if (maxLines != NO_LINE_LIMIT && layout.getLineCount() > maxLines) { 239 return false; 240 } 241 // Return false if the height of the layout is too big. 242 return layout.getHeight() <= availableSpace.bottom; 243 } 244 } 245 246 /** 247 * Computes the final text size to use for this text view, factoring in any previously 248 * cached computations. 249 * 250 * @param minSize the minimum text size to allow, in units of {@link #resizeStepUnit} 251 * @param maxSize the maximum text size to allow, in units of {@link #resizeStepUnit} 252 */ computeTextSize(int minSize, int maxSize, RectF availableSpace)253 private float computeTextSize(int minSize, int maxSize, RectF availableSpace) { 254 CharSequence text = getText(); 255 if (text != null && textSizesCache.get(text.hashCode()) != 0) { 256 return textSizesCache.get(text.hashCode()); 257 } 258 int size = binarySearchSizes(minSize, maxSize, availableSpace); 259 textSizesCache.put(text == null ? 0 : text.hashCode(), size); 260 return size; 261 } 262 263 /** 264 * Performs a binary search to find the largest font size that will still fit within the size 265 * available to this view. 266 * @param minSize the minimum text size to allow, in units of {@link #resizeStepUnit} 267 * @param maxSize the maximum text size to allow, in units of {@link #resizeStepUnit} 268 */ binarySearchSizes(int minSize, int maxSize, RectF availableSpace)269 private int binarySearchSizes(int minSize, int maxSize, RectF availableSpace) { 270 int bestSize = minSize; 271 int low = minSize + 1; 272 int high = maxSize; 273 int sizeToTry; 274 while (low <= high) { 275 sizeToTry = (low + high) / 2; 276 float dimension = TypedValue.applyDimension(resizeStepUnit, sizeToTry, displayMetrics); 277 if (suggestedSizeFitsInSpace(dimension, availableSpace)) { 278 bestSize = low; 279 low = sizeToTry + 1; 280 } else { 281 high = sizeToTry - 1; 282 bestSize = high; 283 } 284 } 285 return bestSize; 286 } 287 convertToResizeStepUnits(float dimension)288 private float convertToResizeStepUnits(float dimension) { 289 // To figure out the multiplier between a raw dimension and the resizeStepUnit, we invert the 290 // conversion of 1 resizeStepUnit to a raw dimension. 291 float multiplier = 1 / TypedValue.applyDimension(resizeStepUnit, 1, displayMetrics); 292 return dimension * multiplier; 293 } 294 295 @Override onTextChanged( final CharSequence text, final int start, final int before, final int after)296 protected final void onTextChanged( 297 final CharSequence text, final int start, final int before, final int after) { 298 super.onTextChanged(text, start, before, after); 299 adjustTextSize(); 300 } 301 302 @Override onSizeChanged(int width, int height, int oldWidth, int oldHeight)303 protected final void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { 304 super.onSizeChanged(width, height, oldWidth, oldHeight); 305 if (width != oldWidth || height != oldHeight) { 306 textSizesCache.clear(); 307 adjustTextSize(); 308 } 309 } 310 311 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)312 protected final void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 313 adjustTextSize(); 314 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 315 } 316 } 317