30

我在布局中有一个 TextView。就是这么简单。我在布局中放置了一个 OnClickListener,TextView 的某些部分设置为 ClickableSpan。我希望 ClickableSpan 在单击时在 onClick 函数中执行某些操作,并且当单击 TextView 的其他部分时,它必须在布局的 OnClickListener 的 onClick 函数中执行某些操作。这是我的代码。

    RelativeLayout l = (RelativeLayout)findViewById(R.id.contentLayout);
    l.setOnClickListener(new OnClickListener(){
        @Override
        public void onClick(View v) {
            Toast.makeText(MainActivity.this, "whole layout", Toast.LENGTH_SHORT).show();
        }
    });

    TextView textView = (TextView)findViewById(R.id.t1);
    textView.setMovementMethod(LinkMovementMethod.getInstance());

    SpannableString spannableString = new SpannableString(textView.getText().toString());
    ClickableSpan span = new ClickableSpan() {
        @Override
        public void onClick(View widget) {
            Toast.makeText(MainActivity.this, "just word", Toast.LENGTH_SHORT).show();
        }
    };
    spannableString.setSpan(span, 0, 5, Spannable.SPAN_INCLUSIVE_INCLUSIVE);        
    textView.setText(spannableString);
4

5 回答 5

30

我也遇到过这个问题,感谢@KMDev 提到的源代码,我想出了一个更简洁的方法。

首先,由于我只有一个TextView可以部分点击的功能,实际上我不需要增加处理按键、滚动等能力的大部分功能LinkMovementMethod(及其超类)。ScrollingMovementMethod

相反,创建一个MovementMethod使用以下OnTouch()代码的自定义LinkMovementMethod

ClickableMovementMethod.java

package com.example.yourapplication;

import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.method.BaseMovementMethod;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.view.MotionEvent;
import android.widget.TextView;

/**
 * A movement method that traverses links in the text buffer and fires clicks. Unlike
 * {@link LinkMovementMethod}, this will not consume touch events outside {@link ClickableSpan}s.
 */
public class ClickableMovementMethod extends BaseMovementMethod {

    private static ClickableMovementMethod sInstance;

    public static ClickableMovementMethod getInstance() {
        if (sInstance == null) {
            sInstance = new ClickableMovementMethod();
        }
        return sInstance;
    }

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

    @Override
    public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {

        int action = event.getActionMasked();
        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 {
                    Selection.setSelection(buffer, buffer.getSpanStart(link[0]),
                            buffer.getSpanEnd(link[0]));
                }
                return true;
            } else {
                Selection.removeSelection(buffer);
            }
        }

        return false;
    }

    @Override
    public void initialize(TextView widget, Spannable text) {
        Selection.removeSelection(text);
    }
}

然后使用它ClickableMovementMethod,触摸事件将不再被移动方法消耗。但是,TextView.setMovementMethod()哪些调用TextView.fixFocusableAndClickableSettings()会将可点击、可长时间点击和可聚焦设置为 true,这将View.onTouchEvent()消耗触摸事件。要解决此问题,只需重置三个属性。

所以最终的实用方法,伴随ClickableMovementMethod,在这里:

public static void setTextViewLinkClickable(TextView textView) {
    textView.setMovementMethod(ClickableMovementMethod.getInstance());
    // Reset for TextView.fixFocusableAndClickableSettings(). We don't want View.onTouchEvent()
    // to consume touch events.
    textView.setClickable(false);
    textView.setLongClickable(false);
}

这对我来说就像一个魅力。

s上的点击事件ClickableSpan被触发,并且在它们之外的点击被传递给父布局监听器。

请注意,如果您要TextView选择,我还没有测试过这种情况,也许您需要自己挖掘源代码:P

于 2015-12-10T11:31:02.767 回答
16

