如何在渲染之前获取textview的行数?

Nan*_*dha 36 java android textview android-layout

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

A ViewTreeObserver不起作用,因为它们仅在渲染后触发.

Tri*_*ode 26

final Rect bounds = new Rect();
final Paint paint = new Paint();
paint.setTextSize(currentTextSize);
paint.getTextBounds(testString, 0, testString.length(), bounds);
Run Code Online (Sandbox Code Playgroud)

现在将文本的宽度除以TextView的宽度以获得总行数.

final int numLines = (int) Math.ceil((float) bounds.width() / currentSize);
Run Code Online (Sandbox Code Playgroud)

  • 他要求在渲染之前测量它 (33认同)
  • 我同意,但是OP要求他在获得视图之前获得界限(在添加到窗口之前) (3认同)
  • 如果文本尚未呈现,那么在某些情况下,textview的宽度也将为零,使得此解决方案无效. (3认同)
  • 当你已经有一个现成的方法`TextView.getLineCount()`时为什么要编码? (2认同)

Den*_*hev 25

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

|hello   |
|world!  |
Run Code Online (Sandbox Code Playgroud)

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是单元测试的一部分:

    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));
Run Code Online (Sandbox Code Playgroud)

现在,人们可以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()
Run Code Online (Sandbox Code Playgroud)

  • 这应该被接受为真正的答案. (3认同)

htt*_*tch 6

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

这就是它的样子:

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

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 谢谢!效果很好。在 RecyclerView 中,它引发异常:“java.lang.NullPointerException:尝试在空对象引用上调用虚拟方法 'android.text.TextPaint android.text.Layout.getPaint()'” 在行 `.textPaint(layout.getPaint( ))`(布局==空)。因此,使用 myTextView.post { ... }` 来获取 RecyclerView 中的行。 (2认同)