富文本开发那些事(二):实现特殊文本的点击和长按监听

1,271 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情

富文本中某段特殊文本实现点击事件大家想也不想的会重写ClickableSpan实现,但是如何实现长点击事件呢,本篇文章介绍一种思路:通过设置TextView的setOnTouchListener()实现特殊文本段的点击和长点击事件

一. 自定义标识来标识特殊文本

标识类ClickOrLongClickSpan

class ClickOrLongClickSpan(val value: Any) {
}

标识TextView中的某段特殊文本:

val tv = TextView(this)
tv.setText("今天非常开心,因为明天就是周五后天就是周六大后天就是周日了。", TextView.BufferType.SPANNABLE)
(tv.text as? Spannable)?.let {
    it[5, 10] = ClickOrLongClickSpan("")
}

接下来的功能都需要通过setOnTouchListener{}实现了。

二.判断用户点击坐标是否落在特殊文本中

先上代码,然后一步步进行分析:

tv.setOnTouchListener { v, event ->
    //1.过滤掉非富文本的实现View
    if (v !is TextView) {
        return@setOnTouchListener false
    }

    val x = event.x
    val y = event.y

    //2.获取点击的坐标在富文本字符中位置
    val layout = v.layout
    val line = layout.getLineForVertical((y - tv.paddingTop).toInt())
    val pos = layout.getOffsetForHorizontal(line, x)

    //3. 判断点击坐标是否落在特殊文本中
    val spannable = v.text as Spannable
    val spans = spannable.getSpans<ClickOrLongClickSpan>(pos, pos + 1)
    if (spans.isEmpty().not()) {
        
    }

    return@setOnTouchListener true
}
  1. 过滤掉非富文本的实现View

    由于富文本的载体是TextViewEditView,所以非TextView实现类直接返回false,不拦截整体的TextView点击事件。

  2. 获取点击的坐标在富文本字符中位置

    这个就得通过Layout来大显身手了:

  • getLineForVertical()传入点击位置的y轴方向的坐标,可以获取当前点击了富文本的第几行。

    image.png

    PS:请注意,EditView顶部存在内边距,所以需要将当前的触摸点坐标减去顶部内边距,即y - tv.paddingTop,这样获取的结果才够准确。

  • getOffsetForHorizontal()该方法传入上面计算出的行数及触摸点的横坐标,就可以获取当前触摸点的字符在富文本中的位置。

    image.png
  1. 判断触摸点是否落在ClickOrLongClickSpan标识的特殊文本中

    关键就是调用spannable.getSpans<ClickOrLongClickSpan>()获取触摸点的ClickOrLongClickSpan数组集合,如果当前数组大小不为0,就代表当前的触摸点落在了ClickOrLongClickSpan标识的特殊文本中。

三.实现特殊文本点击事件

这里我们就参考View源码中点击实现的方式,在MotionEventUP事件中实现:

tv.setOnTouchListener { v, event ->
    //...省略上面触摸点落在特殊文本中的判断
    //点击了特殊文本
    when(event.action) {
        MotionEvent.ACTION_UP -> {
            performClick()
            //拦截触摸事件,不响应TextView本身的点击事件
            return@setOnTouchListener true
        }
    }

    return@setOnTouchListener false
}  

private fun performClick() {
}

请注意,如果实现了特殊文本的点击事件之后,setOnTouchListener{}方法就得返回为true,防止响应TextView本身的点击事件,解决点击冲突。

四.实现特殊文本长按事件

这个我们同样参考下View源码中点击事件如何实现的,直接看下ViewonTouchEvent实现源码:

MotionEvent.DOWN最终会调用checkForLongClick方法:

image.png
  1. CheckForLongPress真正实现了长点击事件的调用

    image.png

    CheckForLongPress是个Runnable对象,最终在其run()中会调用这个熟悉的方法performLongClick,最终会走到performLongClickInternal()中:

    image.png

    mOnLongClickListener就是我们设置的长点击事件,发生了调用。

  2. postDelayed()延迟执行长点击事件

    可以看到,长点击事件的包装类CheckForLongPress并不是直接被执行的,而是通过postDelayed()发送了一个延迟消息执行,这就很符合用户平常的使用习惯了:触摸屏幕一会会才能触发长按事件。

    一般这个延迟执行的时间为300400


经过上面的一番探究,我们就可以在setOnTouchListener{}实现特殊文本的长按事件:

    tv.setOnTouchListener { v, event ->
        //点击了特殊文本
        when(event.action) {            
            MotionEvent.ACTION_DOWN -> {
                //实现长按事件
                v.postDelayed(performLongClick(), 300)
                return@setOnTouchListener true
            }
        }

        return@setOnTouchListener false
    }
}

五.处理特殊文本点击和长按事件冲突

特殊文本的点击事件和长按事件肯定不能同时执行,所以需要进行处理:

//点击了特殊文本
when (event.action) {
    MotionEvent.ACTION_UP -> {
        //移除长点击事件,可能执行可能没执行
        v.removeCallbacks(mRunnable)
        //判断是否发生了长按事件,是不执行点击
        if (mIsLongClickOccur.not()) {
            performClick()
        }
        //拦截触摸事件,不响应TextView本身的点击事件
        return@setOnTouchListener true
    }

    MotionEvent.ACTION_DOWN -> {
        //重置长点击事件发生标识
        mIsLongClickOccur = false
        v.postDelayed(mRunnable, 300)
        return@setOnTouchListener true
    }
}  

//获取长点击执行Runnable
private var mRunnable: Runnable? = null
    get() {
        if (field == null) {
            field = performLongClick()
        }
        return field
    }

//是否发生了长点击事件
private var mIsLongClickOccur = false;

private fun performLongClick() = Runnable {
    //设置长点击标识为true
    mIsLongClickOccur = true
    //执行其他逻辑
}

上面代码上的注释已经很充分了,大家看下就好了。

总结

本篇文章主要讲解了通过setOnTouchListener{}实现富文本中特殊文本段的点击和长按事件的一个思路,上面的代码也是描述一个思路,以及部分简单实现,真正的业务场景还有很多其他的特殊处理。

如果觉得还行,可以点个赞支持下哈,感谢!!