43

What I want to do: A list with messages like this:

<UserName> and here is the mnessage the user writes, that will wrap nicely to the next line. exactly like this.

What I have:

ListView R.layout.list_item:

<TextView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/text_message"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:text="(Message Text)" />

Adapter that inflates the above layout and does:

SpannableStringBuilder f = new SpannableStringBuilder(check.getContent());
f.append(username);
f.setSpan(new InternalURLSpan(new OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(context, "Clicked User", Toast.LENGTH_SHORT).show();
    }
}), f.length() - username.length(), f.length(),
        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

f.append(" " + message);

messageTextView.setText(f);
messageTextView.setMovementMethod(LinkMovementMethod.getInstance());
meesageTextView.setFocusable(false);

The InternalURLSpan class

public class InternalURLSpan extends ClickableSpan {
    OnClickListener mListener;

    public InternalURLSpan(OnClickListener listener) {
        mListener = listener;
    }

    @Override
    public void onClick(View widget) {
        mListener.onClick(widget);
    }

    @Override
    public void updateDrawState(TextPaint ds) {
        super.updateDrawState(ds);
        ds.setUnderlineText(false);
    }
}

In the activity I have in onCreate(...):

listView.setOnItemClickListener(ProgramChecksActivity.this);

and the implementation of the above

@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
    Toast.makeText(context, "Clicked Item", Toast.LENGTH_SHORT).show();
}

The problem:

Clicking on the item, does not show the toast. Only clicking on the username does show the toast.

I am guessing, that setMovementMethod(LinkMovementMethod.getInstance()); makes the TextView clickable. So the items themselves do never get clicked anymore.

How can I make the items clickable again? Having the same functionality as I want.

rekire
  • 47,260
  • 30
  • 167
  • 264
Patrick Boos
  • 6,789
  • 3
  • 35
  • 36

11 Answers11

62

There are THREE show-stoppers in this situation. The root reason is that when you call setMovementMethod or setKeyListener, TextView "fixes" it's settings:

setFocusable(true);
setClickable(true);
setLongClickable(true);

The first problem is that when a View is clickable - it always consumes ACTION_UP event (it returns true in onTouchEvent(MotionEvent event)).
To fix that you should return true in that method only if the user actually clicks the URL.

But the LinkMovementMethod doesn't tell us, if the user actually clicked a link. It returns "true" in it's onTouch if the user clicks the link, but also in many other cases.

So, actually I did a trick here:

public class TextViewFixTouchConsume extends TextView {

    boolean dontConsumeNonUrlClicks = true;
    boolean linkHit;

    public TextViewFixTouchConsume(Context context) {
        super(context);
    }

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

    public TextViewFixTouchConsume(
        Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        linkHit = false;
        boolean res = super.onTouchEvent(event);

        if (dontConsumeNonUrlClicks)
            return linkHit;
        return res;

    }

    public void setTextViewHTML(String html)
    {
        CharSequence sequence = Html.fromHtml(html);
        SpannableStringBuilder strBuilder = 
            new SpannableStringBuilder(sequence);
        setText(strBuilder);
    }

    public static class LocalLinkMovementMethod extends LinkMovementMethod{
        static LocalLinkMovementMethod sInstance;


        public static LocalLinkMovementMethod getInstance() {
            if (sInstance == null)
                sInstance = new LocalLinkMovementMethod();

            return sInstance;
        }

        @Override
        public boolean onTouchEvent(TextView widget, 
            Spannable buffer, MotionEvent event) {
            int action = event.getAction();

            if (action == MotionEvent.ACTION_UP ||
                    action == MotionEvent.ACTION_DOWN) {
                int x = (int) event.getX();
                int y = (int) event.getY();

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

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

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

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

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

                    if (widget instanceof TextViewFixTouchConsume){
                        ((TextViewFixTouchConsume) widget).linkHit = true;
                    }
                    return true;
                } else {
                    Selection.removeSelection(buffer);
                    Touch.onTouchEvent(widget, buffer, event);
                    return false;
                }
            }
            return Touch.onTouchEvent(widget, buffer, event);
        }
    }
}

You should call somewhere

textView.setMovementMethod(
    TextViewFixTouchConsume.LocalLinkMovementMethod.getInstance()
);

to set this MovementMethod for the textView.

This MovementMethod raises a flag in TextViewFixTouchConsume if user actually hits link. (only in ACTION_UP and ACTION_DOWN events) and TextViewFixTouchConsume.onTouchEvent returns true only if user actually hit link.

