46

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 those are only fired after it is rendered.

Sanders
  • 438
  • 3
  • 13
Nandha
  • 6,746
  • 2
  • 23
  • 33
  • 3
    possible duplicate of http://stackoverflow.com/questions/12037377/how-to-get-number-of-lines-of-textview – Krishnabhadra Mar 28 '13 at 10:17
  • 10
    It is not a duplicate question. why u vote to close this question? it is good question and someone is answered my question and it is correct answer. – Nandha Mar 28 '13 at 11:12
  • 2
    Confirmed not a duplicate. I've used this in my own solution because of a need to calculate the number of lines without using a preloaded view (because the preloaded view was shortened by the "SetMaxLines" method of the TextView). The duplicate thread provided doesn't cover this possibility. Upvoting both solution and question because they were extremely useful. – moscro Jun 28 '13 at 21:04
  • Reworded into a real question. I have a better answer to this question so I would appreciate it being reopened. – Sanders Mar 24 '14 at 18:33
  • @Sanders, it was reopened, but they forgot to notify you. :) – CoolMind Nov 09 '20 at 15:09

7 Answers7

33

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()
  • This is the real exact solution. You really helped me so much. Thanks so much. – Efe AYDIN Jul 13 '17 at 04:12
  • This is the best solution so far. Thanks – Nguyen Minh Binh Nov 13 '17 at 11:04
  • agan textview iwdth will be zero before rendering so your textViewWidthPx will be 0. So this will not work properly when I use wrap_content as my textview's width.. – Kannan_SJD Jan 02 '18 at 09:35
  • This is the best answer. – Sanjay Apr 08 '18 at 04:35
  • the `settext` method renders the text, the point is to do this on another thread. – M D P Sep 17 '18 at 08:51
  • @Kannan_SJD you are right for the case if TextView width is not limited. For example in my case, I have a box for the TextView, which has the maximum width. If you have still wrap_content for TextView, you can count the paddings and margins of it (if it is the only item in this Line of Rendering on the screen, and have a width as the Window width minus all paddings. – RelaxedSoul Feb 06 '19 at 10:07
  • Greetings from 2020. This answer is the best one, solved a problem i've been having for almost 2 weeks – user3783123 Apr 29 '20 at 10:10
  • This is a good solution (right line count, but not exact lines). Because it requires TextView width, maybe you will need `textView.post { ... }` to measure it's width in `RecylerView`. – CoolMind Nov 09 '20 at 13:15
29
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.

Triode
  • 11,309
  • 2
  • 38
  • 48
  • 2
    Why to code when you have already a readymade method , `TextView.getLineCount()` ? – Raynold Mar 28 '13 at 10:21
  • 44
    He is asking to measure it before rendering – Triode Mar 28 '13 at 10:22
  • Saved my day, thanks. It's not a big change, but why is not enough to check if textViewWidth > textWidth? – Analizer Feb 06 '14 at 19:01
  • I wont be able to answer this now, but it might be because of the padding and margin you provide inside the view :) – Triode Feb 07 '14 at 09:18
  • This is a better and fail-safe approach than the one above. Works great for getLineCount() from listview as well. http://stackoverflow.com/a/22217849/452487 – JaydeepW Mar 11 '14 at 09:24
  • 3
    I agree but the OP asked he want to get the bounds before it get rendered on the view(Before getting added to the window) – Triode Mar 11 '14 at 09:32
  • @Raynold getLineCount() doesn't work if you have maxLines defined – aryaxt May 13 '14 at 18:53
  • I think it could be wrong in those cases where a whole word is placed in the next line in order to avoid to break it. – jmhostalet Sep 29 '14 at 09:31
  • 6
    If the text hasn't been rendered yet, then in certain cases, the width of the textview will also be zero, making this solution useless. – Johann Nov 21 '15 at 19:54
  • 1
    I am not considering the size of the TextView any where to calculate the size, Can you go through the code first and check your comment again. Also it will be helpful if you can read the question once again. – Triode Nov 23 '15 at 06:06
  • 1
    Could you explain this : "Now devide the width of text with the width of your TextView and get the total lines." ? It is confusing. – cylon Dec 20 '17 at 07:27
  • What is currentSize here? – Kannan_SJD Jan 02 '18 at 06:40
  • @Kannan_SJD Current size will be either the screen width if you give the width to match the screen or it will be the size what you gave for the textfield. – Triode Jan 03 '18 at 12:02
  • @Triode What if I have a textview with layout weight? in tha case, I would have defined the width as 0. – Kannan_SJD Jan 03 '18 at 13:18
  • @Kannan_SJD, In that case, u do have the layout weight from that you can obtain the width the TextView going to take. – Triode Jan 03 '18 at 13:22
  • 1
    Just a note: I compared the results of this with the results of `getLineCount` by adding both to a log entry. Every time the log was displayed, both values were recorded and both returned the same value. The log was printed before the text was rendered. – Abandoned Cart Apr 09 '18 at 02:43
7

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;
    }
}
Eugene Popovich
  • 3,343
  • 2
  • 30
  • 33
  • Regarding the continuous development of the Android SDK, this is the most exact and up-to-date solution – Julian Schweppe Apr 06 '20 at 10:41
  • 2
    Thanks! Works well. In RecyclerView it raises exception: "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). So, use `myTextView.post { ... }` to get lines in RecyclerView. – CoolMind Nov 09 '20 at 12:32
3

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.

M. Palsich
  • 1,748
  • 13
  • 18
2

Use kotlin extension function doOnPreDraw to know the line count before the view is rendered. Example:

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

1

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)
}
CoolMind
  • 26,736
  • 15
  • 188
  • 224
0

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));
Anh Duy
  • 1,145
  • 15
  • 25