12

I have a TextView with maxLines:5 and ellipsize:end applied, I'm also using setMovementMethod(LinkMovementMethod.getInstance()) on the TextView to make links clickable (HTML content).

The combination of all of the above disables the text being truncated and the '...' suffix to be appended.

Any idea what goes wrong and how to work around it?

Without setting the movement method, everything works as expected.

Update regarding bounty: looking for solution other than manually setting ellipses

Siavash
  • 7,583
  • 13
  • 49
  • 69
Aviran
  • 5,160
  • 7
  • 44
  • 76
  • That's because LinkMovementMethod disables the trimming. In some circumstances, the text can be scrolled all the way to the end, few px at a time. So there is nothing to ellipsize. – Agent_L Dec 03 '21 at 12:24

7 Answers7

6

Sorry I'm late on this one.

Here is little work around for this

MainActivity

public class MainActivity extends AppCompatActivity {


    TextView htmlTextView;

    CustomEllipsizeTextView  customEllipsizeTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        htmlTextView = findViewById(R.id.htmlTextView1);

        customEllipsizeTextView = findViewById(R.id.customEllipsizeTextView);

        String value = "Hello this is a dummy textview";

        String myText = "You can visit my  Profile in <a href=\"https://stackoverflow.com/users/7666442/nilesh-rathod?tab=profile\">stackoverflow</a> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis bibendum mattis risus eget pulvinar. Praesenttingd" +
                " commodo erat enim, id564654 congue sem tristique vitae. Proin vitae accumsan justo, ut imperdiet Mauris neque nibh, hendrerit id tortor vel, congue sagittis odio. Morbi elementum lobortis maximus. Etiam sit amet porttitor massa. Fusce sed magna quis arcu tincidunt finibus vitae id erat. " +
                "commodo erat enim, id54654 congue sem tristique vitae. Proin vitae accumsan commodo erat enim, id congue sem tristique vitae. Proin vitae accumsan Pellentesque massa mi, imperdiet eget accums ";


        SpannableString spanText2 = new SpannableString(myText);
        htmlTextView.setText(value);

        if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
            customEllipsizeTextView.setText(Html.fromHtml(spanText2.toString(), Html.FROM_HTML_MODE_LEGACY));
        } else {
            customEllipsizeTextView.setText(Html.fromHtml(spanText2.toString()));
        }

        htmlTextView.setMovementMethod(LinkMovementMethod.getInstance());
        customEllipsizeTextView.setMovementMethod(LinkMovementMethod.getInstance());
        customEllipsizeTextView.setOnTouchListener(new TouchTextView(spanText2));

    }

}

layout.activity_main

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".FirstFragment">



    <TextView
        android:id="@+id/htmlTextView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:maxLines="1"
        android:text="@string/link_text" />

    <neel.com.demo.CustomEllipsizeTextView
        android:id="@+id/customEllipsizeTextView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:linksClickable="true"
        android:maxLines="5"
        android:padding="5dp"
        android:visibility="visible" />


</LinearLayout>

CustomEllipsizeTextView

public class CustomEllipsizeTextView extends android.support.v7.widget.AppCompatTextView {
    public CustomEllipsizeTextView(Context context) {
        super(context);
    }

