很多时候,我们需要将 TextView 的字体自动适应给定的边界。
可悲的是,尽管有很多线程和帖子(和建议的解决方案)谈论这个问题(例如这里、这里和这里),但它们都没有真正运作良好。
这就是为什么,我决定测试它们中的每一个,直到找到真正的交易。
我认为这样一个 textView 的要求应该是:
应该允许使用任何字体、字体、样式和字符集。
应该同时处理宽度和高度
除非文本由于限制而无法截断,否则不会截断,我们已经给出了它(例如:文本太长,可用尺寸太小)。但是,如果我们愿意,我们可以请求水平/垂直滚动条,仅适用于这些情况。
应该允许多行或单行。在多行的情况下,允许最大和最小行。
计算不应该很慢。使用循环寻找最佳尺寸?至少优化它并且不要每次将采样增加 1。
在多行的情况下,应该允许更喜欢调整大小或使用更多行,和/或允许使用“\n”字符自己选择行。
我已经尝试了很多示例(包括我写过的链接的示例),并且我还尝试修改它们以处理我已经讨论过的案例,但没有一个真正起作用。
我制作了一个示例项目,让我可以直观地查看 TextView 是否正确自动适配。
目前,我的示例项目仅随机化文本(英文字母加数字)和 textView 的大小,并让它保持单行,但即使这在我尝试过的任何示例上都不能正常工作。
这是代码(也可在此处获得):
res/layout/activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:text="Button" /> <FrameLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@+id/button1" android:layout_alignParentLeft="true" android:background="#ffff0000" android:layout_alignParentRight="true" android:id="@+id/container" android:layout_alignParentTop="true" /> </RelativeLayout>
src/.../MainActivity.java
public class MainActivity extends Activity { private final Random _random =new Random(); private static final String ALLOWED_CHARACTERS ="qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890"; @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final ViewGroup container=(ViewGroup)findViewById(R.id.container); findViewById(R.id.button1).setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { container.removeAllViews(); final int maxWidth=container.getWidth(); final int maxHeight=container.getHeight(); final FontFitTextView fontFitTextView=new FontFitTextView(MainActivity.this); final int width=_random.nextInt(maxWidth)+1; final int height=_random.nextInt(maxHeight)+1; fontFitTextView.setLayoutParams(new LayoutParams(width,height)); fontFitTextView.setSingleLine(); fontFitTextView.setBackgroundColor(0xff00ff00); final String text=getRandomText(); fontFitTextView.setText(text); container.addView(fontFitTextView); Log.d("DEBUG","width:"+width+" height:"+height+" text:"+text); } }); } private String getRandomText() { final int textLength=_random.nextInt(20)+1; final StringBuilder builder=new StringBuilder(); for(int i=0;i<textLength;++i) builder.append(ALLOWED_CHARACTERS.charAt(_random.nextInt(ALLOWED_CHARACTERS.length()))); return builder.toString(); } }
有谁知道这个常见问题的解决方案实际上有效吗?
即使是一个功能比我写的要少得多的解决方案,例如一个只有恒定数量的文本行,并根据其大小调整其字体,但绝不会出现奇怪的故障并且文本也会出现的解决方案与其可用空间相比大/小。
由于这是一个如此重要的 TextView,我决定发布一个库,以便每个人都可以轻松地使用它,并为它做出贡献, 在这里 。
在此处的简单修复,此代码还负责处理android:drawableLeft、android:drawableRight和标记。android:drawableTop``android:drawableBottom
android:drawableLeft
android:drawableRight
android:drawableTop``android:drawableBottom
已经修改了 M-WaJeEh 的答案,以考虑到侧面的复合绘图。
方法getCompoundPaddingXXXX()返回。padding of the view + drawable space参见例如:TextView.getCompoundPaddingLeft()
getCompoundPaddingXXXX()
padding of the view + drawable space
问题: 这修复了可用于文本的 TextView 空间的宽度和高度的测量。如果我们不考虑drawable的大小,它会被忽略并且文本最终会与drawable重叠。
更新片段adjustTextSize(String):
adjustTextSize(String)
private void adjustTextSize(final String text) { if (!mInitialized) { return; } int heightLimit = getMeasuredHeight() - getCompoundPaddingBottom() - getCompoundPaddingTop(); mWidthLimit = getMeasuredWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight(); mAvailableSpaceRect.right = mWidthLimit; mAvailableSpaceRect.bottom = heightLimit; int maxTextSplits = text.split(" ").length; AutoResizeTextView.super.setMaxLines(Math.min(maxTextSplits, mMaxLines)); super.setTextSize( TypedValue.COMPLEX_UNIT_PX, binarySearch((int) mMinTextSize, (int) mMaxTextSize, mSizeTester, mAvailableSpaceRect)); }
我在这里的回答应该会让你开心Auto Scale TextView Text to Fit inBounds
更新:以下代码也满足理想 AutoScaleTextView 的要求,如下所述:Auto-fit TextView for Android并被标记为获胜者。
更新 2:添加了对 maxlines 的支持,现在在 API 级别 16 之前可以正常工作。
更新 3:android:drawableLeft添加了对、和标签的支持android:drawableRight,这要感谢 MartinH在此处的简单修复。android:drawableTop``android:drawableBottom
我的要求有点不同。我需要一种有效的方法来调整大小,因为我正在为一个整数设置动画,可能TextView在 2 秒内从 0 到 ~4000,我想相应地调整大小。我的解决方案有点不同。这是最终结果的样子:
TextView
以及产生它的代码:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:padding="16dp" > <com.vj.widgets.AutoResizeTextView android:layout_width="match_parent" android:layout_height="100dp" android:ellipsize="none" android:maxLines="2" android:text="Auto Resized Text, max 2 lines" android:textSize="100sp" /> <!-- maximum size --> <com.vj.widgets.AutoResizeTextView android:layout_width="match_parent" android:layout_height="100dp" android:ellipsize="none" android:gravity="center" android:maxLines="1" android:text="Auto Resized Text, max 1 line" android:textSize="100sp" /> <!-- maximum size --> <com.vj.widgets.AutoResizeTextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Auto Resized Text" android:textSize="500sp" /> <!-- maximum size --> </LinearLayout>
最后是java代码:
import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.RectF; 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; public class AutoResizeTextView extends TextView { 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 */ public int onTestSize(int suggestedSize, RectF availableSpace); } private RectF mTextRect = new RectF(); private RectF mAvailableSpaceRect; private SparseIntArray mTextCachedSizes; private TextPaint mPaint; private float mMaxTextSize; private float mSpacingMult = 1.0f; private float mSpacingAdd = 0.0f; private float mMinTextSize = 20; private int mWidthLimit; private static final int NO_LINE_LIMIT = -1; private int mMaxLines; private boolean mEnableSizeCache = true; private boolean mInitiallized; public AutoResizeTextView(Context context) { super(context); initialize(); } public AutoResizeTextView(Context context, AttributeSet attrs) { super(context, attrs); initialize(); } public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initialize(); } private void initialize() { mPaint = new TextPaint(getPaint()); mMaxTextSize = getTextSize(); mAvailableSpaceRect = new RectF(); mTextCachedSizes = new SparseIntArray(); if (mMaxLines == 0) { // no value was assigned during construction mMaxLines = NO_LINE_LIMIT; } mInitiallized = true; } @Override public void setText(final CharSequence text, BufferType type) { super.setText(text, type); adjustTextSize(text.toString()); } @Override public void setTextSize(float size) { mMaxTextSize = size; mTextCachedSizes.clear(); adjustTextSize(getText().toString()); } @Override public void setMaxLines(int maxlines) { super.setMaxLines(maxlines); mMaxLines = maxlines; reAdjust(); } public int getMaxLines() { return mMaxLines; } @Override public void setSingleLine() { super.setSingleLine(); mMaxLines = 1; reAdjust(); } @Override public void setSingleLine(boolean singleLine) { super.setSingleLine(singleLine); if (singleLine) { mMaxLines = 1; } else { mMaxLines = NO_LINE_LIMIT; } reAdjust(); } @Override public void setLines(int lines) { super.setLines(lines); mMaxLines = lines; reAdjust(); } @Override public void setTextSize(int unit, float size) { Context c = getContext(); Resources r; if (c == null) r = Resources.getSystem(); else r = c.getResources(); mMaxTextSize = TypedValue.applyDimension(unit, size, r.getDisplayMetrics()); mTextCachedSizes.clear(); adjustTextSize(getText().toString()); } @Override public void setLineSpacing(float add, float mult) { super.setLineSpacing(add, mult); mSpacingMult = mult; mSpacingAdd = add; } /** * Set the lower text size limit and invalidate the view * * @param minTextSize */ public void setMinTextSize(float minTextSize) { mMinTextSize = minTextSize; reAdjust(); } private void reAdjust() { adjustTextSize(getText().toString()); } private void adjustTextSize(String string) { if (!mInitiallized) { return; } int startSize = (int) mMinTextSize; int heightLimit = getMeasuredHeight() - getCompoundPaddingBottom() - getCompoundPaddingTop(); mWidthLimit = getMeasuredWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight(); mAvailableSpaceRect.right = mWidthLimit; mAvailableSpaceRect.bottom = heightLimit; super.setTextSize( TypedValue.COMPLEX_UNIT_PX, efficientTextSizeSearch(startSize, (int) mMaxTextSize, mSizeTester, mAvailableSpaceRect)); } private final SizeTester mSizeTester = new SizeTester() { @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public int onTestSize(int suggestedSize, RectF availableSPace) { mPaint.setTextSize(suggestedSize); String text = getText().toString(); boolean singleline = getMaxLines() == 1; if (singleline) { mTextRect.bottom = mPaint.getFontSpacing(); mTextRect.right = mPaint.measureText(text); } else { StaticLayout layout = new StaticLayout(text, mPaint, mWidthLimit, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true); // return early if we have more lines if (getMaxLines() != NO_LINE_LIMIT && layout.getLineCount() > getMaxLines()) { return 1; } mTextRect.bottom = layout.getHeight(); int maxWidth = -1; for (int i = 0; i < layout.getLineCount(); i++) { if (maxWidth < layout.getLineWidth(i)) { maxWidth = (int) layout.getLineWidth(i); } } mTextRect.right = maxWidth; } mTextRect.offsetTo(0, 0); if (availableSPace.contains(mTextRect)) { // may be too small, don't worry we will find the best match return -1; } else { // too big return 1; } } }; /** * 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 enableSizeCache(boolean enable) { mEnableSizeCache = enable; mTextCachedSizes.clear(); adjustTextSize(getText().toString()); } private int efficientTextSizeSearch(int start, int end, SizeTester sizeTester, RectF availableSpace) { if (!mEnableSizeCache) { return binarySearch(start, end, sizeTester, availableSpace); } String text = getText().toString(); int key = text == null ? 0 : text.length(); int size = mTextCachedSizes.get(key); if (size != 0) { return size; } size = binarySearch(start, end, sizeTester, availableSpace); mTextCachedSizes.put(key, size); return size; } private static int binarySearch(int start, int end, SizeTester sizeTester, RectF availableSpace) { int lastBest = start; int lo = start; int hi = end - 1; int mid = 0; while (lo <= hi) { mid = (lo + hi) >>> 1; 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(int width, int height, int oldwidth, int oldheight) { mTextCachedSizes.clear(); super.onSizeChanged(width, height, oldwidth, oldheight); if (width != oldwidth || height != oldheight) { reAdjust(); } } }
我修改了你的测试用例:
@Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final ViewGroup container = (ViewGroup) findViewById(R.id.container); findViewById(R.id.button1).setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { container.removeAllViews(); final int maxWidth = container.getWidth(); final int maxHeight = container.getHeight(); final AutoResizeTextView fontFitTextView = new AutoResizeTextView(MainActivity.this); final int width = _random.nextInt(maxWidth) + 1; final int height = _random.nextInt(maxHeight) + 1; fontFitTextView.setLayoutParams(new FrameLayout.LayoutParams( width, height)); int maxLines = _random.nextInt(4) + 1; fontFitTextView.setMaxLines(maxLines); fontFitTextView.setTextSize(500);// max size fontFitTextView.enableSizeCache(false); fontFitTextView.setBackgroundColor(0xff00ff00); final String text = getRandomText(); fontFitTextView.setText(text); container.addView(fontFitTextView); Log.d("DEBUG", "width:" + width + " height:" + height + " text:" + text + " maxLines:" + maxLines); } }); }
我根据 android 开发人员的 要求在此处发布代码:
最终效果:
示例布局文件:
和Java代码:
import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.RectF; 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; public class AutoResizeTextView extends TextView { 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 */ public int onTestSize(int suggestedSize, RectF availableSpace); } private RectF mTextRect = new RectF(); private RectF mAvailableSpaceRect; private SparseIntArray mTextCachedSizes; private TextPaint mPaint; private float mMaxTextSize; private float mSpacingMult = 1.0f; private float mSpacingAdd = 0.0f; private float mMinTextSize = 20; private int mWidthLimit; private static final int NO_LINE_LIMIT = -1; private int mMaxLines; private boolean mEnableSizeCache = true; private boolean mInitializedDimens; public AutoResizeTextView(Context context) { super(context); initialize(); } public AutoResizeTextView(Context context, AttributeSet attrs) { super(context, attrs); initialize(); } public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initialize(); } private void initialize() { mPaint = new TextPaint(getPaint()); mMaxTextSize = getTextSize(); mAvailableSpaceRect = new RectF(); mTextCachedSizes = new SparseIntArray(); if (mMaxLines == 0) { // no value was assigned during construction mMaxLines = NO_LINE_LIMIT; } } @Override public void setTextSize(float size) { mMaxTextSize = size; mTextCachedSizes.clear(); adjustTextSize(); } @Override public void setMaxLines(int maxlines) { super.setMaxLines(maxlines); mMaxLines = maxlines; adjustTextSize(); } public int getMaxLines() { return mMaxLines; } @Override public void setSingleLine() { super.setSingleLine(); mMaxLines = 1; adjustTextSize(); } @Override public void setSingleLine(boolean singleLine) { super.setSingleLine(singleLine); if (singleLine) { mMaxLines = 1; } else { mMaxLines = NO_LINE_LIMIT; } adjustTextSize(); } @Override public void setLines(int lines) { super.setLines(lines); mMaxLines = lines; adjustTextSize(); } @Override public void setTextSize(int unit, float size) { Context c = getContext(); Resources r; if (c == null) r = Resources.getSystem(); else r = c.getResources(); mMaxTextSize = TypedValue.applyDimension(unit, size, r.getDisplayMetrics()); mTextCachedSizes.clear(); adjustTextSize(); } @Override public void setLineSpacing(float add, float mult) { super.setLineSpacing(add, mult); mSpacingMult = mult; mSpacingAdd = add; } /** * Set the lower text size limit and invalidate the view * * @param minTextSize */ public void setMinTextSize(float minTextSize) { mMinTextSize = minTextSize; adjustTextSize(); } private void adjustTextSize() { if (!mInitializedDimens) { return; } int startSize = (int) mMinTextSize; int heightLimit = getMeasuredHeight() - getCompoundPaddingBottom() - getCompoundPaddingTop(); mWidthLimit = getMeasuredWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight(); mAvailableSpaceRect.right = mWidthLimit; mAvailableSpaceRect.bottom = heightLimit; super.setTextSize( TypedValue.COMPLEX_UNIT_PX, efficientTextSizeSearch(startSize, (int) mMaxTextSize, mSizeTester, mAvailableSpaceRect)); } private final SizeTester mSizeTester = new SizeTester() { @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public int onTestSize(int suggestedSize, RectF availableSPace) { mPaint.setTextSize(suggestedSize); String text = getText().toString(); boolean singleline = getMaxLines() == 1; if (singleline) { mTextRect.bottom = mPaint.getFontSpacing(); mTextRect.right = mPaint.measureText(text); } else { StaticLayout layout = new StaticLayout(text, mPaint, mWidthLimit, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true); // Return early if we have more lines if (getMaxLines() != NO_LINE_LIMIT && layout.getLineCount() > getMaxLines()) { return 1; } mTextRect.bottom = layout.getHeight(); int maxWidth = -1; for (int i = 0; i < layout.getLineCount(); i++) { if (maxWidth < layout.getLineWidth(i)) { maxWidth = (int) layout.getLineWidth(i); } } mTextRect.right = maxWidth; } mTextRect.offsetTo(0, 0); if (availableSPace.contains(mTextRect)) { // May be too small, don't worry we will find the best match return -1; } else { // too big return 1; } } }; /** * 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 enableSizeCache(boolean enable) { mEnableSizeCache = enable; mTextCachedSizes.clear(); adjustTextSize(getText().toString()); } private int efficientTextSizeSearch(int start, int end, SizeTester sizeTester, RectF availableSpace) { if (!mEnableSizeCache) { return binarySearch(start, end, sizeTester, availableSpace); } int key = getText().toString().length(); int size = mTextCachedSizes.get(key); if (size != 0) { return size; } size = binarySearch(start, end, sizeTester, availableSpace); mTextCachedSizes.put(key, size); return size; } private static int binarySearch(int start, int end, SizeTester sizeTester, RectF availableSpace) { int lastBest = start; int lo = start; int hi = end - 1; int mid = 0; while (lo <= hi) { mid = (lo + hi) >>> 1; 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 the 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); adjustTextSize(); } @Override protected void onSizeChanged(int width, int height, int oldwidth, int oldheight) { mInitializedDimens = true; mTextCachedSizes.clear(); super.onSizeChanged(width, height, oldwidth, oldheight); if (width != oldwidth || height != oldheight) { adjustTextSize(); } } }
警告:
不过要注意Android 3.1 (Honeycomb)中这个已解决的 错误。