最近有点闲工夫,介绍一下手势的捕获和模拟输入的实现过程。当时是为了写一个自动化的任务应用,想利用Android无障碍手势输入来实现自动化,于是就有了这个组件。
效果图
实现原理
其实Android自带库已经为我们提供了类似的功能可以看这个GestureOverlayView,捕获的方式实际上与该控件是类似的。
创建
- 首先创建一个自定义的View继承自FrameLayout
class GestureCatchView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr){
private val pathColor: Int = Color.BLACK
private val pathWidth: Int = 20
private val fadeDelay: Long = 0
private var timestemp: Long = 0 //开始记录手势的时间点(用来得到开始时间到手指触摸的时间)
private var curGestureItem: GestureItem? = null //当前的手势
//收集到的手势信息
private var collecting = false //开始收集手势的标识符,true:将收集到的手势添加到集合中去。
private val gestureInfoList: ArrayList<GestureInfo> = ArrayList()
//需要显示的手势集合
private val gestureItemList: ArrayList<GestureItem> = ArrayList()}
- 在初始化器上设置各项自定义的参数,需要注意的是想要在ViewGroup执行draw,必须设置
setWillNotDraw(false)
init {
setWillNotDraw(false)
//属性设置省略
}
- 定义每个手势的信息类,用来独立执行绘制和消失渐变动画
inner class GestureItem {
private val path = Path()
private val paint = Paint().apply {
strokeWidth = 20f
color = pathColor
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND //拐角圆形
strokeCap = Paint.Cap.ROUND //开始和结尾加入圆形
isAntiAlias = true
}
//渐变动画器
private val animator = ValueAnimator.ofFloat(1f, 0f).apply {
interpolator = AccelerateInterpolator() //实现先慢后快的效果
duration = 1500
addUpdateListener {
paint.alpha = (pathColor.alpha * it.animatedValue as Float).toInt()
paint.strokeWidth = pathWidth * it.animatedValue as Float
invalidate()
}
}
private var lastX = 0f
private var lastY = 0f
private val points = arrayListOf<GesturePoint>() //记录手势的每个点
private val delayTime: Long = //开始到第一个点被创建的延时时间
if (!collecting || timestemp == 0L) 0 else System.currentTimeMillis() - timestemp
}
手势的捕获
在onTouchEvent获取MotionEvent并且将获取到的点绘制到视图中去。
override fun onTouchEvent(event: MotionEvent): Boolean {
super.onTouchEvent(event)
processEvent(event)
return true
}
将拿到的MotionEvent进行处理
private var curGestureItem: GestureItem? = null //当前的手势
private fun processEvent(event: MotionEvent) {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
//当按下后被认为是一个手势的开始,创建一个手势Item
val newGestureItem = GestureItem()
//添加到需要被绘制的手势集合中
gestureItemList.add(newGestureItem)
//开始画第一个点
newGestureItem.startPath(event)
curGestureItem = newGestureItem
invalidate()
}
MotionEvent.ACTION_MOVE -> {
val item = curGestureItem
//为当前手势每次移动手指捕捉到的点添加到Item中去
item?.run {
addToPath(event)
invalidate()
}
}
MotionEvent.ACTION_UP -> {
//当手指抬起后被认为是手势的结束,当然还有其他的情况暂时不作考虑
curGestureItem?.run {
endPath(event)
invalidate()
}
}
}
}
手势的绘制
inner class GestureItem{
//.....省略
fun startPath(event: MotionEvent) {
val x = event.x
val y = event.y
path.moveTo(x, y)
path.lineTo(x, y)
lastX = x
lastY = y
//添加到集合中
points.add(GesturePoint(x, y, event.eventTime))
}
}
每次移动手指将获取的点添加到需要集合点中去
fun addToPath(event: MotionEvent) {
val x = event.x
val y = event.y
//取上一个点为控制点
val controlX = lastX
val controlY = lastY
//上一个点与当前点的中点为结束点
val endX = (controlX + x) / 2
val endY = (controlY + y) / 2
//使用二次贝塞尔曲线构成光滑曲线
path.quadTo(lastX, lastY, endX, endY)
lastX = x
lastY = y
//添加到集合中
points.add(GesturePoint(x, y, event.eventTime))
}
完成一个手势
fun endPath(event: MotionEvent) {
points.add(GesturePoint(x, y, event.eventTime))
//开始消失动画
animator.startDelay = fadeDelay
animator.start()
val curTime = System.currentTimeMillis()
//创建一个GestureInfo,
//它包含了这个Gesture,颜色延迟时间、总的手势时间,收集到的点。
val gestureInfo = GestureInfo(
gesture = Gesture().apply {
addStroke(GestureStroke(ArrayList(points)))
},
pathColor = pathColor,
delayTime = delayTime,
duration = curTime - timestemp - delayTime,
points = ArrayList(points)
)
if (collecting)
gestureInfoList.add(gestureInfo)
timestemp = curTime
points.clear()
}
最后是把Path画到Canvas中去
override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas)
for (item in gestureItemList) {
item.draw(canvas)
}
}
最后可以看到这样子的效果,
手势的导入和模拟
最后讲一下如何将收集到的手势按照输入时那样模拟出来。
我们可以知道每个MotionEvent都有一个eventTime,它来自于SystemClock.uptimeMillis为参照时间。
然后转换成GesturePoint(event.x, event.y, event.eventTime),我们可以将这些收集到的点转换成MotionEvent,再执行到processEvent中,就可以实现模拟输入的效果了
fun loadGestureInfoWithAnim(list: ArrayList<GestureInfo>) {
list.forEach { fakeMotionEvent.addAll(it.points) }
fakeMotionEventIndex = 0
post(fakeMotionEventRunnable)
}
每个点都记录着执行的间隔,把这些点依次再执行一次
private val fakeMotionEvent = arrayListOf<GesturePoint>()
private var fakeMotionEventIndex = -1
private val fakeMotionEventRunnable = object : Runnable {
override fun run() {
if (fakeMotionEventIndex >= 0) {
val point = fakeMotionEvent[fakeMotionEventIndex]
val action = when (fakeMotionEventIndex) {
//第一个点是按下动作
0 -> MotionEvent.ACTION_DOWN
in 1 until fakeMotionEvent.size -> MotionEvent.ACTION_MOVE
//最后的点是抬起动作
else -> MotionEvent.ACTION_UP
}
val event = MotionEvent.obtain(point.timestamp, point.timestamp, action, x, y, 0)
//模拟执行MotionEvent
processEvent(event)
fakeMotionEventIndex++
if (fakeMotionEventIndex == fakeMotionEvent.size) {
fakeMotionEventIndex = -1
return
}
val nextPoint = fakeMotionEvent[fakeMotionEventIndex]
val delayTime = nextPoint.timestamp - event.eventTime
//延迟执行下一个点
postDelayed(this, delayTime)
}
}
}
补充
可能会问到为什么不直接保存MotionEvent呢?这是因为我们收到的每一个MotionEvent实际上大多是情况下是同一个对象,假如是试着将它保存起来,那么最后你将会得到一系列MotionEvent.action = ACTION_UP的集合,也就是最后抬手拿到的那个事件。
可以看下MotionEvent的源码:
它的构造函数是私有的
private MotionEvent() {
}
只能通过静态方法obtain来获取MotionEvent
static public MotionEvent obtain(long downTime, long eventTime, int action,
float x, float y, int metaState) {
return obtain(downTime, eventTime, action, x, y, 1.0f, 1.0f,
metaState, 1.0f, 1.0f, 0, 0);
}
最后会执行到这个方法,可以看到我们拿到的MotionEvent大多数都是来自于gRecyclerTop,而不是被new出来的
static private MotionEvent obtain() {
final MotionEvent ev;
synchronized (gRecyclerLock) {
ev = gRecyclerTop;
if (ev == null) {
return new MotionEvent();
}
gRecyclerTop = ev.mNext;
gRecyclerUsed -= 1;
}
ev.mNext = null;
ev.prepareForReuse();
return ev;
}
当一个MotionEvent被消费掉时,最后会执行recycle,然后又会被赋值成gRecyclerTop
public final void recycle() {
super.recycle();
synchronized (gRecyclerLock) {
if (gRecyclerUsed < MAX_RECYCLED) {
gRecyclerUsed++;
mNext = gRecyclerTop;
gRecyclerTop = this;
}
}
}
最后附上项目的地址啊
朋友们如果觉得不错可以给个小星星支持一下呗。 有啥建议或者问题都欢迎一起讨论。