    public CustomEllipsizeTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomEllipsizeTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        StaticLayout layout = null;
        Field field = null;
        try {
            Field staticField = DynamicLayout.class.getDeclaredField("sStaticLayout");
            staticField.setAccessible(true);
            layout = (StaticLayout) staticField.get(DynamicLayout.class);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        if (layout != null) {
            try {
                field = StaticLayout.class.getDeclaredField("mMaximumVisibleLineCount");
                field.setAccessible(true);
                field.setInt(layout, getMaxLines());
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (layout != null && field != null) {
            try {
                field.setInt(layout, Integer.MAX_VALUE);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
}

TouchTextView

public class TouchTextView implements View.OnTouchListener {
    Spannable spannable;

    public TouchTextView (Spannable spannable){
        this.spannable = spannable;
    }
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int action = event.getAction();
        if(!(v instanceof TextView)){
            return false;
        }
        TextView textView  = (TextView) v;
        if (action == MotionEvent.ACTION_UP ||
                action == MotionEvent.ACTION_DOWN) {
            int x = (int) event.getX();
            int y = (int) event.getY();

            x -= textView.getTotalPaddingLeft();
            y -= textView.getTotalPaddingTop();

            x += textView.getScrollX();
            y += textView.getScrollY();

            Layout layout = textView.getLayout();
            int line = layout.getLineForVertical(y);
            int off = layout.getOffsetForHorizontal(line, x);

            ClickableSpan[] link = spannable.getSpans(off, off, ClickableSpan.class);

            if (link.length != 0) {
                if (action == MotionEvent.ACTION_UP) {
                    link[0].onClick(textView);
                } else if (action == MotionEvent.ACTION_DOWN) {
                    Selection.setSelection(spannable,
                            spannable.getSpanStart(link[0]),
                            spannable.getSpanEnd(link[0]));
                }

                return true;
            } else {
                Selection.removeSelection(spannable);
            }
        }

        return false;
    }
}

OUTPUT

enter image description here

Here is the explanation :

I have debugged TextView and found out the following :

So when you use LinkMovementMethod() actually text is acting as Spannable. In other case it is String.

There is one following condition inside TextView

if (mText instanceof Spannable) {
//executes incase of LinkMovementMethod
            result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
                    alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad,
                    mBreakStrategy, mHyphenationFrequency, mJustificationMode,
                    getKeyListener() == null ? effectiveEllipsize : null, ellipsisWidth);
        } else { 
   //executes without any movementmethod
..create StaticLayout
}

So DynamicLayout internally calls StaticLayout to render text, but it is not setting mMaximumVisibleLineCountinside StaticLayout when coming from DynamicLayout so it is default Integer.MAX_VALUE. But when creating StaticLayout from String, it is actually setting mMaximumVisibleLineCount as maxLines. This mMaximumVisibleLineCount is used for displaying ellipsize. That's why "..." is not displaying.

For displaying the number of lines, the following code works

if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
            unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
        }

mMaximum will be set to maxLines in both case, but mLayout.getLineCount() will be maxLines for one without MovementMethod and for with MovementMethod it will be number of lines of original string

Sangeet Suresh
  • 2,527
  • 1
  • 18
  • 19
AskNilesh
  • 67,701
  • 16
  • 123
  • 163
1

Try this code.

           <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/white"
            android:textStyle="bold"
            android:textSize="20dp"
            android:id="@+id/txtTitle"
            android:text="" />

           String message="<font color='gray'>"+"YOUR CONTENT"+ "<br>" +"<font color='cyan'>"+"<font size='5'>"+" "+"</font>";
           txtTitle.setBackgroundColor(Color.TRANSPARENT);
           txtTitle.setText(message);
Atman Bhatt
  • 57
  • 6
  • 10
1

Try this code.

XML

<TextView
 android:id="@+id/tvcondition1"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:textColor="@color/white"
 android:textStyle="bold"
 android:ellipsize="end"
 android:maxLines="5"
 android:textSize="15sp" />

Java file Code

    String strTeamsCondition = "<b><u> Terms &amp; Condition </u></b>";
    String conditionlimit = "Terms & Condition";
    String strConditionFirstLine = getString(R.string.condition_1);
    String condition_1_1 = getString(R.string.condition_1_1);
    String htmlAsString2 = strConditionFirstLine + strTeamsCondition + condition_1_1;
    Spanned htmlAsSpannedCondition = Html.fromHtml(htmlAsString2);


    tvcondition1.setText(htmlAsSpannedCondition);
    Spannable spanText = new SpannableString(htmlAsSpannedCondition);
    spanText.setSpan(new MyClickableSpan(htmlAsSpannedCondition), strConditionFirstLine.length(), strConditionFirstLine.length() + conditionlimit.length() + 1, 0);
    tvcondition1.setText(spanText);
    tvcondition1.setMovementMethod(LinkMovementMethod.getInstance());

Touch Selected Text

 class MyClickableSpan extends ClickableSpan {
        public MyClickableSpan(Spanned string) {
            super();
        }

        public void onClick(View tv) {
            Toast.makeText(getApplicationContext(), "Thanks for the click!",
                Toast.LENGTH_SHORT).show();

        }

        public void updateDrawState(TextPaint ds) {

            ds.setColor(getResources().getColor(R.color.black));
            ds.setUnderlineText(true); // set to false to remove underline
        }

    }
0
  <TextView
        android:id="@+id/html"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:maxLines="5"
        android:ellipsize="end"/>

        //inside your activity
        public class A extends AppCompatActivity{

               TextView html = (TextView) view.findViewById(R.id.html);
               html.setText(Html.fromHtml("this is an example <a href=\"www.google.com\">google</a> link to google"));
               html.setMovementMethod(LinkMovementMethod.getInstance());
               .
               .
               .
         }
  • The code sample, as it currently is, is going to result in [android.app.SuperNotCalledException](https://stackoverflow.com/q/23851962/3290339) – Onik Oct 18 '18 at 23:26
0

A possible workaround for this is..

public static void makeTextViewResizable(final TextView tv, final int maxLine, final String expandText, final boolean viewMore) {

    if (tv.getTag() == null) {
        tv.setTag(tv.getText());
    }
    ViewTreeObserver vto = tv.getViewTreeObserver();
    vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

        @SuppressWarnings("deprecation")
        @Override
        public void onGlobalLayout() {

            ViewTreeObserver obs = tv.getViewTreeObserver();
            obs.removeGlobalOnLayoutListener(this);
            if (maxLine == 0) {
                int lineEndIndex = tv.getLayout().getLineEnd(0);
                String text = tv.getText().subSequence(0, lineEndIndex - expandText.length() + 1) + " " + expandText;
                tv.setText(text);
                tv.setMovementMethod(LinkMovementMethod.getInstance());
                tv.setText(
                        addClickablePartTextViewResizable(Html.fromHtml(tv.getText().toString()), tv, maxLine, expandText,
                                viewMore), TextView.BufferType.SPANNABLE);
            } else if (maxLine > 0 && tv.getLineCount() >= maxLine) {
                int lineEndIndex = tv.getLayout().getLineEnd(maxLine - 1);
                String text = tv.getText().subSequence(0, lineEndIndex - expandText.length() + 1) + " " + expandText;
                tv.setText(text);
                tv.setMovementMethod(LinkMovementMethod.getInstance());
                tv.setText(
                        addClickablePartTextViewResizable(Html.fromHtml(tv.getText().toString()), tv, maxLine, expandText,
                                viewMore), TextView.BufferType.SPANNABLE);
            } else {
                int lineEndIndex = tv.getLayout().getLineEnd(tv.getLayout().getLineCount() - 1);
                String text = tv.getText().subSequence(0, lineEndIndex) + " " + expandText;
                tv.setText(text);
                tv.setMovementMethod(LinkMovementMethod.getInstance());
                tv.setText(
                        addClickablePartTextViewResizable(Html.fromHtml(tv.getText().toString()), tv, lineEndIndex, expandText,
                                viewMore), TextView.BufferType.SPANNABLE);
            }
        }
    });

}

private static SpannableStringBuilder addClickablePartTextViewResizable(final Spanned strSpanned, final TextView tv,
                                                                        final int maxLine, final String spanableText, final boolean viewMore) {
    String str = strSpanned.toString();
    SpannableStringBuilder ssb = new SpannableStringBuilder(strSpanned);

    if (str.contains(spanableText)) {


        ssb.setSpan(new MySpannable(false){
            @Override
            public void onClick(View widget) {
                if (viewMore) {
                    tv.setLayoutParams(tv.getLayoutParams());
                    tv.setText(tv.getTag().toString(), TextView.BufferType.SPANNABLE);
                    tv.invalidate();
                    makeTextViewResizable(tv, -1, "See Less", false);
                } else {
                    tv.setLayoutParams(tv.getLayoutParams());
                    tv.setText(tv.getTag().toString(), TextView.BufferType.SPANNABLE);
                    tv.invalidate();
                    makeTextViewResizable(tv, 3, ".. See More", true);
                }
            }
        }, str.indexOf(spanableText), str.indexOf(spanableText) + spanableText.length(), 0);

    }
    return ssb;

}

Spannable Class:

import android.graphics.Color;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.view.View;

public class MySpannable extends ClickableSpan {

private boolean isUnderline = true;

/**
 * Constructor
 */
public MySpannable(boolean isUnderline) {
    this.isUnderline = isUnderline;
}

@Override
public void updateDrawState(TextPaint ds) {
    ds.setUnderlineText(isUnderline);
    ds.setColor(Color.parseColor("#1b76d3"));
}

@Override
public void onClick(View widget) {


 }
}

Call it in this way:

myTextView.setText(discription);
makeTextViewResizable(myTextView, 3, "See More", true);
Asad Ali
  • 309
  • 3
  • 14
0

This is what I have if I use a TextView and android:autoLink="email|web" enter image description here

<TextView
    android:id="@+id/text_link"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:ellipsize="end"
    android:gravity="end"
    android:autoLink="email|web"
    android:maxLines="1"
    android:text="https://stackoverflow.com/questions/46056046/textview-maxlines-movement-method-and-ellipsize"
    android:textColorLink="?attr/colorAccent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toEndOf="@id/text_link_attribute"
    app:layout_constraintTop_toBottomOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

And it looks like that enter image description here

if we use a custom TextView (inspired by):

