package com.lb.auto_fit_textview; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.RectF; import android.graphics.Typeface; import android.os.Build; import android.text.Layout.Alignment; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; import android.util.SparseIntArray; import android.util.TypedValue; import android.widget.TextView; import com.samknows.libcore.SKPorting; /** * a textView that is able to self-adjust its font size depending on the min and max size of the font, and its own size.<br/> * code is heavily based on this StackOverflow thread: * http://stackoverflow.com/questions/16017165/auto-fit-textview-for-android/21851239#21851239 <br/> * It should work fine with most Android versions, but might have some issues on Android 3.1 - 4.04, as setTextSize will only work for the first time. <br/> * More info here: https://code.google.com/p/android/issues/detail?id=22493 and here in case you wish to fix it: http://stackoverflow.com/a/21851239/878126 */ public class AutoResizeTextView extends TextView{ private static final int NO_LINE_LIMIT=-1; private final RectF _availableSpaceRect=new RectF(); private final SparseIntArray _textCachedSizes=new SparseIntArray(); private final SizeTester _sizeTester; private float _maxTextSize; private float _spacingMult=1.0f; private float _spacingAdd=0.0f; private float _minTextSize; private int _widthLimit; private int _maxLines; private boolean _enableSizeCache=true; private boolean _initiallized=false; private TextPaint paint; private interface SizeTester{ /** * @param suggestedSize Size of text to be tested * @param availableSpace available space in which text must fit * @return an integer < 0 if after applying {@code suggestedSize} to * text, it takes less space than {@code availableSpace}, > 0 * otherwise */ int onTestSize(int suggestedSize, RectF availableSpace); } public AutoResizeTextView(final Context context){ this(context,null,0); } public AutoResizeTextView(final Context context,final AttributeSet attrs){ this(context,attrs,0); } public AutoResizeTextView(final Context context,final AttributeSet attrs,final int defStyle){ super(context,attrs,defStyle); // using the minimal recommended font size _minTextSize=TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,12,getResources().getDisplayMetrics()); _maxTextSize=getTextSize(); if(_maxLines==0) // no value was assigned during construction _maxLines=NO_LINE_LIMIT; // prepare size tester: _sizeTester=new SizeTester(){ final RectF textRect=new RectF(); @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public int onTestSize(final int suggestedSize,final RectF availableSPace){ if (paint == null) { // We expect this to have been handled in setTypeface! SKPorting.sAssert(false); paint = new TextPaint(getPaint()); } paint.setTextSize(suggestedSize); final String text=getText().toString(); final boolean singleline=getMaxLines()==1; if(singleline){ textRect.bottom=paint.getFontSpacing(); textRect.right=paint.measureText(text); }else{ final StaticLayout layout=new StaticLayout(text,paint,_widthLimit,Alignment.ALIGN_NORMAL,_spacingMult,_spacingAdd,true); // return early if we have more lines if(getMaxLines()!=NO_LINE_LIMIT&&layout.getLineCount()>getMaxLines()) return 1; textRect.bottom=layout.getHeight(); int maxWidth=-1; for(int i=0;i<layout.getLineCount();i++) if(maxWidth<layout.getLineRight(i)-layout.getLineLeft(i)) maxWidth=(int)layout.getLineRight(i)-(int)layout.getLineLeft(i); textRect.right=maxWidth; } textRect.offsetTo(0,0); if(availableSPace.contains(textRect)) // may be too small, don't worry we will find the best match return -1; // else, too big return 1; } }; _initiallized=true; } @Override public void setTypeface(final Typeface tf) { if (paint == null) { paint = new TextPaint(getPaint()); } paint.setTypeface(tf); super.setTypeface(tf); } @Override public void setTextSize(final float size){ _maxTextSize=size; _textCachedSizes.clear(); adjustTextSize(); } @Override public void setMaxLines(final int maxlines){ super.setMaxLines(maxlines); _maxLines=maxlines; reAdjust(); } @Override public int getMaxLines(){ return _maxLines; } @Override public void setSingleLine(){ super.setSingleLine(); _maxLines=1; reAdjust(); } @Override public void setSingleLine(final boolean singleLine){ super.setSingleLine(singleLine); if(singleLine) _maxLines=1; else _maxLines=NO_LINE_LIMIT; reAdjust(); } @Override public void setLines(final int lines){ super.setLines(lines); _maxLines=lines; reAdjust(); } @Override public void setTextSize(final int unit,final float size){ final Context c=getContext(); Resources r; if(c==null) r=Resources.getSystem(); else r=c.getResources(); _maxTextSize=TypedValue.applyDimension(unit,size,r.getDisplayMetrics()); _textCachedSizes.clear(); adjustTextSize(); } @Override public void setLineSpacing(final float add,final float mult){ super.setLineSpacing(add,mult); _spacingMult=mult; _spacingAdd=add; } /** * Set the lower text size limit and invalidate the view * * @param minTextSize */ public void setMinTextSize(final float minTextSize){ _minTextSize=minTextSize; reAdjust(); } private void reAdjust(){ adjustTextSize(); } private void adjustTextSize(){ // This is a workaround for truncated text issue on ListView, as shown here: https://github.com/AndroidDeveloperLB/AutoFitTextView/pull/14 // TODO think of a nicer, elegant solution. if (isInEditMode()) { return; } post(new Runnable(){ @Override public void run(){ if(!_initiallized) return; final int startSize=(int)_minTextSize; final int heightLimit=getMeasuredHeight()-getCompoundPaddingBottom()-getCompoundPaddingTop(); _widthLimit=getMeasuredWidth()-getCompoundPaddingLeft()-getCompoundPaddingRight(); if(_widthLimit<=0) return; _availableSpaceRect.right=_widthLimit; _availableSpaceRect.bottom=heightLimit; superSetTextSize(startSize); } }); } private void superSetTextSize(int startSize){ super.setTextSize(TypedValue.COMPLEX_UNIT_PX,efficientTextSizeSearch(startSize,(int)_maxTextSize,_sizeTester,_availableSpaceRect)); } /** * Enables or disables size caching, enabling it will improve performance * where you are animating a value inside TextView. This stores the font * size against getText().length() Be careful though while enabling it as 0 * takes more space than 1 on some fonts and so on. * * @param enable enable font size caching */ public void setEnableSizeCache(final boolean enable){ _enableSizeCache=enable; _textCachedSizes.clear(); adjustTextSize(); } private int efficientTextSizeSearch(final int start,final int end,final SizeTester sizeTester,final RectF availableSpace){ if(!_enableSizeCache) return binarySearch(start,end,sizeTester,availableSpace); final String text=getText().toString(); final int key=text==null?0:text.length(); int size=_textCachedSizes.get(key); if(size!=0) return size; size=binarySearch(start,end,sizeTester,availableSpace); _textCachedSizes.put(key,size); return size; } private int binarySearch(final int start,final int end,final SizeTester sizeTester,final RectF availableSpace){ int lastBest=start; int lo=start; int hi=end-1; int mid=0; while(lo<=hi){ mid=lo+hi>>>1; final int midValCmp=sizeTester.onTestSize(mid,availableSpace); if(midValCmp<0){ lastBest=lo; lo=mid+1; }else if(midValCmp>0){ hi=mid-1; lastBest=hi; }else return mid; } // make sure to return last best // this is what should always be returned return lastBest; } @Override protected void onTextChanged(final CharSequence text,final int start,final int before,final int after){ super.onTextChanged(text,start,before,after); reAdjust(); } @Override protected void onSizeChanged(final int width,final int height,final int oldwidth,final int oldheight){ _textCachedSizes.clear(); super.onSizeChanged(width,height,oldwidth,oldheight); if(width!=oldwidth||height!=oldheight) reAdjust(); } }