【问题标题】:How to get line count of textview before rendering?渲染前如何获取textview的行数?
【发布时间】:2026-02-12 10:35:02
【问题描述】:

如何获取字符串在呈现之前将在TextView 中占用的行数。

ViewTreeObserver 将不起作用,因为它们仅在呈现后才会触发。

【问题讨论】:

  • 这不是一个重复的问题。为什么你投票结束这个问题?这是个好问题,有人回答了我的问题,这是正确的答案。
  • 确认不重复。我在自己的解决方案中使用了它,因为需要在不使用预加载视图的情况下计算行数(因为预加载的视图被 TextView 的“SetMaxLines”方法缩短了)。提供的重复线程不包括这种可能性。赞成解决方案和问题,因为它们非常有用。
  • 改写成一个真正的问题。我对这个问题有更好的答案,所以我希望它被重新打开。
  • @Sanders,它已重新打开,但他们忘记通知您。 :)

标签: java android android-layout textview


【解决方案1】:

当整个单词放在下一行时,接受的答案不起作用,以避免破坏单词:

|hello   |
|world!  |

100% 确定行数的唯一方法是使用与 TextView 相同的文本流引擎。由于 TextView 不共享其重排逻辑,这里有一个自定义字符串处理器,它将文本分成多行,每行都适合给定的宽度。除非整个单词不合适,否则它也尽量不打断单词:

public List<String> splitWordsIntoStringsThatFit(String source, float maxWidthPx, Paint paint) {
    ArrayList<String> result = new ArrayList<>();

    ArrayList<String> currentLine = new ArrayList<>();

    String[] sources = source.split("\\s");
    for(String chunk : sources) {
        if(paint.measureText(chunk) < maxWidthPx) {
            processFitChunk(maxWidthPx, paint, result, currentLine, chunk);
        } else {
            //the chunk is too big, split it.
            List<String> splitChunk = splitIntoStringsThatFit(chunk, maxWidthPx, paint);
            for(String chunkChunk : splitChunk) {
                processFitChunk(maxWidthPx, paint, result, currentLine, chunkChunk);
            }
        }
    }

    if(! currentLine.isEmpty()) {
        result.add(TextUtils.join(" ", currentLine));
    }
    return result;
}

/**
 * Splits a string to multiple strings each of which does not exceed the width
 * of maxWidthPx.
 */
private List<String> splitIntoStringsThatFit(String source, float maxWidthPx, Paint paint) {
    if(TextUtils.isEmpty(source) || paint.measureText(source) <= maxWidthPx) {
        return Arrays.asList(source);
    }

    ArrayList<String> result = new ArrayList<>();
    int start = 0;
    for(int i = 1; i <= source.length(); i++) {
        String substr = source.substring(start, i);
        if(paint.measureText(substr) >= maxWidthPx) {
            //this one doesn't fit, take the previous one which fits
            String fits = source.substring(start, i - 1);
            result.add(fits);
            start = i - 1;
        }
        if (i == source.length()) {
            String fits = source.substring(start, i);
            result.add(fits);
        }
    }

    return result;
}

/**
 * Processes the chunk which does not exceed maxWidth.
 */
private void processFitChunk(float maxWidth, Paint paint, ArrayList<String> result, ArrayList<String> currentLine, String chunk) {
    currentLine.add(chunk);
    String currentLineStr = TextUtils.join(" ", currentLine);
    if (paint.measureText(currentLineStr) >= maxWidth) {
        //remove chunk
        currentLine.remove(currentLine.size() - 1);
        result.add(TextUtils.join(" ", currentLine));
        currentLine.clear();
        //ok because chunk fits
        currentLine.add(chunk);
    }
}

这是单元测试的一部分:

    String text = "Hello this is a very long and meanless chunk: abcdefghijkonetuhosnahrc.pgraoneuhnotehurc.pgansohtunsaohtu. Hope you like it!";
    Paint paint = new Paint();
    paint.setTextSize(30);
    paint.setTypeface(Typeface.DEFAULT_BOLD);

    List<String> strings = splitWordsIntoStringsThatFit(text, 50, paint);
    assertEquals(3, strings.size());
    assertEquals("Hello this is a very long and meanless chunk:", strings.get(0));
    assertEquals("abcdefghijkonetuhosnahrc.pgraoneuhnotehurc.pganso", strings.get(1));
    assertEquals("htunsaohtu. Hope you like it!", strings.get(2));

现在可以 100% 确定 TextView 中的行数,而无需渲染它:

TextView textView = ...         //text view must be of fixed width