   class NoScrollTextView : androidx.appcompat.widget.AppCompatTextView {

      constructor(context: Context?) : super(context)
      constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
      constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
          context,
          attrs,
          defStyleAttr
      )

      override fun scrollTo(x: Int, y: Int) {
          super.scrollTo(x, 0)
      }

      override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
          super.onLayout(changed, left, top, right, bottom)
          post { moveCursorToVisibleOffset() }
      }
  }

and respectively

<com.YOU_PACKAGE_NAME_HERE.NoScrollTextView
    android:id="@+id/text_link"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:ellipsize="end"
    android:gravity="end"
    android:autoLink="email|web"
    android:singleLine="true"
    android:text="https://stackoverflow.com/questions/46056046/textview-maxlines-movement-method-and-ellipsize"
    android:textColorLink="?attr/colorAccent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toEndOf="@id/text_link_attribute"
    app:layout_constraintTop_toBottomOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

The key features (they are necessary and sufficient):

  1. super.scrollTo(x, 0)
  2. post { moveCursorToVisibleOffset() }
  3. android:singleLine="true"
Ievgen
  • 426
  • 6
  • 10
-1

Try this.

Way 1. use setMovementMethod and Html.fromHtml

I didn't set maxLines and ellipsize. It 's work ok .

In your XML code

