-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Text view that auto adjusts text size to fit within the view.If the t…
…ext size equals the minimum text size and still does not fit, append with an ellipsis.
- Loading branch information
1 parent
d74bc38
commit 19d4c24
Showing
1 changed file
with
299 additions
and
0 deletions.
There are no files selected for viewing
299 changes: 299 additions & 0 deletions
299
WordPressUtils/src/main/java/org/wordpress/android/util/AutoResizeTextView.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,299 @@ | ||
package org.wordpress.android.util; | ||
|
||
import android.content.Context; | ||
import android.text.Layout; | ||
import android.text.StaticLayout; | ||
import android.text.TextPaint; | ||
import android.util.AttributeSet; | ||
import android.util.TypedValue; | ||
import android.widget.TextView; | ||
|
||
/** | ||
* 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. | ||
* | ||
* See http://stackoverflow.com/a/5535672 | ||
* | ||
*/ | ||
public class AutoResizeTextView extends TextView { | ||
// Minimum text size for this text view | ||
public static final float MIN_TEXT_SIZE = 20; | ||
|
||
// Interface for resize notifications | ||
public interface OnTextResizeListener { | ||
void onTextResize(TextView textView, float oldSize, float newSize); | ||
} | ||
|
||
// Our ellipse string | ||
private static final String M_ELLIPSIS = "..."; | ||
|
||
// Registered resize listener | ||
private OnTextResizeListener mTextResizeListener; | ||
|
||
// Flag for text and/or size changes to force a resize | ||
private boolean mNeedsResize = false; | ||
|
||
// Text size that is set from code. This acts as a starting point for resizing | ||
private float mTextSize; | ||
|
||
// Temporary upper bounds on the starting text size | ||
private float mMaxTextSize = 0; | ||
|
||
// Lower bounds for text size | ||
private float mMinTextSize = MIN_TEXT_SIZE; | ||
|
||
// Text view line spacing multiplier | ||
private float mSpacingMult = 1.0f; | ||
|
||
// Text view additional line spacing | ||
private float mSpacingAdd = 0.0f; | ||
|
||
// Add ellipsis to text that overflows at the smallest text size | ||
private boolean mAddEllipsis = true; | ||
|
||
// Default constructor override | ||
public AutoResizeTextView(Context context) { | ||
this(context, null); | ||
} | ||
|
||
// Default constructor when inflating from XML file | ||
public AutoResizeTextView(Context context, AttributeSet attrs) { | ||
this(context, attrs, 0); | ||
} | ||
|
||
// Default constructor override | ||
public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) { | ||
super(context, attrs, defStyle); | ||
mTextSize = getTextSize(); | ||
} | ||
|
||
/** | ||
* When text changes, set the force resize flag to true and reset the text size. | ||
*/ | ||
@Override | ||
protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) { | ||
mNeedsResize = true; | ||
// Since this view may be reused, it is good to reset the text size | ||
resetTextSize(); | ||
} | ||
|
||
/** | ||
* If the text view size changed, set the force resize flag to true | ||
*/ | ||
@Override | ||
protected void onSizeChanged(int w, int h, int oldw, int oldh) { | ||
if (w != oldw || h != oldh) { | ||
mNeedsResize = true; | ||
} | ||
} | ||
|
||
/** | ||
* Register listener to receive resize notifications | ||
* @param listener | ||
*/ | ||
public void setOnResizeListener(OnTextResizeListener listener) { | ||
mTextResizeListener = listener; | ||
} | ||
|
||
/** | ||
* Override the set text size to update our internal reference values | ||
*/ | ||
@Override | ||
public void setTextSize(float size) { | ||
super.setTextSize(size); | ||
mTextSize = getTextSize(); | ||
} | ||
|
||
/** | ||
* Override the set text size to update our internal reference values | ||
*/ | ||
@Override | ||
public void setTextSize(int unit, float size) { | ||
super.setTextSize(unit, size); | ||
mTextSize = getTextSize(); | ||
} | ||
|
||
/** | ||
* Override the set line spacing to update our internal reference values | ||
*/ | ||
@Override | ||
public void setLineSpacing(float add, float mult) { | ||
super.setLineSpacing(add, mult); | ||
mSpacingMult = mult; | ||
mSpacingAdd = add; | ||
} | ||
|
||
/** | ||
* Set the upper text size limit and invalidate the view | ||
* @param maxTextSize | ||
*/ | ||
public void setMaxTextSize(float maxTextSize) { | ||
mMaxTextSize = maxTextSize; | ||
requestLayout(); | ||
invalidate(); | ||
} | ||
|
||
/** | ||
* Return upper text size limit | ||
* @return | ||
*/ | ||
public float getMaxTextSize() { | ||
return mMaxTextSize; | ||
} | ||
|
||
/** | ||
* Set the lower text size limit and invalidate the view | ||
* @param minTextSize | ||
*/ | ||
public void setMinTextSize(float minTextSize) { | ||
mMinTextSize = minTextSize; | ||
requestLayout(); | ||
invalidate(); | ||
} | ||
|
||
/** | ||
* Return lower text size limit | ||
* @return | ||
*/ | ||
public float getMinTextSize() { | ||
return mMinTextSize; | ||
} | ||
|
||
/** | ||
* Set flag to add ellipsis to text that overflows at the smallest text size | ||
* @param addEllipsis | ||
*/ | ||
public void setAddEllipsis(boolean addEllipsis) { | ||
mAddEllipsis = addEllipsis; | ||
} | ||
|
||
/** | ||
* Return flag to add ellipsis to text that overflows at the smallest text size | ||
* @return | ||
*/ | ||
public boolean getAddEllipsis() { | ||
return mAddEllipsis; | ||
} | ||
|
||
/** | ||
* Reset the text to the original size | ||
*/ | ||
public void resetTextSize() { | ||
if (mTextSize > 0) { | ||
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); | ||
mMaxTextSize = mTextSize; | ||
} | ||
} | ||
|
||
/** | ||
* Resize text after measuring | ||
*/ | ||
@Override | ||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { | ||
if (changed || mNeedsResize) { | ||
int widthLimit = (right - left) - getCompoundPaddingLeft() - getCompoundPaddingRight(); | ||
int heightLimit = (bottom - top) - getCompoundPaddingBottom() - getCompoundPaddingTop(); | ||
resizeText(widthLimit, heightLimit); | ||
} | ||
super.onLayout(changed, left, top, right, bottom); | ||
} | ||
|
||
/** | ||
* Resize the text size with default width and height | ||
*/ | ||
public void resizeText() { | ||
int heightLimit = getHeight() - getPaddingBottom() - getPaddingTop(); | ||
int widthLimit = getWidth() - getPaddingLeft() - getPaddingRight(); | ||
resizeText(widthLimit, heightLimit); | ||
} | ||
|
||
/** | ||
* Resize the text size with specified width and height | ||
* @param width | ||
* @param height | ||
*/ | ||
public void resizeText(int width, int height) { | ||
CharSequence text = getText(); | ||
// Do not resize if the view does not have dimensions or there is no text | ||
if (text == null || text.length() == 0 || height <= 0 || width <= 0 || mTextSize == 0) { | ||
return; | ||
} | ||
|
||
// Get the text view's paint object | ||
TextPaint textPaint = getPaint(); | ||
|
||
// Store the current text size | ||
float oldTextSize = textPaint.getTextSize(); | ||
// If there is a max text size set, use the lesser of that and the default text size | ||
float targetTextSize = mMaxTextSize > 0 ? Math.min(mTextSize, mMaxTextSize) : mTextSize; | ||
|
||
// Get the required text height | ||
int textHeight = getTextHeight(text, textPaint, width, targetTextSize); | ||
|
||
// Until we either fit within our text view or we had reached our min text size, incrementally try smaller sizes | ||
while (textHeight > height && targetTextSize > mMinTextSize) { | ||
targetTextSize = Math.max(targetTextSize - 2, mMinTextSize); | ||
textHeight = getTextHeight(text, textPaint, width, targetTextSize); | ||
} | ||
|
||
// If we had reached our minimum text size and still don't fit, append an ellipsis | ||
if (mAddEllipsis && targetTextSize == mMinTextSize && textHeight > height) { | ||
// Draw using a static layout | ||
// modified: use a copy of TextPaint for measuring | ||
TextPaint paint = new TextPaint(textPaint); | ||
// Draw using a static layout | ||
StaticLayout layout = new StaticLayout(text, paint, width, Layout.Alignment.ALIGN_NORMAL, | ||
mSpacingMult, mSpacingAdd, false); | ||
// Check that we have a least one line of rendered text | ||
if (layout.getLineCount() > 0) { | ||
// Since the line at the specific vertical position would be cut off, | ||
// we must trim up to the previous line | ||
int lastLine = layout.getLineForVertical(height) - 1; | ||
// If the text would not even fit on a single line, clear it | ||
if (lastLine < 0) { | ||
setText(""); | ||
} else { | ||
// Otherwise, trim to the previous line and add an ellipsis | ||
int start = layout.getLineStart(lastLine); | ||
int end = layout.getLineEnd(lastLine); | ||
float lineWidth = layout.getLineWidth(lastLine); | ||
float ellipseWidth = paint.measureText(M_ELLIPSIS); | ||
|
||
// Trim characters off until we have enough room to draw the ellipsis | ||
while (width < lineWidth + ellipseWidth) { | ||
lineWidth = paint.measureText(text.subSequence(start, --end + 1).toString()); | ||
} | ||
setText(text.subSequence(0, end) + M_ELLIPSIS); | ||
} | ||
} | ||
} | ||
|
||
// Some devices try to auto adjust line spacing, so force default line spacing | ||
// and invalidate the layout as a side effect | ||
setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize); | ||
setLineSpacing(mSpacingAdd, mSpacingMult); | ||
|
||
// Notify the listener if registered | ||
if (mTextResizeListener != null) { | ||
mTextResizeListener.onTextResize(this, oldTextSize, targetTextSize); | ||
} | ||
|
||
// Reset force resize flag | ||
mNeedsResize = false; | ||
} | ||
|
||
// Set the text size of the text paint object and use a static layout to render text off screen before measuring | ||
private int getTextHeight(CharSequence source, TextPaint paint, int width, float textSize) { | ||
// modified: make a copy of the original TextPaint object for measuring | ||
// (apparently the object gets modified while measuring, see also the | ||
// docs for TextView.getPaint() (which states to access it read-only) | ||
TextPaint paintCopy = new TextPaint(paint); | ||
// Update the text paint object | ||
paintCopy.setTextSize(textSize); | ||
// Measure using a static layout | ||
StaticLayout layout = new StaticLayout(source, paintCopy, width, Layout.Alignment.ALIGN_NORMAL, | ||
mSpacingMult, mSpacingAdd, true); | ||
return layout.getHeight(); | ||
} | ||
} |