But that's not all!!!! The third problem is that ListView (AbsListView) calls it's performClick method (that calls onItemClick event handler) ONLY if ListView's item view has no focusables. So, you need to override

@Override
public boolean hasFocusable() {
    return false;
}

in a view that you add to ListView. (in my case that is a layout that contains textView)

or you can use setOnClickLIstener for that view. The trick is not very good, but it works.

babay
  • 4,689
  • 1
  • 26
  • 40
  • This is the only approach that has worked for me reliably and without introducing any other bugs. Awesome work babay! – Neil Sainsbury Apr 09 '13 at 23:34
  • Been wondering about this problem for a long time. Tried this fix and it works perfectly in Mono. – BigFwoosh Apr 24 '13 at 19:10
  • 2
    For anyone who needs it, here's the corresponding Mono code: http://pastebin.com/8SvMRaN9 – BigFwoosh Apr 24 '13 at 19:18
  • This solution invoke another problem —— the longItemClick invokes after just click the list item but not press and held. – jerry Aug 12 '13 at 09:40
  • Android 5.0 on Samsung S5 throws `java.lang.IllegalStateException: Unable to create layer for TextViewFixTouchConsume`. clear (Nexus) version works like a charm... What now? – snachmsm Feb 13 '15 at 13:29
  • Why do not simply call `setFocusable(false)` ? – srain Mar 24 '15 at 08:10
  • @srain - because TextView "fixes" that - it calls: setFocusable(true); – babay Aug 25 '15 at 15:19
47

babay's answer is very nice. But if you don't want to subclass TextView and don't care about LinkMovementMethod features other than clicking on links you could use this approach (this is basically copying LinkMovementMethod's onTouch functionality into TextView's OnTouchListener):

        myTextView.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                boolean ret = false;
                CharSequence text = ((TextView) v).getText();
                Spannable stext = Spannable.Factory.getInstance().newSpannable(text);
                TextView widget = (TextView) v;
                int action = event.getAction();

                if (action == MotionEvent.ACTION_UP ||
                        action == MotionEvent.ACTION_DOWN) {
                    int x = (int) event.getX();
                    int y = (int) event.getY();

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

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

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

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

                    if (link.length != 0) {
                        if (action == MotionEvent.ACTION_UP) {
                            link[0].onClick(widget);
                        }
                        ret = true;
                    }
                }
                return ret;
            }
        });

Just assign this listener to your TextView in your list adapter getView() method.

Dennis K
  • 1,828
  • 1
  • 16
  • 27
  • doesn't work, always returns 0 spans in the line that calls "getSpans" – SpaceMonkey Aug 23 '15 at 19:56
  • @Spacemonkey I'd debug it following these steps: put a breakpoint on the line with getSpans, add an expression stext.getSpans(0, text.length(), ClickableSpan.class), make sure you actually have clickable spans there, if so, check their start/stop positions and compare to line/off values. If there is a mismatch - look where miscalculation is happening. Just a shot in the dark - check the type of spans you're setting, may be you need SPAN_INCLUSIVE_INCLUSIVE – Dennis K Aug 24 '15 at 22:31
  • @DennisK There are no links at all, despite the fact that I can see the text highlighted and underlined. I wonder what's going wrong – SpaceMonkey Aug 24 '15 at 23:39
  • @Spacemonkey You need to look at how you're applying those spans or where you're getting this text from. Try specifying Object.class in getSpans to get the list of all spans set on your text. This might give an idea. – Dennis K Aug 26 '15 at 19:02
  • This fix will make the click on the area outside the ClickableSpan being consider as clicking the ClickableSpan. I think you should check the area is inside bound first. – Morty Choi Sep 10 '15 at 03:32
  • 1
    @DennisK Thank you, this works great. I just extended OnTouchListener, and `setOnTouchListener(new OnLinkTouchListener)` on any `TextView` that needs this functionality. – Bryan Feb 29 '16 at 21:17
  • @DennisK, This gives a warning: 'onTouch should call View#performClick when a click is detected '. – Tushar Kathuria Oct 05 '20 at 11:06
5

Here is quick fix that makes ListView items and TextView UrlSpans clickable:

   private class YourListadapter extends BaseAdapter {

       @Override
       public View getView(int position, View convertView, ViewGroup parent) {
           ((ViewGroup)convertView).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);

           return convertView
       }

    }
user1888162
  • 1,735
  • 21
  • 27
  • 3
    This worked in my case too, except I was able to set it straight in the XML: `android:descendantFocusability="blocksDescendants"` – akent Oct 10 '14 at 10:10
  • @akent not working to me, i set android:descendantFocusability="blocksDescendants" for all view but can not get longclick when longclick to textview, when textView have mention spannableString – famfamfam Mar 15 '21 at 18:47