Paint paint = new Paint();
paint.setTextSize(yourTextViewTextSizePx);
paint.setTypeface(yourTextViewTypeface);

float textViewWidthPx = ...;

List<String> strings = splitWordsIntoStringsThatFit(yourText, textViewWidthPx, paint);
textView.setText(TextUtils.join("\n", strings);

int lineCount = strings.size();        //will be the same as textView.getLineCount()

【讨论】:

  • 这是真正的精确解决方案。你真的帮了我很多。非常感谢。
  • 这是迄今为止最好的解决方案。谢谢
  • agan textview iwdth 在渲染之前将为零,因此您的 textViewWidthPx 将为 0。因此,当我使用 wrap_content 作为我的 textview 的宽度时,这将无法正常工作..
  • 这是最好的答案。
  • settext 方法渲染文本,重点是在另一个线程上执行此操作。
【解决方案2】:
final Rect bounds = new Rect();
final Paint paint = new Paint();
paint.setTextSize(currentTextSize);
paint.getTextBounds(testString, 0, testString.length(), bounds);

现在将文本宽度除以 TextView 的宽度得到总行数。

final int numLines = (int) Math.ceil((float) bounds.width() / currentSize);

currentSize :将呈现文本的视图的预期大小。大小不应超过屏幕宽度。

【讨论】:

  • 既然已经有了现成的方法,为什么还要编码,TextView.getLineCount()
  • 他要求在渲染之前测量它
  • 我同意,但 OP 要求他在视图呈现之前获取边界(在添加到窗口之前)
  • 如果文本还没有被渲染,那么在某些情况下,textview的宽度也将为零,使得这个解决方案毫无用处。
  • 我不考虑 TextView 的大小在哪里计算大小,您可以先浏览代码并再次检查您的评论。如果您能再次阅读该问题,也会有所帮助。
【解决方案3】:

@denis-kniazhev 的回答非常好。但是,它使用自定义逻辑将文本分成几行。可以使用标准的TextView 布局组件来测量文本。

可能是这样的:

TextView myTextView = findViewById(R.id.text);
TextMeasurementUtils.TextMeasurementParams params = TextMeasurementUtils.TextMeasurementParams.Builder
.from(myTextView).build();
List<CharSequence> lines = TextMeasurementUtils.getTextLines(text, params);

TextMeasurementUtils.java

import android.os.Build;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextDirectionHeuristic;
import android.text.TextPaint;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

public class TextMeasurementUtils {
    /**
     * Split text into lines using specified parameters and the same algorithm
     * as used by the {@link TextView} component
     *
     * @param text   the text to split
     * @param params the measurement parameters
     * @return
     */
    public static List<CharSequence> getTextLines(CharSequence text, TextMeasurementParams params) {
        StaticLayout layout;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            StaticLayout.Builder builder = StaticLayout.Builder
                    .obtain(text, 0, text.length(), params.textPaint, params.width)
                    .setAlignment(params.alignment)
                    .setLineSpacing(params.lineSpacingExtra, params.lineSpacingMultiplier)
                    .setIncludePad(params.includeFontPadding)
                    .setBreakStrategy(params.breakStrategy)
                    .setHyphenationFrequency(params.hyphenationFrequency);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                builder.setJustificationMode(params.justificationMode);
            }
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                builder.setUseLineSpacingFromFallbacks(params.useFallbackLineSpacing);
            }
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                builder.setTextDirection((TextDirectionHeuristic) params.textDirectionHeuristic);
            }
            layout = builder.build();
        } else {
            layout = new StaticLayout(
                    text,
                    params.textPaint,
                    params.width,
                    params.alignment,
                    params.lineSpacingMultiplier,
                    params.lineSpacingExtra,
                    params.includeFontPadding);
        }
        List<CharSequence> result = new ArrayList<>();
        for (int i = 0; i < layout.getLineCount(); i++) {
            result.add(layout.getText().subSequence(layout.getLineStart(i), layout.getLineEnd(i)));
        }
        return result;
    }

    /**
     * The text measurement parameters
     */
    public static class TextMeasurementParams {
        public final TextPaint textPaint;
        public final Layout.Alignment alignment;
        public final float lineSpacingExtra;
        public final float lineSpacingMultiplier;
        public final boolean includeFontPadding;
        public final int breakStrategy;
        public final int hyphenationFrequency;
        public final int justificationMode;
        public final boolean useFallbackLineSpacing;
        public final Object textDirectionHeuristic;
        public final int width;

        private TextMeasurementParams(Builder builder) {
            textPaint = requireNonNull(builder.textPaint);
            alignment = requireNonNull(builder.alignment);
            lineSpacingExtra = builder.lineSpacingExtra;
            lineSpacingMultiplier = builder.lineSpacingMultiplier;
            includeFontPadding = builder.includeFontPadding;
            breakStrategy = builder.breakStrategy;
            hyphenationFrequency = builder.hyphenationFrequency;
            justificationMode = builder.justificationMode;
            useFallbackLineSpacing = builder.useFallbackLineSpacing;
            textDirectionHeuristic = builder.textDirectionHeuristic;
            width = builder.width;
        }


        public static final class Builder {
            private TextPaint textPaint;
            private Layout.Alignment alignment;
            private float lineSpacingExtra;
            private float lineSpacingMultiplier = 1.0f;
            private boolean includeFontPadding = true;
            private int breakStrategy;
            private int hyphenationFrequency;
            private int justificationMode;
            private boolean useFallbackLineSpacing;
            private Object textDirectionHeuristic;
            private int width;

            public Builder() {
            }

            public Builder(TextMeasurementParams copy) {
                this.textPaint = copy.textPaint;
                this.alignment = copy.alignment;
                this.lineSpacingExtra = copy.lineSpacingExtra;
                this.lineSpacingMultiplier = copy.lineSpacingMultiplier;
                this.includeFontPadding = copy.includeFontPadding;
                this.breakStrategy = copy.breakStrategy;
                this.hyphenationFrequency = copy.hyphenationFrequency;
                this.justificationMode = copy.justificationMode;
                this.useFallbackLineSpacing = copy.useFallbackLineSpacing;
                this.textDirectionHeuristic = copy.textDirectionHeuristic;
                this.width = copy.width;
            }

            public static Builder from(TextView view) {
                Layout layout = view.getLayout();
                Builder result = new Builder()
                        .textPaint(layout.getPaint())
                        .alignment(layout.getAlignment())
                        .width(view.getWidth() -
                                view.getCompoundPaddingLeft() - view.getCompoundPaddingRight());
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    result.lineSpacingExtra(view.getLineSpacingExtra())
                            .lineSpacingMultiplier(view.getLineSpacingMultiplier())
                            .includeFontPadding(view.getIncludeFontPadding());
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        result.breakStrategy(view.getBreakStrategy())
                                .hyphenationFrequency(view.getHyphenationFrequency());
                    }
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                        result.justificationMode(view.getJustificationMode());
                    }
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                        result.useFallbackLineSpacing(view.isFallbackLineSpacing());
                    }
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                        result.textDirectionHeuristic(view.getTextDirectionHeuristic());
                    }
                }
                return result;
            }

            public Builder textPaint(TextPaint val) {
                textPaint = val;
                return this;
            }

            public Builder alignment(Layout.Alignment val) {
                alignment = val;
                return this;
            }

            public Builder lineSpacingExtra(float val) {
                lineSpacingExtra = val;
                return this;
            }

            public Builder lineSpacingMultiplier(float val) {
                lineSpacingMultiplier = val;
                return this;
            }

            public Builder includeFontPadding(boolean val) {
                includeFontPadding = val;
                return this;
            }

            public Builder breakStrategy(int val) {
                breakStrategy = val;
                return this;
            }

            public Builder hyphenationFrequency(int val) {
                hyphenationFrequency = val;
                return this;
            }

            public Builder justificationMode(int val) {
                justificationMode = val;
                return this;
            }

            public Builder useFallbackLineSpacing(boolean val) {
                useFallbackLineSpacing = val;
                return this;
            }

            public Builder textDirectionHeuristic(Object val) {
                textDirectionHeuristic = val;
                return this;
            }

            public Builder width(int val) {
                width = val;
                return this;
            }

            public TextMeasurementParams build() {
                return new TextMeasurementParams(this);
            }
        }
    }

    public static <T> T requireNonNull(T obj) {
      if (obj == null)
          throw new NullPointerException();
      return obj;
    }
}

