Android自适应TextView

236

背景

很多时候我们需要让TextView的字体自适应其给定的边界。

问题

可悲的是,尽管有许多线程和帖子(以及建议的解决方案)谈论这个问题(例如这里这里这里),但它们中没有一个真正有效。

这就是为什么我决定测试每一个直到找到真正的解决方案。

我认为这样的textView应该有以下要求:

  1. 应允许使用任何字体、字形、样式和字符集。

  2. 应处理宽度和高度。

  3. 不截断文本,除非由于我们给出的限制(例如:文本太长,可用空间太小)而无法容纳。但是,我们可以在需要时请求水平/垂直滚动条,仅针对这些情况。

  4. 应允许多行或单行。在多行的情况下,允许最大和最小行。

  5. 计算速度不应过慢。使用循环找到最佳大小?至少要进行优化,不要每次递增采样量。

  6. 在多行的情况下,应允许优先调整大小或使用更多行,并/或通过使用“\n”字符自己选择行。

我尝试过什么

我尝试了许多示例(包括我写过的那些链接),我也尝试修改它们以处理我所说的情况,但没有一个真正有效。

我制作了一个示例项目,可以让我直观地查看TextView是否正确地自适应。

目前,我的示例项目仅随机生成文本(英文字母加数字)和textView的大小,并使其保持单行,但即使在我尝试的任何示例中,这也无法良好运行。

以下是代码(也可在此处找到):

文件res/layout/activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
  android:layout_height="match_parent" tools:context=".MainActivity">
  <Button android:id="@+id/button1" android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_centerHorizontal="true" android:text="Button" />
  <FrameLayout android:layout_width="match_parent"
    android:layout_height="wrap_content" android:layout_above="@+id/button1"
    android:layout_alignParentLeft="true" android:background="#ffff0000"
    android:layout_alignParentRight="true" android:id="@+id/container"
    android:layout_alignParentTop="true" />

</RelativeLayout>

文件 src/.../MainActivity.java

public class MainActivity extends Activity
  {
  private final Random        _random            =new Random();
  private static final String ALLOWED_CHARACTERS ="qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890";

  @Override
  protected void onCreate(final Bundle savedInstanceState)
    {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final ViewGroup container=(ViewGroup)findViewById(R.id.container);
    findViewById(R.id.button1).setOnClickListener(new OnClickListener()
      {
        @Override
        public void onClick(final View v)
          {
          container.removeAllViews();
          final int maxWidth=container.getWidth();
          final int maxHeight=container.getHeight();
          final FontFitTextView fontFitTextView=new FontFitTextView(MainActivity.this);
          final int width=_random.nextInt(maxWidth)+1;
          final int height=_random.nextInt(maxHeight)+1;
          fontFitTextView.setLayoutParams(new LayoutParams(width,height));
          fontFitTextView.setSingleLine();
          fontFitTextView.setBackgroundColor(0xff00ff00);
          final String text=getRandomText();
          fontFitTextView.setText(text);
          container.addView(fontFitTextView);
          Log.d("DEBUG","width:"+width+" height:"+height+" text:"+text);
          }
      });
    }

  private String getRandomText()
    {
    final int textLength=_random.nextInt(20)+1;
    final StringBuilder builder=new StringBuilder();
    for(int i=0;i<textLength;++i)
      builder.append(ALLOWED_CHARACTERS.charAt(_random.nextInt(ALLOWED_CHARACTERS.length())));
    return builder.toString();
    }
  }

问题

有没有人知道一个实际可行的解决这个常见问题的方案?

即使是一个功能远不如我所写的那么多的解决方案,比如只有固定数量的文本行,并根据其大小调整字体,但永远不会出现奇怪的毛刺和文本相对于可用空间变得太大/太小。


GitHub 项目

由于这是如此重要的 TextView,我决定发布一个库,以便每个人都可以轻松使用它并为其做出贡献,在此处


你可以在这里找到一个好的解决方案。 - rule
1
@规则 这是我已经阅读过的文章之一,我测试了所有代码示例。同时我认为你发了重复的帖子。 - android developer
有什么想法为什么如果Auto-fit扩展EditText,它就无法工作? - ViksaaSkool
你尝试过 https://github.com/grantland/android-autofittextview 这个库吗?它可以在 MavenCentral 上获取。 - 13rac1
android:maxLines="1"android:gravity="center_vertical"(垂直居中)似乎存在冲突(垂直居中未应用)。 - matoni
显示剩余27条评论
16个回答

