15

I have a TextView with maxlines=3 and I would like to use my own ellipsis, instead of

"Lore ipsum ..."

I need

"Lore ipsum ... [See more]"

in order to give the user a clue that clicking on the view is going to expand the full text.

Is it possible ?

I was thinking about check whether TextView has ellipsis and in such a case add the text "[See more]" and after that set ellipsis just before, but I couldn't find the way to do that.

Maybe if I find the position where the text is cutted, I can disable the ellipsis and make a substring and later add "... [See more]", but again I dont know how to get that position.

jmhostalet
  • 4,399
  • 4
  • 38
  • 47

5 Answers5

8

I've finally managed it in this way (may be not the best one):

private void setLabelAfterEllipsis(TextView textView, int labelId, int maxLines){

    if(textView.getLayout().getEllipsisCount(maxLines-1)==0) {
        return; // Nothing to do
    }

    int start = textView.getLayout().getLineStart(0);
    int end = textView.getLayout().getLineEnd(textView.getLineCount() - 1);
    String displayed = textView.getText().toString().substring(start, end);
    int displayedWidth = getTextWidth(displayed, textView.getTextSize());

    String strLabel = textView.getContext().getResources().getString(labelId);
    String ellipsis = "...";
    String suffix = ellipsis + strLabel;

    int textWidth;
    String newText = displayed;
    textWidth = getTextWidth(newText + suffix, textView.getTextSize());

    while(textWidth>displayedWidth){
        newText = newText.substring(0, newText.length()-1).trim();
        textWidth = getTextWidth(newText + suffix, textView.getTextSize());
    }

    textView.setText(newText + suffix);
}

private int getTextWidth(String text, float textSize){
    Rect bounds = new Rect();
    Paint paint = new Paint();
    paint.setTextSize(textSize);
    paint.getTextBounds(text, 0, text.length(), bounds);

    int width = (int) Math.ceil( bounds.width());
    return width;
}
jmhostalet
  • 4,399
  • 4
  • 38
  • 47
  • 2
    instead of using three dots like in `ellipsis = "...";` you should use HORIZONTAL ELLIPSIS character (… entity -> `…`) – Marcin Orlowski Sep 29 '14 at 11:46
  • 6
    textView.getLayout() returning null – Shahjahan Khan Jun 17 '15 at 07:48
  • 2
    Android uses `TextUtils.ELLIPSIS_NORMAL[0]` for standard ellipsis, which resolves to `'\u2026'`. This can be found in the [Layout#getEllipsisChar](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/text/Layout.java#1835) method that TextView uses, indirectly. – gMale Dec 29 '15 at 15:59
  • Tried using this way in recyclerView. textView.getLayout() returns null. Is there ay modification needed to make it useful with recycler/ list views? – Max Droid Jan 03 '17 at 05:16
  • if having null for getLayout use ViewTreeObserver.OnGlobalLayoutListener. – asozcan Mar 29 '17 at 14:48
  • Nice solution, it works well and it ended up being the foundation of the solution I ended up using. I see three issues with it, though: the way you test if the text is ellipsized isn't the best and might cause crashes (see https://stackoverflow.com/questions/4005933/how-do-i-tell-if-my-textview-has-been-ellipsized), your code within the while might crash in case the suffix never fits, and I believe you should consider the whole width of the TextView as the available width, instead of the width of the displayed text. – Fred Porciúncula Nov 08 '17 at 12:11
2

I think the answer from @jmhostalet will degrade the performance (especially when dealing with lists and lots of TextViews) because the TextView draws the text more than once. I've created a custom TextView that solves this in the onMeasure() and therefore only draws the text once.

I've originally posted my answer here: https://stackoverflow.com/a/52589927/1680301

And here's the link to the repo: https://github.com/TheCodeYard/EllipsizedTextView

Georgios
  • 4,764
  • 35
  • 48
2

Here's a nice way to do it with a Kotlin extension. Note that we need to wait for the view to layout before we can measure and append the suffix.

In TextViewExtensions.kt

fun TextView.setEllipsizedSuffix(maxLines: Int, suffix: String) {
    addOnLayoutChangeListener(object: View.OnLayoutChangeListener {
        override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom:     Int) {

            val allText = text.toString()
            var newText = allText
            val tvWidth = width
            val textSize = textSize

            if(!TextUtil.textHasEllipsized(newText, tvWidth, textSize, maxLines)) return

            while (TextUtil.textHasEllipsized(newText, tvWidth, textSize, maxLines)) {
                newText = newText.substring(0, newText.length - 1).trim()
            }

            //now replace the last few chars with the suffix if we can
            val endIndex = newText.length - suffix.length - 1 //minus 1 just to make sure we have enough room
            if(endIndex > 0) {
                newText = "${newText.substring(0, endIndex).trim()}$suffix"
            }

            text = newText

            removeOnLayoutChangeListener(this)
        }
    })
}

In TextUtil.kt

fun textHasEllipsized(text: String, tvWidth: Int, textSize: Float, maxLines: Int): Boolean {
    val paint = Paint()
    paint.textSize = textSize
    val size = paint.measureText(text).toInt()

    return size > tvWidth * maxLines
}

Then actually using it like this myTextView.setEllipsizedSuffix(2, "...See more")


Note: if your text comes from a server and may have new line characters, then you can use this method to determine if the text has ellipsized.

fun textHasEllipsized(text: String, tvWidth: Int, textSize: Float, maxLines: Int): Boolean {
    val paint = Paint()
    paint.textSize = textSize
    val size = paint.measureText(text).toInt()
    val newLineChars = StringUtils.countMatches(text, "\n")

    return size > tvWidth * maxLines || newLineChars >= maxLines
}

StringUtils is from implementation 'org.apache.commons:commons-lang3:3.4'

Markymark
  • 2,804
  • 1
  • 32
  • 37
1

Here is a solution for Kotlin.

The yourTextView.post{} is necessary because the textview won't be ellipsized until after it is rendered.

  val customSuffix = "... [See more]"
  yourTextView.post {
    if (yourTextView.layout.getEllipsisStart(-1) != -1) {
      val newText = yourTextView.text.removeRange(
          yourTextView.layout.getEllipsisStart(-1) - customSuffix.length, yourTextView.text.length
      )
      yourTextView.text = String.format("%s%s", newText, customSuffix)
    }
  }
mskolnick
  • 1,062
  • 8
  • 12
0

@George @jmhostalet i was doing this in my recycler view and it degraded the whole performance. `

ViewTreeObserver vto = previewContent.getViewTreeObserver();
    vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                try {
                    Layout layout = previewContent.getLayout();
                    int index1 = layout.getLineStart(previewContent.getMaxLines());
                    if (index1 > 10 && index1 < ab.getPreviewContent().length()) {
                        String s =
                                previewContent.getText().toString().substring(0, index1 - 10);
                        previewContent
                                .setText(Html.fromHtml(
                                        s + "<font color='#DC5530'>...और पढ़ें</font>"));
                    }
                    return true;
                }catch (Exception e)
                {
                    Crashlytics.logException(e);
                }
                return true;
            }
        });` 
JSONParser
  • 1,112
  • 2
  • 15
  • 30