最近做项目使用TextView加SpannableString实现某几个字的点击事件,发现当要点击的文字是从某个位置开始到最后,这个时候点击最后文字后面的空白区域也会触发点击事件,但是我那儿没有设置点击事件啊,这是怎么回事呢?在网上找了很久也没有找到很好的解答,难道就只有我发现了这个问题?通过对源码的研究终于发现了问题的所在,下面看看怎么会出现这个问题和怎么解决吧。
我的测试代码如下:
MainActivity
package com.android.you.textviewspannablestringtest;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView helloWorldText = findViewById(R.id.hello_world_text);
SpannableString spannableString = new SpannableString(getString(R.string.hello_world));
spannableString.setSpan(new ClickableSpan() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "点击了world", Toast.LENGTH_LONG).show();
}
}, 6, spannableString.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
helloWorldText.setText(spannableString);
helloWorldText.setMovementMethod(LinkMovementMethod.getInstance());
}
}
activity_main.xml
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/hello_world_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
android.support.constraint.ConstraintLayout>
string.xml
<resources>
<string name="hello_world">Hello World!string>
resources>
上面的代码很简单,这里就不赘述SpannableString的用法了,主要说明出现的问题和解决方法,从上面的代码可以看出,我没有做其他的任何处理,只是设置了点击事件,给TextView里面的World!设置了点击事件,当点击World!的时候会弹出点击了world的提示。效果如下:
从上面看来,貌似没有什么问题啊,但是当我点击world!后面的空白区域也会弹出点击了world的提示,why?,为了找到问题我看了SpannableString
、TextView
和 LinkMovementMethod
的源码。最后发现是LinkMovementMethod
里面的问题,下面我们一起来看看LinkMovementMethod
实现的源码。
其他的不变,将上述代码不设置helloWorldText.setMovementMethod(LinkMovementMethod.getInstance());
会发现world字符的点击事件没有任何反应,所以可以知道实现字符点击效果的是LinkMovementMethod类。具体源码如下:
@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[] links = buffer.getSpans(off, off, ClickableSpan.class);
if (links.length != 0) {
if (action == MotionEvent.ACTION_UP) {
links[0].onClick(widget);
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(links[0]),
buffer.getSpanEnd(links[0]));
}
return true;
} else {
Selection.removeSelection(buffer);
}
}
return super.onTouchEvent(widget, buffer, event);
}
从上面可以看出点击事件是通过onTouchEvent方法实现的,那为什么会出现上面所说的问题呢?主要是layout.getOffsetForHorizontal(line, x)
和buffer.getSpans(off, off, ClickableSpan.class)
在特定的条件下会计算出错导致的,这里的特定条件有两种情况:
android:layout_width="match_parent"
android:layout_width="wrap_content"
,但是存在换行时,也会出现上述问题。总之,当你的TextView没有完全填满时,layout.getOffsetForHorizontal(line, x)
和buffer.getSpans(off, off, ClickableSpan.class)
会把后面的空白区域也算作是文字的点击区域,从而导致了上述的情况的出现。
这也是最简单的一种方法,在字符串最后添加一个空格,如下:
<resources>
<string name="hello_world">Hello World! string>
resources>
然后,将上面的代码的spannableString.length()
改为spannableString.length()-1
,同样的实现了点击world会弹出提示,但是这样就避免了点击后面的空白区域也会触发点击事件。这种方法很简单推荐使用。
如果你只有一行文字并且确定不会换行,你可以将TextView的宽度改为android:layout_width="wrap_content"
也可以避免出现上述问题,但是这种方法的局限性很大,不推荐使用。
从上面LinkMovementMethod类可以看出,它是通过onTouchEvent实现的文字点击。同样的我们也可以通过设置TextView的setOnTouchListener来自定义实现我们的文字点击效果。这种方法我没有做过测试,但是肯定是可以实现的,只是实现起来比较麻烦,如果有兴趣的同学可以自己试试!