148
感谢MartinH的简单修复这里,此代码还处理了android:drawableLeftandroid:drawableRightandroid:drawableTopandroid:drawableBottom标签。
我的回答应该能让你满意自动缩放TextView文本以适应边界 我修改了你的测试用例:
@Override
protected void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final ViewGroup container = (ViewGroup) findViewById(R.id.container);
    findViewById(R.id.button1).setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(final View v) {
            container.removeAllViews();
            final int maxWidth = container.getWidth();
            final int maxHeight = container.getHeight();
            final AutoResizeTextView fontFitTextView = new AutoResizeTextView(MainActivity.this);
            final int width = _random.nextInt(maxWidth) + 1;
            final int height = _random.nextInt(maxHeight) + 1;
            fontFitTextView.setLayoutParams(new FrameLayout.LayoutParams(
                    width, height));
            int maxLines = _random.nextInt(4) + 1;
            fontFitTextView.setMaxLines(maxLines);
            fontFitTextView.setTextSize(500);// max size
            fontFitTextView.enableSizeCache(false);
            fontFitTextView.setBackgroundColor(0xff00ff00);
            final String text = getRandomText();
            fontFitTextView.setText(text);
            container.addView(fontFitTextView);
            Log.d("DEBUG", "width:" + width + " height:" + height
                    + " text:" + text + " maxLines:" + maxLines);
        }
    });
}

根据Android开发者的要求,我在这里发布代码:

最终效果:

输入图像描述

示例布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp" >

<com.vj.widgets.AutoResizeTextView
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:ellipsize="none"
    android:maxLines="2"
    android:text="Auto Resized Text, max 2 lines"
    android:textSize="100sp" /> <!-- maximum size -->

<com.vj.widgets.AutoResizeTextView
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:ellipsize="none"
    android:gravity="center"
    android:maxLines="1"
    android:text="Auto Resized Text, max 1 line"
    android:textSize="100sp" /> <!-- maximum size -->

<com.vj.widgets.AutoResizeTextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Auto Resized Text"
    android:textSize="500sp" /> <!-- maximum size -->

</LinearLayout>

Java代码:

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.RectF;
import android.os.Build;
import android.text.Layout.Alignment;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.SparseIntArray;
import android.util.TypedValue;
import android.widget.TextView;

public class AutoResizeTextView extends TextView {
    private interface SizeTester {
        /**
         *
         * @param suggestedSize
         *            Size of text to be tested
         * @param availableSpace
         *            available space in which text must fit
         * @return an integer < 0 if after applying {@code suggestedSize} to
         *         text, it takes less space than {@code availableSpace}, > 0
         *         otherwise
         */
        public int onTestSize(int suggestedSize, RectF availableSpace);
    }

    private RectF mTextRect = new RectF();

    private RectF mAvailableSpaceRect;

    private SparseIntArray mTextCachedSizes;

    private TextPaint mPaint;

    private float mMaxTextSize;

    private float mSpacingMult = 1.0f;

    private float mSpacingAdd = 0.0f;

    private float mMinTextSize = 20;

    private int mWidthLimit;

    private static final int NO_LINE_LIMIT = -1;
    private int mMaxLines;

    private boolean mEnableSizeCache = true;
    private boolean mInitializedDimens;

    public AutoResizeTextView(Context context) {
        super(context);
        initialize();
    }