3

The problem is in that LinkMovementMethod indicates that are going to manage the touch event, independiently the touch is in a Spannable or in normal text.

This should work.

public class HtmlTextView extends TextView {

    ...

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (getMovementMethod() == null ) {
            boolean result = super.onTouchEvent(event); 
            return result;
        }

        MovementMethod m = getMovementMethod();     
        setMovementMethod(null);

        boolean mt = m.onTouchEvent(this, (Spannable) getText(), event);
        if (mt && event.getAction() == MotionEvent.ACTION_DOWN) {
            event.setAction(MotionEvent.ACTION_UP);
            mt = m.onTouchEvent(this, (Spannable) getText(), event);
            event.setAction(MotionEvent.ACTION_DOWN);
        }

        boolean st = super.onTouchEvent(event);

        setMovementMethod(m);
        setFocusable(false);

        return mt || st;
    }

    ...
}
Narkha
  • 1,197
  • 2
  • 12
  • 30
2

This is happened because when we press on list item it sends the press event to all its children, so the child's setPressed calls rather than the list item. Hence for clicking the list item, you have to set the child's setPressed to false. For this, you have to make custom TextView class and override the desired method. Here is the sample code

public class DontPressWithParentTextView extends TextView {

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

@Override
public void setPressed(boolean pressed) {
    // If the parent is pressed, do not set to pressed.
    if (pressed && ((View) getParent()).isPressed()) {
        return;
    }
    super.setPressed(pressed);
}

}

Neeraj Nama
  • 1,562
  • 1
  • 18
  • 24
  • Sadly this does not work. But I like the idea. Trying to figure out how to make something like this work. – Patrick Boos Jan 03 '12 at 03:02
  • @PatrickBoos this doesn't work in your case since the the textview holds the all space of the row, if there are more than one item in the row(list item) than try it. – Neeraj Nama Jan 03 '12 at 04:27
1

I have another solution use the list item layout with two different text:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="fill_parent" >

<com.me.test.DontPressWithParentTextView
    android:id="@+id/text_user"
    android:layout_width="wrap_content"
    android:layout_height="fill_parent"
    android:text="(Message Text)" />

<TextView
    android:id="@+id/text_message"
    android:layout_width="wrap_content"
    android:layout_height="fill_parent"
    android:text="(Message Text)" />

and the adapter code as:

DontPressWithParentTextView text1 =  (DontPressWithParentTextView) convertView.findViewById(R.id.text_user);

                TextView text2 = (TextView) convertView.findViewById(R.id.text_message);
                text2.setText(message);

                SpannableStringBuilder f = new SpannableStringBuilder();
                CharSequence username = names1[position];
                f.append(username );
                f.setSpan(new InternalURLSpan(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Toast.makeText(getBaseContext(), "Clicked User", Toast.LENGTH_SHORT).show();
                    }
                }), f.length() - username.length(), f.length(),
                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

                f.append(" ");

                text1.setText(f);
                text1.setMovementMethod(LinkMovementMethod.getInstance());
                text1.setFocusable(false);

This will work.. :-)

Neeraj Nama
  • 1,562
  • 1
  • 18
  • 24
  • problem here is, that the message text will not break the new line and start below the username. it will break the line and start at the right of the username. – Patrick Boos Jan 04 '12 at 01:48
  • @PatrickBoos I think you got the idea, now its your call how to do? – Neeraj Nama Jan 04 '12 at 04:56
  • Simply adding setFocusable(false); in my subclass of TextView right after setting setMovementMethod made my row clickable again! – slott May 14 '14 at 09:47
0

For all who interessted to do that with EmojisTextView from what @babay said in the fist answer i make some chages like that:

public class EmojiconTextView extends TextView {
private int mEmojiconSize;
private int mTextStart = 0;
private int mTextLength = -1;

boolean dontConsumeNonUrlClicks = true;
boolean linkHit;

public EmojiconTextView(Context context) {
    super(context);
    init(null);
}

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

public EmojiconTextView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init(attrs);
}

private void init(AttributeSet attrs) {
    if (attrs == null) {
        mEmojiconSize = (int) getTextSize();
    } else {
        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.Emojicon);
        mEmojiconSize = (int) a.getDimension(R.styleable.Emojicon_emojiconSize, getTextSize());
        mTextStart = a.getInteger(R.styleable.Emojicon_emojiconTextStart, 0);
        mTextLength = a.getInteger(R.styleable.Emojicon_emojiconTextLength, -1);
        a.recycle();
    }
    setText(getText());
}

