• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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