android ClickableSpan截获click事件

我有一个布局的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); 

Related of "android ClickableSpan截获click事件"

你的问题的第一个答案是,你没有设置一个点击监听器在你的TextView消费点击事件,因为user2558882指出。 在TextView上设置点击监听器后,您会看到ClickableSpans触摸区域外的区域将按预期工作。 然而,你会发现,当你点击一个ClickableSpans时,TextView的onClickcallback也会被触发。 如果同时发生火灾,对我们来说就是一个难题。 user2558882的答复不能保证你的ClickableSpan的onClickcallback会在你的TextView之前被触发。 以下是一些类似的线索的解决scheme,可以更好地实施,并从源头上进行解释。 线程应该可以在大多数设备上使用的接受答案,但是针对该答案的评论提到了某些有问题的设备。 它看起来像一些设备与定制载体/制造商用户界面是责备,但这是猜测。

那么为什么你不能保证onClickcallback命令? 如果你看看TextView的源代码(Android 4.3),你会注意到在onTouchEvent方法中, boolean superResult = super.onTouchEvent(event); (super是View)在handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);之前调用handled |= mMovement.onTouchEvent(this, (Spannable) mText, event); 这是调用您的移动方法,然后调用您的ClickableSpan的onClick。 看看super的(查看)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的单击监听器。 这意味着,你不会知道你的onClickcallback会以什么顺序触发。 你知道的是,你的ClickableSpan和TextView点击监听器将被调用。 我之前提到的线程解决scheme有助于确保顺序,以便您可以使用标志。

如果要确保与许多设备的兼容性是重中之重,那么最好通过再次查看布局来了解是否可以避免陷入这种情况。 通常有很多布局选项来避免这种情况。

编辑评论回答:

当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坐标,然后find哪个行号和偏移量(文本中的位置)触摸命中。 最后,如果有包含该点的ClickableSpans,则会检索它们,并调用onClick方法。 既然我们想把任何接触传递给你的父级布局,你可以调用你的布局onTouchEvent,如果你想让它接触到它所做的一切事情,或者如果实现了你需要的function,你可以把它叫做click监听器。 这里是做什么的:

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

所以要回顾一下,你将创build一个新的类来扩展LinkMovementMethod,覆盖它的onTouchEvent方法,将这个源代码复制并粘贴到正确的位置,并确保你将TextView的移动方法设置为这个新的子类,你应该设置。

再次编辑避免可能的副作用看看ScrollingMovementMethod的源代码(LinkMovementMethod的父代),你会发现它是一个调用静态方法的委托方法, return Touch.onTouchEvent(widget, buffer, event); 这意味着您可以将其添加为方法中的最后一行,并避免调用super(LinkMovementMethod's)onTouchEvent实现,该实现将重复您正在粘贴的内容,而其他事件可能会按预期进行。

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

首先,由于我只有一个TextView可以被部分点击,实际上我并不需要LinkMovementMethod (和它的超类ScrollingMovementMethod )的function,它增加了处理按键,滚动等的function。

相反,创build一个使用OnTouch()代码的自定义MovementMethod

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 ,touch事件不再被移动方法所消耗。 但是,调用TextView.fixFocusableAndClickableSettings()会将clickable,long-clickable和View.onTouchEvent()设置为true,这将使View.onTouchEvent()消耗touch事件。 要解决这个问题,只需重置三个属性。

所以最后的实用方法,伴随着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); } 

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

ClickableSpan的Click事件被触发,并且在它们外面的点击被传递给父布局侦听器。

请注意,如果你的TextView可选的,我还没有testing过这种情况,也许你需要自己挖掘源代码:P

这是一个简单的解决scheme,它为我工作

您可以使用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 } } }); 

声明一个全局booleanvariables:

 boolean wordClicked = false; 

声明并初始化l作为final

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

将一个OnClickListener添加到OnClickListener

 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的回答,下面的代码将符合你的规范。 我们创build两个跨度:一个用于指定的长度: 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注意到我的原始答案的问题。 不需要使用布尔variables执行(错误)检查,并且OnclickListener为TextView设置OnclickListener