您的问题的第一个答案是您没有在 TextView 上设置点击侦听器,正如 user2558882 指出的那样,它正在消耗点击事件。在 TextView 上设置单击侦听器后,您会看到 ClickableSpans 触摸区域之外的区域将按预期工作。但是,您随后会发现,当您单击其中一个 ClickableSpan 时,TextView 的 onClick 回调也会被触发。如果两个火都对你来说是个问题,这会给我们带来一个难题。user2558882 的回复不能保证您的 ClickableSpan 的 onClick 回调将在您的 TextView 之前触发。这是来自类似线程的一些解决方案更好地实施并从源头解释。线程应该适用于大多数设备的公认答案,但该答案的评论提到某些设备存在问题。看起来应该归咎于某些具有自定义运营商/制造商 UI 的设备,但这是猜测。

那么为什么不能保证onClick回调顺序呢?如果您查看 TextView (Android 4.3) 的源代码,您会注意到在 onTouchEvent 方法中, boolean superResult = super.onTouchEvent(event);(super is View) 在调用之前handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);调用了移动方法,然后调用了 ClickableSpan 的 onClick。看看 super 的 (View) onTouchEvent(..),你会注意到:

    // Use a Runnable and post this rather than 
    // performClick directly. This lets other visual 
    // of the view update before click actions start.
    if (mPerformClick == null) {
        mPerformClick = new PerformClick();
    }
    if (!post(mPerformClick)) { // <---- In the case that this won't post, 
        performClick();         //    it'll fallback to calling it directly
    }

performClick() 调用点击侦听器集,在本例中是我们的 TextView 的点击侦听器。这意味着,您将不知道 onClick 回调将按什么顺序触发。您所知道的是,您的 ClickableSpan 和 TextView 单击侦听器将被调用。我之前提到的线程上的解决方案有助于确保顺序,以便您可以使用标志。

如果确保与许多设备的兼容性是一个优先事项,那么最好再看看你的布局,看看你是否可以避免陷入这种情况。像这样的裙子通常有很多布局选项。

编辑评论答案:

当您的 TextView 执行 onTouchEvent 时,它会调用您的 LinkMovementMethod 的 onTouchEvent,以便它可以处理对您的各种 ClickableSpan 的 onClick 方法的调用。您的 LinkMovementMethod 在其 onTouchEvent 中执行以下操作:

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

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

            }
         }

        return super.onTouchEvent(widget, buffer, event);
    }

您会注意到它需要 MotionEvent,获取动作(ACTION_UP:抬起手指,ACTION_DOWN:按下手指),触摸起源的 x 和 y 坐标,然后找到哪个行号和偏移量(文本中的位置)触摸击中。最后,如果存在包含该点的 ClickableSpans,则会检索它们并调用它们的 onClick 方法。由于我们希望将任何触摸传递给您的父布局,您可以调用布局 onTouchEvent 如果您希望它在触摸时执行所有操作,或者如果它实现了您需要的功能,您可以调用它的点击侦听器。这是在哪里做的:

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

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

                // Your call to your layout's onTouchEvent or it's 
                //onClick listener depending on your needs

            }
         }

因此,回顾一下,您将创建一个扩展 LinkMovementMethod 的新类,覆盖它的 onTouchEvent 方法,使用您的调用复制并粘贴此源代码到我评论的正确位置,确保您将 TextView 的移动方法设置为这个新的子类和你应该被设置。

再次编辑以避免可能的副作用 查看 ScrollingMovementMethod 的源(LinkMovementMethod 的父级),您会看到它是调用静态方法的委托方法return Touch.onTouchEvent(widget, buffer, event); 这意味着您可以将其添加为方法中的最后一行并避免调用super 的(LinkMovementMethod 的)onTouchEvent 实现将复制您粘贴的内容,并且其他事件可能会按预期失败。

于 2013-09-02T15:25:24.067 回答
5

这是一个简单的解决方案,它对我有用

您可以使用 Textview 类的 getSelectionStart() 和 getSelectionEnd() 函数中的变通方法来实现此目的,

