之前在『Android 使 TextView 支持长按选择』一文中介绍了使用 android:textIsSelectable 属性就能简单实现 TextView 可选择的小技巧,文末也提到实际使用的时候往往会有一些坑。

比如,当你为 TextView 设置了 android:textIsSelectable 属性的同时还设置了点击事件,那么你会发现,当首次点击 TextView 时,TextView 是不会响应的:

首次点击无响应

不难猜到,TextView 在设置了 android:textIsSelectable 属性后,对原有的 View 触摸事件监听有入侵。

我们可以继承 TextView,重写 onTouchEvent() 方法,对触摸事件重新做处理:

public class SelectableTextView extends AppCompatTextView {

    private long mActionDownTime = 0L;

    public SelectableTextView(@NonNull Context context) {
        super(context);
    }

    public SelectableTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public SelectableTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean click = false;
        boolean textSelectable = isTextSelectable();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mActionDownTime = System.currentTimeMillis();
                break;
            case MotionEvent.ACTION_UP:
                long actionUpTime = System.currentTimeMillis();
                if (actionUpTime - mActionDownTime < ViewConfiguration.getLongPressTimeout()) {
                    setTextIsSelectable(false);
                    performClick();
                    click = true;
                }
                break;
        }
        setTextIsSelectable(textSelectable);
        return click || super.onTouchEvent(event);
    }

    @Override
    public boolean performClick() {
        return super.performClick();
    }
}

代码的逻辑非常简单,首先拿到 TextView 有没有设置为可选择,并声明一个点击标识,当监听到 ACTION_DOWN 事件时,记录事件产生的时间,而当监听到 ACTION_UP 事件时,计算两次事件的时间差,假如不为长按,则将当前 TextView 设置为不可选择,再执行点击事件,并将点击标识设置为 true,不要忘了将 TextView 是否可选择的属性重置回来;假如为长按,则不需要执行我们的操作,交还给父类处理即可。最后 onTouchEvent() 需要一个返回值,当执行点击时,我们接管了事件的处理逻辑,返回 true 即可,当执行长按时,交还给父类处理。

有几个地方需要解释一下。判断是否为长按是由 ViewConfigurationgetLongPressTimeout() 返回值进行比较的,默认为 400ms,由 Android 系统定义,我们也可以根据实际情况自行判定。

执行点击时我先将是否可选择的属性设置为 false,是因为如果不设置的话,执行点击事件时,TextView 会莫名选中点击区域的文字:

点击的同时文字被选中

当事件响应完之后我必须要将该属性重置回来,所以一开始就必须获取该属性的值。

最后返回值这行代码我用了简写,可能会看得有些莫名奇妙,其实只要熟悉逻辑运算符 || 就不难理解,当点击标识为 true 时,该表达式必为 true,不需要再计算后面的值,onTouchEvent() 就会直接返回 true,而当点击标识为 false 时,则会执行 super.onTouchEvent(event) 并返回其值,当然你也可以用更加易读的写法:

public class SelectableTextView extends AppCompatTextView {
    ...
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean click = false;
        ...
        if (click) {
            return true;
        } else {
            return super.onTouchEvent(event);
        }
    }
}

最后看看最终效果:

最终效果