    public AutoResizeTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialize();
    }

    public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initialize();
    }

    private void initialize() {
        mPaint = new TextPaint(getPaint());
        mMaxTextSize = getTextSize();
        mAvailableSpaceRect = new RectF();
        mTextCachedSizes = new SparseIntArray();
        if (mMaxLines == 0) {
            // no value was assigned during construction
            mMaxLines = NO_LINE_LIMIT;
        }
    }

    @Override
    public void setTextSize(float size) {
        mMaxTextSize = size;
        mTextCachedSizes.clear();
        adjustTextSize();
    }

    @Override
    public void setMaxLines(int maxlines) {
        super.setMaxLines(maxlines);
        mMaxLines = maxlines;
        adjustTextSize();
    }

    public int getMaxLines() {
        return mMaxLines;
    }

    @Override
    public void setSingleLine() {
        super.setSingleLine();
        mMaxLines = 1;
        adjustTextSize();
    }

    @Override
    public void setSingleLine(boolean singleLine) {
        super.setSingleLine(singleLine);
        if (singleLine) {
            mMaxLines = 1;
        } else {
            mMaxLines = NO_LINE_LIMIT;
        }
        adjustTextSize();
    }

    @Override
    public void setLines(int lines) {
        super.setLines(lines);
        mMaxLines = lines;
        adjustTextSize();
    }

    @Override
    public void setTextSize(int unit, float size) {
        Context c = getContext();
        Resources r;

        if (c == null)
            r = Resources.getSystem();
        else
            r = c.getResources();
        mMaxTextSize = TypedValue.applyDimension(unit, size,
                r.getDisplayMetrics());
        mTextCachedSizes.clear();
        adjustTextSize();
    }

    @Override
    public void setLineSpacing(float add, float mult) {
        super.setLineSpacing(add, mult);
        mSpacingMult = mult;
        mSpacingAdd = add;
    }

    /**
     * Set the lower text size limit and invalidate the view
     *
     * @param minTextSize
     */
    public void setMinTextSize(float minTextSize) {
        mMinTextSize = minTextSize;
        adjustTextSize();
    }

    private void adjustTextSize() {
        if (!mInitializedDimens) {
            return;
        }
        int startSize = (int) mMinTextSize;
        int heightLimit = getMeasuredHeight() - getCompoundPaddingBottom()
                - getCompoundPaddingTop();
        mWidthLimit = getMeasuredWidth() - getCompoundPaddingLeft()
                - getCompoundPaddingRight();
        mAvailableSpaceRect.right = mWidthLimit;
        mAvailableSpaceRect.bottom = heightLimit;
        super.setTextSize(
                TypedValue.COMPLEX_UNIT_PX,
                efficientTextSizeSearch(startSize, (int) mMaxTextSize,
                        mSizeTester, mAvailableSpaceRect));
    }

    private final SizeTester mSizeTester = new SizeTester() {
        @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
        @Override
        public int onTestSize(int suggestedSize, RectF availableSPace) {
            mPaint.setTextSize(suggestedSize);
            String text = getText().toString();
            boolean singleline = getMaxLines() == 1;
            if (singleline) {
                mTextRect.bottom = mPaint.getFontSpacing();
                mTextRect.right = mPaint.measureText(text);
            } else {
                StaticLayout layout = new StaticLayout(text, mPaint,
                        mWidthLimit, Alignment.ALIGN_NORMAL, mSpacingMult,
                        mSpacingAdd, true);

                // Return early if we have more lines
                if (getMaxLines() != NO_LINE_LIMIT
                        && layout.getLineCount() > getMaxLines()) {
                    return 1;
                }
                mTextRect.bottom = layout.getHeight();
                int maxWidth = -1;
                for (int i = 0; i < layout.getLineCount(); i++) {
                    if (maxWidth < layout.getLineWidth(i)) {
                        maxWidth = (int) layout.getLineWidth(i);
                    }
                }
                mTextRect.right = maxWidth;
            }

            mTextRect.offsetTo(0, 0);
            if (availableSPace.contains(mTextRect)) {

                // May be too small, don't worry we will find the best match
                return -1;
            } else {
                // too big
                return 1;
            }
        }
    };

    /**
     * Enables or disables size caching, enabling it will improve performance
     * where you are animating a value inside TextView. This stores the font
     * size against getText().length() Be careful though while enabling it as 0
     * takes more space than 1 on some fonts and so on.
     *
     * @param enable
     *            Enable font size caching
     */
    public void enableSizeCache(boolean enable) {
        mEnableSizeCache = enable;
        mTextCachedSizes.clear();
        adjustTextSize(getText().toString());
    }

    private int efficientTextSizeSearch(int start, int end,
            SizeTester sizeTester, RectF availableSpace) {
        if (!mEnableSizeCache) {
            return binarySearch(start, end, sizeTester, availableSpace);
        }
        int key = getText().toString().length();
        int size = mTextCachedSizes.get(key);
        if (size != 0) {
            return size;
        }
        size = binarySearch(start, end, sizeTester, availableSpace);
        mTextCachedSizes.put(key, size);
        return size;
    }

    private static int binarySearch(int start, int end, SizeTester sizeTester,
            RectF availableSpace) {
        int lastBest = start;
        int lo = start;
        int hi = end - 1;
        int mid = 0;
        while (lo <= hi) {
            mid = (lo + hi) >>> 1;
            int midValCmp = sizeTester.onTestSize(mid, availableSpace);
            if (midValCmp < 0) {
                lastBest = lo;
                lo = mid + 1;
            } else if (midValCmp > 0) {
                hi = mid - 1;
                lastBest = hi;
            } else {
                return mid;
            }
        }
        // Make sure to return the last best.
        // This is what should always be returned.
        return lastBest;

    }

    @Override
    protected void onTextChanged(final CharSequence text, final int start,
            final int before, final int after) {
        super.onTextChanged(text, start, before, after);
        adjustTextSize();
    }

    @Override
    protected void onSizeChanged(int width, int height, int oldwidth,
            int oldheight) {
        mInitializedDimens = true;
        mTextCachedSizes.clear();
        super.onSizeChanged(width, height, oldwidth, oldheight);
        if (width != oldwidth || height != oldheight) {
            adjustTextSize();
        }
    }
}

