Compose 手势处理,增进交互体验
- 概述
- 常用手势处理Modifier
- clickable()
- combinedClickable()
- draggable()
- swipeable()
- transformable()
- scrollable()
- nestedScroll
- NestedScrollConnection
- NestedScrollDispatcher
- 定制手势处理
- 使用 PointerInput Modifier
- PointerInputScope
- awaitPointerEventScope
概述
在处理手势时,应将手势处理修饰符尽可能放到 Modifier 末尾,从而可以避免产生不可预期的行为。
常用手势处理Modifier
clickable()
监听点击事件,在绝大多数情况下,只需要出入 onClick 回调即可。当然也可以将 enable 参数设置为一个可变状态,通过状态来动态控制启用点击监听。
@Composable
private fun GestureOfClick(){
var colorState by remember { mutableStateOf(false) }
Box(modifier = Modifier
.size(60.dp)
.background(color = if (colorState) Color.LightGray else Color.Gray)
.clickable { colorState = !colorState },
contentAlignment = Alignment.Center
){
Text(text = "点击")
}
}
combinedClickable()
和 clickable() 类似,但支持长按/双击/单击:
// Clickable.kt
fun Modifier.combinedClickable(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onLongClickLabel: String? = null,
onLongClick: (() -> Unit)? = null,
onDoubleClick: (() -> Unit)? = null,
onClick: () -> Unit
)
draggable()
只支持检测单一方向的拖动(水平方向或垂直方向),不支持同时监听两个方向上的拖动偏移,要实现这种效果,需要使用更底层的 PointerInputModifier。
fun Modifier.draggable(
state: DraggableState,//用于获取拖动手势偏移量,并且允许动态控制发生偏移行为。
orientation: Orientation,//拖动手势方向
enabled: Boolean = true,//是否启用拖动手势监听。
interactionSource: MutableInteractionSource? = null,//监听组件的拖动、按压、悬停、焦点等状态
startDragImmediately: Boolean = false,//是否立即开始拖动。
onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {},//拖动开始的回调。
onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {},//拖动结束的回调。
reverseDirection: Boolean = false//是否反转方向。
): Modifier
swipeable()
可以拖动元素,释放后,这些元素通常朝一个方向定义的两个或多个锚点继续滑动以呈现动画效果。其常见用途是实现“滑动关闭”模式。
必传的参数有:
- state:是一个SwipeableState,可以记录当前的偏移数据
- anchors:锚点,用来记录不同滑动数据对应的状态
- orientation:滑动方向
- thresholds:不同锚点之间的临界值
transformable()
多点触控,在日常生活当中,多点触控这样的操作多数是在浏览图片,网页或者地图之类的场景下被用到
transformable有三个参数:
- state:TransformableState,用来获取多点触控时候目标组件大小,位移,旋转角度变化情况的
- lockRotationOnZoomPan:Boolean,这个参数的意思是如果设置为false,那么多点触控的时候将会同时监听双指拖动,缩放以及旋转,但是如果设置为true的时候,除非旋转动作比其余两个动作先执行,这样会被监听到,不然的话,只会监听双指拖动和缩放动作,旋转事件将不会被监听
- enabled:Boolean,是否可用
@Composable
private fun TransformableSample() {
// set up all transformation states
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
var offset by remember { mutableStateOf(Offset.Zero) }
val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
scale *= zoomChange
rotation += rotationChange
offset += offsetChange
}
Box(
Modifier
// apply other transformations like rotation and zoom
// on the pizza slice emoji
.graphicsLayer(
scaleX = scale,
scaleY = scale,
rotationZ = rotation,
translationX = offset.x,
translationY = offset.y
)
// add transformable to listen to multitouch transformation events
// after offset
.transformable(state = state)
.background(Color.Blue)
.fillMaxSize()
)
}
scrollable()
虽然 verticalScroll() / horizontalScroll() 和 scrollable() 的名字很像,但它们并不是相同的东西,scrollable() 修饰符仅负责检测滚动手势,并不会帮我们自动偏移元素内容,滚动行为由开发者定义,用法类似 draggable() 修饰符
var offsetX by remember { mutableFloatStateOf(0f) }
Column {
Text(text = "OffsetX: $offsetX")
Box(
Modifier
.size(200.dp)
.background(Pink)
.scrollable(
// 检测水平方向的滚动手势
orientation = Orientation.Horizontal,
// 使用 rememberScrollableState 创建并传递一个 ScrollableState 对象。
// 通过 ScrollableState 可以获取到滚动手势的偏移量,进一步定义滚动行为。
state = rememberScrollableState { delta ->
offsetX += delta
delta // 为了支持嵌套滚动,必须返回消费的滚动距离量
}
)
)
}
nestedScroll
嵌套滑动,需要传递两个参数,connection: NestedScrollConnection 和 dispatcher: NestedScrollDispatcher,源码如下:
fun Modifier.nestedScroll(
connection: NestedScrollConnection,
dispatcher: NestedScrollDispatcher? = null
){...}
- connection:包含了嵌套滑动的和姓逻辑,通过回调可以在子布局获得滑动事件前,预先消费掉部分或全部手势偏移量,当然也可以获取子布局消费后剩下的手势偏移量。
- dispatcher:包含用于父布局的NestedScrollConnection,可以使用包含的 dispatch**系列方法动态控制组件完成滑动。
NestedScrollConnection
interface NestedScrollConnection {
fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = Offset.Zero
fun onPostScroll(consumed: Offset,available: Offset,source: NestedScrollSource): Offset = Offset.Zero
suspend fun onPreFling(available: Velocity): Velocity = Velocity.Zero
suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {return Velocity.Zero}
}
- onPreScroll:在子控件滑动之前,会先使用NestedScrollDispatcher询问父控件是否需要消费available的偏移量,父控件可以在该方法内计算自身需要消费的量,然后返回自身消费了的偏移量。
- onPostScroll:在子控件滑动之后,会使用NestedScrollDispatcher通知父控件,告知其consumed的偏移量以及剩余available的偏移量,而父控件则可以根据情况判断是否还要再偏移,以及使用和子控件同等的偏移还是剩余的偏移。完成之后返回自身消费了的偏移量
- onPreFling:在子控件进行惯性滑行之前,会先使用NestedScrollDispatcher询问父控件是否需要消费available的速度值,父控件可以在该方法内计算自身需要消费的量,然后返回自身消费了的速度值
- onPostFling:在子控件进行惯性滑行之后,会使用NestedScrollDispatcher通知父控件,告知其consumed的速度值以及剩余available的速度值,而父控件则可以根据情况判断是否还要再偏移,以及使用和子控件同等的速度还是剩余的速度。完成之后返回自身消费了的速度值。
一句话概括:在滑动前父控件可以通过onPreScroll回调先消费部分或全部偏移量;待子控件消费完后,父控件依旧可以通过onPostScroll方法进行消费,区别在于在onPreScroll回调中,父控件是优先消费的,而onPostScroll则是子控件优先消费,fling的两个方法同理。
NestedScrollDispatcher
class NestedScrollDispatcher {
// ....
// 在滑动之前调用,将可用的偏移量传递给父控件
fun dispatchPreScroll(available: Offset, source: NestedScrollSource): Offset {
return parent?.onPreScroll(available, source) ?: Offset.Zero
}
//在滑动之后调用,将已经消费的偏移量以及剩余可用的偏移量传递给父控件
fun dispatchPostScroll(consumed: Offset,available: Offset,source: NestedScrollSource): Offset {
return parent?.onPostScroll(consumed, available, source) ?: Offset.Zero
}
//在滑动之后,如果产生了惯性滑动,那么需要将相应的速度值传递给父控件
suspend fun dispatchPreFling(available: Velocity): Velocity {
return parent?.onPreFling(available) ?: Velocity.Zero
}
//在惯性滑动之后,需要将已经消费了的速度值以及剩余可用的速度值传递给父控件
suspend fun dispatchPostFling(consumed: Velocity, available: Velocity): Velocity {
return parent?.onPostFling(consumed, available) ?: Velocity.Zero
}
一句话概括:通过dispatchPreScroll方法询问父控件是否需要消费,方法返回的就是父控件消费了的量,然后就可以做自己的滑动操作了,在滑动完成之后,再通过dispatchPostScroll方法通知父控件。
定制手势处理
前面提到的手势处理修饰符都是基于低级别的 pointerInput 修饰符进行封装实现的。
使用 PointerInput Modifier
// SuspendingPointerInputFilter.kt
fun Modifier.pointerInput(
key1: Any?,
block: suspend PointerInputScope.() -> Unit
): Modifier
- keys:当 Composable 发生重组时,如果传入的 keys 发生了变化,则手势事件处理过程会被中断
- block:在这个 PointerInputScope 作用域代码块中,变可以声明手势事件的处理逻辑了,发生在协程中。
PointerInputScope
- detectTapGestures():设置更细粒度的点击监听回调
- detectDragGestures():设置更细粒度的拖动手势监听回调
- detectTransformGestures():双指拖动、缩放与旋转手势操作中更具体的手势信息
- detectDragGesturesAfterLongPress():监听长按后的拖动手势
- detectHorizontalDragGestures():监听水平拖动手势
- detectVerticalDragGestures():监听垂直拖动手势
- forEachGesture:允许用户可以对每一个手势事件序列进行相同的定制处理
awaitPointerEventScope
AwaitPointerEventScope 作用域中,可以使用 Compose 中所有低级别的手势处理挂起方法。
suspend fun <R> awaitPointerEventScope(
block: suspend AwaitPointerEventScope.() -> R
): R
API名称 | 作用 |
---|---|
awaitPointerEvent | 手势事件 |
awaitFirstDown | 第一根手指的按下事件 |
drag | 拖动事件 |
horizontalDrag | 水平拖动事件 |
verticalDrag | 垂直拖动事件 |
awaitDragOrCancellation | 单次拖动事件 |
awaitHorizontalDragOrCancellation | 单次水平拖动事件 |
awaitVerticalDragOrCancellation | 单次垂直拖动事件 |
awaitTouchSlopOrCancellation | 有效拖动事件 |
awaitHorizontalTouchSlopOrCancellation | 有效水平拖动事件 |
awaitVerticalTouchSlopOrCancellation | 有效垂直拖动事件 |
参考资料:巧用Compose来实现手势拖拽效果