【讨论】:

  • 关于Android SDK的不断开发,这是最准确和最新的解决方案
  • 谢谢!效果很好。在 RecyclerView 中,它引发异常:“java.lang.NullPointerException: Attempt to invoke virtual method 'android.text.TextPaint android.text.Layout.getPaint()' on a null object reference” in line .textPaint(layout.getPaint()) (layout == null )。因此,使用myTextView.post { ... } 在 RecyclerView 中获取行。
【解决方案4】:

如果您知道或可以确定 TextView 父级的宽度,则可以调用视图测量,从而计算行数。

val parentWidth = PARENT_WIDTH // assumes this is known/can be found
myTextView.measure(
    MeasureSpec.makeMeasureSpec(parentWidth, MeasureSpec.EXACTLY),
    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED))

TextView 的layout 不再为空,您可以使用myTextView.lineCount 检查计算的行数。

【讨论】:

  • 谢谢。如果设置android:maxLines="2",则返回2。
【解决方案5】:

使用 kotlin 扩展函数 doOnPreDraw 在视图渲染之前知道行数。示例:

my_text_view.text = "text with multiple lines \n\n\n"
my_text_view.doOnPreDraw {

    // before it is drawn, use lineCount...
    print(my_text_view.lineCount)
}

https://developer.android.com/reference/kotlin/androidx/core/view/package-summary#doonpredraw