警告:

虽然 这个 Android 3.1 (Honeycomb) 中已经被修复的 bug,但仍需小心。


现在检查,添加了最大行支持。 - M-Wajeeh
看起来不错。你应该在onTestSize()方法中添加"@TargetApi(Build.VERSION_CODES.JELLY_BEAN)",这样Lint就不会给出错误/警告,并在构造函数中初始化它。将二分搜索设置为静态也是一个好的选择,你可以在最大的构造函数中进行初始化,并从其他构造函数中调用它。然而,这确实是最好的答案,你值得一个V。这终于是对这个老问题的一个好回答了。干得好! - android developer
@M-WaJeEh,请查看此主题中的新帖子。一位名为“MartinH”的用户表示他已经修复了你的代码。 - android developer
缓存大小可能导致行尾的一个或两个字符无法显示。问题在于,例如字符串"---"的大小将被缓存为后续所有三个字符字符串的大小。但是,如果我将其更改为"+++",它只会显示"++",因为这些较宽的字符需要更多的空间。禁用缓存会导致每次设置文本时都重新计算大小,因此不会丢失任何字符。代码注释中有关于此的警告,但我认为默认情况下应禁用缓存。对于我的使用情况,我没有看到性能下降的情况。 - Doug Simonton
1
initialize@Override public void setTypeface(Typeface tf, int style)中需要添加以下内容:mPaint.setTypeface(getTypeface());同时还需要处理TypeFace。 - s.shostko
显示剩余14条评论

13

我稍微修改了M-WaJeEh的答案,以考虑到侧面的复合绘制。

getCompoundPaddingXXXX()方法返回视图填充 + drawable空间的padding。例如:TextView.getCompoundPaddingLeft()

问题: 这解决了TextView可用于文本的宽度和高度的测量问题。如果我们不考虑drawable大小,它将被忽略,文本最终会重叠在drawable上。


更新的段落 adjustTextSize(String):

private void adjustTextSize(final String text) {
  if (!mInitialized) {
    return;
  }
  int heightLimit = getMeasuredHeight() - getCompoundPaddingBottom() - getCompoundPaddingTop();
  mWidthLimit = getMeasuredWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();

  mAvailableSpaceRect.right = mWidthLimit;
  mAvailableSpaceRect.bottom = heightLimit;

  int maxTextSplits = text.split(" ").length;
  AutoResizeTextView.super.setMaxLines(Math.min(maxTextSplits, mMaxLines));

  super.setTextSize(
      TypedValue.COMPLEX_UNIT_PX,
      binarySearch((int) mMinTextSize, (int) mMaxTextSize,
                   mSizeTester, mAvailableSpaceRect));
}

