| package org.wordpress.android.util.widgets; |
| |
| 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 |
| private 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 - Unicode Character 'HORIZONTAL ELLIPSIS' |
| private static final String M_ELLIPSIS = "\u2026"; |
| |
| // 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 |
| */ |
| private 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(); |
| } |
| } |