我正在寻找一种最佳方式来调整 TextView
中的换行文本的大小,使其适合其 getHeight 和 getWidth 范围。我不只是在寻找一种包装文本的方法——我想确保它既能包装又足够小以完全适合屏幕。
我在 StackOverflow 上看到了一些需要自动调整大小的案例,但它们要么是非常特殊的情况,有 hack 解决方案,没有解决方案,要么涉及递归地重新绘制 TextView
直到它足够小(这是内存密集型并强制用户在每次递归时逐步缩小文本)。
但我确信有人已经找到了一个很好的解决方案,它不涉及我正在做的事情:编写几个解析和测量文本、调整文本大小并重复直到找到合适的小尺寸的繁重例程。
TextView
使用什么例程来换行文本?这些不能以某种方式用于预测文本是否足够小吗?
tl;dr:是否有最佳实践方法来自动调整 TextView
的大小以适应、包裹在其 getHeight 和 getWidth 范围内?
作为一名移动开发人员,我很遗憾没有找到支持自动调整大小的原生程序。我的搜索没有找到任何对我有用的东西,最后,我度过了周末的大部分时间,并创建了自己的自动调整大小文本视图。我将在这里发布代码,希望对其他人有用。
此类使用带有原始文本视图的文本绘制的静态布局来测量高度。从那里,我降低 2 个字体像素并重新测量,直到我有合适的尺寸。最后,如果文本仍然不合适,我会附加一个省略号。我需要为文本设置动画并重用视图,这似乎在我拥有的设备上运行良好,并且对我来说运行速度似乎足够快。
/**
* DO WHAT YOU WANT TO PUBLIC LICENSE
* Version 2, December 2004
*
* Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
*
* Everyone is permitted to copy and distribute verbatim or modified
* copies of this license document, and changing it is allowed as long
* as the name is changed.
*
* DO WHAT YOU WANT TO PUBLIC LICENSE
* TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
*
* 0. You just DO WHAT YOU WANT TO.
*/
import android.content.Context;
import android.text.Layout.Alignment;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.TextView;
/**
* Text view that auto adjusts text size to fit within the view.
* If the text size equals the minimum text size and still does not
* fit, append with an ellipsis.
*
* @author Chase Colburn
* @since Apr 4, 2011
*/
public class AutoResizeTextView extends TextView {
// Minimum text size for this text view
public static final float MIN_TEXT_SIZE = 20;
// Interface for resize notifications
public interface OnTextResizeListener {
public void onTextResize(TextView textView, float oldSize, float newSize);
}
// Our ellipse string
private static final String mEllipsis = "...";
// Registered resize listener
private OnTextResizeListener mTextResizeListener;
// Flag for text and/or size changes to force a resize
private boolean mNeedsResize = false;
// Text size that is set from code. This acts as a starting point for resizing
private float mTextSize;
// Temporary upper bounds on the starting text size
private float mMaxTextSize = 0;
// Lower bounds for text size
private float mMinTextSize = MIN_TEXT_SIZE;
// Text view line spacing multiplier
private float mSpacingMult = 1.0f;
// Text view additional line spacing
private float mSpacingAdd = 0.0f;
// Add ellipsis to text that overflows at the smallest text size
private boolean mAddEllipsis = true;
// Default constructor override
public AutoResizeTextView(Context context) {
this(context, null);
}
// Default constructor when inflating from XML file
public AutoResizeTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
// Default constructor override
public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mTextSize = getTextSize();
}
/**
* When text changes, set the force resize flag to true and reset the text size.
*/
@Override
protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
mNeedsResize = true;
// Since this view may be reused, it is good to reset the text size
resetTextSize();
}
/**
* If the text view size changed, set the force resize flag to true
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (w != oldw || h != oldh) {
mNeedsResize = true;
}
}
/**
* Register listener to receive resize notifications
* @param listener
*/
public void setOnResizeListener(OnTextResizeListener listener) {
mTextResizeListener = listener;
}
/**
* Override the set text size to update our internal reference values
*/
@Override
public void setTextSize(float size) {
super.setTextSize(size);
mTextSize = getTextSize();
}
/**
* Override the set text size to update our internal reference values
*/
@Override
public void setTextSize(int unit, float size) {
super.setTextSize(unit, size);
mTextSize = getTextSize();
}
/**
* Override the set line spacing to update our internal reference values
*/
@Override
public void setLineSpacing(float add, float mult) {
super.setLineSpacing(add, mult);
mSpacingMult = mult;
mSpacingAdd = add;
}
/**
* Set the upper text size limit and invalidate the view
* @param maxTextSize
*/
public void setMaxTextSize(float maxTextSize) {
mMaxTextSize = maxTextSize;
requestLayout();
invalidate();
}
/**
* Return upper text size limit
* @return
*/
public float getMaxTextSize() {
return mMaxTextSize;
}
/**
* Set the lower text size limit and invalidate the view
* @param minTextSize
*/
public void setMinTextSize(float minTextSize) {
mMinTextSize = minTextSize;
requestLayout();
invalidate();
}
/**
* Return lower text size limit
* @return
*/
public float getMinTextSize() {
return mMinTextSize;
}
/**
* Set flag to add ellipsis to text that overflows at the smallest text size
* @param addEllipsis
*/
public void setAddEllipsis(boolean addEllipsis) {
mAddEllipsis = addEllipsis;
}
/**
* Return flag to add ellipsis to text that overflows at the smallest text size
* @return
*/
public boolean getAddEllipsis() {
return mAddEllipsis;
}
/**
* Reset the text to the original size
*/
public void resetTextSize() {
if (mTextSize > 0) {
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
mMaxTextSize = mTextSize;
}
}
/**
* Resize text after measuring
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (changed || mNeedsResize) {
int widthLimit = (right - left) - getCompoundPaddingLeft() - getCompoundPaddingRight();
int heightLimit = (bottom - top) - getCompoundPaddingBottom() - getCompoundPaddingTop();
resizeText(widthLimit, heightLimit);
}
super.onLayout(changed, left, top, right, bottom);
}
/**
* Resize the text size with default width and height
*/
public void resizeText() {
int heightLimit = getHeight() - getPaddingBottom() - getPaddingTop();
int widthLimit = getWidth() - getPaddingLeft() - getPaddingRight();
resizeText(widthLimit, heightLimit);
}
/**
* Resize the text size with specified width and height
* @param width
* @param height
*/
public void resizeText(int width, int height) {
CharSequence text = getText();
// Do not resize if the view does not have dimensions or there is no text
if (text == null || text.length() == 0 || height <= 0 || width <= 0 || mTextSize == 0) {
return;
}
if (getTransformationMethod() != null) {
text = getTransformationMethod().getTransformation(text, this);
}
// Get the text view's paint object
TextPaint textPaint = getPaint();
// Store the current text size
float oldTextSize = textPaint.getTextSize();
// If there is a max text size set, use the lesser of that and the default text size
float targetTextSize = mMaxTextSize > 0 ? Math.min(mTextSize, mMaxTextSize) : mTextSize;
// Get the required text height
int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
// Until we either fit within our text view or we had reached our min text size, incrementally try smaller sizes
while (textHeight > height && targetTextSize > mMinTextSize) {
targetTextSize = Math.max(targetTextSize - 2, mMinTextSize);
textHeight = getTextHeight(text, textPaint, width, targetTextSize);
}
// If we had reached our minimum text size and still don't fit, append an ellipsis
if (mAddEllipsis && targetTextSize == mMinTextSize && textHeight > height) {
// Draw using a static layout
// modified: use a copy of TextPaint for measuring
TextPaint paint = new TextPaint(textPaint);
// Draw using a static layout
StaticLayout layout = new StaticLayout(text, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, false);
// Check that we have a least one line of rendered text
if (layout.getLineCount() > 0) {
// Since the line at the specific vertical position would be cut off,
// we must trim up to the previous line
int lastLine = layout.getLineForVertical(height) - 1;
// If the text would not even fit on a single line, clear it
if (lastLine < 0) {
setText("");
}
// Otherwise, trim to the previous line and add an ellipsis
else {
int start = layout.getLineStart(lastLine);
int end = layout.getLineEnd(lastLine);
float lineWidth = layout.getLineWidth(lastLine);
float ellipseWidth = textPaint.measureText(mEllipsis);
// Trim characters off until we have enough room to draw the ellipsis
while (width < lineWidth + ellipseWidth) {
lineWidth = textPaint.measureText(text.subSequence(start, --end + 1).toString());
}
setText(text.subSequence(0, end) + mEllipsis);
}
}
}
// Some devices try to auto adjust line spacing, so force default line spacing
// and invalidate the layout as a side effect
setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize);
setLineSpacing(mSpacingAdd, mSpacingMult);
// Notify the listener if registered
if (mTextResizeListener != null) {
mTextResizeListener.onTextResize(this, oldTextSize, targetTextSize);
}
// Reset force resize flag
mNeedsResize = false;
}
// Set the text size of the text paint object and use a static layout to render text off screen before measuring
private int getTextHeight(CharSequence source, TextPaint paint, int width, float textSize) {
// modified: make a copy of the original TextPaint object for measuring
// (apparently the object gets modified while measuring, see also the
// docs for TextView.getPaint() (which states to access it read-only)
TextPaint paintCopy = new TextPaint(paint);
// Update the text paint object
paintCopy.setTextSize(textSize);
// Measure using a static layout
StaticLayout layout = new StaticLayout(source, paintCopy, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
return layout.getHeight();
}
}
警告。 修复了一个影响 Android 3.1 - 4.04 的重要错误,导致所有 AutoResizingTextView 小部件无法工作。请阅读:https://stackoverflow.com/a/21851157/2075875
从 2018 年 6 月起,Android 正式开始为 Android 4.0(API 级别 14)及更高版本支持此功能。
请访问:Autosizing TextViews
使用 Android 8.0(API 级别 26)及更高版本:
<?xml version="1.0" encoding="utf-8"?>
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:autoSizeTextType="uniform"
android:autoSizeMinTextSize="12sp"
android:autoSizeMaxTextSize="100sp"
android:autoSizeStepGranularity="2sp" />
以编程方式:
setAutoSizeTextTypeUniformWithConfiguration(int autoSizeMinTextSize, int autoSizeMaxTextSize,
int autoSizeStepGranularity, int unit)
textView.setAutoSizeTextTypeUniformWithConfiguration(
1, 17, 1, TypedValue.COMPLEX_UNIT_DIP);
Android 8.0(API 级别 26)之前的 Android 版本:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
app:autoSizeTextType="uniform"
app:autoSizeMinTextSize="12sp"
app:autoSizeMaxTextSize="100sp"
app:autoSizeStepGranularity="2sp" />
</LinearLayout>
以编程方式:
TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(
TextView textView, int autoSizeMinTextSize, int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit)
TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(textView, 1, 17, 1,
TypedValue.COMPLEX_UNIT_DIP);
注意:TextView 必须有 layout_width="match_parent" 或绝对大小!
app/build.gradle
中添加 implementation 'com.android.support:support-compat:28.0.0'
以使 app: ...
属性起作用。
android:lines="1"
并且在 ConstraintLayout 中的 layout_width="0dp"
更新:以下代码还满足此处所述的理想 AutoScaleTextView 的要求:Auto-fit TextView for Android 并被标记为获胜者。
更新 2:添加了对 maxlines 的支持,现在在 API 级别 16 之前可以正常工作。
更新 3: 添加了对 android:drawableLeft
、android:drawableRight
、android:drawableTop
和 android:drawableBottom
标签的支持,这要归功于 MartinH 的简单修复here。
我的要求有点不同。我需要一种有效的方法来调整大小,因为我在 2 秒内对一个整数进行动画处理,可能是 TextView
中的 0 到 ~4000,我想相应地调整大小。我的解决方案有点不同。这是最终结果的样子:
https://i.stack.imgur.com/x9z0I.png
以及产生它的代码:
<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();
}
}
}
StaticLayout
不尊重 width
。只需将 onTestSize()
的最后返回行替换为此 'if (availableSpace.contains(mTextRect)) { // 可能太小,别担心我们会找到最佳匹配 return -1; } else { if (mTextRect.bottom < availableSpace.bottom && mTextRect.right > availableSpace.right) { // hack :O return -1; } // 太大 return 1; }'
实际上,Google 的 DialogTitle 类中有一个解决方案......虽然它不如公认的有效,但它更简单且易于适应。
public class SingleLineTextView extends TextView {
public SingleLineTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setSingleLine();
setEllipsize(TruncateAt.END);
}
public SingleLineTextView(Context context, AttributeSet attrs) {
super(context, attrs);
setSingleLine();
setEllipsize(TruncateAt.END);
}
public SingleLineTextView(Context context) {
super(context);
setSingleLine();
setEllipsize(TruncateAt.END);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final Layout layout = getLayout();
if (layout != null) {
final int lineCount = layout.getLineCount();
if (lineCount > 0) {
final int ellipsisCount = layout.getEllipsisCount(lineCount - 1);
if (ellipsisCount > 0) {
final float textSize = getTextSize();
// textSize is already expressed in pixels
setTextSize(TypedValue.COMPLEX_UNIT_PX, (textSize - 1));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
}
}
}
onTextChanged
中调用 requestLayout
,否则此解决方案不起作用。即使有了这个修复,这里的解决方案在许多情况下也不适用,因为将 textSize 减少 1 似乎并非在所有情况下都有效:我们可能需要进一步减少 textSize。
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
不会导致递归,而是 measure(widthMeasureSpec, heightMeasureSpec);
。所以我改为'measure(widthMeasureSpec,heightMeasureSpec);“并且它有效。我刚刚在Android 4.4(Nexus 5)和Android 4.0.4(Samsung SII-LTE)上进行了测试,并且它们都可以正常工作(我什至使用过自定义 OTF 日文字体)。在这里获取我的修复:gist.github.com/mrleolink/0dfeef749da1b854a44b
setText
上使用 maxLines
和 requestLayout()
,就像 @adbie 说的那样。 FixedLineTextView -> gist.github.com/Kevinrob/09742d9069e4e4e4ab66
我从 Chase 的解决方案开始,但在它在我的设备(Galaxy Nexus,Android 4.1)上按预期工作之前必须调整两件事:
使用 TextPaint 的副本来测量布局 TextView.getPaint() 的文档声明它应该以只读方式使用,所以我在我们使用绘制对象进行测量的两个地方都做了一个副本: // 1. in resizeText( ) if (mAddEllipsis && targetTextSize == mminTextSize && textHeight > height) { // 使用静态布局绘制 // 修改:使用 TextPaint 的副本来测量 TextPaint paint = new TextPaint(textPaint); // 2. in getTextHeight() private int getTextHeight(CharSequence source, TextPaint originalPaint, int width, float textSize) { // 修改:复制原始 TextPaint 对象进行测量 // (显然对象在测量时被修改,另请参见 // TextView.getPaint() 的文档(声明以只读方式访问它) TextPaint paint = new TextPaint(originalPaint); // 更新文本绘制对象 paint.setTextSize(textSize); ... 添加一个设置文本大小的单位 // 修改:通过 this.setTextSize 设置文本大小(而不是 textPaint.setTextSize(targetTextSize)) setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize); setLineSpacing(mSpacingAdd, mSpacingMult);
通过这两个修改,解决方案对我来说非常有效,谢谢 Chase!我不知道是否是由于 Android 4.x 导致原始解决方案无法正常工作。如果您想查看它的实际效果或测试它是否真的在您的设备上运行,您可以查看我的抽认卡应用程序 Flashcards ToGo,我在其中使用此解决方案来缩放抽认卡的文本。文本可以有任意长度,并且抽认卡显示在不同的活动中,有时更小有时更大,加上横向+纵向模式,我还没有发现任何解决方案无法正常工作的极端情况......
AppcompatTextView 现在支持从支持库 26.0 开始的自动调整大小。 Android O 中的 TextView 也以同样的方式工作。 More info can be found here. 可以找到一个简单的演示应用程序here。
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:autoSizeTextType="uniform"
app:autoSizeMinTextSize="12sp"
app:autoSizeMaxTextSize="100sp"
app:autoSizeStepGranularity="2sp"
/>
</LinearLayout>
android:lines="1"
才能调整文本大小。如果没有此属性,文本将被包裹在两行中。
我从 Chase 的 AutoResizeTextView 类开始,并做了一个小的改动,使其垂直和水平都适合。
我还发现了一个错误,它在一些相当模糊的条件下导致布局编辑器(在 Eclipse 中)中出现空指针异常。
更改 1:垂直和水平适应文本
Chase 的原始版本减小了文本大小,直到它垂直适合,但允许文本比目标更宽。就我而言,我需要文本适合指定的宽度。
此更改使其调整大小,直到文本适合垂直和水平。
在 resizeText(
int,
int)
中从:
// Get the required text height
int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
// Until we either fit within our text view or we had reached our min text size, incrementally try smaller sizes
while(textHeight > height && targetTextSize > mMinTextSize) {
targetTextSize = Math.max(targetTextSize - 2, mMinTextSize);
textHeight = getTextHeight(text, textPaint, width, targetTextSize);
}
至:
// Get the required text height
int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
int textWidth = getTextWidth(text, textPaint, width, targetTextSize);
// Until we either fit within our text view or we had reached our min text size, incrementally try smaller sizes
while(((textHeight >= height) || (textWidth >= width) ) && targetTextSize > mMinTextSize) {
targetTextSize = Math.max(targetTextSize - 2, mMinTextSize);
textHeight = getTextHeight(text, textPaint, width, targetTextSize);
textWidth = getTextWidth(text, textPaint, width, targetTextSize);
}
然后,在文件末尾附加 getTextWidth()
例程;它只是稍作修改的 getTextHeight()
。将它们组合到一个返回高度和宽度的例程中可能会更有效。
// Set the text size of the text paint object and use a static layout to render text off screen before measuring
private int getTextWidth(CharSequence source, TextPaint paint, int width, float textSize) {
// Update the text paint object
paint.setTextSize(textSize);
// Draw using a static layout
StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
layout.draw(sTextResizeCanvas);
return layout.getWidth();
}
更改 2:修复 Eclipse Android 布局编辑器中的 EmptyStackException
在相当模糊和非常精确的条件下,布局编辑器将无法显示布局的图形显示;它将在 com.android.ide.eclipse.adt 中引发“EmptyStackException:null”异常。
所需条件是: - 创建 AutoResizeTextView 小部件 - 为该小部件创建样式 - 指定样式中的文本项;不在小部件定义中
如:
资源/布局/main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<com.ajw.DemoCrashInADT.AutoResizeTextView
android:id="@+id/resizingText"
style="@style/myTextStyle" />
</LinearLayout>
res/values/myStyles.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="myTextStyle" parent="@android:style/Widget.TextView">
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">fill_parent</item>
<item name="android:text">some message</item>
</style>
</resources>
使用这些文件,在编辑 main.xml
时选择 Graphical Layout 选项卡将显示:
错误! EmptyStackException: null 异常详细信息记录在 Window > Show View > Error Log
而不是布局的图形视图。
为了缩短已经太长的故事,我将其追踪到以下几行(再次在 resizeText
中):
// If there is a max text size set, use the lesser of that and the default text size
float targetTextSize = mMaxTextSize > 0 ? Math.min(mTextSize, mMaxTextSize) : mTextSize;
问题是在特定条件下,mTextSize 永远不会被初始化;它的值为 0。
使用上述方法,targetTextSize
设置为零(作为 Math.min 的结果)。
该零作为 textSize
参数传递给 getTextHeight()
(和 getTextWidth()
)。当它到达
layout.draw(sTextResizeCanvas);
时,我们得到了异常。
在 resizeText()
开头测试 if (mTextSize == 0)
比在 getTextHeight()
和 getTextWidth()
中测试效率更高;早期测试可以节省所有干预工作。
通过这些更新,文件(如在我的崩溃演示测试应用程序中)现在是:
//
// from: http://stackoverflow.com/questions/5033012/auto-scale-textview-text-to-fit-within-bounds
//
//
package com.ajw.DemoCrashInADT;
import android.content.Context;
import android.graphics.Canvas;
import android.text.Layout.Alignment;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.TextView;
/**
* Text view that auto adjusts text size to fit within the view. If the text
* size equals the minimum text size and still does not fit, append with an
* ellipsis.
*
* 2011-10-29 changes by Alan Jay Weiner
* * change to fit both vertically and horizontally
* * test mTextSize for 0 in resizeText() to fix exception in Layout Editor
*
* @author Chase Colburn
* @since Apr 4, 2011
*/
public class AutoResizeTextView extends TextView {
// Minimum text size for this text view
public static final float MIN_TEXT_SIZE = 20;
// Interface for resize notifications
public interface OnTextResizeListener {
public void onTextResize(TextView textView, float oldSize, float newSize);
}
// Off screen canvas for text size rendering
private static final Canvas sTextResizeCanvas = new Canvas();
// Our ellipse string
private static final String mEllipsis = "...";
// Registered resize listener
private OnTextResizeListener mTextResizeListener;
// Flag for text and/or size changes to force a resize
private boolean mNeedsResize = false;
// Text size that is set from code. This acts as a starting point for
// resizing
private float mTextSize;
// Temporary upper bounds on the starting text size
private float mMaxTextSize = 0;
// Lower bounds for text size
private float mMinTextSize = MIN_TEXT_SIZE;
// Text view line spacing multiplier
private float mSpacingMult = 1.0f;
// Text view additional line spacing
private float mSpacingAdd = 0.0f;
// Add ellipsis to text that overflows at the smallest text size
private boolean mAddEllipsis = true;
// Default constructor override
public AutoResizeTextView(Context context) {
this(context, null);
}
// Default constructor when inflating from XML file
public AutoResizeTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
// Default constructor override
public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mTextSize = getTextSize();
}
/**
* When text changes, set the force resize flag to true and reset the text
* size.
*/
@Override
protected void onTextChanged(final CharSequence text, final int start,
final int before, final int after) {
mNeedsResize = true;
// Since this view may be reused, it is good to reset the text size
resetTextSize();
}
/**
* If the text view size changed, set the force resize flag to true
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (w != oldw || h != oldh) {
mNeedsResize = true;
}
}
/**
* Register listener to receive resize notifications
*
* @param listener
*/
public void setOnResizeListener(OnTextResizeListener listener) {
mTextResizeListener = listener;
}
/**
* Override the set text size to update our internal reference values
*/
@Override
public void setTextSize(float size) {
super.setTextSize(size);
mTextSize = getTextSize();
}
/**
* Override the set text size to update our internal reference values
*/
@Override
public void setTextSize(int unit, float size) {
super.setTextSize(unit, size);
mTextSize = getTextSize();
}
/**
* Override the set line spacing to update our internal reference values
*/
@Override
public void setLineSpacing(float add, float mult) {
super.setLineSpacing(add, mult);
mSpacingMult = mult;
mSpacingAdd = add;
}
/**
* Set the upper text size limit and invalidate the view
*
* @param maxTextSize
*/
public void setMaxTextSize(float maxTextSize) {
mMaxTextSize = maxTextSize;
requestLayout();
invalidate();
}
/**
* Return upper text size limit
*
* @return
*/
public float getMaxTextSize() {
return mMaxTextSize;
}
/**
* Set the lower text size limit and invalidate the view
*
* @param minTextSize
*/
public void setMinTextSize(float minTextSize) {
mMinTextSize = minTextSize;
requestLayout();
invalidate();
}
/**
* Return lower text size limit
*
* @return
*/
public float getMinTextSize() {
return mMinTextSize;
}
/**
* Set flag to add ellipsis to text that overflows at the smallest text size
*
* @param addEllipsis
*/
public void setAddEllipsis(boolean addEllipsis) {
mAddEllipsis = addEllipsis;
}
/**
* Return flag to add ellipsis to text that overflows at the smallest text
* size
*
* @return
*/
public boolean getAddEllipsis() {
return mAddEllipsis;
}
/**
* Reset the text to the original size
*/
public void resetTextSize() {
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
mMaxTextSize = mTextSize;
}
/**
* Resize text after measuring
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (changed || mNeedsResize) {
int widthLimit = (right - left) - getCompoundPaddingLeft()
- getCompoundPaddingRight();
int heightLimit = (bottom - top) - getCompoundPaddingBottom()
- getCompoundPaddingTop();
resizeText(widthLimit, heightLimit);
}
super.onLayout(changed, left, top, right, bottom);
}
/**
* Resize the text size with default width and height
*/
public void resizeText() {
int heightLimit = getHeight() - getPaddingBottom() - getPaddingTop();
int widthLimit = getWidth() - getPaddingLeft() - getPaddingRight();
resizeText(widthLimit, heightLimit);
}
/**
* Resize the text size with specified width and height
*
* @param width
* @param height
*/
public void resizeText(int width, int height) {
CharSequence text = getText();
// Do not resize if the view does not have dimensions or there is no
// text
// or if mTextSize has not been initialized
if (text == null || text.length() == 0 || height <= 0 || width <= 0
|| mTextSize == 0) {
return;
}
// Get the text view's paint object
TextPaint textPaint = getPaint();
// Store the current text size
float oldTextSize = textPaint.getTextSize();
// If there is a max text size set, use the lesser of that and the
// default text size
float targetTextSize = mMaxTextSize > 0 ? Math.min(mTextSize, mMaxTextSize)
: mTextSize;
// Get the required text height
int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
int textWidth = getTextWidth(text, textPaint, width, targetTextSize);
// Until we either fit within our text view or we had reached our min
// text size, incrementally try smaller sizes
while (((textHeight > height) || (textWidth > width))
&& targetTextSize > mMinTextSize) {
targetTextSize = Math.max(targetTextSize - 2, mMinTextSize);
textHeight = getTextHeight(text, textPaint, width, targetTextSize);
textWidth = getTextWidth(text, textPaint, width, targetTextSize);
}
// If we had reached our minimum text size and still don't fit, append
// an ellipsis
if (mAddEllipsis && targetTextSize == mMinTextSize && textHeight > height) {
// Draw using a static layout
StaticLayout layout = new StaticLayout(text, textPaint, width,
Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, false);
layout.draw(sTextResizeCanvas);
int lastLine = layout.getLineForVertical(height) - 1;
int start = layout.getLineStart(lastLine);
int end = layout.getLineEnd(lastLine);
float lineWidth = layout.getLineWidth(lastLine);
float ellipseWidth = textPaint.measureText(mEllipsis);
// Trim characters off until we have enough room to draw the
// ellipsis
while (width < lineWidth + ellipseWidth) {
lineWidth = textPaint.measureText(text.subSequence(start, --end + 1)
.toString());
}
setText(text.subSequence(0, end) + mEllipsis);
}
// Some devices try to auto adjust line spacing, so force default line
// spacing
// and invalidate the layout as a side effect
textPaint.setTextSize(targetTextSize);
setLineSpacing(mSpacingAdd, mSpacingMult);
// Notify the listener if registered
if (mTextResizeListener != null) {
mTextResizeListener.onTextResize(this, oldTextSize, targetTextSize);
}
// Reset force resize flag
mNeedsResize = false;
}
// Set the text size of the text paint object and use a static layout to
// render text off screen before measuring
private int getTextHeight(CharSequence source, TextPaint paint, int width,
float textSize) {
// Update the text paint object
paint.setTextSize(textSize);
// Draw using a static layout
StaticLayout layout = new StaticLayout(source, paint, width,
Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
layout.draw(sTextResizeCanvas);
return layout.getHeight();
}
// Set the text size of the text paint object and use a static layout to
// render text off screen before measuring
private int getTextWidth(CharSequence source, TextPaint paint, int width,
float textSize) {
// Update the text paint object
paint.setTextSize(textSize);
// Draw using a static layout
StaticLayout layout = new StaticLayout(source, paint, width,
Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
layout.draw(sTextResizeCanvas);
return layout.getWidth();
}
}
非常感谢 Chase 发布初始代码。我喜欢通读它以了解它是如何工作的,我很高兴能够添加它。
getTextWidth()
根本不起作用,因为您在 StaticLayout
构造函数中传递了所需的宽度。猜猜在这种情况下 getWidth()
方法会返回什么宽度?
在 2017 年的 google IO 大会上,google 介绍了 TextView 的 autoSize 属性
<android.support.v7.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/my_text"
app:autoSizeTextType="uniform"
app:autoSizeMaxTextSize="10sp"
app:autoSizeMinTextSize="6sp"
app:autoSizeStepGranularity="1sp"/>
Android 4.x 的解决方法:
我找到了 AutoResizeTextView,它在我的 Android 2.1 模拟器上运行良好。我太喜欢它了。但不幸的是,它在我自己的 4.0.4 手机和 4.1 模拟器上失败了。尝试后我发现它可以通过在 xml 中的 AutoResizeTextView 类中添加以下属性来轻松解决:
android:ellipsize="none" android:singleLine="true"
有了上面的 2 行,现在 AutoResizeTextView 可以在我的 2.1 和 4.1 模拟器和我自己的 4.0.4 手机上完美运行。
希望这对您有所帮助。 :-)
警告,Android Honeycomb and Ice Cream Sandwich 中的错误
Androids 版本:3.1 - 4.04 有一个错误,TextView 内的 setTextSize() 仅在第一次(第一次调用)时有效。
此处描述了错误:http://code.google.com/p/android/issues/detail?id=22493 http://code.google.com/p/android/issues/detail?id=17343#c9
解决方法是在更改大小之前向分配给 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”字符,以使其居中。如果您将其与左侧对齐,则仅附加到右侧。当然它也可以嵌入 AutoResizeTextView 小部件,但我想将修复代码保留在外面。
我需要调整文本大小以完全适合视图边界。 Chase 的解决方案只是减小文本大小,如果有足够的空间,这个也会放大文本。
让所有快速&精确 我使用了二分法而不是迭代 while,正如您在 resizeText()
方法中看到的那样。这就是为什么您还有一个 MAX_TEXT_SIZE
选项。我还包括了 onoelle 的提示。
在安卓 4.4 上测试
/**
* DO WHAT YOU WANT TO PUBLIC LICENSE
* Version 2, December 2004
*
* Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
*
* Everyone is permitted to copy and distribute verbatim or modified
* copies of this license document, and changing it is allowed as long
* as the name is changed.
*
* DO WHAT YOU WANT TO PUBLIC LICENSE
* TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
*
* 0. You just DO WHAT YOU WANT TO.
*/
import android.content.Context;
import android.text.Layout.Alignment;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.TextView;
/**
* Text view that auto adjusts text size to fit within the view.
* If the text size equals the minimum text size and still does not
* fit, append with an ellipsis.
*
* @author Chase Colburn
* @since Apr 4, 2011
*/
public class AutoResizeTextView extends TextView {
// Minimum text size for this text view
public static final float MIN_TEXT_SIZE = 26;
// Maximum text size for this text view
public static final float MAX_TEXT_SIZE = 128;
private static final int BISECTION_LOOP_WATCH_DOG = 30;
// Interface for resize notifications
public interface OnTextResizeListener {
public void onTextResize(TextView textView, float oldSize, float newSize);
}
// Our ellipse string
private static final String mEllipsis = "...";
// Registered resize listener
private OnTextResizeListener mTextResizeListener;
// Flag for text and/or size changes to force a resize
private boolean mNeedsResize = false;
// Text size that is set from code. This acts as a starting point for resizing
private float mTextSize;
// Temporary upper bounds on the starting text size
private float mMaxTextSize = MAX_TEXT_SIZE;
// Lower bounds for text size
private float mMinTextSize = MIN_TEXT_SIZE;
// Text view line spacing multiplier
private float mSpacingMult = 1.0f;
// Text view additional line spacing
private float mSpacingAdd = 0.0f;
// Add ellipsis to text that overflows at the smallest text size
private boolean mAddEllipsis = true;
// Default constructor override
public AutoResizeTextView(Context context) {
this(context, null);
}
// Default constructor when inflating from XML file
public AutoResizeTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
// Default constructor override
public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mTextSize = getTextSize();
}
/**
* When text changes, set the force resize flag to true and reset the text size.
*/
@Override
protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
mNeedsResize = true;
// Since this view may be reused, it is good to reset the text size
resetTextSize();
}
/**
* If the text view size changed, set the force resize flag to true
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (w != oldw || h != oldh) {
mNeedsResize = true;
}
}
/**
* Register listener to receive resize notifications
* @param listener
*/
public void setOnResizeListener(OnTextResizeListener listener) {
mTextResizeListener = listener;
}
/**
* Override the set text size to update our internal reference values
*/
@Override
public void setTextSize(float size) {
super.setTextSize(size);
mTextSize = getTextSize();
}
/**
* Override the set text size to update our internal reference values
*/
@Override
public void setTextSize(int unit, float size) {
super.setTextSize(unit, size);
mTextSize = getTextSize();
}
/**
* Override the set line spacing to update our internal reference values
*/
@Override
public void setLineSpacing(float add, float mult) {
super.setLineSpacing(add, mult);
mSpacingMult = mult;
mSpacingAdd = add;
}
/**
* Set the upper text size limit and invalidate the view
* @param maxTextSize
*/
public void setMaxTextSize(float maxTextSize) {
mMaxTextSize = maxTextSize;
requestLayout();
invalidate();
}
/**
* Return upper text size limit
* @return
*/
public float getMaxTextSize() {
return mMaxTextSize;
}
/**
* Set the lower text size limit and invalidate the view
* @param minTextSize
*/
public void setMinTextSize(float minTextSize) {
mMinTextSize = minTextSize;
requestLayout();
invalidate();
}
/**
* Return lower text size limit
* @return
*/
public float getMinTextSize() {
return mMinTextSize;
}
/**
* Set flag to add ellipsis to text that overflows at the smallest text size
* @param addEllipsis
*/
public void setAddEllipsis(boolean addEllipsis) {
mAddEllipsis = addEllipsis;
}
/**
* Return flag to add ellipsis to text that overflows at the smallest text size
* @return
*/
public boolean getAddEllipsis() {
return mAddEllipsis;
}
/**
* Reset the text to the original size
*/
public void resetTextSize() {
if(mTextSize > 0) {
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
//mMaxTextSize = mTextSize;
}
}
/**
* Resize text after measuring
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if(changed || mNeedsResize) {
int widthLimit = (right - left) - getCompoundPaddingLeft() - getCompoundPaddingRight();
int heightLimit = (bottom - top) - getCompoundPaddingBottom() - getCompoundPaddingTop();
resizeText(widthLimit, heightLimit);
}
super.onLayout(changed, left, top, right, bottom);
}
/**
* Resize the text size with default width and height
*/
public void resizeText() {
int heightLimit = getHeight() - getPaddingBottom() - getPaddingTop();
int widthLimit = getWidth() - getPaddingLeft() - getPaddingRight();
resizeText(widthLimit, heightLimit);
}
/**
* Resize the text size with specified width and height
* @param width
* @param height
*/
public void resizeText(int width, int height) {
CharSequence text = getText();
// Do not resize if the view does not have dimensions or there is no text
if(text == null || text.length() == 0 || height <= 0 || width <= 0 || mTextSize == 0) {
return;
}
// Get the text view's paint object
TextPaint textPaint = getPaint();
// Store the current text size
float oldTextSize = textPaint.getTextSize();
// Bisection method: fast & precise
float lower = mMinTextSize;
float upper = mMaxTextSize;
int loop_counter=1;
float targetTextSize = (lower+upper)/2;
int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
while(loop_counter < BISECTION_LOOP_WATCH_DOG && upper - lower > 1) {
targetTextSize = (lower+upper)/2;
textHeight = getTextHeight(text, textPaint, width, targetTextSize);
if(textHeight > height)
upper = targetTextSize;
else
lower = targetTextSize;
loop_counter++;
}
targetTextSize = lower;
textHeight = getTextHeight(text, textPaint, width, targetTextSize);
// If we had reached our minimum text size and still don't fit, append an ellipsis
if(mAddEllipsis && targetTextSize == mMinTextSize && textHeight > height) {
// Draw using a static layout
// modified: use a copy of TextPaint for measuring
TextPaint paintCopy = new TextPaint(textPaint);
paintCopy.setTextSize(targetTextSize);
StaticLayout layout = new StaticLayout(text, paintCopy, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, false);
// Check that we have a least one line of rendered text
if(layout.getLineCount() > 0) {
// Since the line at the specific vertical position would be cut off,
// we must trim up to the previous line
int lastLine = layout.getLineForVertical(height) - 1;
// If the text would not even fit on a single line, clear it
if(lastLine < 0) {
setText("");
}
// Otherwise, trim to the previous line and add an ellipsis
else {
int start = layout.getLineStart(lastLine);
int end = layout.getLineEnd(lastLine);
float lineWidth = layout.getLineWidth(lastLine);
float ellipseWidth = paintCopy.measureText(mEllipsis);
// Trim characters off until we have enough room to draw the ellipsis
while(width < lineWidth + ellipseWidth) {
lineWidth = paintCopy.measureText(text.subSequence(start, --end + 1).toString());
}
setText(text.subSequence(0, end) + mEllipsis);
}
}
}
// Some devices try to auto adjust line spacing, so force default line spacing
// and invalidate the layout as a side effect
setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize);
setLineSpacing(mSpacingAdd, mSpacingMult);
// Notify the listener if registered
if(mTextResizeListener != null) {
mTextResizeListener.onTextResize(this, oldTextSize, targetTextSize);
}
// Reset force resize flag
mNeedsResize = false;
}
// Set the text size of the text paint object and use a static layout to render text off screen before measuring
private int getTextHeight(CharSequence source, TextPaint originalPaint, int width, float textSize) {
// modified: make a copy of the original TextPaint object for measuring
// (apparently the object gets modified while measuring, see also the
// docs for TextView.getPaint() (which states to access it read-only)
TextPaint paint = new TextPaint(originalPaint);
// Update the text paint object
paint.setTextSize(textSize);
// Measure using a static layout
StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
return layout.getHeight();
}
}
由于我一直在寻找这个,并且我前一段时间找到了一个在这里丢失的解决方案,所以我将在这里写下来,以供将来参考。
注意:这段代码是不久前直接从 Google Android Lollipop dialer 中获取的,我不记得当时是否进行了更改。另外,我不知道这是在哪个许可证下,但我有理由认为它是 Apache 2.0
。
类 ResizeTextView
,实际的 View
public class ResizeTextView extends TextView {
private final int mOriginalTextSize;
private final int mMinTextSize;
private final static int sMinSize = 20;
public ResizeTextView(Context context, AttributeSet attrs) {
super(context, attrs);
mOriginalTextSize = (int) getTextSize();
mMinTextSize = (int) sMinSize;
}
@Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
super.onTextChanged(text, start, lengthBefore, lengthAfter);
ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize);
}
正如我所理解的,这个 ResizeTextView
类可以扩展 TextView 及其所有子级,所以 EditText 也是如此。
类 ViewUtil
和方法 resizeText(...)
/*
* Copyright (C) 2012 The Android Open Source Project
*
* 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.
*/
import android.graphics.Paint;
import android.util.TypedValue;
import android.widget.TextView;
public class ViewUtil {
private ViewUtil() {}
public static void resizeText(TextView textView, int originalTextSize, int minTextSize) {
final Paint paint = textView.getPaint();
final int width = textView.getWidth();
if (width == 0) return;
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalTextSize);
float ratio = width / paint.measureText(textView.getText().toString());
if (ratio <= 1.0f) {
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
Math.max(minTextSize, originalTextSize * ratio));
}
}
}
您应该将视图设置为
<yourpackage.yourapp.ResizeTextView
android:layout_width="match_parent"
android:layout_height="64dp"
android:gravity="center"
android:maxLines="1"/>
希望能帮助到你!
适合边界的文本(1 行)
要使文本缩小以适合一行的范围:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:autoSizeTextType="uniform"
android:lines:"1"
/>
我希望这可以帮助你
import android.content.Context;
import android.graphics.Rect;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.widget.TextView;
/* Based on
* from http://stackoverflow.com/questions/2617266/how-to-adjust-text-font-size-to-fit-textview
*/
public class FontFitTextView extends TextView {
private static float MAX_TEXT_SIZE = 20;
public FontFitTextView(Context context) {
this(context, null);
}
public FontFitTextView(Context context, AttributeSet attrs) {
super(context, attrs);
float size = this.getTextSize();
if (size > MAX_TEXT_SIZE)
setTextSize(MAX_TEXT_SIZE);
}
private void refitText(String text, int textWidth) {
if (textWidth > 0) {
float availableWidth = textWidth - this.getPaddingLeft()
- this.getPaddingRight();
TextPaint tp = getPaint();
Rect rect = new Rect();
tp.getTextBounds(text, 0, text.length(), rect);
float size = rect.width();
if (size > availableWidth)
setTextScaleX(availableWidth / size);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
refitText(this.getText().toString(), parentWidth);
this.setMeasuredDimension(parentWidth, parentHeight);
}
@Override
protected void onTextChanged(final CharSequence text, final int start,
final int before, final int after) {
refitText(text.toString(), this.getWidth());
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (w != oldw) {
refitText(this.getText().toString(), w);
}
}
}
注意:如果文本大小大于 20,我使用 MAX_TEXT_SIZE,因为我不想让大字体应用于我的视图,如果不是你的情况,你可以简单地删除它。
scale > availableWidth
?这将使其仅在可用大小小于原始大小时才调整大小;不会发生拉伸(或使文本变大)。
这是一个简单的解决方案,它使用 TextView 本身并添加了 TextChangedListened :
expressionView = (TextView) findViewById(R.id.expressionView);
expressionView.addTextChangedListener(textAutoResizeWatcher(expressionView, 25, 55));
private TextWatcher textAutoResizeWatcher(final TextView view, final int MIN_SP, final int MAX_SP) {
return new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override
public void afterTextChanged(Editable editable) {
final int widthLimitPixels = view.getWidth() - view.getPaddingRight() - view.getPaddingLeft();
Paint paint = new Paint();
float fontSizeSP = pixelsToSp(view.getTextSize());
paint.setTextSize(spToPixels(fontSizeSP));
String viewText = view.getText().toString();
float widthPixels = paint.measureText(viewText);
// Increase font size if necessary.
if (widthPixels < widthLimitPixels){
while (widthPixels < widthLimitPixels && fontSizeSP <= MAX_SP){
++fontSizeSP;
paint.setTextSize(spToPixels(fontSizeSP));
widthPixels = paint.measureText(viewText);
}
--fontSizeSP;
}
// Decrease font size if necessary.
else {
while (widthPixels > widthLimitPixels || fontSizeSP > MAX_SP) {
if (fontSizeSP < MIN_SP) {
fontSizeSP = MIN_SP;
break;
}
--fontSizeSP;
paint.setTextSize(spToPixels(fontSizeSP));
widthPixels = paint.measureText(viewText);
}
}
view.setTextSize(fontSizeSP);
}
};
}
private float pixelsToSp(float px) {
float scaledDensity = getResources().getDisplayMetrics().scaledDensity;
return px/scaledDensity;
}
private float spToPixels(float sp) {
float scaledDensity = getResources().getDisplayMetrics().scaledDensity;
return sp * scaledDensity;
}
这种方法将根据需要增加或减少字体大小以适应文本,同时尊重作为参数接收的 MIN_SP 和 MAX_SP 边界。
我写了一篇关于这个的博客文章。
我根据 Kirill Grouchnikov 关于新 android 市场应用程序中使用的自定义组件的 blog post 创建了一个名为 ResizableButton
的组件。我放置了 src 代码 here。
另一方面,mosabua 阅读了我的帖子并告诉我他将开源他的实现,这比我的要快。我希望他尽快发布它:)
我发现以下内容对我很有效。它不会循环并同时考虑高度和宽度。请注意,在视图上调用 setTextSize 时指定 PX 单元很重要。
Paint paint = adjustTextSize(getPaint(), numChars, maxWidth, maxHeight);
setTextSize(TypedValue.COMPLEX_UNIT_PX,paint.getTextSize());
这是我使用的例程,从视图中传入 getPaint()。带有“宽”字符的 10 个字符的字符串用于估计与实际字符串无关的宽度。
private static final String text10="OOOOOOOOOO";
public static Paint adjustTextSize(Paint paint, int numCharacters, int widthPixels, int heightPixels) {
float width = paint.measureText(text10)*numCharacters/text10.length();
float newSize = (int)((widthPixels/width)*paint.getTextSize());
paint.setTextSize(newSize);
// remeasure with font size near our desired result
width = paint.measureText(text10)*numCharacters/text10.length();
newSize = (int)((widthPixels/width)*paint.getTextSize());
paint.setTextSize(newSize);
// Check height constraints
FontMetricsInt metrics = paint.getFontMetricsInt();
float textHeight = metrics.descent-metrics.ascent;
if (textHeight > heightPixels) {
newSize = (int)(newSize * (heightPixels/textHeight));
paint.setTextSize(newSize);
}
return paint;
}
我的实现有点复杂,但附带以下好处:
考虑可用宽度和可用高度
适用于单行和多行标签
在达到最小字体大小的情况下使用省略号
由于内部文本表示已更改,因此将最初设置的文本记住在单独的变量中
确保画布总是只有它需要的那么大,同时它使用父级的所有可用高度
/**
* Text view that auto adjusts text size to fit within the view. If the text
* size equals the minimum text size and still does not fit, append with an
* ellipsis.
*
* Based on the original work from Chase Colburn
* <http://stackoverflow.com/a/5535672/305532>
*
* @author Thomas Keller <me@thomaskeller.biz>
*/
public class AutoResizeTextView extends TextView {
// in dip
private static final int MIN_TEXT_SIZE = 20;
private static final boolean SHRINK_TEXT_SIZE = true;
private static final char ELLIPSIS = '\u2026';
private static final float LINE_SPACING_MULTIPLIER_MULTILINE = 0.8f;
private static final float LINE_SPACING_MULTIPLIER_SINGLELINE = 1f;
private static final float LINE_SPACING_EXTRA = 0.0f;
private CharSequence mOriginalText;
// temporary upper bounds on the starting text size
private float mMaxTextSize;
// lower bounds for text size
private float mMinTextSize;
// determines whether we're currently in the process of measuring ourselves,
// so we do not enter onMeasure recursively
private boolean mInMeasure = false;
// if the text size should be shrinked or if the text size should be kept
// constant and only characters should be removed to hit the boundaries
private boolean mShrinkTextSize;
public AutoResizeTextView(Context context) {
this(context, null);
init(context, null);
}
public AutoResizeTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
init(context, attrs);
}
public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
// the current text size is used as maximum text size we can apply to
// our widget
mMaxTextSize = getTextSize();
if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AutoResizeTextView);
mMinTextSize = a.getFloat(R.styleable.AutoResizeTextView_minFontSize, MIN_TEXT_SIZE);
mShrinkTextSize = a.getBoolean(R.styleable.AutoResizeTextView_shrinkTextSize, SHRINK_TEXT_SIZE);
a.recycle();
}
}
@Override
public void setTextSize(float size) {
mMaxTextSize = size;
super.setTextSize(size);
}
/**
* Returns the original, unmodified text of this widget
*
* @return
*/
public CharSequence getOriginalText() {
// text has not been resized yet
if (mOriginalText == null) {
return getText();
}
return mOriginalText;
}
@Override
public void setText(CharSequence text, BufferType type) {
if (!mInMeasure) {
mOriginalText = text.toString();
}
super.setText(text, type);
}
@SuppressLint("DrawAllocation")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mInMeasure = true;
try {
int availableWidth = MeasureSpec.getSize(widthMeasureSpec) - getCompoundPaddingLeft()
- getCompoundPaddingRight();
int availableHeight = MeasureSpec.getSize(heightMeasureSpec) - getCompoundPaddingTop()
- getCompoundPaddingBottom();
// Do not resize if the view does not have dimensions or there is no
// text
if (mOriginalText == null || mOriginalText.length() == 0 || availableWidth <= 0) {
return;
}
TextPaint textPaint = getPaint();
// start with the recorded max text size
float targetTextSize = mMaxTextSize;
String originalText = mOriginalText.toString();
String finalText = originalText;
Rect textSize = getTextSize(originalText, textPaint, targetTextSize);
boolean textExceedsBounds = textSize.height() > availableHeight || textSize.width() > availableWidth;
if (mShrinkTextSize && textExceedsBounds) {
// check whether all lines can be rendered in the available
// width / height without violating the bounds of the parent and
// without using a text size that is smaller than the minimum
// text size
float heightMultiplier = availableHeight / (float) textSize.height();
float widthMultiplier = availableWidth / (float) textSize.width();
float multiplier = Math.min(heightMultiplier, widthMultiplier);
targetTextSize = Math.max(targetTextSize * multiplier, mMinTextSize);
// measure again
textSize = getTextSize(finalText, textPaint, targetTextSize);
}
// we cannot shrink the height further when we hit the available
// height, but we can shrink the width by applying an ellipsis on
// each line
if (textSize.width() > availableWidth) {
StringBuilder modifiedText = new StringBuilder();
String lines[] = originalText.split(System.getProperty("line.separator"));
for (int i = 0; i < lines.length; i++) {
modifiedText.append(resizeLine(textPaint, lines[i], availableWidth));
// add the separator back to all but the last processed line
if (i != lines.length - 1) {
modifiedText.append(System.getProperty("line.separator"));
}
}
finalText = modifiedText.toString();
// measure again
textSize = getTextSize(finalText, textPaint, targetTextSize);
}
textPaint.setTextSize(targetTextSize);
boolean isMultiline = finalText.indexOf('\n') > -1;
// do not include extra font padding (for accents, ...) for
// multiline texts, this will prevent proper placement with
// Gravity.CENTER_VERTICAL
if (isMultiline) {
setLineSpacing(LINE_SPACING_EXTRA, LINE_SPACING_MULTIPLIER_MULTILINE);
setIncludeFontPadding(false);
} else {
setLineSpacing(LINE_SPACING_EXTRA, LINE_SPACING_MULTIPLIER_SINGLELINE);
setIncludeFontPadding(true);
}
// according to
// <http://code.google.com/p/android/issues/detail?id=22493>
// we have to add a unicode character to trigger the text centering
// in ICS. this particular character is known as "zero-width" and
// does no harm.
setText(finalText + "\u200B");
int measuredWidth = textSize.width() + getCompoundPaddingLeft() + getCompoundPaddingRight();
int measuredHeight = textSize.height() + getCompoundPaddingTop() + getCompoundPaddingBottom();
// expand the view to the parent's height in case it is smaller or
// to the minimum height that has been set
// FIXME: honor the vertical measure mode (EXACTLY vs AT_MOST) here
// somehow
measuredHeight = Math.max(measuredHeight, MeasureSpec.getSize(heightMeasureSpec));
setMeasuredDimension(measuredWidth, measuredHeight);
} finally {
mInMeasure = false;
}
}
private Rect getTextSize(String text, TextPaint textPaint, float textSize) {
textPaint.setTextSize(textSize);
// StaticLayout depends on a given width in which it should lay out the
// text (and optionally also split into separate lines).
// Therefor we calculate the current text width manually and start with
// a fake (read: maxmimum) width for the height calculation.
// We do _not_ use layout.getLineWidth() here since this returns
// slightly smaller numbers and therefor would lead to exceeded text box
// drawing.
StaticLayout layout = new StaticLayout(text, textPaint, Integer.MAX_VALUE, Alignment.ALIGN_NORMAL, 1f, 0f, true);
int textWidth = 0;
String lines[] = text.split(System.getProperty("line.separator"));
for (int i = 0; i < lines.length; ++i) {
textWidth = Math.max(textWidth, measureTextWidth(textPaint, lines[i]));
}
return new Rect(0, 0, textWidth, layout.getHeight());
}
private String resizeLine(TextPaint textPaint, String line, int availableWidth) {
checkArgument(line != null && line.length() > 0, "expected non-empty string");
int textWidth = measureTextWidth(textPaint, line);
int lastDeletePos = -1;
StringBuilder builder = new StringBuilder(line);
while (textWidth > availableWidth && builder.length() > 0) {
lastDeletePos = builder.length() / 2;
builder.deleteCharAt(builder.length() / 2);
// don't forget to measure the ellipsis character as well; it
// doesn't matter where it is located in the line, it just has to be
// there, since there are no (known) ligatures that use this glyph
String textToMeasure = builder.toString() + ELLIPSIS;
textWidth = measureTextWidth(textPaint, textToMeasure);
}
if (lastDeletePos > -1) {
builder.insert(lastDeletePos, ELLIPSIS);
}
return builder.toString();
}
// there are several methods in Android to determine the text width, namely
// getBounds() and measureText().
// The latter works for us the best as it gives us the best / nearest
// results without that our text canvas needs to wrap its text later on
// again.
private int measureTextWidth(TextPaint textPaint, String line) {
return Math.round(textPaint.measureText(line));
}
}
[2012-11-21修订]
修复了省略号的位置(差一个错误)
重做文本大小计算;现在总是测量包括换行符在内的全文,以解决当添加两个单个测量行的高度并没有导致与整个文本高度测量结果相同时的问题
而不是循环查找最小的可用文本大小,只需在第一次测量后计算它
以下是我为仍在搜索的任何人找到的其他内容的枚举:
1) Here's a solution 递归地重新绘制 textview 直到它适合。这意味着从字面上看你的文本缩小到位,但至少它在完成时适合。代码需要一些调整才能实现,但大部分都在那里。
2)您可以尝试将自定义解决方案(如 this 或 this 中的 dunni 类)组合在一起,这就是我使用 getPaint().measureText(str) 搜索正确大小的方法,但它得到了很多更混乱,因为我需要它只包装在空格上......
3)你可以继续搜索——我已经尝试了比我数不清的更多选择。 Ted 关于 StaticLayout 的建议对我来说并没有得到回报,但也许有一些东西;我尝试使用 StaticLayout.getEllipsis(line) 来确定文本是否离开屏幕,但没有效果。请参阅我关于该 here 的(目前未答复的)帖子。
我的方法是:
public void changeTextSize(int initialSize, TextView tv) {
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
double width = displayMetrics.widthPixels / displayMetrics.xdpi;
double height = displayMetrics.heightPixels / displayMetrics.ydpi;
Log.i("LOG", "The width of the tested emulator is: " + width);
Log.i("LOG", "The height of the tested emulator is: " + height);
double scale = Math.min(width / 2.25, height / 4.0); //See the logcat >>> width = 2.25 and heigt = 4.0
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, (int) (initialSize * scale));
}
例如:
changeTextSize(16, findViewById(R.id.myTextView));
changeTextSize(12, findViewById(R.id.myEditText));
我需要一个具体的解决方案。我的布局中有一个edittext和textview。 textview 是固定的高度和宽度。当用户开始输入编辑文本时,文本应该立即出现在文本视图中。文本字段中的文本应自动调整大小以适合文本视图。所以我更新了 Chase 的解决方案来为我工作。因此,当文本视图中的文本发生更改时,开始调整大小。我的和 Chase 的解决方案之间的区别:即使用户删除了一些字符,也会调整大小。我希望它可以帮助某人。
public class TextFitTextView extends TextView {
// Minimum text size for this text view
public static final float MIN_TEXT_SIZE = 10;
// Maximum text size for this text view - if it is 0, then the text acts
// like match_parent
public static final float MAX_TEXT_SIZE = 0;
// Our ellipse string
private static final String mEllipsis = "...";
// Text size that is set from code. This acts as a starting point for
// resizing
private float mTextSize;
// Lower bounds for text size
private float mMinTextSize = MIN_TEXT_SIZE;
// Max bounds for text size
private float mMaxTextSize = MAX_TEXT_SIZE;
// Text view line spacing multiplier
private float mSpacingMult = 1.0f;
// Text view additional line spacing
private float mSpacingAdd = 0.0f;
// Add ellipsis to text that overflows at the smallest text size
private boolean mAddEllipsis = true;
// Add ellipsis to text that overflows at the smallest text size
private int heightLimit;
private int widthLimit;
// Default constructor override
public TextFitTextView(Context context) {
this(context, null);
}
// Default constructor when inflating from XML file
public TextFitTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
// Default constructor override
public TextFitTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mTextSize = getTextSize();
}
/**
* When text changes resize the text size.
*/
@Override
protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
// if we are adding new chars to text
if (before <= after && after != 1) {
resizeText(true);
// now we are deleting chars
} else {
resizeText(false);
}
}
/**
* Override the set text size to update our internal reference values
*/
@Override
public void setTextSize(float size) {
super.setTextSize(size);
mTextSize = getTextSize();
}
/**
* Override the set text size to update our internal reference values
*/
@Override
public void setTextSize(int unit, float size) {
super.setTextSize(unit, size);
mTextSize = getTextSize();
}
/**
* Override the set line spacing to update our internal reference values
*/
@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;
requestLayout();
invalidate();
}
/**
* Return lower text size limit
*
* @return
*/
public float getMinTextSize() {
return mMinTextSize;
}
/**
* Set flag to add ellipsis to text that overflows at the smallest text size
*
* @param addEllipsis
*/
public void setAddEllipsis(boolean addEllipsis) {
mAddEllipsis = addEllipsis;
}
/**
* Return flag to add ellipsis to text that overflows at the smallest text
* size
*
* @return
*/
public boolean getAddEllipsis() {
return mAddEllipsis;
}
/**
* Get width and height limits
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (widthLimit == 0 && heightLimit == 0) {
widthLimit = (right - left) - getCompoundPaddingLeft() - getCompoundPaddingRight();
heightLimit = (bottom - top) - getCompoundPaddingBottom() - getCompoundPaddingTop();
}
super.onLayout(changed, left, top, right, bottom);
}
/**
* Resize the text size with specified width and height
*
* @param width
* @param height
*/
public void resizeText(boolean increase) {
CharSequence text = getText();
// Do not resize if the view does not have dimensions or there is no
// text
if (text == null || text.length() == 0 || heightLimit <= 0 || widthLimit <= 0 || mTextSize == 0) {
return;
}
// Get the text view's paint object
TextPaint textPaint = getPaint();
// Get the required text height
int textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize);
// If the text length is increased
// Until we either fit within our text view or we had reached our min
// text size, incrementally try smaller sizes
if (increase) {
while (textHeight > heightLimit && mTextSize > mMinTextSize) {
mTextSize = Math.max(mTextSize - 2, mMinTextSize);
textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize);
}
}
// text length has been decreased
else {
// if max test size is set then add it to while condition
if (mMaxTextSize != 0) {
while (textHeight < heightLimit && mTextSize <= mMaxTextSize) {
mTextSize = mTextSize + 2;
textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize);
}
} else {
while (textHeight < heightLimit) {
mTextSize = mTextSize + 2;
textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize);
}
}
mTextSize = textHeight > heightLimit ? mTextSize - 2 : mTextSize;
}
// If we had reached our minimum text size and still don't fit, append
// an ellipsis
if (mAddEllipsis && mTextSize == mMinTextSize && textHeight > heightLimit) {
// Draw using a static layout
TextPaint paint = new TextPaint(textPaint);
StaticLayout layout = new StaticLayout(text, paint, widthLimit, Alignment.ALIGN_NORMAL, mSpacingMult,
mSpacingAdd, false);
// Check that we have a least one line of rendered text
if (layout.getLineCount() > 0) {
// Since the line at the specific vertical position would be cut
// off,
// we must trim up to the previous line
int lastLine = layout.getLineForVertical(heightLimit) - 1;
// If the text would not even fit on a single line, clear it
if (lastLine < 0) {
setText("");
}
// Otherwise, trim to the previous line and add an ellipsis
else {
int start = layout.getLineStart(lastLine);
int end = layout.getLineEnd(lastLine);
float lineWidth = layout.getLineWidth(lastLine);
float ellipseWidth = paint.measureText(mEllipsis);
// Trim characters off until we have enough room to draw the
// ellipsis
while (widthLimit < lineWidth + ellipseWidth) {
lineWidth = paint.measureText(text.subSequence(start, --end + 1).toString());
}
setText(text.subSequence(0, end) + mEllipsis);
}
}
}
// Some devices try to auto adjust line spacing, so force default line
// spacing
// and invalidate the layout as a side effect
setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
setLineSpacing(mSpacingAdd, mSpacingMult);
}
// Set the text size of the text paint object and use a static layout to
// render text off screen before measuring
private int getTextHeight(CharSequence source, TextPaint originalPaint, int width, float textSize) {
// Update the text paint object
TextPaint paint = new TextPaint(originalPaint);
paint.setTextSize(textSize);
// Measure using a static layout
StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd,
true);
return layout.getHeight();
}
}
为在 Xamarin.Android 上编码的人提供此版本的在 C# 上重写的最佳答案。为我工作得很好。
/**
* DO WHAT YOU WANT TO PUBLIC LICENSE
* Version 2, December 2004
*
* Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
*
* Everyone is permitted to copy and distribute verbatim or modified
* copies of this license document, and changing it is allowed as long
* as the name is changed.
*
* DO WHAT YOU WANT TO PUBLIC LICENSE
* TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
*
* 0. You just DO WHAT YOU WANT TO.
*/
using System;
using Android.Content;
using Android.Runtime;
using Android.Text;
using Android.Util;
using Android.Widget;
using Java.Lang;
namespace App.GuestGuide.Droid.Controls
{
public class OnTextResizeEventArgs : EventArgs
{
public TextView TextView { get; set; }
public float OldSize { get; set; }
public float NewSize { get; set; }
}
/// <inheritdoc />
/// <summary>
/// Text view that auto adjusts text size to fit within the view.
/// If the text size equals the minimum text size and still does not
/// fit, append with an ellipsis.
/// </summary>
public class AutoResizeTextView : TextView
{
/// <summary>
/// Minimum text size for this text view
/// </summary>
public static float MIN_TEXT_SIZE = 10;
/// <summary>
/// Our ellipse string
/// </summary>
private const string Ellipsis = "...";
private float _mMaxTextSize;
private float _mMinTextSize = MIN_TEXT_SIZE;
/// <summary>
/// Register subscriber to receive resize notifications
/// </summary>
public event EventHandler<OnTextResizeEventArgs> OnTextResize;
/// <summary>
/// Flag for text and/or size changes to force a resize
/// </summary>
private bool _needsResize;
/// <summary>
/// Text size that is set from code. This acts as a starting point for resizing
/// </summary>
private float _textSize;
/// <summary>
/// Text view line spacing multiplier
/// </summary>
private float _spacingMult = 1.0f;
/// <summary>
/// Text view additional line spacing
/// </summary>
private float _spacingAdd;
/// <summary>
/// Add ellipsis to text that overflows at the smallest text size
/// </summary>
public bool ShouldAddEllipsis { get; set; }
/// <inheritdoc />
/// <summary>
/// Override the set text size to update our internal reference values
/// </summary>
public override float TextSize
{
get => base.TextSize;
set
{
base.TextSize = value;
_textSize = TextSize;
}
}
/// <summary>
/// Temporary upper bounds on the starting text size
/// </summary>
public float MaxTextSize
{
get => _mMaxTextSize;
// Set the upper text size limit and invalidate the view
set
{
_mMaxTextSize = value;
RequestLayout();
Invalidate();
}
}
/// <summary>
/// Lower bounds for text size
/// </summary>
public float MinTextSize
{
get => _mMinTextSize;
//Set the lower text size limit and invalidate the view
set
{
_mMinTextSize = value;
RequestLayout();
Invalidate();
}
}
public AutoResizeTextView(Context context) : this(context, null)
{
}
public AutoResizeTextView(Context context, IAttributeSet attrs) : this(context, attrs, 0)
{
}
public AutoResizeTextView(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr)
{
_textSize = TextSize;
}
public AutoResizeTextView(Context context, IAttributeSet attrs, int defStyleAttr, int defStyleRes) : base(context, attrs, defStyleAttr, defStyleRes)
{
_textSize = TextSize;
}
protected AutoResizeTextView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
{
_textSize = TextSize;
}
/// <inheritdoc />
/// <summary>
/// When text changes, set the force resize flag to true and reset the text size.
/// </summary>
/// <param name="text"></param>
/// <param name="start"></param>
/// <param name="lengthBefore"></param>
/// <param name="lengthAfter"></param>
protected override void OnTextChanged(ICharSequence text, int start, int lengthBefore, int lengthAfter)
{
_needsResize = true;
// Since this view may be reused, it is good to reset the text size
ResetTextSize();
}
/// <inheritdoc />
/// <summary>
/// If the text view size changed, set the force resize flag to true
/// </summary>
/// <param name="w"></param>
/// <param name="h"></param>
/// <param name="oldw"></param>
/// <param name="oldh"></param>
protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
{
if (w != oldw || h != oldh)
{
_needsResize = true;
}
}
public override void SetTextSize([GeneratedEnum] ComplexUnitType unit, float size)
{
base.SetTextSize(unit, size);
_textSize = TextSize;
}
/// <inheritdoc />
/// <summary>
/// Override the set line spacing to update our internal reference values
/// </summary>
/// <param name="add"></param>
/// <param name="mult"></param>
public override void SetLineSpacing(float add, float mult)
{
base.SetLineSpacing(add, mult);
_spacingMult = mult;
_spacingAdd = add;
}
/// <summary>
/// Reset the text to the original size
/// </summary>
public void ResetTextSize()
{
if (_textSize > 0)
{
base.SetTextSize(ComplexUnitType.Px, _textSize);
_mMaxTextSize = _textSize;
}
}
/// <inheritdoc />
/// <summary>
/// Resize text after measuring
/// </summary>
/// <param name="changed"></param>
/// <param name="left"></param>
/// <param name="top"></param>
/// <param name="right"></param>
/// <param name="bottom"></param>
protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
{
if (changed || _needsResize)
{
var widthLimit = (right - left) - CompoundPaddingLeft - CompoundPaddingRight;
var heightLimit = (bottom - top) - CompoundPaddingBottom - CompoundPaddingTop;
ResizeText(widthLimit, heightLimit);
}
base.OnLayout(changed, left, top, right, bottom);
}
/// <summary>
/// Resize the text size with default width and height
/// </summary>
public void ResizeText()
{
var heightLimit = Height - PaddingBottom - PaddingTop;
var widthLimit = Width - PaddingLeft - PaddingRight;
ResizeText(widthLimit, heightLimit);
}
/// <summary>
/// Resize the text size with specified width and height
/// </summary>
/// <param name="width"></param>
/// <param name="height"></param>
public void ResizeText(int width, int height)
{
ICharSequence text = null;
if (!string.IsNullOrEmpty(Text))
{
text = new Java.Lang.String(Text);
}
// Do not resize if the view does not have dimensions or there is no text
if (text == null || text.Length() == 0 || height <= 0 || width <= 0 || _textSize == 0)
{
return;
}
if (TransformationMethod != null)
{
text = TransformationMethod.GetTransformationFormatted(text, this);
}
// Get the text view's paint object
var textPaint = Paint;
// Store the current text size
var oldTextSize = textPaint.TextSize;
// If there is a max text size set, use the lesser of that and the default text size
var targetTextSize = _mMaxTextSize > 0 ? System.Math.Min(_textSize, _mMaxTextSize) : _textSize;
// Get the required text height
var textHeight = GetTextHeight(text, textPaint, width, targetTextSize);
// Until we either fit within our text view or we had reached our min text size, incrementally try smaller sizes
while (textHeight > height && targetTextSize > _mMinTextSize)
{
targetTextSize = System.Math.Max(targetTextSize - 2, _mMinTextSize);
textHeight = GetTextHeight(text, textPaint, width, targetTextSize);
}
// If we had reached our minimum text size and still don't fit, append an ellipsis
if (ShouldAddEllipsis && targetTextSize == _mMinTextSize && textHeight > height)
{
// Draw using a static layout
// modified: use a copy of TextPaint for measuring
var paint = new TextPaint(textPaint);
// Draw using a static layout
var layout = new StaticLayout(text, paint, width, Layout.Alignment.AlignNormal, _spacingMult, _spacingAdd, false);
// Check that we have a least one line of rendered text
if (layout.LineCount > 0)
{
// Since the line at the specific vertical position would be cut off,
// we must trim up to the previous line
var lastLine = layout.GetLineForVertical(height) - 1;
// If the text would not even fit on a single line, clear it
if (lastLine < 0)
{
Text = string.Empty;
}
// Otherwise, trim to the previous line and add an ellipsis
else
{
var start = layout.GetLineStart(lastLine);
var end = layout.GetLineEnd(lastLine);
var lineWidth = layout.GetLineWidth(lastLine);
var ellipseWidth = textPaint.MeasureText(Ellipsis);
// Trim characters off until we have enough room to draw the ellipsis
while (width < lineWidth + ellipseWidth)
{
lineWidth = textPaint.MeasureText(text.SubSequence(start, --end + 1));
}
Text = text.SubSequence(0, end) + Ellipsis;
}
}
}
// Some devices try to auto adjust line spacing, so force default line spacing
// and invalidate the layout as a side effect
SetTextSize(ComplexUnitType.Px, targetTextSize);
SetLineSpacing(_spacingAdd, _spacingMult);
var notifyArgs = new OnTextResizeEventArgs
{
TextView = this,
NewSize = targetTextSize,
OldSize = oldTextSize
};
// Notify the listener if registered
OnTextResize?.Invoke(this, notifyArgs);
// Reset force resize flag
_needsResize = false;
}
/// <summary>
/// Set the text size of the text paint object and use a static layout to render text off screen before measuring
/// </summary>
/// <param name="source"></param>
/// <param name="paint"></param>
/// <param name="width"></param>
/// <param name="textSize"></param>
/// <returns></returns>
private int GetTextHeight(ICharSequence source, TextPaint paint, int width, float textSize)
{
// modified: make a copy of the original TextPaint object for measuring
// (apparently the object gets modified while measuring, see also the
// docs for TextView.getPaint() (which states to access it read-only)
// Update the text paint object
var paintCopy = new TextPaint(paint)
{
TextSize = textSize
};
// Measure using a static layout
var layout = new StaticLayout(source, paintCopy, width, Layout.Alignment.AlignNormal, _spacingMult, _spacingAdd, true);
return layout.Height;
}
}
}
您可以为此使用 android.text.StaticLayout
类。这就是 TextView
在内部使用的。
我刚刚创建了以下方法(基于 Chase 的想法),如果您想在任何画布上绘制文本,它可能会对您有所帮助:
private static void drawText(Canvas canvas, int xStart, int yStart,
int xWidth, int yHeigth, String textToDisplay,
TextPaint paintToUse, float startTextSizeInPixels,
float stepSizeForTextSizeSteps) {
// Text view line spacing multiplier
float mSpacingMult = 1.0f;
// Text view additional line spacing
float mSpacingAdd = 0.0f;
StaticLayout l = null;
do {
paintToUse.setTextSize(startTextSizeInPixels);
startTextSizeInPixels -= stepSizeForTextSizeSteps;
l = new StaticLayout(textToDisplay, paintToUse, xWidth,
Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
} while (l.getHeight() > yHeigth);
int textCenterX = xStart + (xWidth / 2);
int textCenterY = (yHeigth - l.getHeight()) / 2;
canvas.save();
canvas.translate(textCenterX, textCenterY);
l.draw(canvas);
canvas.restore();
}
这可以用在任何自定义视图的任何 onDraw() 方法中。
这是另一种解决方案,只是为了好玩。它可能不是很有效,但它确实可以处理文本的高度和宽度,以及标记的文本。
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)
{
if ((MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED)
&& (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.UNSPECIFIED)) {
final float desiredWidth = MeasureSpec.getSize(widthMeasureSpec);
final float desiredHeight = MeasureSpec.getSize(heightMeasureSpec);
float textSize = getTextSize();
float lastScale = Float.NEGATIVE_INFINITY;
while (textSize > MINIMUM_AUTO_TEXT_SIZE_PX) {
// Measure how big the textview would like to be with the current text size.
super.onMeasure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
// Calculate how much we'd need to scale it to fit the desired size, and
// apply that scaling to the text size as an estimate of what we need.
final float widthScale = desiredWidth / getMeasuredWidth();
final float heightScale = desiredHeight / getMeasuredHeight();
final float scale = Math.min(widthScale, heightScale);
// If we don't need to shrink the text, or we don't seem to be converging, we're done.
if ((scale >= 1f) || (scale <= lastScale)) {
break;
}
// Shrink the text size and keep trying.
textSize = Math.max((float) Math.floor(scale * textSize), MINIMUM_AUTO_TEXT_SIZE_PX);
setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
lastScale = scale;
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
我结合了上面的一些建议,用二分法制作了一个可以放大和缩小的建议。它还在宽度内缩放。
/**
* DO WHAT YOU WANT TO PUBLIC LICENSE
* Version 2, December 2004
*
* Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
*
* Everyone is permitted to copy and distribute verbatim or modified
* copies of this license document, and changing it is allowed as long
* as the name is changed.
*
* DO WHAT YOU WANT TO PUBLIC LICENSE
* TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
*
* 0. You just DO WHAT YOU WANT TO.
*/
import android.content.Context;
import android.text.Layout.Alignment;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.TextView;
/**
* Text view that auto adjusts text size to fit within the view. If the text
* size equals the minimum text size and still does not fit, append with an
* ellipsis.
*
* @author Chase Colburn
* @since Apr 4, 2011
*/
public class AutoResizeTextView extends TextView {
// Minimum text size for this text view
public static final float MIN_TEXT_SIZE = 10;
// Minimum text size for this text view
public static final float MAX_TEXT_SIZE = 128;
private static final int BISECTION_LOOP_WATCH_DOG = 30;
// Interface for resize notifications
public interface OnTextResizeListener {
public void onTextResize(TextView textView, float oldSize, float newSize);
}
// Our ellipse string
private static final String mEllipsis = "...";
// Registered resize listener
private OnTextResizeListener mTextResizeListener;
// Flag for text and/or size changes to force a resize
private boolean mNeedsResize = false;
// Text size that is set from code. This acts as a starting point for
// resizing
private float mTextSize;
// Temporary upper bounds on the starting text size
private float mMaxTextSize = MAX_TEXT_SIZE;
// Lower bounds for text size
private float mMinTextSize = MIN_TEXT_SIZE;
// Text view line spacing multiplier
private float mSpacingMult = 1.0f;
// Text view additional line spacing
private float mSpacingAdd = 0.0f;
// Add ellipsis to text that overflows at the smallest text size
private boolean mAddEllipsis = true;
// Default constructor override
public AutoResizeTextView(Context context) {
this(context, null);
}
// Default constructor when inflating from XML file
public AutoResizeTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
// Default constructor override
public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mTextSize = getTextSize();
}
/**
* When text changes, set the force resize flag to true and reset the text
* size.
*/
@Override
protected void onTextChanged(final CharSequence text, final int start,
final int before, final int after) {
mNeedsResize = true;
// Since this view may be reused, it is good to reset the text size
resetTextSize();
}
/**
* If the text view size changed, set the force resize flag to true
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (w != oldw || h != oldh) {
mNeedsResize = true;
}
}
/**
* Register listener to receive resize notifications
*
* @param listener
*/
public void setOnResizeListener(OnTextResizeListener listener) {
mTextResizeListener = listener;
}
/**
* Override the set text size to update our internal reference values
*/
@Override
public void setTextSize(float size) {
super.setTextSize(size);
mTextSize = getTextSize();
}
/**
* Override the set text size to update our internal reference values
*/
@Override
public void setTextSize(int unit, float size) {
super.setTextSize(unit, size);
mTextSize = getTextSize();
}
/**
* Override the set line spacing to update our internal reference values
*/
@Override
public void setLineSpacing(float add, float mult) {
super.setLineSpacing(add, mult);
mSpacingMult = mult;
mSpacingAdd = add;
}
/**
* Set the upper text size limit and invalidate the view
*
* @param maxTextSize
*/
public void setMaxTextSize(float maxTextSize) {
mMaxTextSize = maxTextSize;
requestLayout();
invalidate();
}
/**
* Return upper text size limit
*
* @return
*/
public float getMaxTextSize() {
return mMaxTextSize;
}
/**
* Set the lower text size limit and invalidate the view
*
* @param minTextSize
*/
public void setMinTextSize(float minTextSize) {
mMinTextSize = minTextSize;
requestLayout();
invalidate();
}
/**
* Return lower text size limit
*
* @return
*/
public float getMinTextSize() {
return mMinTextSize;
}
/**
* Set flag to add ellipsis to text that overflows at the smallest text size
*
* @param addEllipsis
*/
public void setAddEllipsis(boolean addEllipsis) {
mAddEllipsis = addEllipsis;
}
/**
* Return flag to add ellipsis to text that overflows at the smallest text
* size
*
* @return
*/
public boolean getAddEllipsis() {
return mAddEllipsis;
}
/**
* Reset the text to the original size
*/
public void resetTextSize() {
if (mTextSize > 0) {
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
// mMaxTextSize = mTextSize;
}
}
/**
* Resize text after measuring
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
if (changed || mNeedsResize) {
int widthLimit = (right - left) - getCompoundPaddingLeft()
- getCompoundPaddingRight();
int heightLimit = (bottom - top) - getCompoundPaddingBottom()
- getCompoundPaddingTop();
resizeText(widthLimit, heightLimit);
}
super.onLayout(changed, left, top, right, bottom);
}
/**
* Resize the text size with default width and height
*/
public void resizeText() {
// Height and width with a padding as a percentage of height
int heightLimit = getHeight() - getPaddingBottom() - getPaddingTop();
int widthLimit = getWidth() - getPaddingLeft() - getPaddingRight();
resizeText(widthLimit, heightLimit);
}
/**
* Resize the text size with specified width and height
*
* @param width
* @param height
*/
public void resizeText(int width, int height) {
CharSequence text = getText();
// Do not resize if the view does not have dimensions or there is no
// text
if (text == null || text.length() == 0 || height <= 0 || width <= 0
|| mTextSize == 0) {
return;
}
// Get the text view's paint object
TextPaint textPaint = getPaint();
// Store the current text size
float oldTextSize = textPaint.getTextSize();
// Bisection method: fast & precise
float lower = mMinTextSize;
float upper = mMaxTextSize;
int loop_counter = 1;
float targetTextSize = (lower + upper) / 2;
int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
int textWidth = getTextWidth(text, textPaint, width, targetTextSize);
while (loop_counter < BISECTION_LOOP_WATCH_DOG && upper - lower > 1) {
targetTextSize = (lower + upper) / 2;
textHeight = getTextHeight(text, textPaint, width, targetTextSize);
textWidth = getTextWidth(text, textPaint, width, targetTextSize);
if (textHeight > (height) || textWidth > (width))
upper = targetTextSize;
else
lower = targetTextSize;
loop_counter++;
}
targetTextSize = lower;
textHeight = getTextHeight(text, textPaint, width, targetTextSize);
// If we had reached our minimum text size and still don't fit, append
// an ellipsis
if (mAddEllipsis && targetTextSize == mMinTextSize
&& textHeight > height) {
// Draw using a static layout
// modified: use a copy of TextPaint for measuring
TextPaint paintCopy = new TextPaint(textPaint);
paintCopy.setTextSize(targetTextSize);
StaticLayout layout = new StaticLayout(text, paintCopy, width,
Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, false);
// Check that we have a least one line of rendered text
if (layout.getLineCount() > 0) {
// Since the line at the specific vertical position would be cut
// off,
// we must trim up to the previous line
int lastLine = layout.getLineForVertical(height) - 1;
// If the text would not even fit on a single line, clear it
if (lastLine < 0) {
setText("");
}
// Otherwise, trim to the previous line and add an ellipsis
else {
int start = layout.getLineStart(lastLine);
int end = layout.getLineEnd(lastLine);
float lineWidth = layout.getLineWidth(lastLine);
float ellipseWidth = paintCopy.measureText(mEllipsis);
// Trim characters off until we have enough room to draw the
// ellipsis
while (width < lineWidth + ellipseWidth) {
lineWidth = paintCopy.measureText(text.subSequence(
start, --end + 1).toString());
}
setText(text.subSequence(0, end) + mEllipsis);
}
}
}
// Some devices try to auto adjust line spacing, so force default line
// spacing
// and invalidate the layout as a side effect
setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize);
setLineSpacing(mSpacingAdd, mSpacingMult);
// Notify the listener if registered
if (mTextResizeListener != null) {
mTextResizeListener.onTextResize(this, oldTextSize, targetTextSize);
}
// Reset force resize flag
mNeedsResize = false;
}
// Set the text size of the text paint object and use a static layout to
// render text off screen before measuring
private int getTextHeight(CharSequence source, TextPaint originalPaint,
int width, float textSize) {
// modified: make a copy of the original TextPaint object for measuring
// (apparently the object gets modified while measuring, see also the
// docs for TextView.getPaint() (which states to access it read-only)
TextPaint paint = new TextPaint(originalPaint);
// Update the text paint object
paint.setTextSize(textSize);
// Measure using a static layout
StaticLayout layout = new StaticLayout(source, paint, width,
Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
return layout.getHeight();
}
// Set the text size of the text paint object and use a static layout to
// render text off screen before measuring
private int getTextWidth(CharSequence source, TextPaint originalPaint,
int width, float textSize) {
// Update the text paint object
TextPaint paint = new TextPaint(originalPaint);
// Draw using a static layout
paint.setTextSize(textSize);
StaticLayout layout = new StaticLayout(source, paint, width,
Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
return (int) layout.getLineWidth(0);
}
}
我使用了来自追逐和 M-WaJeEh 的代码,我在这里发现了一些优点和缺点
从追逐
优点:非常适合 1 行 TextView 缺点:如果超过 1 行带有自定义字体,如果启用椭圆,一些文本会消失,如果是自定义字体(字体),它没有为椭圆准备空间,它不支持
来自 M-WaJeEh
优点:它非常适合多行缺点:如果将高度设置为 wrap-content,此代码将从最小尺寸开始,它会尽可能减小,而不是从 setSize 开始,如果它是自定义字体,则减少有限宽度(字体),它不支持
getTextHeight()
中崩溃。安卓 4.0.4 模拟器。 java.lang.IllegalArgumentException: Layout: -40 < 0 at android.text.Layout.<init>(Layout.java:140) at android.text.StaticLayout.<init>(StaticLayout.java:104) at android.text.StaticLayout.<init>(StaticLayout.java:90) at android.text.StaticLayout.<init>(StaticLayout.java:68) at android.text.StaticLayout.<init>(StaticLayout.java:48)
该解决方案适用于我们:
public class CustomFontButtonTextFit extends CustomFontButton
{
private final float DECREMENT_FACTOR = .1f;
public CustomFontButtonTextFit(Context context) {
super(context);
}
public CustomFontButtonTextFit(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomFontButtonTextFit(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
}
private synchronized void refitText(String text, int textWidth) {
if (textWidth > 0)
{
float availableWidth = textWidth - this.getPaddingLeft()
- this.getPaddingRight();
TextPaint tp = getPaint();
Rect rect = new Rect();
tp.getTextBounds(text, 0, text.length(), rect);
float size = rect.width();
while(size > availableWidth)
{
setTextSize( getTextSize() - DECREMENT_FACTOR );
tp = getPaint();
tp.getTextBounds(text, 0, text.length(), rect);
size = rect.width();
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
refitText(this.getText().toString(), parentWidth);
if(parentWidth < getSuggestedMinimumWidth())
parentWidth = getSuggestedMinimumWidth();
if(parentHeight < getSuggestedMinimumHeight())
parentHeight = getSuggestedMinimumHeight();
this.setMeasuredDimension(parentWidth, parentHeight);
}
@Override
protected void onTextChanged(final CharSequence text, final int start,
final int before, final int after)
{
super.onTextChanged(text, start, before, after);
refitText(text.toString(), this.getWidth());
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w, h, oldw, oldh);
if (w != oldw)
refitText(this.getText().toString(), w);
}
}
Button
类扩展 CustomFontButtonTextFit
,则 UI 永远不会显示。我会说这门课被打破了......
感谢 Chase 和 onoelle,对于懒惰的程序员,让我在这里发布他们出色的合并代码的工作版本,适用于 Button,而不是 TextView。
用 AutoResizeTextButtons 替换所有按钮(不是 ImageButtons),同样的无聊问题也为它们解决了。
这是代码。我刚刚删除了导入。
/**
* DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
* Version 2, December 2004
*
* Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
*
* Everyone is permitted to copy and distribute verbatim or modified
* copies of this license document, and changing it is allowed as long
* as the name is changed.
*
* DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
* TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
*
* 0. You just DO WHAT THE FUCK YOU WANT TO.
* made better by onoelle
* adapted for button by beppi
*/
/**
* Text Button that auto adjusts text size to fit within the view.
* If the text size equals the minimum text size and still does not
* fit, append with an ellipsis.
*
* @author Chase Colburn
* @since Apr 4, 2011
*/
public class AutoResizeTextButton extends Button {
// Minimum text size for this text view
public static final float MIN_TEXT_SIZE = 20;
// Interface for resize notifications
public interface OnTextResizeListener {
public void onTextResize(Button textView, float oldSize, float newSize);
}
// Our ellipse string
private static final String mEllipsis = "...";
// Registered resize listener
private OnTextResizeListener mTextResizeListener;
// Flag for text and/or size changes to force a resize
private boolean mNeedsResize = false;
// Text size that is set from code. This acts as a starting point for resizing
private float mTextSize;
// Temporary upper bounds on the starting text size
private float mMaxTextSize = 0;
// Lower bounds for text size
private float mMinTextSize = MIN_TEXT_SIZE;
// Text view line spacing multiplier
private float mSpacingMult = 1.0f;
// Text view additional line spacing
private float mSpacingAdd = 0.0f;
// Add ellipsis to text that overflows at the smallest text size
private boolean mAddEllipsis = true;
// Default constructor override
public AutoResizeTextButton(Context context) {
this(context, null);
}
// Default constructor when inflating from XML file
public AutoResizeTextButton(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
// Default constructor override
public AutoResizeTextButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mTextSize = getTextSize();
}
/**
* When text changes, set the force resize flag to true and reset the text size.
*/
@Override
protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
mNeedsResize = true;
// Since this view may be reused, it is good to reset the text size
resetTextSize();
}
/**
* If the text view size changed, set the force resize flag to true
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (w != oldw || h != oldh) {
mNeedsResize = true;
}
}
/**
* Register listener to receive resize notifications
* @param listener
*/
public void setOnResizeListener(OnTextResizeListener listener) {
mTextResizeListener = listener;
}
/**
* Override the set text size to update our internal reference values
*/
@Override
public void setTextSize(float size) {
super.setTextSize(size);
mTextSize = getTextSize();
}
/**
* Override the set text size to update our internal reference values
*/
@Override
public void setTextSize(int unit, float size) {
super.setTextSize(unit, size);
mTextSize = getTextSize();
}
/**
* Override the set line spacing to update our internal reference values
*/
@Override
public void setLineSpacing(float add, float mult) {
super.setLineSpacing(add, mult);
mSpacingMult = mult;
mSpacingAdd = add;
}
/**
* Set the upper text size limit and invalidate the view
* @param maxTextSize
*/
public void setMaxTextSize(float maxTextSize) {
mMaxTextSize = maxTextSize;
requestLayout();
invalidate();
}
/**
* Return upper text size limit
* @return
*/
public float getMaxTextSize() {
return mMaxTextSize;
}
/**
* Set the lower text size limit and invalidate the view
* @param minTextSize
*/
public void setMinTextSize(float minTextSize) {
mMinTextSize = minTextSize;
requestLayout();
invalidate();
}
/**
* Return lower text size limit
* @return
*/
public float getMinTextSize() {
return mMinTextSize;
}
/**
* Set flag to add ellipsis to text that overflows at the smallest text size
* @param addEllipsis
*/
public void setAddEllipsis(boolean addEllipsis) {
mAddEllipsis = addEllipsis;
}
/**
* Return flag to add ellipsis to text that overflows at the smallest text size
* @return
*/
public boolean getAddEllipsis() {
return mAddEllipsis;
}
/**
* Reset the text to the original size
*/
public void resetTextSize() {
if(mTextSize > 0) {
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
mMaxTextSize = mTextSize;
}
}
/**
* Resize text after measuring
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if(changed || mNeedsResize) {
int widthLimit = (right - left) - getCompoundPaddingLeft() - getCompoundPaddingRight();
int heightLimit = (bottom - top) - getCompoundPaddingBottom() - getCompoundPaddingTop();
resizeText(widthLimit, heightLimit);
}
super.onLayout(changed, left, top, right, bottom);
}
/**
* Resize the text size with default width and height
*/
public void resizeText() {
int heightLimit = getHeight() - getPaddingBottom() - getPaddingTop();
int widthLimit = getWidth() - getPaddingLeft() - getPaddingRight();
resizeText(widthLimit, heightLimit);
}
/**
* Resize the text size with specified width and height
* @param width
* @param height
*/
public void resizeText(int width, int height) {
CharSequence text = getText();
// Do not resize if the view does not have dimensions or there is no text
if(text == null || text.length() == 0 || height <= 0 || width <= 0 || mTextSize == 0) {
return;
}
// Get the text view's paint object
TextPaint textPaint = getPaint();
// Store the current text size
float oldTextSize = textPaint.getTextSize();
// If there is a max text size set, use the lesser of that and the default text size
float targetTextSize = mMaxTextSize > 0 ? Math.min(mTextSize, mMaxTextSize) : mTextSize;
// Get the required text height
int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
// Until we either fit within our text view or we had reached our min text size, incrementally try smaller sizes
while(textHeight > height && targetTextSize > mMinTextSize) {
targetTextSize = Math.max(targetTextSize - 2, mMinTextSize);
textHeight = getTextHeight(text, textPaint, width, targetTextSize);
}
// If we had reached our minimum text size and still don't fit, append an ellipsis
if(mAddEllipsis && targetTextSize == mMinTextSize && textHeight > height) {
// Draw using a static layout
// modified: use a copy of TextPaint for measuring
TextPaint paint = new TextPaint(textPaint);
StaticLayout layout = new StaticLayout(text, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, false);
// Check that we have a least one line of rendered text
if(layout.getLineCount() > 0) {
// Since the line at the specific vertical position would be cut off,
// we must trim up to the previous line
int lastLine = layout.getLineForVertical(height) - 1;
// If the text would not even fit on a single line, clear it
if(lastLine < 0) {
setText("");
}
// Otherwise, trim to the previous line and add an ellipsis
else {
int start = layout.getLineStart(lastLine);
int end = layout.getLineEnd(lastLine);
float lineWidth = layout.getLineWidth(lastLine);
float ellipseWidth = textPaint.measureText(mEllipsis);
// Trim characters off until we have enough room to draw the ellipsis
while(width < lineWidth + ellipseWidth) {
lineWidth = textPaint.measureText(text.subSequence(start, --end + 1).toString());
}
setText(text.subSequence(0, end) + mEllipsis);
}
}
}
// Some devices try to auto adjust line spacing, so force default line spacing
// and invalidate the layout as a side effect
// textPaint.setTextSize(targetTextSize);
// modified: setting text size via this.setTextSize (instead of textPaint.setTextSize(targetTextSize))
setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize);
setLineSpacing(mSpacingAdd, mSpacingMult);
// Notify the listener if registered
if(mTextResizeListener != null) {
mTextResizeListener.onTextResize(this, oldTextSize, targetTextSize);
}
// Reset force resize flag
mNeedsResize = false;
}
// Set the text size of the text paint object and use a static layout to render text off screen before measuring
private int getTextHeight(CharSequence source, TextPaint originalPaint, int width, float textSize) {
// modified: make a copy of the original TextPaint object for measuring
// (apparently the object gets modified while measuring, see also the
// docs for TextView.getPaint() (which states to access it read-only)
// Update the text paint object
TextPaint paint = new TextPaint(originalPaint);
paint.setTextSize(textSize);
// Measure using a static layout
StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
return layout.getHeight();
}
}
用法:
在您的 xml 中放置一个 AutoResizeTextButton 代替普通按钮,而不更改任何其他内容。在 onCreate() 中放置(例如):
myButton = (AutoResizeTextButton)getView().findViewById(id.myButton);
myButton.setMinTextSize(8f);
myButton.resizeText();
这是我采取的方法。这很简单。它在字体大小上使用逐次逼近为零,并且通常可以在不到 10 次迭代中计算出来。只需将“activityWidth”替换为您用来显示文本的任何视图的宽度。在我的示例中,它被设置为屏幕宽度的私有字段。 198 的初始字体大小仅在方法生成异常的情况下设置(这真的不应该发生):
private float GetFontSizeForScreenWidth(String text)
{
float fontsize = 198;
try
{
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
Typeface typeface = Typeface.create("Helvetica", Typeface.BOLD);
paint.setTypeface(typeface);
paint.setTextAlign(Align.CENTER);
int lowVal = 0;
int highVal = 2000;
int currentVal = highVal;
/*
* Successively approximate the screen size until it is
* within 2 pixels of the maximum screen width. Generally
* this will get you to the closest font size within about 10
* iterations.
*/
do
{
paint.setTextSize(currentVal);
float textWidth = paint.measureText(text);
float diff = activityWidth - textWidth;
if ((diff >= 0) && (diff <= 2))
{
fontsize = paint.getTextSize();
return fontsize;
}
if (textWidth > activityWidth)
highVal = currentVal;
else if (textWidth < activityWidth)
lowVal = currentVal;
else
{
fontsize = paint.getTextSize();
return fontsize;
}
currentVal = (highVal - lowVal) / 2 + lowVal;
} while (true);
}
catch (Exception ex)
{
return fontsize;
}
}
不定期副业成功案例分享