自动适应Android的TextView
背景
很多时候,我们需要将TextView的字体自动适应给定的边界。
问题
可悲的是,即使有许multithreading和post(和build议的解决scheme)在谈论这个问题(例如这里 , 这里和这里 ),但其中没有一个能够很好地工作。
这就是为什么,我决定testing他们每一个,直到我find真正的交易。
我认为这样的textView的要求应该是:
-
应该允许使用任何字体,字体,样式和字符集。
-
应该处理宽度和高度
-
除非由于限制,我们已经给予它(例如:太长的文本,太小的可用大小),除非文本不适合,否则不要截断。 但是,如果我们愿意的话,我们可以请求横向/纵向滚动条,只是针对这些情况。
-
应该允许多线或单线。 在多线的情况下,允许最大和最小线。
-
计算速度不宜太慢。 使用循环find最佳尺寸? 至less优化它,不要每次增加1次采样。
-
在多行的情况下,应该允许selectresize或使用更多的行,和/或允许使用“\ n”字符自己select行。
我试过了
我已经尝试了很多样本(包括那些链接,我已经写了),我也试图修改它们来处理这些案例,我已经提到过,但是没有一个真正起作用。
我做了一个示例项目,使我可以直观地看到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(); } }
这个问题
有人知道这个常见问题的解决scheme,实际上工作吗?
即使是一个解决scheme,我所写的东西也less得多,例如一个文本行数不变的字符,根据其大小调整字体,但却不会有奇怪的错误,并且文本也会得到大/小可用空间相比。
GitHub项目
既然这是一个非常重要的TextView,我决定发布一个库,这样每个人都可以轻松地使用它,并为之做出贡献。
感谢MartinH 在这里的简单修复,这段代码还照顾了android:drawableLeft
, android:drawableRight
, android:drawableTop
和android:drawableBottom
标签。
我的答案在这里应该让你快乐自动缩放TextView文本以适合边界
我修改了你的testing用例:
@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开发者的请求发布代码:
最终效果:
示例布局文件:
<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 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)的这个已解决的 bug 。
我已经修改了M-WaJeEh的答案,以考虑侧面的复合绘图。
getCompoundPaddingXXXX()
方法返回padding of the view + drawable space
。 看例如: TextView.getCompoundPaddingLeft()
问题:这将修复文本可用的TextView空间的宽度和高度的度量。 如果我们不考虑可绘制的大小,它将被忽略,并且文本将最终与drawable重叠。
更新的段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)); }
好吧,我已经使用了上周大规模重写我的代码,以准确地适合您的testing。 您现在可以复制这个1:1,它会立即工作 – 包括setSingleLine()
。 请记住调整MIN_TEXT_SIZE
和MAX_TEXT_SIZE
如果你想要极端的价值。
融合algorithm如下所示:
for (float testSize; (upperTextSize - lowerTextSize) > mThreshold;) { // Go to the mean value... testSize = (upperTextSize + lowerTextSize) / 2; // ... inflate the dummy TextView by setting a scaled textSize and the text... mTestView.setTextSize(TypedValue.COMPLEX_UNIT_SP, testSize / mScaledDensityFactor); mTestView.setText(text); // ... call measure to find the current values that the text WANTS to occupy mTestView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); int tempHeight = mTestView.getMeasuredHeight(); // ... decide whether those values are appropriate. if (tempHeight >= targetFieldHeight) { upperTextSize = testSize; // Font is too big, decrease upperSize } else { lowerTextSize = testSize; // Font is too small, increase lowerSize } }
全class可以在这里find。
结果现在非常灵活。 这在xml中的作用就像这样:
<com.example.myProject.AutoFitText android:id="@+id/textView" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="4" android:text="@string/LoremIpsum" />
…以及像你的testing一样编程。
我真的希望你现在可以使用它。 您现在可以调用setText(CharSequence text)
来顺便使用它。 class级照顾极其罕见的例外,应该坚如磐石。 algorithm不支持的唯一的东西是:
- 调用
setMaxLines(x)
,其中x >= 2
但是,如果您愿意,我已经添加了大量的意见来帮助您构build这个function!
请注意:
如果你正确地使用这个,而不是把它限制在一个单独的行中,那么你可能会像前面提到的那样破坏单词。 这是一个Androidfunction , 而不是 AutoFitText
的错误。 对于TextView来说,Android总是会打破太长的文字,这实际上相当方便。 如果你想在这里介入,请参阅我在203行开始的注释和代码。我已经写了一个足够的分裂和对你的认可,所有你今后需要做的就是把这些词分开,然后根据你的意愿进行修改。
总之:你应该高度考虑重写你的testing,以支持空间字符,如下所示:
final Random _random = new Random(); final String ALLOWED_CHARACTERS = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890"; final int textLength = _random.nextInt(80) + 20; final StringBuilder builder = new StringBuilder(); for (int i = 0; i < textLength; ++i) { if (i % 7 == 0 && i != 0) { builder.append(" "); } builder.append(ALLOWED_CHARACTERS.charAt(_random.nextInt(ALLOWED_CHARACTERS.length()))); } ((AutoFitText) findViewById(R.id.textViewMessage)).setText(builder.toString());
这将产生非常美丽的(更现实的)结果。
你会发现评论,以及让你开始在这个问题。
祝你好运,最好的问候
我的要求是
- 点击ScalableTextView
- 打开一个listActivity并显示各种长度的string项目。
- 从列表中select一个文本。
- 在另一个活动中将文本设回到ScalableTextView。
我提到的链接: 自动缩放TextView文本以适合边界 (包括评论)和DialogTitle.java
我发现提供的解决scheme非常简单,但不会dynamic更改文本框的大小。 当从列表视图中select的文本长度大于 ScalableTextView
现有的文本长度时,它会很好用。 当select长度小于ScalableTextView
现有文本长度的文本时,不会增加文本的大小,从而显示较小大小的文本。
我修改了ScalableTextView.java来根据文本长度重新调整文本大小。 这是我的ScalableTextView.java
public class ScalableTextView extends TextView { float defaultTextSize = 0.0f; public ScalableTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setSingleLine(); setEllipsize(TruncateAt.END); defaultTextSize = getTextSize(); } public ScalableTextView(Context context, AttributeSet attrs) { super(context, attrs); setSingleLine(); setEllipsize(TruncateAt.END); defaultTextSize = getTextSize(); } public ScalableTextView(Context context) { super(context); setSingleLine(); setEllipsize(TruncateAt.END); defaultTextSize = getTextSize(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setTextSize(TypedValue.COMPLEX_UNIT_PX, defaultTextSize); super.onMeasure(widthMeasureSpec, heightMeasureSpec); final Layout layout = getLayout(); if (layout != null) { final int lineCount = layout.getLineCount(); if (lineCount > 0) { int ellipsisCount = layout.getEllipsisCount(lineCount - 1); while (ellipsisCount > 0) { final float textSize = getTextSize(); // textSize is already expressed in pixels setTextSize(TypedValue.COMPLEX_UNIT_PX, (textSize - 1)); super.onMeasure(widthMeasureSpec, heightMeasureSpec); ellipsisCount = layout.getEllipsisCount(lineCount - 1); } } } } }
快乐编码….
警告, Android 3 (Honeycomb)和Android 4.0 (冰淇淋三明治)
Android版本:3.1 – 4.04有一个bug,在TextView里面的setTextSize()只能在第一次(第一次调用)中工作。
在问题22493中描述了该错误:Android 4.0和Issue 17343中的 TextView高度错误 :在增加和减小HoneyComb上的文本大小后,button的高度和文本不会恢复到原始状态 。
解决方法是在更改大小之前将一个换行符添加到分配给TextView的文本中:
final String DOUBLE_BYTE_SPACE = "\u3000"; textView.append(DOUBLE_BYTE_SPACE);
我使用它在我的代码如下:
final String DOUBLE_BYTE_SPACE = "\u3000"; AutoResizeTextView textView = (AutoResizeTextView) view.findViewById(R.id.aTextView); String fixString = ""; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR1 && android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { fixString = DOUBLE_BYTE_SPACE; } textView.setText(fixString + "The text" + fixString);
我在文本的左侧和右侧添加这个“\ u3000”字符,以保持居中。 如果你把它alignment到左边,那么只能追加到右边。 当然,它也可以embedded到AutoResizeTextView小部件,但我想保持修复代码以外。
现在有一个官方的解决scheme来解决这个问题。 支持库26中提供了自动调整与Android O一起引入的TextView,并向下兼容Android 4.0。
https://developer.android.com/preview/features/autosizing-textview.html
我不知道为什么https://stackoverflow.com/a/42940171/47680其中也包括这些信息被pipe理员删除。;
将文本视图转换为图像,并在边界内缩放图像。
下面是如何将视图转换为图像的示例: 将视图转换为位图而不在Android中显示它?
问题是,你的文本将不可select,但它应该做的伎俩。 我没有尝试过,所以我不知道它会如何看(因为缩放)。
也许我是最后一个答案在这里,但我find了一个简单的解决scheme,并按我的预期工作。
我的解决scheme: 使TEXT_VIEW INVISIBLE – >缩小尺寸直到FIT PARENT – >然后使其可见。
要求:TextView是WRAP_CONTENT宽度,而不是边距开始或结束到父项。
public void autoResizeTextView(final TextView tvContent, final View parent, final float originalSize, final int maxLine) { tvContent.setVisibility(View.INVISIBLE); tvContent.getViewTreeObserver() .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { private float mOriginalSize = originalSize; private float mParentWidth = 0f; @Override public void onGlobalLayout() { // get parent width just only one time. if (mParentWidth == 0f) { mParentWidth = parent.getMeasuredWidth(); } if (tvContent.getMeasuredWidth() == mParentWidth && tvContent.getLineCount() > maxLine) { mOriginalSize -= 0.5f; tvContent.setTextSize(mOriginalSize); } else { tvContent.setVisibility(View.VISIBLE); tvContent.getViewTreeObserver() .removeOnGlobalLayoutListener(this); } } }); }
在调用TextView的setText()之后使用此方法。 也许它需要0.5秒的时间来查看。
下面是avalancha TextView,增加了自定义字体的function。
用法:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:foo="http://schemas.android.com/apk/res-auto" android:layout_width="wrap_content" android:layout_height="match_parent" > <de.meinprospekt.androidhd.view.AutoFitText android:layout_width="wrap_content" android:layout_height="10dp" android:text="Small Text" android:textColor="#FFFFFF" android:textSize="100sp" foo:customFont="fonts/Roboto-Light.ttf" /> </FrameLayout>
不要忘记添加:xmlns:foo =“http://schemas.android.com/apk/res-auto”。; 字体应该在资产火灾中
import java.util.ArrayList; import java.util.List; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Paint; import android.graphics.Typeface; import android.os.Build; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.widget.TextView; import de.meinprospekt.androidhd.R; import de.meinprospekt.androidhd.adapter.BrochuresHorizontalAdapter; import de.meinprospekt.androidhd.util.LOG; /** * https://stackoverflow.com/a/16174468/2075875 This class builds a new android Widget named AutoFitText which can be used instead of a TextView to * have the text font size in it automatically fit to match the screen width. Credits go largely to Dunni, gjpc, gregm and speedplane from * Stackoverflow, method has been (style-) optimized and rewritten to match android coding standards and our MBC. This version upgrades the original * "AutoFitTextView" to now also be adaptable to height and to accept the different TextView types (Button, TextClock etc.) * * @author pheuschk * @createDate: 18.04.2013 * * combined with: https://stackoverflow.com/a/7197867/2075875 */ @SuppressWarnings("unused") public class AutoFitText extends TextView { private static final String TAG = AutoFitText.class.getSimpleName(); /** Global min and max for text size. Remember: values are in pixels! */ private final int MIN_TEXT_SIZE = 10; private final int MAX_TEXT_SIZE = 400; /** Flag for singleLine */ private boolean mSingleLine = false; /** * A dummy {@link TextView} to test the text size without actually showing anything to the user */ private TextView mTestView; /** * A dummy {@link Paint} to test the text size without actually showing anything to the user */ private Paint mTestPaint; /** * Scaling factor for fonts. It's a method of calculating independently (!) from the actual density of the screen that is used so users have the * same experience on different devices. We will use DisplayMetrics in the Constructor to get the value of the factor and then calculate SP from * pixel values */ private float mScaledDensityFactor; /** * Defines how close we want to be to the factual size of the Text-field. Lower values mean higher precision but also exponentially higher * computing cost (more loop runs) */ private final float mThreshold = 0.5f; /** * Constructor for call without attributes --> invoke constructor with AttributeSet null * * @param context */ public AutoFitText(Context context) { this(context, null); } public AutoFitText(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public AutoFitText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs); } private void init(Context context, AttributeSet attrs) { //TextViewPlus part https://stackoverflow.com/a/7197867/2075875 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AutoFitText); String customFont = a.getString(R.styleable.AutoFitText_customFont); setCustomFont(context, customFont); a.recycle(); // AutoFitText part mScaledDensityFactor = context.getResources().getDisplayMetrics().scaledDensity; mTestView = new TextView(context); mTestPaint = new Paint(); mTestPaint.set(this.getPaint()); this.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { // make an initial call to onSizeChanged to make sure that refitText is triggered onSizeChanged(AutoFitText.this.getWidth(), AutoFitText.this.getHeight(), 0, 0); // Remove the LayoutListener immediately so we don't run into an infinite loop //AutoFitText.this.getViewTreeObserver().removeOnGlobalLayoutListener(this); removeOnGlobalLayoutListener(AutoFitText.this, this); } }); } public boolean setCustomFont(Context ctx, String asset) { Typeface tf = null; try { tf = Typeface.createFromAsset(ctx.getAssets(), asset); } catch (Exception e) { LOG.e(TAG, "Could not get typeface: "+e.getMessage()); return false; } setTypeface(tf); return true; } @SuppressLint("NewApi") public static void removeOnGlobalLayoutListener(View v, ViewTreeObserver.OnGlobalLayoutListener listener){ if (Build.VERSION.SDK_INT < 16) { v.getViewTreeObserver().removeGlobalOnLayoutListener(listener); } else { v.getViewTreeObserver().removeOnGlobalLayoutListener(listener); } } /** * Main method of this widget. Resizes the font so the specified text fits in the text box assuming the text box has the specified width. This is * done via a dummy text view that is refit until it matches the real target width and height up to a certain threshold factor * * @param targetFieldWidth The width that the TextView currently has and wants filled * @param targetFieldHeight The width that the TextView currently has and wants filled */ private void refitText(String text, int targetFieldWidth, int targetFieldHeight) { // Variables need to be visible outside the loops for later use. Remember size is in pixels float lowerTextSize = MIN_TEXT_SIZE; float upperTextSize = MAX_TEXT_SIZE; // Force the text to wrap. In principle this is not necessary since the dummy TextView // already does this for us but in rare cases adding this line can prevent flickering this.setMaxWidth(targetFieldWidth); // Padding should not be an issue since we never define it programmatically in this app // but just to to be sure we cut it off here targetFieldWidth = targetFieldWidth - this.getPaddingLeft() - this.getPaddingRight(); targetFieldHeight = targetFieldHeight - this.getPaddingTop() - this.getPaddingBottom(); // Initialize the dummy with some params (that are largely ignored anyway, but this is // mandatory to not get a NullPointerException) mTestView.setLayoutParams(new LayoutParams(targetFieldWidth, targetFieldHeight)); // maxWidth is crucial! Otherwise the text would never line wrap but blow up the width mTestView.setMaxWidth(targetFieldWidth); if (mSingleLine) { // the user requested a single line. This is very easy to do since we primarily need to // respect the width, don't have to break, don't have to measure... /*************************** Converging algorithm 1 ***********************************/ for (float testSize; (upperTextSize - lowerTextSize) > mThreshold;) { // Go to the mean value... testSize = (upperTextSize + lowerTextSize) / 2; mTestView.setTextSize(TypedValue.COMPLEX_UNIT_SP, testSize / mScaledDensityFactor); mTestView.setText(text); mTestView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); if (mTestView.getMeasuredWidth() >= targetFieldWidth) { upperTextSize = testSize; // Font is too big, decrease upperSize } else { lowerTextSize = testSize; // Font is too small, increase lowerSize } } /**************************************************************************************/ // In rare cases with very little letters and width > height we have vertical overlap! mTestView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); if (mTestView.getMeasuredHeight() > targetFieldHeight) { upperTextSize = lowerTextSize; lowerTextSize = MIN_TEXT_SIZE; /*************************** Converging algorithm 1.5 *****************************/ for (float testSize; (upperTextSize - lowerTextSize) > mThreshold;) { // Go to the mean value... testSize = (upperTextSize + lowerTextSize) / 2; mTestView.setTextSize(TypedValue.COMPLEX_UNIT_SP, testSize / mScaledDensityFactor); mTestView.setText(text); mTestView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); if (mTestView.getMeasuredHeight() >= targetFieldHeight) { upperTextSize = testSize; // Font is too big, decrease upperSize } else { lowerTextSize = testSize; // Font is too small, increase lowerSize } } /**********************************************************************************/ } } else { /*********************** Converging algorithm 2 ***************************************/ // Upper and lower size converge over time. As soon as they're close enough the loop // stops // TODO probe the algorithm for cost (ATM possibly O(n^2)) and optimize if possible for (float testSize; (upperTextSize - lowerTextSize) > mThreshold;) { // Go to the mean value... testSize = (upperTextSize + lowerTextSize) / 2; // ... inflate the dummy TextView by setting a scaled textSize and the text... mTestView.setTextSize(TypedValue.COMPLEX_UNIT_SP, testSize / mScaledDensityFactor); mTestView.setText(text); // ... call measure to find the current values that the text WANTS to occupy mTestView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); int tempHeight = mTestView.getMeasuredHeight(); // int tempWidth = mTestView.getMeasuredWidth(); // LOG.debug("Measured: " + tempWidth + "x" + tempHeight); // LOG.debug("TextSize: " + testSize / mScaledDensityFactor); // ... decide whether those values are appropriate. if (tempHeight >= targetFieldHeight) { upperTextSize = testSize; // Font is too big, decrease upperSize } else { lowerTextSize = testSize; // Font is too small, increase lowerSize } } /**************************************************************************************/ // It is possible that a single word is wider than the box. The Android system would // wrap this for us. But if you want to decide fo yourself where exactly to break or to // add a hyphen or something than you're going to want to implement something like this: mTestPaint.setTextSize(lowerTextSize); List<String> words = new ArrayList<String>(); for (String s : text.split(" ")) { Log.i("tag", "Word: " + s); words.add(s); } for (String word : words) { if (mTestPaint.measureText(word) >= targetFieldWidth) { List<String> pieces = new ArrayList<String>(); // pieces = breakWord(word, mTestPaint.measureText(word), targetFieldWidth); // Add code to handle the pieces here... } } } /** * We are now at most the value of threshold away from the actual size. To rather undershoot than overshoot use the lower value. To match * different screens convert to SP first. See {@link http://developer.android.com/guide/topics/resources/more-resources.html#Dimension} for * more details */ this.setTextSize(TypedValue.COMPLEX_UNIT_SP, lowerTextSize / mScaledDensityFactor); return; } /** * This method receives a call upon a change in text content of the TextView. Unfortunately it is also called - among others - upon text size * change which means that we MUST NEVER CALL {@link #refitText(String)} from this method! Doing so would result in an endless loop that would * ultimately result in a stack overflow and termination of the application * * So for the time being this method does absolutely nothing. If you want to notify the view of a changed text call {@link #setText(CharSequence)} */ @Override protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { // Super implementation is also intentionally empty so for now we do absolutely nothing here super.onTextChanged(text, start, lengthBefore, lengthAfter); } @Override protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { if (width != oldWidth && height != oldHeight) { refitText(this.getText().toString(), width, height); } } /** * This method is guaranteed to be called by {@link TextView#setText(CharSequence)} immediately. Therefore we can safely add our modifications * here and then have the parent class resume its work. So if text has changed you should always call {@link TextView#setText(CharSequence)} or * {@link TextView#setText(CharSequence, BufferType)} if you know whether the {@link BufferType} is normal, editable or spannable. Note: the * method will default to {@link BufferType#NORMAL} if you don't pass an argument. */ @Override public void setText(CharSequence text, BufferType type) { int targetFieldWidth = this.getWidth(); int targetFieldHeight = this.getHeight(); if (targetFieldWidth <= 0 || targetFieldHeight <= 0 || text.equals("")) { // Log.v("tag", "Some values are empty, AutoFitText was not able to construct properly"); } else { refitText(text.toString(), targetFieldWidth, targetFieldHeight); } super.setText(text, type); } /** * TODO add sensibility for {@link #setMaxLines(int)} invocations */ @Override public void setMaxLines(int maxLines) { // TODO Implement support for this. This could be relatively easy. The idea would probably // be to manipulate the targetHeight in the refitText-method and then have the algorithm do // its job business as usual. Nonetheless, remember the height will have to be lowered // dynamically as the font size shrinks so it won't be a walk in the park still if (maxLines == 1) { this.setSingleLine(true); } else { throw new UnsupportedOperationException("MaxLines != 1 are not implemented in AutoFitText yet, use TextView instead"); } } @Override public void setSingleLine(boolean singleLine) { // save the requested value in an instance variable to be able to decide later mSingleLine = singleLine; super.setSingleLine(singleLine); } }
known bugs: Doesn't work with Android 4.03 – fonts are invisible or very small (original avalancha doesn't work too) below is workaround for that bug: https://stackoverflow.com/a/21851239/2075875
尝试这个
TextWatcher changeText = new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { tv3.setText(et.getText().toString()); tv3.post(new Runnable() { @Override public void run() { while(tv3.getLineCount() >= 3){ tv3.setTextSize((tv3.getTextSize())-1); } } }); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void afterTextChanged(Editable s) { } };
If you are looking for something easier:
public MyTextView extends TextView{ public void resize(String text, float textViewWidth, float textViewHeight) { Paint p = new Paint(); Rect bounds = new Rect(); p.setTextSize(1); p.getTextBounds(text, 0, text.length(), bounds); float widthDifference = (textViewWidth)/bounds.width(); float heightDifference = (textViewHeight); textSize = Math.min(widthDifference, heightDifference); setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); }
Quick fix for the issue described by @Malachiasz
I've fixed the issue by adding custom support for this in the auto resize class:
public void setTextCompat(final CharSequence text) { setTextCompat(text, BufferType.NORMAL); } public void setTextCompat(final CharSequence text, BufferType type) { // Quick fix for Android Honeycomb and Ice Cream Sandwich which sets the text only on the first call if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1 && Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { super.setText(DOUBLE_BYTE_WORDJOINER + text + DOUBLE_BYTE_WORDJOINER, type); } else { super.setText(text, type); } } @Override public CharSequence getText() { String originalText = super.getText().toString(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1 && Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { // We try to remove the word joiners we added using compat method - if none found - this will do nothing. return originalText.replaceAll(DOUBLE_BYTE_WORDJOINER, ""); } else { return originalText; } }
Just call yourView.setTextCompat(newTextValue)
instead of yourView.setText(newTextValue)
Try adding LayoutParams
and MaxWidth
and MaxHeight
to the TextView
. It will force the layout to respect the parent container and not overflow.
textview.setLayoutParams(new LayoutParams(LinearLayout.MATCH_PARENT,LinearLayout.WRAP_CONTENT)); int GeneralApproxWidthOfContainer = 400; int GeneralApproxHeightOfContainer = 600; textview.setMaxWidth(400); textview.setMaxHeight(600);`
Since Android O, it's possible to auto resize text in xml:
https://developer.android.com/preview/features/autosizing-textview.html
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" app:autoSizeTextType="uniform" app:autoSizeMinTextSize="12sp" app:autoSizeMaxTextSize="100sp" app:autoSizeStepGranularity="2sp" />
Android O allows you to instruct a TextView to let the text size expand or contract automatically to fill its layout based on the TextView's characteristics and boundaries. This setting makes it easier to optimize the text size on different screens with dynamic content.
The Support Library 26.0 Beta provides full support to the autosizing TextView feature on devices running Android versions prior to Android O. The library provides support to Android 4.0 (API level 14) and higher. The android.support.v4.widget package contains the TextViewCompat class to access features in a backward-compatible fashion.
I'll explain how works this attribute lower android versions step by step:
1- Import android support library 26.xx on your project gradle file. If there is no support library on IDE, they will download automatically.
dependencies { compile 'com.android.support:support-v4:26.1.0' compile 'com.android.support:appcompat-v7:26.1.0' compile 'com.android.support:support-v13:26.1.0' } allprojects { repositories { jcenter() maven { url "https://maven.google.com" } } }
2- Open your layout XML file and refactor like this tag your TextView. This scenario is: when incrased font size on system, fit text to avaliable width, not word wrap.
<android.support.v7.widget.AppCompatTextView android:id="@+id/textViewAutoSize" android:layout_width="match_parent" android:layout_height="25dp" android:ellipsize="none" android:text="Auto size text with compatible lower android versions." android:textSize="12sp" app:autoSizeMaxTextSize="14sp" app:autoSizeMinTextSize="4sp" app:autoSizeStepGranularity="0.5sp" app:autoSizeTextType="uniform" />