ListView:带LinkMovementMethod的TextView使列表项无法点击?

时间:2011-12-19 08:28:24

标签: android

我想做什么:包含这样的消息的列表:

  

<用户名>这是用户写的mnessage,它将很好地包装到下一行。完全是这样的。

我有什么:

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)" />

扩充上述布局的适配器并执行:

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

InternalURLSpan类

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

我在onCreate(...)中的活动:

listView.setOnItemClickListener(ProgramChecksActivity.this);

并执行上述

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

问题:

点击该项目,不显示祝酒词。只需点击用户名即可显示祝酒词。

我猜,setMovementMethod(LinkMovementMethod.getInstance());使TextView可以点击。因此,物品本身永远不会被点击了。

如何让商品再次点击?拥有我想要的相同功能。

11 个答案:

答案 0 :(得分:57)

在这种情况下有三个显示停止者。根本原因是当您致电setMovementMethodsetKeyListener时,TextView“修复”了它的设置:

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

第一个问题是,当一个视图可点击时 - 它总是消耗ACTION_UP事件(它在onTouchEvent(MotionEvent event)中返回true)。
要解决这个问题,只有当用户实际点击了URL时,才应该在该方法中返回true。

但如果用户实际点击了某个链接,则LinkMovementMethod不会告诉我们。如果用户点击链接,它会在onTouch中返回“true”,但在许多其他情况下也是如此。

所以,实际上我在这里做了一个技巧:

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

你应该在某个地方打电话

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

为textView设置此MovementMethod。

如果用户实际点击链接,此MovementMethod会在TextViewFixTouchConsume中引发一个标记。 (仅在ACTION_UPACTION_DOWN事件中),TextViewFixTouchConsume.onTouchEvent仅在用户实际点击链接时才返回true。

但那不是全部!!!! 第三个问题是ListViewAbsListView)仅在performClick的项目视图无焦点时调用它的onItemClick方法(调用ListView事件处理程序) 。 所以,你需要覆盖

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

在您添加到ListView的视图中。 (在我的情况下,这是一个包含textView的布局)

或者您可以将setOnClickLIstener用于该视图。 诀窍并不是很好,但它确实有效。

答案 1 :(得分:44)

巴贝的回答非常好。但是如果您不想继承TextView并且不关心LinkMovementMethod功能而不是点击链接,您可以使用这种方法(这基本上是将LinkMovementMethod的onTouch功能复制到TextView的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;
            }
        });

只需将此侦听器分配给列表适配器getView()方法中的TextView。

答案 2 :(得分:5)

以下是使ListView项和TextView UrlSpans可点击的快速解决方法:

   private class YourListadapter extends BaseAdapter {

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

           return convertView
       }

    }

答案 3 :(得分:3)

问题在于LinkMovementMethod表示将管理触摸事件,独立地触摸是在Spannable或普通文本中。

这应该有用。

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

    ...
}

答案 4 :(得分:2)

发生这种情况是因为当我们按下列表项时,它会将新闻事件发送给其所有子项,因此孩子的 setPressed 调用而不是列表项。因此,要单击列表项,您必须将子项的 setPressed 设置为false。为此,您必须自定义 TextView 类并覆盖所需的方法。这是示例代码

public class DontPressWithParentTextView扩展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);
}

}

答案 5 :(得分:1)

我有另一个解决方案使用带有两个不同文本的列表项布局:

<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)" />

,适配器代码为:

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

这将有效..: - )

答案 6 :(得分:0)

为什么使用ListView.setOnItemClickListener()?您可以通过messageTextView.setOnClickListener()在适配器中提供相同的内容。

另一种方法 - 为消息设置第二个可点击范围 - 并在那里提供操作。如果你不希望第二部分看起来像一个linke创建

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

答案 7 :(得分:0)

对于所有有兴趣用EmojisTextView从@babay在第一个答案中所说的那样做的人,我会做出一些改变:

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

}

之后你会在这里做同样的事情:

yourTextView.setMovementMethod(EmojiconTextView.LocalLinkMovementMethod.getInstance());

答案 8 :(得分:0)

答案 9 :(得分:0)

您必须将此行放在适配器项父视图android:descendantFocusability="blocksDescendants"

答案 10 :(得分:-1)

好吧,@ babay的回答是正确的,但他似乎已经忘记了一些事情,你应该写一下&#34; TextViewFixTouchConsume&#34;而不是&#34; TextView&#34;在xml中,然后它将工作!例如:

<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" />