public classAutofitHelper {
private static finalString TAG= "AutoFitTextHelper";private static final booleanSPEW= false;// Minimum size of the text in pixelsprivate static final intDEFAULT_MIN_TEXT_SIZE= 8;//sp// How precise we want to be when reaching the target textWidth sizeprivate static final floatDEFAULT_PRECISION= 0.5f;/*** Creates a new instance of {@codeAutofitHelper} that wraps a {@linkTextView} and enables* automatically sizing the text to fit.*/public staticAutofitHelper create(TextView view) {
returncreate(view, null,0);}
/*** Creates a new instance of {@codeAutofitHelper} that wraps a {@linkTextView} and enables* automatically sizing the text to fit.*/public staticAutofitHelper create(TextView view,AttributeSet attrs) {
returncreate(view,attrs,0);}
/*** Creates a new instance of {@codeAutofitHelper} that wraps a {@linkTextView} and enables* automatically sizing the text to fit.*/public staticAutofitHelper create(TextView view,AttributeSet attrs, intdefStyle) {
AutofitHelper helper = newAutofitHelper(view);booleansizeToFit = true;if(attrs != null) {
Context context = view.getContext();intminTextSize = (int) helper.getMinTextSize();floatprecision = helper.getPrecision();TypedArray ta = context.obtainStyledAttributes(
attrs,R.styleable.AutofitTextView,defStyle,0);sizeToFit = ta.getBoolean(R.styleable.AutofitTextView_sizeToFit,sizeToFit);minTextSize = ta.getDimensionPixelSize(R.styleable.AutofitTextView_minTextSize,minTextSize);precision = ta.getFloat(R.styleable.AutofitTextView_precision,precision);ta.recycle();helper.setMinTextSize(TypedValue.COMPLEX_UNIT_PX,minTextSize)
.setPrecision(precision);}
helper.setEnabled(sizeToFit);returnhelper;}
/*** Re-sizes the textSize of the TextView so that the text fits within the bounds of the View.*/private static voidautofit(TextView view,TextPaint paint, floatminTextSize, floatmaxTextSize,intmaxLines, floatprecision) {
if(maxLines <= 0|| maxLines == Integer.MAX_VALUE) {
// Don't auto-size since there's no limit on lines.return;}
inttargetWidth = view.getWidth() - view.getPaddingLeft() - view.getPaddingRight();if(targetWidth <= 0) {
return;}
CharSequence text = view.getText();TransformationMethod method = view.getTransformationMethod();if(method != null) {
text = method.getTransformation(text,view);}
Context context = view.getContext();Resources r = Resources.getSystem();DisplayMetrics displayMetrics;floatsize = maxTextSize;floathigh = size;floatlow = 0;if(context != null) {
r = context.getResources();}
displayMetrics = r.getDisplayMetrics();paint.set(view.getPaint());paint.setTextSize(size);if((maxLines == 1&& paint.measureText(text,0,text.length()) > targetWidth)
|| getLineCount(text,paint,size,targetWidth,displayMetrics) > maxLines) {
size = getAutofitTextSize(text,paint,targetWidth,maxLines,low,high,precision,displayMetrics);}
if(size < minTextSize) {
size = minTextSize;}
view.setTextSize(TypedValue.COMPLEX_UNIT_PX,size);}
/*** Recursive binary search to find the best size for the text.*/private static floatgetAutofitTextSize(CharSequence text,TextPaint paint,floattargetWidth, intmaxLines, floatlow, floathigh, floatprecision,DisplayMetrics displayMetrics) {
floatmid = (low + high) / 2.0f;intlineCount = 1;StaticLayout layout = null;paint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,mid,displayMetrics));if(maxLines != 1) {
layout = newStaticLayout(text,paint,(int)targetWidth,Layout.Alignment.ALIGN_NORMAL,1.0f,0.0f, true);lineCount = layout.getLineCount();}
if(SPEW) Log.d(TAG,"low="+ low + " high="+ high + " mid="+ mid +
" target="+ targetWidth + " maxLines="+ maxLines + " lineCount="+ lineCount);if(lineCount > maxLines) {
// For the case that `text` has more newline characters than `maxLines`.if((high - low) < precision) {
returnlow;}
returngetAutofitTextSize(text,paint,targetWidth,maxLines,low,mid,precision,displayMetrics);}
else if(lineCount < maxLines) {
returngetAutofitTextSize(text,paint,targetWidth,maxLines,mid,high,precision,displayMetrics);}
else{
floatmaxLineWidth = 0;if(maxLines == 1) {
maxLineWidth = paint.measureText(text,0,text.length());} else{
for(inti = 0;i < lineCount;i++) {
if(layout.getLineWidth(i) > maxLineWidth) {
maxLineWidth = layout.getLineWidth(i);}
}
}
if((high - low) < precision) {
returnlow;} else if(maxLineWidth > targetWidth) {
returngetAutofitTextSize(text,paint,targetWidth,maxLines,low,mid,precision,displayMetrics);} else if(maxLineWidth < targetWidth) {
returngetAutofitTextSize(text,paint,targetWidth,maxLines,mid,high,precision,displayMetrics);} else{
returnmid;}
}
}
private static intgetLineCount(CharSequence text,TextPaint paint, floatsize, floatwidth,DisplayMetrics displayMetrics) {
paint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,size,displayMetrics));StaticLayout layout = newStaticLayout(text,paint,(int)width,Layout.Alignment.ALIGN_NORMAL,1.0f,0.0f, true);returnlayout.getLineCount();}
private static intgetMaxLines(TextView view) {
intmaxLines = -1;// No limit (Integer.MAX_VALUE also means no limit)TransformationMethod method = view.getTransformationMethod();if(method != null&& method instanceofSingleLineTransformationMethod) {
maxLines = 1;}
else if(Build.VERSION.SDK_INT>= Build.VERSION_CODES.JELLY_BEAN) {
// setMaxLines() and getMaxLines() are only available on android-16+maxLines = view.getMaxLines();}
returnmaxLines;}
// AttributesprivateTextView mTextView;privateTextPaint mPaint;/*** Original textSize of the TextView.*/private floatmTextSize;private intmMaxLines;private floatmMinTextSize;private floatmMaxTextSize;private floatmPrecision;private booleanmEnabled;private booleanmIsAutofitting;privateArrayList mListeners;privateTextWatcher mTextWatcher= newAutofitTextWatcher();privateView.OnLayoutChangeListener mOnLayoutChangeListener=
newAutofitOnLayoutChangeListener();privateAutofitHelper(TextView view) {
finalContext context = view.getContext();floatscaledDensity = context.getResources().getDisplayMetrics().scaledDensity;mTextView= view;mPaint= newTextPaint();setRawTextSize(view.getTextSize());mMaxLines= getMaxLines(view);mMinTextSize= scaledDensity * DEFAULT_MIN_TEXT_SIZE;mMaxTextSize= mTextSize;mPrecision= DEFAULT_PRECISION;}
/*** Adds an {@linkOnTextSizeChangeListener} to the list of those whose methods are called* whenever the {@linkTextView}'s {@codetextSize} changes.*/publicAutofitHelper addOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
if(mListeners== null) {
mListeners= newArrayList();}
mListeners.add(listener);return this;}
/*** Removes the specified {@linkOnTextSizeChangeListener} from the list of those whose methods* are called whenever the {@linkTextView}'s {@codetextSize} changes.*/publicAutofitHelper removeOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
if(mListeners!= null) {
mListeners.remove(listener);}
return this;}
/*** Returns the amount of precision used to calculate the correct text size to fit within its* bounds.*/public floatgetPrecision() {
returnmPrecision;}
/*** Set the amount of precision used to calculate the correct text size to fit within its* bounds. Lower precision is more precise and takes more time.**@paramprecisionThe amount of precision.*/publicAutofitHelper setPrecision(floatprecision) {
if(mPrecision!= precision) {
mPrecision= precision;autofit();}
return this;}
/*** Returns the minimum size (in pixels) of the text.*/public floatgetMinTextSize() {
returnmMinTextSize;}
/*** Set the minimum text size to the given value, interpreted as "scaled pixel" units. This size* is adjusted based on the current density and user font size preference.**@paramsizeThe scaled pixel size.**@attrref me.grantland.R.styleable#AutofitTextView_minTextSize*/publicAutofitHelper setMinTextSize(floatsize) {
returnsetMinTextSize(TypedValue.COMPLEX_UNIT_SP,size);}
/*** Set the minimum text size to a given unit and value. See TypedValue for the possible* dimension units.**@paramunitThe desired dimension unit.*@paramsizeThe desired size in the given units.**@attrref me.grantland.R.styleable#AutofitTextView_minTextSize*/publicAutofitHelper setMinTextSize(intunit, floatsize) {
Context context = mTextView.getContext();Resources r = Resources.getSystem();if(context != null) {
r = context.getResources();}
setRawMinTextSize(TypedValue.applyDimension(unit,size,r.getDisplayMetrics()));return this;}
private voidsetRawMinTextSize(floatsize) {
if(size != mMinTextSize) {
mMinTextSize= size;autofit();}
}
/*** Returns the maximum size (in pixels) of the text.*/public floatgetMaxTextSize() {
returnmMaxTextSize;}
/*** Set the maximum text size to the given value, interpreted as "scaled pixel" units. This size* is adjusted based on the current density and user font size preference.**@paramsizeThe scaled pixel size.**@attrref android.R.styleable#TextView_textSize*/publicAutofitHelper setMaxTextSize(floatsize) {
returnsetMaxTextSize(TypedValue.COMPLEX_UNIT_SP,size);}
/*** Set the maximum text size to a given unit and value. See TypedValue for the possible* dimension units.**@paramunitThe desired dimension unit.*@paramsizeThe desired size in the given units.**@attrref android.R.styleable#TextView_textSize*/publicAutofitHelper setMaxTextSize(intunit, floatsize) {
Context context = mTextView.getContext();Resources r = Resources.getSystem();if(context != null) {
r = context.getResources();}
setRawMaxTextSize(TypedValue.applyDimension(unit,size,r.getDisplayMetrics()));return this;}
private voidsetRawMaxTextSize(floatsize) {
if(size != mMaxTextSize) {
mMaxTextSize= size;autofit();}
}
/***@seeTextView#getMaxLines()*/public intgetMaxLines() {
returnmMaxLines;}
/***@seeTextView#setMaxLines(int)*/publicAutofitHelper setMaxLines(intlines) {
if(mMaxLines!= lines) {
mMaxLines= lines;autofit();}
return this;}
/*** Returns whether or not automatically resizing text is enabled.*/public booleanisEnabled() {
returnmEnabled;}
/*** Set the enabled state of automatically resizing text.*/publicAutofitHelper setEnabled(booleanenabled) {
if(mEnabled!= enabled) {
mEnabled= enabled;if(enabled) {
mTextView.addTextChangedListener(mTextWatcher);mTextView.addOnLayoutChangeListener(mOnLayoutChangeListener);autofit();} else{
mTextView.removeTextChangedListener(mTextWatcher);mTextView.removeOnLayoutChangeListener(mOnLayoutChangeListener);mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX,mTextSize);}
}
return this;}
/*** Returns the original text size of the View.**@seeTextView#getTextSize()*/public floatgetTextSize() {
returnmTextSize;}
/*** Set the original text size of the View.**@seeTextView#setTextSize(float)*/public voidsetTextSize(floatsize) {
setTextSize(TypedValue.COMPLEX_UNIT_SP,size);}
/*** Set the original text size of the View.**@seeTextView#setTextSize(int, float)*/public voidsetTextSize(intunit, floatsize) {
if(mIsAutofitting) {
// We don't want to update the TextView's actual textSize while we're autofitting// since it'd get set to the autofitTextSizereturn;}
Context context = mTextView.getContext();Resources r = Resources.getSystem();if(context != null) {
r = context.getResources();}
setRawTextSize(TypedValue.applyDimension(unit,size,r.getDisplayMetrics()));}
private voidsetRawTextSize(floatsize) {
if(mTextSize!= size) {
mTextSize= size;}
}
private voidautofit() {
floatoldTextSize = mTextView.getTextSize();floattextSize;mIsAutofitting= true;autofit(mTextView,mPaint,mMinTextSize,mMaxTextSize,mMaxLines,mPrecision);mIsAutofitting= false;textSize = mTextView.getTextSize();if(textSize != oldTextSize) {
sendTextSizeChange(textSize,oldTextSize);}
}
private voidsendTextSizeChange(floattextSize, floatoldTextSize) {
if(mListeners== null) {
return;}
for(OnTextSizeChangeListener listener : mListeners) {
listener.onTextSizeChange(textSize,oldTextSize);}
}
private classAutofitTextWatcher implementsTextWatcher {
@Overridepublic voidbeforeTextChanged(CharSequence charSequence, intstart, intcount, intafter) {
// do nothing}
@Overridepublic voidonTextChanged(CharSequence charSequence, intstart, intbefore, intcount) {
autofit();}
@Overridepublic voidafterTextChanged(Editable editable) {
// do nothing}
}
private classAutofitOnLayoutChangeListener implementsView.OnLayoutChangeListener {
@Overridepublic voidonLayoutChange(View view, intleft, inttop, intright, intbottom,intoldLeft, intoldTop, intoldRight, intoldBottom) {
autofit();}
}
/*** When an object of a type is attached to an {@codeAutofitHelper}, its methods will be called* when the {@codetextSize} is changed.*/public interfaceOnTextSizeChangeListener {
/*** This method is called to notify you that the size of the text has changed to* {@codetextSize} from {@codeoldTextSize}.*/public voidonTextSizeChange(floattextSize, floatoldTextSize);}
}