【讨论】:

    【解决方案6】:

    感谢Eugene Popovich我得到了:

    import android.os.Build
    import android.text.Layout
    import android.text.StaticLayout
    import android.text.TextDirectionHeuristic
    import android.text.TextPaint
    import android.widget.TextView
    
    
    object TextMeasurementUtil {
        /**
         * Split text into lines using specified parameters and the same algorithm
         * as used by the [TextView] component
         *
         * @param text   the text to split
         * @param params the measurement parameters
         * @return
         */
        fun getTextLines(text: CharSequence, params: TextViewParams): List<CharSequence> {
            val layout = getStaticLayout(text, params)
            return (0 until layout.lineCount).map {
                layout.text.subSequence(layout.getLineStart(it), layout.getLineEnd(it))
            }
        }
    
        fun getTextLineCount(text: CharSequence, params: TextViewParams): Int {
            val layout = getStaticLayout(text, params)
            return layout.lineCount
        }
    
        fun getTextLines(textView: TextView): List<CharSequence> {
            val layout = getStaticLayout(textView)
            return (0 until layout.lineCount).map {
                layout.text.subSequence(layout.getLineStart(it), layout.getLineEnd(it))
            }
        }
    
        fun getTextLineCount(textView: TextView): Int {
            val layout = getStaticLayout(textView)
            return layout.lineCount
        }
    
        /**
         * The text measurement parameters
         */
        fun getTextViewParams(textView: TextView): TextViewParams {
            val layout = textView.layout
            val width = textView.width - textView.compoundPaddingLeft - textView.compoundPaddingRight
            var lineSpacingExtra = 0f
            var lineSpacingMultiplier = 1.0f
            var includeFontPadding = true
            var breakStrategy = 0
            var hyphenationFrequency = 0
            var justificationMode = 0
            var useFallbackLineSpacing = false
            var textDirectionHeuristic: TextDirectionHeuristic? = null
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                lineSpacingExtra = textView.lineSpacingExtra
                lineSpacingMultiplier = textView.lineSpacingMultiplier
                includeFontPadding = textView.includeFontPadding
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    breakStrategy = textView.breakStrategy
                    hyphenationFrequency = textView.hyphenationFrequency
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    justificationMode = textView.justificationMode
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                    useFallbackLineSpacing = textView.isFallbackLineSpacing
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    textDirectionHeuristic = textView.textDirectionHeuristic
                }
            }
    
            return TextViewParams(
                textPaint = layout.paint,
                alignment = layout.alignment,
                lineSpacingExtra = lineSpacingExtra,
                lineSpacingMultiplier = lineSpacingMultiplier,
                includeFontPadding = includeFontPadding,
                breakStrategy = breakStrategy,
                hyphenationFrequency = hyphenationFrequency,
                justificationMode = justificationMode,
                useFallbackLineSpacing = useFallbackLineSpacing,
                textDirectionHeuristic = textDirectionHeuristic,
                width = width
            )
        }
    
        private fun getStaticLayout(text: CharSequence,
                                    params: TextViewParams): StaticLayout =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                val builder = StaticLayout.Builder
                    .obtain(text, 0, text.length, params.textPaint, params.width)
                    .setAlignment(params.alignment)
                    .setLineSpacing(params.lineSpacingExtra, params.lineSpacingMultiplier)
                    .setIncludePad(params.includeFontPadding)
                    .setBreakStrategy(params.breakStrategy)
                    .setHyphenationFrequency(params.hyphenationFrequency)
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    builder.setJustificationMode(params.justificationMode)
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                    builder.setUseLineSpacingFromFallbacks(params.useFallbackLineSpacing)
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    builder.setTextDirection(params.textDirectionHeuristic!!)
                }
                builder.build()
            } else {
                @Suppress("DEPRECATION")
                StaticLayout(
                    text,
                    params.textPaint,
                    params.width,
                    params.alignment,
                    params.lineSpacingMultiplier,
                    params.lineSpacingExtra,
                    params.includeFontPadding)
            }
    
        private fun getStaticLayout(textView: TextView): StaticLayout =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                val builder = StaticLayout.Builder
                    .obtain(textView.text, 0, textView.text.length, textView.layout.paint,
                        textView.width)
                    .setAlignment(textView.layout.alignment)
                    .setLineSpacing(textView.lineSpacingExtra, textView.lineSpacingMultiplier)
                    .setIncludePad(textView.includeFontPadding)
                    .setBreakStrategy(textView.breakStrategy)
                    .setHyphenationFrequency(textView.hyphenationFrequency)
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    builder.setJustificationMode(textView.justificationMode)
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                    builder.setUseLineSpacingFromFallbacks(textView.isFallbackLineSpacing)
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    builder.setTextDirection(textView.textDirectionHeuristic)
                }
                builder.build()
            } else {
                @Suppress("DEPRECATION")
                StaticLayout(
                    textView.text,
                    textView.layout.paint,
                    textView.width,
                    textView.layout.alignment,
                    textView.lineSpacingMultiplier,
                    textView.lineSpacingExtra,
                    textView.includeFontPadding)
            }
    
        data class TextViewParams(
            val textPaint: TextPaint,
            val alignment: Layout.Alignment,
            val lineSpacingExtra: Float,
            val lineSpacingMultiplier: Float,
            val includeFontPadding: Boolean,
            val breakStrategy: Int,
            val hyphenationFrequency: Int,
            val justificationMode: Int,
            val useFallbackLineSpacing: Boolean,
            val textDirectionHeuristic: TextDirectionHeuristic?,
            val width: Int
        )
    }
    

    用法:

    1. 如果您想以相同的TextViews 打印不同的文本(例如,在RecyclerView 中与一个或类似的ViewHolders):

       val params = TextMeasurementUtil.getTextViewParams(textView)
      
       val lines = TextMeasurementUtil.getTextLines(textView.text, params)
       val count = TextMeasurementUtil.getTextLineCount(textView.text, params)
      
    2. 在任何其他情况下:

       val lines = TextMeasurementUtil.getTextLines(textView)
       val count = TextMeasurementUtil.getTextLineCount(textView)
      

    RecyclerView 中,在调用postdoOnPreDraw 方法之前,您不会知道TextView 的参数,因此请使用:

    textView.doOnPreDraw {
        val lines = TextMeasurementUtil.getTextLines(textView)
        val count = TextMeasurementUtil.getTextLineCount(textView)
    }
    

    【讨论】:

      【解决方案7】:

      参考:Getting height of text view before rendering to layout

      在渲染前获取TextView的行。

      这是我上面链接的代码库。它对我有用。

      private int widthMeasureSpec;
      private int heightMeasureSpec;
      private int heightOfEachLine;
      private int paddingFirstLine;
      private void calculateHeightOfEachLine() {
          WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
          Display display = wm.getDefaultDisplay();
          Point size = new Point();
          display.getSize(size);
          int deviceWidth = size.x;
          widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(deviceWidth, View.MeasureSpec.AT_MOST);
          heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
          //1 line = 76; 2 lines = 76 + 66; 3 lines = 76 + 66 + 66
          //=> height of first line = 76 pixel; height of second line = third line =... n line = 66 pixel
          int heightOfFirstLine = getHeightOfTextView("A");
          int heightOfSecondLine = getHeightOfTextView("A\nA") - heightOfFirstLine;
          paddingFirstLine = heightOfFirstLine - heightOfSecondLine;
          heightOfEachLine = heightOfSecondLine;
      }
      
      private int getHeightOfTextView(String text) {
          // Getting height of text view before rendering to layout
          TextView textView = new TextView(context);
          textView.setPadding(10, 0, 10, 0);
          //textView.setTypeface(typeface);
          textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimension(R.dimen.tv_size_14sp));
          textView.setText(text, TextView.BufferType.SPANNABLE);
          textView.measure(widthMeasureSpec, heightMeasureSpec);
          return textView.getMeasuredHeight();
      }
      
      private int getLineCountOfTextViewBeforeRendering(String text) {
          return (getHeightOfTextView(text) - paddingFirstLine) / heightOfEachLine;
      }
      

      注意:此代码也必须为屏幕上的真实文本视图设置

      textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimension(R.dimen.tv_size_14sp));
      

      【讨论】: