通过前面的一篇文章我们学会了如何用各种高级别抽象的扩展函数来进行手势处理。像clickable,draggable,scrollable和anchoredDraggable都是类似于View系统中的各种回调(如onClick或者onScroll)是高级别的抽象,这里我们只能处理手势,大部分情况下这也够用了。
但是,对于一些复杂的交互 场景需要我们先识别手势,然后再处理手势,这时就不能再用封装好的扩展函数了,而必须要用到像View系统中的onTouchEvent那样的低级别的回调,直接拿到触点事件,然后再识别手势,最后再处理手势。这篇文章就学习一下如何使用Jetpack Compose中的低级别函数来识别和处理手势。
处理原始触点事件
除了使用一些封装好了的扩展函数来直接处理某个手势以外,还可以直接接收原始的事件输入。
Jetpack Compose在Modifier中提供了扩展函数pointerInput.pointerInput(kotlin.Any,kotlin.Any,kotlin.coroutines.SuspendFunction1))来接收原始的触点事件,与View系统中的onTouch是类似的,这是触点事件的低级别的API。通过此API能获得触点事件,之后可以进行手势识别和手势处理,因为拿到的是原始的触点事件,所以很多逻辑要自己写,有些麻烦,但因为完全可控,所以可以实现一些更为复杂的手势识别和手势处理,比如任意方向的拖拽和滑动,长按后的拖拽,多点触控等等。
接收触点事件
扩展函数pointerInput.pointerInput(kotlin.Any,kotlin.Any,kotlin.coroutines.SuspendFunction1))接收三个参数,前两个都是作为事件处理回调的标识(keys),第三个参数是事件处理回调,是一个尾部lambda,当某个key发生变化,这个lambda会重新执行,否则即使发生重组(Recomposition),这个lambda也会不会重新执行。
1
|
|
第三参数就是我们要提供的事件处理回调,它是运行在PointerInputScope上下文中的lambda,这个上下文作用域里面有很多扩展函数可以直接使用。仔细看这个lambda是suspend的,这是因为输入事件可能不是即时的,可能会有等待的情况,也就是说lambda是在有事件的时候才会执行。PointerInputScope中的函数也都定义为suspend的,这些函数在lambda中可以直接调用,所以lambda本身也必须 是suspend的。
比如说想要打印触点事件,就可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
filter是一个事件的类型参数当作key,也当作过滤条件。在事件处理回调lambda中,用awaitPointerEvent)获得每一个触点事件,然后打印出来,awaitPoniterEventScope函数是创建一个协程上下文作用域用以等待事件输入,在其内调用awaitPointerEvent来获得事件。
识别手势
虽然pointerInput是一个低级别的接口,但也并不意味着所有的逻辑都必须从头写,在PointerInputScope中已经定义了大量的函数可以识别大部分手势:
- 非连续性手势如单击(tap),双击(double tap)和长按(long press):detectTapGestures.detectTapGestures(kotlin.Function1,kotlin.Function1,kotlin.coroutines.SuspendFunction2,kotlin.Function1))
- 拖拽(drag):detectDragGestures,detectDragGesturesAfterLongPress,以及固定方向拖拽detectHorizontalDragGestures和detectVerticalDragGestures
- 多点触控变幻的手势识别(旋转,平移和缩放):detectTransformGestures.detectTransformGestures(kotlin.Boolean,kotlin.Function4))
比如像前面文章中提到的拖拽也可以用pointerInput实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
前面提到了像detectTapGestures和detectDragGestures都是suspend的函数,所以在一个pointerInter的lambda中只能有一个,比如像下面这样写detectDragGestures不会得到执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
一个可行的解决办法就是可以写多个pointerInput,每个ponterInput处理一种手势:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
直接处理事件
遇到交互比较复杂的场景,或者当PointerInputScope中提供的识别函数不能解决问题时,或者需要把几种不同的手势组合在一起时,就需要直接处理事件。像View系统中的onTouch一样,我们需要知道不同的事件类型,比如pointer down,pointer move和pointer up等。PointerInputScope中提供了一个函数awaitEachGesture可以取代while (true)来获得每个事件;awaitFirstDown,是手势的开始相当于ACTION_DOWN;waitForUpOrCancellation是事件结束,相当于ACTION_UP和ACTION_CANCEL;drag相当于ACTION_MOVE。我们来看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
需要注意的是这些函数都是suspend的,也就是说当预期的行为发生时才会带着结果返回,比如awaitFirstDown()当有第一个触点事件发生时结束suspend然后返回;awaitTouchSlopOrCancellation当有超过拖拽阈值时结束suspend然后返回;awaitDragOrCancellation当有拖拽发生时结束suspend返回。
触点事件的派发流程
为了更好的处理事件,需要了解一下Jetpack Compose的事件派发流程,与View系统是类似的,事件派发的过程也是沿着Composable的树形结构,从父Composable到子Composable,同一层级的顺序则是从上到下,从前到后(Z轴方向),依次做『Hit test』,直到事件被消费,就停止派发。
事件的消费过程则是反过来,子Composable如果未消费就返回给父Composable,前面的Composable未消费,就继续向下传递,直到事件被消费。
如果是自己在pointerInput中直接处理事件,就要特别注意手动的把事件给消费掉,否则可能会继续传递。像awaitPointerEvent,awaitFirstDown,awaitDragOrCancellation等返回的都是PointerInputChange对象,调用它的consume())方法即可把事件消费掉。再比如像上面的例子awaitTouchSlopOrCancellation中,也需要手动的把事件给消费掉,如果把ch.consume()这句删除,就会发现awaitDragOrCancellation不会得到执行,这是因为awaitTouchSlopOrCancellation这个方法还在执行中,调用ch.consume()把事件消费掉,这个函数才会返回。
这也说明了,还是要尽量用系统封装好的手势识别和手势处理函数,不到万不得已不要直接处理原始事件,因为逻辑写起来肯定相当复杂。
多点触控
多点触控是超过一个触点同时在屏幕上操作,最为常见的手势就是旋转和缩放,可以使用扩展函数transformable.transformable(androidx.compose.foundation.gestures.TransformableState,kotlin.Boolean,kotlin.Boolean))监听旋转,平移和缩放手势,其中平移单个触点也能触发,缩放和旋转则需要两个触点,当超过3个触点时这个函数不会回调,也即不会触发任何手势:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
除了直接使用Modifier.transformable以外,还可以用前面提到过的pointerInput中的detectTransformGestures.detectTransformGestures(kotlin.Boolean,kotlin.Function4))这个函数也能得到平移,旋转和缩放的变化数值,把这些数值应用到graphicsLayer去做变幻就可以了,使用detectTransformGestures的另一个好处是可以与其他的手势结合起来。
总结
通过本文我们学习如何得到原始的触点事件,以及如何识别手势,相信对触点事件以及手势识别有了更深入的理解,并且借助这些扩展函数就可以写出交互性更好的应用程序界面。即使遇到一些复杂的交互 场景,或者需要组合多种手势时,也都能从容应对。