Skip to content

Commit 19d4c24

Browse files
committed
Text view that auto adjusts text size to fit within the view.If the text size equals the minimum text size and still does not
fit, append with an ellipsis.
1 parent d74bc38 commit 19d4c24

File tree

1 file changed

+299
-0
lines changed

1 file changed

+299
-0
lines changed
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
package org.wordpress.android.util;
2+
3+
import android.content.Context;
4+
import android.text.Layout;
5+
import android.text.StaticLayout;
6+
import android.text.TextPaint;
7+
import android.util.AttributeSet;
8+
import android.util.TypedValue;
9+
import android.widget.TextView;
10+
11+
/**
12+
* Text view that auto adjusts text size to fit within the view.
13+
* If the text size equals the minimum text size and still does not
14+
* fit, append with an ellipsis.
15+
*
16+
* See http://stackoverflow.com/a/5535672
17+
*
18+
*/
19+
public class AutoResizeTextView extends TextView {
20+
// Minimum text size for this text view
21+
public static final float MIN_TEXT_SIZE = 20;
22+
23+
// Interface for resize notifications
24+
public interface OnTextResizeListener {
25+
void onTextResize(TextView textView, float oldSize, float newSize);
26+
}
27+
28+
// Our ellipse string
29+
private static final String M_ELLIPSIS = "...";
30+
31+
// Registered resize listener
32+
private OnTextResizeListener mTextResizeListener;
33+
34+
// Flag for text and/or size changes to force a resize
35+
private boolean mNeedsResize = false;
36+
37+
// Text size that is set from code. This acts as a starting point for resizing
38+
private float mTextSize;
39+
40+
// Temporary upper bounds on the starting text size
41+
private float mMaxTextSize = 0;
42+
43+
// Lower bounds for text size
44+
private float mMinTextSize = MIN_TEXT_SIZE;
45+
46+
// Text view line spacing multiplier
47+
private float mSpacingMult = 1.0f;
48+
49+
// Text view additional line spacing
50+
private float mSpacingAdd = 0.0f;
51+
52+
// Add ellipsis to text that overflows at the smallest text size
53+
private boolean mAddEllipsis = true;
54+
55+
// Default constructor override
56+
public AutoResizeTextView(Context context) {
57+
this(context, null);
58+
}
59+
60+
// Default constructor when inflating from XML file
61+
public AutoResizeTextView(Context context, AttributeSet attrs) {
62+
this(context, attrs, 0);
63+
}
64+
65+
// Default constructor override
66+
public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
67+
super(context, attrs, defStyle);
68+
mTextSize = getTextSize();
69+
}
70+
71+
/**
72+
* When text changes, set the force resize flag to true and reset the text size.
73+
*/
74+
@Override
75+
protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
76+
mNeedsResize = true;
77+
// Since this view may be reused, it is good to reset the text size
78+
resetTextSize();
79+
}
80+
81+
/**
82+
* If the text view size changed, set the force resize flag to true
83+
*/
84+
@Override
85+
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
86+
if (w != oldw || h != oldh) {
87+
mNeedsResize = true;
88+
}
89+
}
90+
91+
/**
92+
* Register listener to receive resize notifications
93+
* @param listener
94+
*/
95+
public void setOnResizeListener(OnTextResizeListener listener) {
96+
mTextResizeListener = listener;
97+
}
98+
99+
/**
100+
* Override the set text size to update our internal reference values
101+
*/
102+
@Override
103+
public void setTextSize(float size) {
104+
super.setTextSize(size);
105+
mTextSize = getTextSize();
106+
}
107+
108+
/**
109+
* Override the set text size to update our internal reference values
110+
*/
111+
@Override
112+
public void setTextSize(int unit, float size) {
113+
super.setTextSize(unit, size);
114+
mTextSize = getTextSize();
115+
}
116+
117+
/**
118+
* Override the set line spacing to update our internal reference values
119+
*/
120+
@Override
121+
public void setLineSpacing(float add, float mult) {
122+
super.setLineSpacing(add, mult);
123+
mSpacingMult = mult;
124+
mSpacingAdd = add;
125+
}
126+
127+
/**
128+
* Set the upper text size limit and invalidate the view
129+
* @param maxTextSize
130+
*/
131+
public void setMaxTextSize(float maxTextSize) {
132+
mMaxTextSize = maxTextSize;
133+
requestLayout();
134+
invalidate();
135+
}
136+
137+
/**
138+
* Return upper text size limit
139+
* @return
140+
*/
141+
public float getMaxTextSize() {
142+
return mMaxTextSize;
143+
}
144+
145+
/**
146+
* Set the lower text size limit and invalidate the view
147+
* @param minTextSize
148+
*/
149+
public void setMinTextSize(float minTextSize) {
150+
mMinTextSize = minTextSize;
151+
requestLayout();
152+
invalidate();
153+
}
154+
155+
/**
156+
* Return lower text size limit
157+
* @return
158+
*/
159+
public float getMinTextSize() {
160+
return mMinTextSize;
161+
}
162+
163+
/**
164+
* Set flag to add ellipsis to text that overflows at the smallest text size
165+
* @param addEllipsis
166+
*/
167+
public void setAddEllipsis(boolean addEllipsis) {
168+
mAddEllipsis = addEllipsis;
169+
}
170+
171+
/**
172+
* Return flag to add ellipsis to text that overflows at the smallest text size
173+
* @return
174+
*/
175+
public boolean getAddEllipsis() {
176+
return mAddEllipsis;
177+
}
178+
179+
/**
180+
* Reset the text to the original size
181+
*/
182+
public void resetTextSize() {
183+
if (mTextSize > 0) {
184+
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
185+
mMaxTextSize = mTextSize;
186+
}
187+
}
188+
189+
/**
190+
* Resize text after measuring
191+
*/
192+
@Override
193+
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
194+
if (changed || mNeedsResize) {
195+
int widthLimit = (right - left) - getCompoundPaddingLeft() - getCompoundPaddingRight();
196+
int heightLimit = (bottom - top) - getCompoundPaddingBottom() - getCompoundPaddingTop();
197+
resizeText(widthLimit, heightLimit);
198+
}
199+
super.onLayout(changed, left, top, right, bottom);
200+
}
201+
202+
/**
203+
* Resize the text size with default width and height
204+
*/
205+
public void resizeText() {
206+
int heightLimit = getHeight() - getPaddingBottom() - getPaddingTop();
207+
int widthLimit = getWidth() - getPaddingLeft() - getPaddingRight();
208+
resizeText(widthLimit, heightLimit);
209+
}
210+
211+
/**
212+
* Resize the text size with specified width and height
213+
* @param width
214+
* @param height
215+
*/
216+
public void resizeText(int width, int height) {
217+
CharSequence text = getText();
218+
// Do not resize if the view does not have dimensions or there is no text
219+
if (text == null || text.length() == 0 || height <= 0 || width <= 0 || mTextSize == 0) {
220+
return;
221+
}
222+
223+
// Get the text view's paint object
224+
TextPaint textPaint = getPaint();
225+
226+
// Store the current text size
227+
float oldTextSize = textPaint.getTextSize();
228+
// If there is a max text size set, use the lesser of that and the default text size
229+
float targetTextSize = mMaxTextSize > 0 ? Math.min(mTextSize, mMaxTextSize) : mTextSize;
230+
231+
// Get the required text height
232+
int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
233+
234+
// Until we either fit within our text view or we had reached our min text size, incrementally try smaller sizes
235+
while (textHeight > height && targetTextSize > mMinTextSize) {
236+
targetTextSize = Math.max(targetTextSize - 2, mMinTextSize);
237+
textHeight = getTextHeight(text, textPaint, width, targetTextSize);
238+
}
239+
240+
// If we had reached our minimum text size and still don't fit, append an ellipsis
241+
if (mAddEllipsis && targetTextSize == mMinTextSize && textHeight > height) {
242+
// Draw using a static layout
243+
// modified: use a copy of TextPaint for measuring
244+
TextPaint paint = new TextPaint(textPaint);
245+
// Draw using a static layout
246+
StaticLayout layout = new StaticLayout(text, paint, width, Layout.Alignment.ALIGN_NORMAL,
247+
mSpacingMult, mSpacingAdd, false);
248+
// Check that we have a least one line of rendered text
249+
if (layout.getLineCount() > 0) {
250+
// Since the line at the specific vertical position would be cut off,
251+
// we must trim up to the previous line
252+
int lastLine = layout.getLineForVertical(height) - 1;
253+
// If the text would not even fit on a single line, clear it
254+
if (lastLine < 0) {
255+
setText("");
256+
} else {
257+
// Otherwise, trim to the previous line and add an ellipsis
258+
int start = layout.getLineStart(lastLine);
259+
int end = layout.getLineEnd(lastLine);
260+
float lineWidth = layout.getLineWidth(lastLine);
261+
float ellipseWidth = paint.measureText(M_ELLIPSIS);
262+
263+
// Trim characters off until we have enough room to draw the ellipsis
264+
while (width < lineWidth + ellipseWidth) {
265+
lineWidth = paint.measureText(text.subSequence(start, --end + 1).toString());
266+
}
267+
setText(text.subSequence(0, end) + M_ELLIPSIS);
268+
}
269+
}
270+
}
271+
272+
// Some devices try to auto adjust line spacing, so force default line spacing
273+
// and invalidate the layout as a side effect
274+
setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize);
275+
setLineSpacing(mSpacingAdd, mSpacingMult);
276+
277+
// Notify the listener if registered
278+
if (mTextResizeListener != null) {
279+
mTextResizeListener.onTextResize(this, oldTextSize, targetTextSize);
280+
}
281+
282+
// Reset force resize flag
283+
mNeedsResize = false;
284+
}
285+
286+
// Set the text size of the text paint object and use a static layout to render text off screen before measuring
287+
private int getTextHeight(CharSequence source, TextPaint paint, int width, float textSize) {
288+
// modified: make a copy of the original TextPaint object for measuring
289+
// (apparently the object gets modified while measuring, see also the
290+
// docs for TextView.getPaint() (which states to access it read-only)
291+
TextPaint paintCopy = new TextPaint(paint);
292+
// Update the text paint object
293+
paintCopy.setTextSize(textSize);
294+
// Measure using a static layout
295+
StaticLayout layout = new StaticLayout(source, paintCopy, width, Layout.Alignment.ALIGN_NORMAL,
296+
mSpacingMult, mSpacingAdd, true);
297+
return layout.getHeight();
298+
}
299+
}

0 commit comments

Comments
 (0)