/* * Copyright (C) 2014 Grantland Chew * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.text.method.TransformationMethod; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import android.widget.TextView; /** * A TextView that resizes it's text to be no larger than the width of the view. * * @author Grantland Chew */ public class AutoFitTextView extends TextView { private static final String TAG = "AutoFitTextView"; private static final boolean SPEW = false; // Minimum size of the text in pixels private static final int DEFAULT_MIN_TEXT_SIZE = 8; //sp // How precise we want to be when reaching the target textWidth size private static final float PRECISION = 0.5f; // Attributes private boolean mSizeToFit; private int mMaxLines; private float mMinTextSize; private float mMaxTextSize; private float mPrecision; private TextPaint mPaint; public AutoFitTextView(Context context) { super(context); init(context, null, 0); } public AutoFitTextView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, 0); } public AutoFitTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs, defStyle); } private void init(Context context, AttributeSet attrs, int defStyle) { float scaledDensity = context.getResources().getDisplayMetrics().scaledDensity; boolean sizeToFit = true; int minTextSize = (int) scaledDensity * DEFAULT_MIN_TEXT_SIZE; float precision = PRECISION; if (attrs != null) { 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(); } mPaint = new TextPaint(); setSizeToFit(sizeToFit); setRawTextSize(super.getTextSize()); setRawMinTextSize(minTextSize); setPrecision(precision); } // Getters and Setters /** * @return whether or not the text will be automatically resized to fit its constraints. */ public boolean isSizeToFit() { return mSizeToFit; } /** * Sets the property of this field (singleLine, to automatically resize the text to fit its constraints. */ public void setSizeToFit() { setSizeToFit(true); } /** * If true, the text will automatically be resized to fit its constraints; if false, it will * act like a normal TextView. * * @param sizeToFit */ public void setSizeToFit(boolean sizeToFit) { mSizeToFit = sizeToFit; refitText(); } /** * {@inheritDoc} */ @Override public float getTextSize() { return mMaxTextSize; } /** * {@inheritDoc} */ @Override public void setTextSize(int unit, float size) { Context context = getContext(); Resources r = Resources.getSystem(); if (context != null) { r = context.getResources(); } setRawTextSize(TypedValue.applyDimension(unit, size, r.getDisplayMetrics())); } private void setRawTextSize(float size) { if (size != mMaxTextSize) { mMaxTextSize = size; refitText(); } } /** * @return the minimum size (in pixels) of the text size in this AutofitTextView */ public float getMinTextSize() { return mMinTextSize; } /** * Set the minimum text size to a given unit and value. See TypedValue for the possible * dimension units. * * @param unit The desired dimension unit. * @param minSize The desired size in the given units. * * @attr ref me.grantland.R.styleable#AutofitTextView_minTextSize */ public void setMinTextSize(int unit, float minSize) { Context context = getContext(); Resources r = Resources.getSystem(); if (context != null) { r = context.getResources(); } setRawMinTextSize(TypedValue.applyDimension(unit, minSize, r.getDisplayMetrics())); } /** * 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. * * @param minSize The scaled pixel size. * * @attr ref me.grantland.R.styleable#AutofitTextView_minTextSize */ public void setMinTextSize(int minSize) { setMinTextSize(TypedValue.COMPLEX_UNIT_SP, minSize); } private void setRawMinTextSize(float minSize) { if (minSize != mMinTextSize) { mMinTextSize = minSize; refitText(); } } /** * @return the amount of precision used to calculate the correct text size to fit within it's * bounds. */ public float getPrecision() { return mPrecision; } /** * Set the amount of precision used to calculate the correct text size to fit within it's * bounds. Lower precision is more precise and takes more time. * * @param precision The amount of precision. */ public void setPrecision(float precision) { if (precision != mPrecision) { mPrecision = precision; refitText(); } } /** * {@inheritDoc} */ @Override public void setLines(int lines) { super.setLines(lines); mMaxLines = lines; refitText(); } /** * {@inheritDoc} */ @Override public int getMaxLines() { return mMaxLines; } /** * {@inheritDoc} */ @Override public void setMaxLines(int maxLines) { super.setMaxLines(maxLines); if (maxLines != mMaxLines) { mMaxLines = maxLines; refitText(); } } /** * Re size the font so the specified text fits in the text box assuming the text box is the * specified width. */ private void refitText() { if (!mSizeToFit) { return; } if (mMaxLines <= 0) { // Don't auto-size since there's no limit on lines. return; } CharSequence text = getText(); TransformationMethod method = getTransformationMethod(); if (method != null) { text = method.getTransformation(text, this); } int targetWidth = getWidth() - getPaddingLeft() - getPaddingRight(); if (targetWidth > 0) { Context context = getContext(); Resources r = Resources.getSystem(); DisplayMetrics displayMetrics; float size = mMaxTextSize; float high = size; float low = 0; if (context != null) { r = context.getResources(); } displayMetrics = r.getDisplayMetrics(); mPaint.set(getPaint()); mPaint.setTextSize(size); if ((mMaxLines == 1 && mPaint.measureText(text, 0, text.length()) > targetWidth) || getLineCount(text, mPaint, size, targetWidth, displayMetrics) > mMaxLines) { size = getTextSize(text, mPaint, targetWidth, mMaxLines, low, high, mPrecision, displayMetrics); } if (size < mMinTextSize) { size = mMinTextSize; } super.setTextSize(TypedValue.COMPLEX_UNIT_PX, size); } } /** * Recursive binary search to find the best size for the text */ private static float getTextSize(CharSequence text, TextPaint paint, float targetWidth, int maxLines, float low, float high, float precision, DisplayMetrics displayMetrics) { float mid = (low + high) / 2.0f; int lineCount = 1; StaticLayout layout = null; paint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, mid, displayMetrics)); if (maxLines != 1) { layout = new StaticLayout(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) { return getTextSize(text, paint, targetWidth, maxLines, low, mid, precision, displayMetrics); } else if (lineCount < maxLines) { return getTextSize(text, paint, targetWidth, maxLines, mid, high, precision, displayMetrics); } else { float maxLineWidth = 0; if (maxLines == 1) { maxLineWidth = paint.measureText(text, 0, text.length()); } else { for (int i = 0; i < lineCount; i++) { if (layout.getLineWidth(i) > maxLineWidth) { maxLineWidth = layout.getLineWidth(i); } } } if ((high - low) < precision) { return low; } else if (maxLineWidth > targetWidth) { return getTextSize(text, paint, targetWidth, maxLines, low, mid, precision, displayMetrics); } else if (maxLineWidth < targetWidth) { return getTextSize(text, paint, targetWidth, maxLines, mid, high, precision, displayMetrics); } else { return mid; } } } private static int getLineCount(CharSequence text, TextPaint paint, float size, float width, DisplayMetrics displayMetrics) { paint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, size, displayMetrics)); StaticLayout layout = new StaticLayout(text, paint, (int)width, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true); return layout.getLineCount(); } @Override protected void onTextChanged(final CharSequence text, final int start, final int lengthBefore, final int lengthAfter) { super.onTextChanged(text, start, lengthBefore, lengthAfter); refitText(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (w != oldw) { refitText(); } } }