43

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

AViewTreeObserver将不起作用,因为它们仅在渲染后才被触发。

4

7 回答 7

33

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

|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()
于 2015-02-15T10:43:40.223 回答
29
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:将呈现文本的视图的预期大小。大小不应超过屏幕宽度。

于 2013-03-28T10:17:46.793 回答
6

@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;
    }
}
于 2019-11-21T13:00:56.927 回答
3

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

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

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

于 2018-04-04T14:58:59.423 回答
2

使用 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

于 2021-02-15T22:05:47.233 回答
1

感谢尤金·波波维奇,我得到了:

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. 如果要在 equal TextViews 中打印不同的文本(例如,在RecyclerView一个或类似ViewHolder的 s 中):

     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)
    

在你调用或doOnPreDrawRecyclerView方法之前你不会知道参数,所以使用:TextViewpost

textView.doOnPreDraw {
    val lines = TextMeasurementUtil.getTextLines(textView)
    val count = TextMeasurementUtil.getTextLineCount(textView)
}
于 2020-11-09T15:01:19.037 回答
0

参考:在渲染到布局之前获取文本视图的高度

在渲染之前获取 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));
于 2018-04-02T07:50:36.573 回答