你更新了M-WaJeEh的源代码,但为什么把它放在一个新的回答里?我知道你出于善意,但是作为一个答案来发布这个更新是否合适?我也不确定他能看到你的回答通知... - android developer
2
我刚刚注册才能提供那个更新。我的声望计数仅为1,这阻止了我在不是我自己的帖子上发表评论(最低要求为15)。 - MartinH
没问题,也许你想把我的答案传达给M-WajeEh,因为我现在甚至无法联系他 :/ - MartinH
请问您能否更好地描述一下您所做的事情?您修复了一个错误吗?您能展示一张截图来演示这个错误吗? - android developer
1
嗨@MartinH,欢迎来到SO。我一定会查看这个问题,并在测试后更新我的答案,提及您的名字并发布您的帖子链接。请给我几天时间。我现在有些困扰。 - M-Wajeeh
显示剩余5条评论

8

好的,我已经利用上周大量重写了我的代码,以精确地适应您的测试。您现在可以将此内容复制到您的项目中,并立即使用——包括setSingleLine()。请记得,如果您要使用极端值,请调整MIN_TEXT_SIZEMAX_TEXT_SIZE

收敛算法如下:

for (float testSize; (upperTextSize - lowerTextSize) > mThreshold;) {

    // Go to the mean value...
    testSize = (upperTextSize + lowerTextSize) / 2;

    // ... inflate the dummy TextView by setting a scaled textSize and the text...
    mTestView.setTextSize(TypedValue.COMPLEX_UNIT_SP, testSize / mScaledDensityFactor);
    mTestView.setText(text);

    // ... call measure to find the current values that the text WANTS to occupy
    mTestView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
    int tempHeight = mTestView.getMeasuredHeight();

    // ... decide whether those values are appropriate.
    if (tempHeight >= targetFieldHeight) {
        upperTextSize = testSize; // Font is too big, decrease upperSize
    }
    else {
        lowerTextSize = testSize; // Font is too small, increase lowerSize
    }
}

整个班级可以在这里找到

现在结果非常灵活。这与在xml中声明的方式相同:

<com.example.myProject.AutoFitText
    android:id="@+id/textView"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="4"
    android:text="@string/LoremIpsum" />

此外,你还可以像测试中那样通过编程来构建。我希望你现在能够使用它。顺便说一句,你现在可以调用setText(CharSequence text)来使用它了。该类会处理极为罕见的异常,并且应该非常可靠。算法目前唯一不支持的是:

  • setMaxLines(x)的调用,其中x >=2

但是,我已经添加了详尽的注释,以帮助你构建这个功能。


请注意:

如果你正常使用本功能而没有将其限制为单行,则可能会出现字断行,就像你之前提到的那样。这是一个Android特性,不是AutoFitText的错误。Android将始终断开过长的TextView单词,实际上这是非常方便的。如果你想在这里进行干预,请参见从第203行开始的我的注释和代码。我已经为你编写了一个足够的分割和识别程序,你只需要分割单词,然后按照需要进行修改即可。

总之:你应该高度考虑重写你的测试,也支持空格字符,例如:

final Random _random = new Random();
final String ALLOWED_CHARACTERS = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890";
final int textLength = _random.nextInt(80) + 20;
final StringBuilder builder = new StringBuilder();
for (int i = 0; i < textLength; ++i) {
    if (i % 7 == 0 && i != 0) {
        builder.append(" ");
    }
    builder.append(ALLOWED_CHARACTERS.charAt(_random.nextInt(ALLOWED_CHARACTERS.length())));
}
((AutoFitText) findViewById(R.id.textViewMessage)).setText(builder.toString());

这将产生非常漂亮(更真实)的结果。
您还会发现一些注释,以帮助您入手。
祝好运,顺颂商祺。

我认为,与其使用纯二分查找(特别是两个),不如通过目标与测量大小的比率来得出更好的猜测。还有其他方法(牛顿和其他一些我不记得的方法),但我认为它们不适用于这里。我现在建议的这种方法更好,因为你可以从最小尺寸开始,而不是测试最大尺寸。事实上,由于你不断地做出更好的猜测,所以你不需要一个最大尺寸。 - android developer
1
我发现的另一个问题是文本重力。我认为它无法在垂直方向上居中。 - android developer
我在使用这个视图时没有得到预期的行为,对于长单词(例如电子邮件地址),会缺少几个字符。 - ThomasRS
+1,这是我找到的最正确的解决方案。我正在运行时更改textview布局参数:宽度、高度、上下边距,但之后请求布局。有时候我会得到太小的文本大小:宽度和高度减半,但在其他设备上是正常的。 - user529543
无法在Android 4.03上使用 - 字体不可见,而在其他Android版本中它们是正常的。 - Malachiasz
显示剩余6条评论

6

我会逐步解释这个属性在低版本的安卓系统中是如何工作的:

1- 在你的项目 gradle 文件中导入 Android support library 26.x.x。如果 IDE 中没有支持库,它们将自动下载。

dependencies {
    compile 'com.android.support:support-v4:26.1.0'
    compile 'com.android.support:appcompat-v7:26.1.0'
    compile 'com.android.support:support-v13:26.1.0' }

allprojects {
    repositories {
        jcenter()
        maven {
            url "https://maven.google.com"
        }
    } }

2- 打开您的布局XML文件,并将您的TextView重构为这样的标记。该场景是:当系统字体大小增加时,适应可用宽度的文本,而不是换行。

<android.support.v7.widget.AppCompatTextView
            android:id="@+id/textViewAutoSize"
            android:layout_width="match_parent"
            android:layout_height="25dp"
            android:ellipsize="none"
            android:text="Auto size text with compatible lower android versions."
            android:textSize="12sp"
            app:autoSizeMaxTextSize="14sp"
            app:autoSizeMinTextSize="4sp"
            app:autoSizeStepGranularity="0.5sp"
            app:autoSizeTextType="uniform" />

他们的自适应TextView实现效果不太好。有时候,文本并没有调整字体大小,而是糟糕地换到下一行...甚至在他们的视频中也展示了这种情况,好像这是一件好事:https://youtu.be/fjUdJ2aVqE4?t=14。请注意,“TextView”中的“w”跑到了下一行...无论如何,我在这里创建了一个新的请求:https://issuetracker.google.com/issues/68787108。请考虑给它点赞。 - android developer
@androiddeveloper 你说得对。我没有解释这个标签的单行实现方式。如果你需要换行,你必须更改这个属性:android:layout_height="wrap_content"。 但是android.support.v7.widget.AppCompatTextView和gradle实现保证了这个属性的工作。 - The Goat
我认为你不理解。我不希望部分单词换行到下一行,除非没有其他解决方法。在视频中,你可以很容易地看到字体本可以变小来解决这个问题。这就是问题所在。你是说你知道如何修复它吗?意思是不会有单词换行,只有字体大小改变? - android developer
@androiddeveloper 有许多情况。我的情况是:自适应文本到固定宽度而不换行。在增加系统字体大小时,文本必须在TextView中显示而不换行。如果对于您来说不需要换行,那么不要设置固定高度。这个属性不仅使用换行,您可以自适应文本到TextView的固定宽度。 - The Goat
根据我的测试,字体大小不正确地变化,而且可能会出现换行问题。 - android developer
你能不能把最大行数设置为1呢? - iamsujan

5
我的要求是:
  • 点击ScalableTextView
  • 打开一个列表Activity并显示各种长度的字符串项。
  • 从列表中选择一个文本。
  • 在另一个Activity中将文本设置回ScalableTextView。
我参考了链接:Auto Scale TextView Text to Fit within Bounds(包括评论)和DialogTitle.java
我发现提供的解决方案很好,很简单,但它不会动态更改文本框的大小。当从列表视图中选择的文本长度小于ScalableTextView中的现有文本长度时,它不会增加文本的大小,而是以较小的大小显示文本。
我修改了ScalableTextView.java以根据文本长度重新调整文本大小。这是我的ScalableTextView.java
public class ScalableTextView extends TextView
{
float defaultTextSize = 0.0f;

public ScalableTextView(Context context, AttributeSet attrs, int defStyle)
{
    super(context, attrs, defStyle);
    setSingleLine();
    setEllipsize(TruncateAt.END);
    defaultTextSize = getTextSize();
}

public ScalableTextView(Context context, AttributeSet attrs)
{
    super(context, attrs);
    setSingleLine();
    setEllipsize(TruncateAt.END);
    defaultTextSize = getTextSize();
}

public ScalableTextView(Context context)
{
    super(context);
    setSingleLine();
    setEllipsize(TruncateAt.END);
    defaultTextSize = getTextSize();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
    setTextSize(TypedValue.COMPLEX_UNIT_PX, defaultTextSize);
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    final Layout layout = getLayout();
    if (layout != null)
    {
        final int lineCount = layout.getLineCount();
        if (lineCount > 0)
        {
            int ellipsisCount = layout.getEllipsisCount(lineCount - 1);
            while (ellipsisCount > 0)
            {
                final float textSize = getTextSize();

                // textSize is already expressed in pixels
                setTextSize(TypedValue.COMPLEX_UNIT_PX, (textSize - 1));

                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                ellipsisCount = layout.getEllipsisCount(lineCount - 1);
            }
        }
    }
}
}

