|
| 1 | +package com.example.autofittextviewtest; |
| 2 | +import android.annotation.TargetApi; |
| 3 | +import android.content.Context; |
| 4 | +import android.content.res.Resources; |
| 5 | +import android.graphics.RectF; |
| 6 | +import android.os.Build; |
| 7 | +import android.text.Layout.Alignment; |
| 8 | +import android.text.StaticLayout; |
| 9 | +import android.text.TextPaint; |
| 10 | +import android.util.AttributeSet; |
| 11 | +import android.util.SparseIntArray; |
| 12 | +import android.util.TypedValue; |
| 13 | +import android.widget.TextView; |
| 14 | + |
| 15 | +/** |
| 16 | + * 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/> |
| 17 | + * code is heavily based on this StackOverflow thread: |
| 18 | + * http://stackoverflow.com/questions/16017165/auto-fit-textview-for-android/21851239#21851239 <br/> |
| 19 | + * 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/> |
| 20 | + * 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 |
| 21 | + */ |
| 22 | +public class AutoResizeTextView extends TextView |
| 23 | + { |
| 24 | + private static final int NO_LINE_LIMIT =-1; |
| 25 | + private final RectF mAvailableSpaceRect =new RectF(); |
| 26 | + private final SparseIntArray mTextCachedSizes =new SparseIntArray(); |
| 27 | + private float mMaxTextSize; |
| 28 | + private float mSpacingMult =1.0f; |
| 29 | + private float mSpacingAdd =0.0f; |
| 30 | + private float mMinTextSize =20; |
| 31 | + private int mWidthLimit; |
| 32 | + private int mMaxLines; |
| 33 | + private boolean mEnableSizeCache =true; |
| 34 | + private final SizeTester mSizeTester; |
| 35 | + private boolean mInitiallized =false; |
| 36 | + |
| 37 | + private interface SizeTester |
| 38 | + { |
| 39 | + /** |
| 40 | + * @param suggestedSize |
| 41 | + * Size of text to be tested |
| 42 | + * @param availableSpace |
| 43 | + * available space in which text must fit |
| 44 | + * @return an integer < 0 if after applying {@code suggestedSize} to |
| 45 | + * text, it takes less space than {@code availableSpace}, > 0 |
| 46 | + * otherwise |
| 47 | + */ |
| 48 | + public int onTestSize(int suggestedSize,RectF availableSpace); |
| 49 | + } |
| 50 | + |
| 51 | + public AutoResizeTextView(final Context context) |
| 52 | + { |
| 53 | + this(context,null,0); |
| 54 | + } |
| 55 | + |
| 56 | + public AutoResizeTextView(final Context context,final AttributeSet attrs) |
| 57 | + { |
| 58 | + this(context,attrs,0); |
| 59 | + } |
| 60 | + |
| 61 | + public AutoResizeTextView(final Context context,final AttributeSet attrs,final int defStyle) |
| 62 | + { |
| 63 | + super(context,attrs,defStyle); |
| 64 | + mMaxTextSize=getTextSize(); |
| 65 | + if(mMaxLines==0) |
| 66 | + // no value was assigned during construction |
| 67 | + mMaxLines=NO_LINE_LIMIT; |
| 68 | + // prepare size tester: |
| 69 | + final TextPaint paint=new TextPaint(getPaint()); |
| 70 | + mSizeTester=new SizeTester() |
| 71 | + { |
| 72 | + final RectF textRect =new RectF(); |
| 73 | + |
| 74 | + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) |
| 75 | + @Override |
| 76 | + public int onTestSize(final int suggestedSize,final RectF availableSPace) |
| 77 | + { |
| 78 | + paint.setTextSize(suggestedSize); |
| 79 | + final String text=getText().toString(); |
| 80 | + final boolean singleline=getMaxLines()==1; |
| 81 | + if(singleline) |
| 82 | + { |
| 83 | + textRect.bottom=paint.getFontSpacing(); |
| 84 | + textRect.right=paint.measureText(text); |
| 85 | + } |
| 86 | + else |
| 87 | + { |
| 88 | + final StaticLayout layout=new StaticLayout(text,paint,mWidthLimit,Alignment.ALIGN_NORMAL,mSpacingMult,mSpacingAdd,true); |
| 89 | + // return early if we have more lines |
| 90 | + if(getMaxLines()!=NO_LINE_LIMIT&&layout.getLineCount()>getMaxLines()) |
| 91 | + return 1; |
| 92 | + textRect.bottom=layout.getHeight(); |
| 93 | + int maxWidth=-1; |
| 94 | + for(int i=0;i<layout.getLineCount();i++) |
| 95 | + if(maxWidth<layout.getLineWidth(i)) |
| 96 | + maxWidth=(int)layout.getLineWidth(i); |
| 97 | + textRect.right=maxWidth; |
| 98 | + } |
| 99 | + textRect.offsetTo(0,0); |
| 100 | + if(availableSPace.contains(textRect)) |
| 101 | + // may be too small, don't worry we will find the best match |
| 102 | + return -1; |
| 103 | + // else, too big |
| 104 | + return 1; |
| 105 | + } |
| 106 | + }; |
| 107 | + mInitiallized=true; |
| 108 | + } |
| 109 | + |
| 110 | + @Override |
| 111 | + public void setTextSize(final float size) |
| 112 | + { |
| 113 | + mMaxTextSize=size; |
| 114 | + mTextCachedSizes.clear(); |
| 115 | + adjustTextSize(); |
| 116 | + } |
| 117 | + |
| 118 | + @Override |
| 119 | + public void setMaxLines(final int maxlines) |
| 120 | + { |
| 121 | + super.setMaxLines(maxlines); |
| 122 | + mMaxLines=maxlines; |
| 123 | + reAdjust(); |
| 124 | + } |
| 125 | + |
| 126 | + @Override |
| 127 | + public int getMaxLines() |
| 128 | + { |
| 129 | + return mMaxLines; |
| 130 | + } |
| 131 | + |
| 132 | + @Override |
| 133 | + public void setSingleLine() |
| 134 | + { |
| 135 | + super.setSingleLine(); |
| 136 | + mMaxLines=1; |
| 137 | + reAdjust(); |
| 138 | + } |
| 139 | + |
| 140 | + @Override |
| 141 | + public void setSingleLine(final boolean singleLine) |
| 142 | + { |
| 143 | + super.setSingleLine(singleLine); |
| 144 | + if(singleLine) |
| 145 | + mMaxLines=1; |
| 146 | + else mMaxLines=NO_LINE_LIMIT; |
| 147 | + reAdjust(); |
| 148 | + } |
| 149 | + |
| 150 | + @Override |
| 151 | + public void setLines(final int lines) |
| 152 | + { |
| 153 | + super.setLines(lines); |
| 154 | + mMaxLines=lines; |
| 155 | + reAdjust(); |
| 156 | + } |
| 157 | + |
| 158 | + @Override |
| 159 | + public void setTextSize(final int unit,final float size) |
| 160 | + { |
| 161 | + final Context c=getContext(); |
| 162 | + Resources r; |
| 163 | + if(c==null) |
| 164 | + r=Resources.getSystem(); |
| 165 | + else r=c.getResources(); |
| 166 | + mMaxTextSize=TypedValue.applyDimension(unit,size,r.getDisplayMetrics()); |
| 167 | + mTextCachedSizes.clear(); |
| 168 | + adjustTextSize(); |
| 169 | + } |
| 170 | + |
| 171 | + @Override |
| 172 | + public void setLineSpacing(final float add,final float mult) |
| 173 | + { |
| 174 | + super.setLineSpacing(add,mult); |
| 175 | + mSpacingMult=mult; |
| 176 | + mSpacingAdd=add; |
| 177 | + } |
| 178 | + |
| 179 | + /** |
| 180 | + * Set the lower text size limit and invalidate the view |
| 181 | + * |
| 182 | + * @param minTextSize |
| 183 | + */ |
| 184 | + public void setMinTextSize(final float minTextSize) |
| 185 | + { |
| 186 | + mMinTextSize=minTextSize; |
| 187 | + reAdjust(); |
| 188 | + } |
| 189 | + |
| 190 | + private void reAdjust() |
| 191 | + { |
| 192 | + adjustTextSize(); |
| 193 | + } |
| 194 | + |
| 195 | + private void adjustTextSize() |
| 196 | + { |
| 197 | + if(!mInitiallized) |
| 198 | + return; |
| 199 | + final int startSize=(int)mMinTextSize; |
| 200 | + final int heightLimit=getMeasuredHeight()-getCompoundPaddingBottom()-getCompoundPaddingTop(); |
| 201 | + mWidthLimit=getMeasuredWidth()-getCompoundPaddingLeft()-getCompoundPaddingRight(); |
| 202 | + mAvailableSpaceRect.right=mWidthLimit; |
| 203 | + mAvailableSpaceRect.bottom=heightLimit; |
| 204 | + super.setTextSize(TypedValue.COMPLEX_UNIT_PX,efficientTextSizeSearch(startSize,(int)mMaxTextSize,mSizeTester,mAvailableSpaceRect)); |
| 205 | + } |
| 206 | + |
| 207 | + /** |
| 208 | + * Enables or disables size caching, enabling it will improve performance |
| 209 | + * where you are animating a value inside TextView. This stores the font |
| 210 | + * size against getText().length() Be careful though while enabling it as 0 |
| 211 | + * takes more space than 1 on some fonts and so on. |
| 212 | + * |
| 213 | + * @param enable |
| 214 | + * enable font size caching |
| 215 | + */ |
| 216 | + public void setEnableSizeCache(final boolean enable) |
| 217 | + { |
| 218 | + mEnableSizeCache=enable; |
| 219 | + mTextCachedSizes.clear(); |
| 220 | + adjustTextSize(); |
| 221 | + } |
| 222 | + |
| 223 | + private int efficientTextSizeSearch(final int start,final int end,final SizeTester sizeTester,final RectF availableSpace) |
| 224 | + { |
| 225 | + if(!mEnableSizeCache) |
| 226 | + return binarySearch(start,end,sizeTester,availableSpace); |
| 227 | + final String text=getText().toString(); |
| 228 | + final int key=text==null ? 0 : text.length(); |
| 229 | + int size=mTextCachedSizes.get(key); |
| 230 | + if(size!=0) |
| 231 | + return size; |
| 232 | + size=binarySearch(start,end,sizeTester,availableSpace); |
| 233 | + mTextCachedSizes.put(key,size); |
| 234 | + return size; |
| 235 | + } |
| 236 | + |
| 237 | + private int binarySearch(final int start,final int end,final SizeTester sizeTester,final RectF availableSpace) |
| 238 | + { |
| 239 | + int lastBest=start; |
| 240 | + int lo=start; |
| 241 | + int hi=end-1; |
| 242 | + int mid=0; |
| 243 | + while(lo<=hi) |
| 244 | + { |
| 245 | + mid=lo+hi>>>1; |
| 246 | + final int midValCmp=sizeTester.onTestSize(mid,availableSpace); |
| 247 | + if(midValCmp<0) |
| 248 | + { |
| 249 | + lastBest=lo; |
| 250 | + lo=mid+1; |
| 251 | + } |
| 252 | + else if(midValCmp>0) |
| 253 | + { |
| 254 | + hi=mid-1; |
| 255 | + lastBest=hi; |
| 256 | + } |
| 257 | + else return mid; |
| 258 | + } |
| 259 | + // make sure to return last best |
| 260 | + // this is what should always be returned |
| 261 | + return lastBest; |
| 262 | + } |
| 263 | + |
| 264 | + @Override |
| 265 | + protected void onTextChanged(final CharSequence text,final int start,final int before,final int after) |
| 266 | + { |
| 267 | + super.onTextChanged(text,start,before,after); |
| 268 | + reAdjust(); |
| 269 | + } |
| 270 | + |
| 271 | + @Override |
| 272 | + protected void onSizeChanged(final int width,final int height,final int oldwidth,final int oldheight) |
| 273 | + { |
| 274 | + mTextCachedSizes.clear(); |
| 275 | + super.onSizeChanged(width,height,oldwidth,oldheight); |
| 276 | + if(width!=oldwidth||height!=oldheight) |
| 277 | + reAdjust(); |
| 278 | + } |
| 279 | + } |
0 commit comments