android TextView加SpannableString设置点击某几个文字的问题

最近做项目使用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的提示。效果如下:
android TextView加SpannableString设置点击某几个文字的问题_第1张图片

从上面看来,貌似没有什么问题啊,但是当我点击world!后面的空白区域也会弹出点击了world的提示,why?,为了找到问题我看了SpannableStringTextViewLinkMovementMethod 的源码。最后发现是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) 在特定的条件下会计算出错导致的,这里的特定条件有两种情况:

  1. 当你的TextView的宽度设置为android:layout_width="match_parent"
    ,并且你的字符没有完全填满TextView,这样你使用SpannableString设置最后几个字符串穿的点击事件,就会导致空白区域也可以点击。
  2. 当你的TextView的宽度设置为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来自定义实现我们的文字点击效果。这种方法我没有做过测试,但是肯定是可以实现的,只是实现起来比较麻烦,如果有兴趣的同学可以自己试试!

你可能感兴趣的:(android问题)