Happy Coding....


你不应该使用二分查找来代替每次迭代减一的方式吗?此外,这段代码似乎强制 TextView 只能显示单行文本,而不能显示多行。 - android developer
这是唯一对我有效的解决方案。我的问题与TextView在4.1中无法适应视图有关。 - FOliveira
看起来它与父级不完全匹配。请参见截图:https://s14.postimg.org/93c2xgs75/Screenshot_2.png - user25

4

从2018年6月开始,Android 官方支持这个功能,支持Android 4.0(API 级别14)及以上。
从 Android 8.0(API 级别26)及以上:

setAutoSizeTextTypeUniformWithConfiguration(int autoSizeMinTextSize, int autoSizeMaxTextSize, 
        int autoSizeStepGranularity, int unit);

Android 8.0之前的版本(API级别为26以下):

TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(TextView textView,
int autoSizeMinTextSize, int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit)

请查看我详细的答案


3

注意,Android 3(蜂巢)和Android 4.0(冰淇淋三明治)存在漏洞

Android版本:3.1-4.04存在一个bug,即在TextView中setTextSize()仅在第一次调用时有效。

该漏洞在Issue 22493: Android 4.0中的TextView高度错误HoneyComb上增加和减小文本大小后按钮高度和文本不返回其原始状态的问题17343中有描述。

解决方法是在更改大小之前向分配给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小部件中,但我希望保持固定代码的外部。

请问您能否准确指出哪些行(在哪些行之前/之后)以及在哪个函数中? - android developer
好的,谢谢。我希望这能解决所有问题,并且对于阅读此内容的人有所帮助。目前我无法验证它。也许下次我会使用它来检查。 - android developer
你可以重写textView.setText()方法,并将此代码放在TextView类中。 - Malachiasz
这是否也意味着“getText()”会返回带有额外字符的文本? - android developer
如果有问题,那就重写getText吧 :) - Malachiasz
显示剩余9条评论

3

1
是的,我也听说过它,但它有多好呢?有没有使用它的示例?即使在他们的视频中,我也注意到了一个错误:一个单词的字母被移到了下一行(换行):https://youtu.be/1N9KveJ-FU8?t=781(请注意“W”) - android developer

1
自 Android O 开始,可以在 xml 中自动调整文本大小:

https://developer.android.com/preview/features/autosizing-textview.html

  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:autoSizeTextType="uniform"
    app:autoSizeMinTextSize="12sp"
    app:autoSizeMaxTextSize="100sp"
    app:autoSizeStepGranularity="2sp"
  />

Android O允许您指示TextView根据TextView的特征和边界自动展开或收缩以填充其布局的文本大小。这个设置使得在具有动态内容的不同屏幕上优化文本大小更加容易。
支持库26.0 Beta全面支持在运行Android O之前版本的设备上使用autosizing TextView功能。该库提供对Android 4.0(API级别14)及更高版本的支持。android.support.v4.widget包含TextViewCompat类,以便以向后兼容的方式访问功能。

很遗憾,根据我的测试,甚至在Google IO的视频中,我注意到它存在问题,例如错误地换行部分单词,而不是调整字体大小。我已经在这里报告了这个问题:https://issuetracker.google.com/issues/38468964,这就是为什么我仍然不使用它的原因。 - android developer

1
将文本视图转换为图像,并在边界内缩放图像。
以下是如何将视图转换为图像的示例:在Android中将视图转换为位图而不显示它? 问题是,您的文本将无法选择,但应该可以解决问题。我没有尝试过,所以不确定它看起来会如何(因为缩放)。

这是个好主意,但这也会让文本变得像素化并移除我能在textView中使用的任何功能。 - android developer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接