@Override
public void setText(CharSequence text, BufferType type) {
    SpannableStringBuilder builder = new SpannableStringBuilder(text);
    EmojiconHandler.addEmojis(getContext(), builder, mEmojiconSize, mTextStart, mTextLength);
    super.setText(builder, type);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    linkHit = false;
    boolean res = super.onTouchEvent(event);

    if (dontConsumeNonUrlClicks)
        return linkHit;
    return res;

}






/**
 * Set the size of emojicon in pixels.
 */
public void setEmojiconSize(int pixels) {
    mEmojiconSize = pixels;
}


public static class LocalLinkMovementMethod extends LinkMovementMethod {
    static LocalLinkMovementMethod sInstance;


    public static LocalLinkMovementMethod getInstance() {
        if (sInstance == null)
            sInstance = new LocalLinkMovementMethod();

        return sInstance;
    }

    @Override
    public boolean onTouchEvent(TextView widget,
                                Spannable buffer, MotionEvent event) {
        int action = event.getAction();

        if (action == MotionEvent.ACTION_UP ||
                action == MotionEvent.ACTION_DOWN) {
            int x = (int) event.getX();
            int y = (int) event.getY();

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

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

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

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

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

                if (widget instanceof EmojiconTextView){
                    ((EmojiconTextView) widget).linkHit = true;
                }
                return true;
            } else {
                Selection.removeSelection(buffer);
                Touch.onTouchEvent(widget, buffer, event);
                return false;
            }
        }
        return Touch.onTouchEvent(widget, buffer, event);
    }
}

}

after that you just do the same here:

yourTextView.setMovementMethod(EmojiconTextView.LocalLinkMovementMethod.getInstance());
Context
  • 1,873
  • 1
  • 20
  • 24
0

apply this textview in layout. https://gist.github.com/amilcar-andrade/e4b76840da1dc92febfc

Arslan
  • 249
  • 2
  • 13
0

You have to place this line in your adapter item parent view android:descendantFocusability="blocksDescendants"

Deepak Gupta
  • 552
  • 7
  • 22
0

Why do you use ListView.setOnItemClickListener()? You can provide the same in adapter by messageTextView.setOnClickListener().

Another method - set second clickable span for message - and provide actions there. If you don't want second part looks like a linke create

public class InternalClickableSpan extends ClickableSpan {
    OnClickListener mListener;

    public InternalClickableSpan(OnClickListener listener) {
        mListener = listener;
    }

    @Override
    public void onClick(View widget) {
        mListener.onClick(widget);
    }

    @Override
    public void updateDrawState(TextPaint ds) {
        super.updateDrawState(ds);
        ds.setUnderlineText(false);
        ds.setColor(Color.WHITE);// Here you can provide any color - take it from resources or from theme.
    }
}
Jin35
  • 8,602
  • 3
  • 32
  • 52
  • 1
    Because setOnItemClickListener is the nicer way to go. The Activity should handle what happens if an item is clicked. Since the adapter can be used in different places and a click could be handled in different ways. – Patrick Boos Dec 28 '11 at 06:40
  • A second clickable span would work. But I would prefer if there is a way to get it work through setOnItemClickListener. – Patrick Boos Dec 28 '11 at 06:42
  • If nobody comes up with a better idea than "a second clickable span", than I will reward the points to you for good thinking. – Patrick Boos Dec 28 '11 at 06:47
  • just found one reason, why the second clickable span is not a good solution :( it turns the second part into a link (blue), which is not what i wanted. Sorry. So can't award you the points in that case. – Patrick Boos Jan 03 '12 at 01:44
  • And what is code for `InternalURLSpan`? There you can setup lookup of text. – Jin35 Jan 03 '12 at 11:06
  • sorry. added it to the post now. – Patrick Boos Jan 04 '12 at 01:53
-2

well,@babay's answer is right,but he seems had forgot something, you should write "TextViewFixTouchConsume" instead of "TextView" in xml, and then it will be work! for example :

<com.gongchang.buyer.widget.TextViewFixTouchConsume xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/wx_comment_friendsname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/comment_bg_with_grey_selector"
android:lineSpacingExtra="1dp"
android:paddingBottom="1dp"
android:paddingLeft="@dimen/spacing_third"
android:paddingRight="@dimen/spacing_third"
android:paddingTop="1dp"
android:textColor="@color/text_black_2d"
android:textSize="@dimen/content_second" />
benka
  • 4,732
  • 35
  • 47
  • 58