tv.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ClassroomLog.log(TAG, "Textview Click listener ");
        if (tv.getSelectionStart() == -1 && tv.getSelectionEnd() == -1) {
            //This condition will satisfy only when it is not an autolinked text
            //Fired only when you touch the part of the text that is not hyperlinked 
        }
    }
});
于 2016-02-29T10:06:44.607 回答
0

声明一个全局boolean变量:

boolean wordClicked = false;

声明并初始化lfinal

final RelativeLayout l = (RelativeLayout)findViewById(R.id.contentLayout);

添加OnClickListenertextView

textView.setOnClickListener(new OnClickListener() {

    @Override
    public void onClick(View v) {
        if (!wordClicked) {
            // Let the click be handled by `l's` OnClickListener
            l.performClick();   
        }
    }
});

更改span为:

ClickableSpan span = new ClickableSpan() {

    @Override
    public void onClick(View widget) {
        wordClicked = true;
        Toast.makeText(Trial.this, "just word", Toast.LENGTH_SHORT).show();

        // A 100 millisecond delay to let the click event propagate to `textView's` 
        // OnClickListener and to let the check `if (!wordClicked)` fail
        new Handler().postDelayed(new Runnable() {

            @Override
            public void run() {
                wordClicked = false;                        
            }
        }, 100L);
    }
};

编辑:

考虑到用户 KMDev 的答案,以下代码将符合您的规范。我们创建了两个跨度:一个具有指定长度:spannableString.setSpan(.., 0, 5, ..);另一个具有余数:spannableString.setSpan(.., 6, spannableString.legth(), ..);。第二个ClickableSpan(span2) 对RelativeLayout. 此外,通过覆盖updateDrawState(TextPaint),我们能够为第二个跨度赋予非独特(非风格)的外观。然而,第一个跨度具有链接颜色并带有下划线。

final RelativeLayout l = (RelativeLayout)findViewById(R.id.contentLayout);
    l.setOnClickListener(new OnClickListener(){
        @Override
        public void onClick(View v) {
            Toast.makeText(Trial.this, "whole layout", Toast.LENGTH_SHORT).show();
        }
    });

TextView textView = (TextView)findViewById(R.id.t1);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setHighlightColor(Color.TRANSPARENT);

SpannableString spannableString = new SpannableString(textView.getText().toString());

ClickableSpan span = new ClickableSpan() {
    @Override
    public void onClick(View widget) {
        Toast.makeText(Trial.this, "just word", Toast.LENGTH_SHORT).show();
    }
};

spannableString.setSpan(span, 0, 5, Spannable.SPAN_INCLUSIVE_INCLUSIVE);     

ClickableSpan span2 = new ClickableSpan() {
    @Override
    public void onClick(View widget) {
        l.performClick();
    }

    @Override
    public void updateDrawState(TextPaint tp) {
        tp.bgColor = getResources().getColor(android.R.color.transparent);
        tp.setUnderlineText(false);
    }
};

spannableString.setSpan(span2, 6, spannableString.length(), 
                                      Spannable.SPAN_INCLUSIVE_INCLUSIVE);  

textView.setText(spannableString);

特别感谢用户 KMDev 注意到我原始答案的问题。无需使用布尔变量执行(错误)检查,OnclickListener也不需要为 TextView 设置一个。

于 2013-09-02T12:58:49.757 回答
0

实现 ClickableSpan 的最简单和最快的方法是:

new SmartClickableSpan
   .Builder(this)
   .regularText("I agree to all ")
   .clickableText(new ClickableOptions().setText("Terms of Use").setOnClick(new 
       ClickableSpan() {
           @Override
           public void onClick(@NonNull View view) {
               // Your Code..
           }
   }))
   .into(myTextView);

在 Android 中添加常规 Clickable Span 需要计算每个可点击文本的大小,当在 TextView 中添加大量可点击和常规单词或句子时会变得一团糟。通过使用 SmartClickableSpan,您将能够添加任何内容可点击的单词或句子的数量,无需担心每次更新时计算每个文本的长度。

SmartClickableSpan Github:

https://github.com/HseinNd98/SmartClickableSpan
于 2020-09-20T21:25:23.373 回答