【问题标题】:ListView: TextView with LinkMovementMethod makes list item unclickable?ListView:带有 LinkMovementMethod 的 TextView 使列表项不可点击?
【发布时间】:2011-12-19 08:28:24
【问题描述】:

我想要做的:一个包含这样消息的列表:

这是用户写入的信息,它将很好地换行到下一行。完全一样。

我有什么:

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

问题:

点击项目,不显示吐司。只有点击用户名才会显示 toast。

我猜,setMovementMethod(LinkMovementMethod.getInstance()); 使 TextView 可点击。所以项目本身不再被点击。

如何使这些项目再次可点击?拥有我想要的相同功能。

【问题讨论】:

  • 不确定只是尝试在 xml android 中尝试将 ListView 行的 TextView 设为 focusable=false。
  • 试过了,还是不行:(
  • 这是我现在的完全相同的问题,我似乎找不到合适的解决方案......

标签: android


【解决方案1】:

在这种情况下,有 3 个阻碍因素。根本原因是当你调用setMovementMethodsetKeyListener 时,TextView “修复”了它的设置:

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

第一个问题是当 View 可点击时 - 它总是消耗 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。

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

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

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

或者您可以为该视图使用setOnClickLIstener。 这个技巧不是很好,但是很有效。

【讨论】:

  • 这是唯一对我有效且不会引入任何其他错误的方法。很棒的工作宝贝!
  • 这个问题想了很久。尝试了这个修复,它在 Mono 中完美运行。
  • 有需要的人,这里是对应的Mono代码:pastebin.com/8SvMRaN9
  • 这个解决方案调用了另一个问题——longItemClick是在点击列表项而不长按之后调用的。
  • 太棒了!你节省了我的时间,很好的解决方案。谢谢。
【解决方案2】:

babay 的回答非常好。但是,如果您不想继承 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。

【讨论】:

  • 不起作用,总是在调用“getSpans”的行中返回 0 个跨度
  • @Spacemonkey 我会按照以下步骤调试它:在 getSpan 的行上放置一个断点,添加一个表达式 stext.getSpans(0, text.length(), ClickableSpan.class),确保您实际上在那里有可点击的跨度,如果是这样,请检查它们的开始/停止位置并与行/关闭值进行比较。如果存在不匹配 - 查看发生错误的位置。只是在黑暗中拍摄 - 检查您设置的跨度类型,可能您需要 SPAN_INCLUSIVE_INCLUSIVE
  • @DennisK 根本没有链接,尽管我可以看到突出显示和下划线的文本。我想知道怎么了
  • @Spacemonkey 您需要查看如何应用这些跨度或从何处获取此文本。尝试在 getSpans 中指定 Object.class 以获取文本上设置的所有跨度的列表。这可能会提供一个想法。
  • @DennisK 谢谢,这很好用。我刚刚扩展了 OnTouchListener 和 setOnTouchListener(new OnLinkTouchListener) 到任何需要此功能的 TextView
【解决方案3】:

这是使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
       }

    }

【讨论】:

  • 这也适用于我的情况,除了我能够直接在 XML 中设置它:android:descendantFocusability="blocksDescendants"
  • @akent 对我不起作用,我为所有视图设置了 android:descendantFocusability="blocksDescendants" 但当 textView 提到 spannableString 时,当长按到 textview 时无法获得长按
【解决方案4】:

问题在于 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;
    }

    ...
}

【讨论】:

  • 我试过这个,但我的 ClickableSpans 上发生了两次 onClick
【解决方案5】:

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

公共类 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);
}

}

【讨论】:

  • 遗憾的是这不起作用。但我喜欢这个主意。试图弄清楚如何做这样的事情。
  • @PatrickBoos 这在您的情况下不起作用,因为 textview 包含行的所有空间,如果行(列表项)中有多个项目而不是尝试。跨度>
【解决方案6】:

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

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

这会起作用.. :-)

【讨论】:

  • 这里的问题是,消息文本不会换行并从用户名下方开始。它将换行并从用户名的右侧开始。
  • @PatrickBoos 我想你明白了,现在你的电话怎么办?
  • 只需添加 setFocusable(false);在设置 setMovementMethod 之后,在我的 TextView 子类中让我的行再次可点击!
【解决方案7】:

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

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

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

【讨论】:

  • 因为 setOnItemClickListener 是更好的方法。 Activity 应处理单击项目时发生的情况。由于适配器可以在不同的地方使用,并且可以以不同的方式处理点击。
  • 第二个可点击的 span 会起作用。但如果有办法通过 setOnItemClickListener 使其工作,我更愿意。
  • 如果没有人想出比“第二个可点击的跨度”更好的主意,那么我会奖励你的好想法。
  • 刚刚找到一个原因,为什么第二个可点击跨度不是一个好的解决方案:(它将第二部分变成一个链接(蓝色),这不是我想要的。对不起。所以不能在这种情况下奖励你积分。
  • InternalURLSpan 的代码是什么?在那里您可以设置文本查找。
【解决方案8】:

对于所有有兴趣与@babay 在第一个答案中所说的EmojisTextView 一起做这件事的人,我做了一些这样的事情:

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

【讨论】:

    【解决方案9】:

    在布局中应用这个文本视图。 https://gist.github.com/amilcar-andrade/e4b76840da1dc92febfc

    【讨论】:

      【解决方案10】:

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

      【讨论】:

        【解决方案11】:

        嗯,@babay 的回答是对的,但他似乎忘记了什么,你应该在 xml 中写“TextViewFixTouchConsume”而不是“TextView”,然后它就会工作!例如:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-10-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-03-10
          • 2012-06-25
          • 2016-11-03
          相关资源
          最近更新 更多