How to get line count of textview before rendering?

后端 未结 6 491
南笙
南笙 2020-12-02 20:29

How can I get the number of lines a string will take up in a TextView before it is rendered.

A ViewTreeObserver will not work because thos

相关标签:
6条回答
  • 2020-12-02 20:37
    final Rect bounds = new Rect();
    final Paint paint = new Paint();
    paint.setTextSize(currentTextSize);
    paint.getTextBounds(testString, 0, testString.length(), bounds);
    

    Now divide the width of text with the width of your TextView to get the total number of lines.

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

    currentSize : Expected size of the view in which the text will be rendered. The size should not go beyond the screen width.

    0 讨论(0)
  • 2020-12-02 20:37

    The accepted answer doesn't work when a whole word is placed on the next line in order to avoid breaking the word:

    |hello   |
    |world!  |
    

    The only way to be 100% sure about the number of lines is to use the same text flow engine that TextView uses. Since TextView doesn't share its re-flow logic here's a custom string processor which splits text into multiple lines each of which fits the given width. It also does its best to not break the words unless the whole word does not fit:

    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);
        }
    }
    

    Here's a part of a unit test:

        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));
    

    Now one can be 100% sure about the line count in TextView without a need to render it:

    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()
    
    0 讨论(0)
  • 2020-12-02 20:37

    Reference: Getting height of text view before rendering to layout

    Get line of TextView before rendering.

    This is my code base the link above. It's working for me.

    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;
    }
    

    Note: This code also must be set for real textview on screen

    textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimension(R.dimen.tv_size_14sp));
    
    0 讨论(0)
  • 2020-12-02 20:44

    Thanks to Eugene Popovich I got:

    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
        )
    }
    

    Usage:

    1. If you want to print different texts in equal TextViews (for instance, in RecyclerView with one or similar ViewHolders):

       val params = TextMeasurementUtil.getTextViewParams(textView)
      
       val lines = TextMeasurementUtil.getTextLines(textView.text, params)
       val count = TextMeasurementUtil.getTextLineCount(textView.text, params)
      
    2. In any other case:

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

    In RecyclerView you won't know parameters of TextView until you call post or doOnPreDraw method, so use:

    textView.doOnPreDraw {
        val lines = TextMeasurementUtil.getTextLines(textView)
        val count = TextMeasurementUtil.getTextLineCount(textView)
    }
    
    0 讨论(0)
  • 2020-12-02 20:45

    The @denis-kniazhev answer is very good. However it uses custom logic to break text into lines. It is possible to use standard TextView layout components to measure text.

    That is how it may look like:

    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;
        }
    }
    
    0 讨论(0)
  • 2020-12-02 20:51

    If you know or can determine the width of the TextView's parent, you are able to invoke a view measurement which results in line count being calculated.

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

    The TextView's layout is no longer null and you can check the calculated line count with myTextView.lineCount.

    0 讨论(0)
提交回复
热议问题