<TextView  
    android:id="@+id/tv_html"  
    android:ellipsize="end"
    android:maxLines="5"
    android:layout_width="wrap_content"  
    android:layout_height="wrap_content" /> 

Then In your java code

 TextView tv_html = (TextView) findViewById(R.id.tv_html);
 tv_html.setText(Html.fromHtml("google:" + "<a href='https://www.google.com.hk'>link to it</a> "));
 tv_html.setMovementMethod(LinkMovementMethod.getInstance());// make it active

Way 2. use android:autoLink="all" in the XML code

<TextView  
    android:id="@+id/tv_html"  
    android:layout_width="wrap_content"  
    android:layout_height="wrap_content"  
    android:autoLink="all"/>  

Then in your java code.

TextView tv_html = (TextView) findViewById(R.id.tv_html);
tv_html.setText("google: https://www.google.com.hk"));

Way 3. use SpannableString in the code.

TextView tv_html = (TextView) findViewById(R.id.tv_html); 
SpannableString ss = new SpannableString("google: link to google");  
ss.setSpan(new URLSpan("https://www.google.com.hk"), 8, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 

tv_html.setText(ss);  
tv_html .setMovementMethod(LinkMovementMethod.getInstance());  

Edit

It can scroll but not show the ....

You can add

 android:scrollbars="vertical"
KeLiuyue
  • 8,149
  • 4
  • 25
  • 42
  • 1
    if you're not setting maxLines, how are you limiting the text? I have a block of text which I want to show at most 5 lines... it's a rich text (with html) so I need to use fromHtml to make it spannable – Aviran Sep 07 '17 at 13:10
  • I checked a lot of information and tried many times . `...` not displayed.@aviran – KeLiuyue Sep 08 '17 at 01:15