网站怎么接广告,取消wordpress 注册邮箱验证码,网站怎么做到秒收录,微信怎么开通公众号1、自定义触摸与一维滑动监测
之前我们在讲 Modifier 时讲过如下与手势检测相关的 Modifier#xff1a;
Modifier.clickable { }
Modifier.combinedClickable { }
Modifier.pointerInput {detectTapGestures { }
}这里对以上内容就不再赘述了#xff0c;直接去讲解更复杂的…1、自定义触摸与一维滑动监测
之前我们在讲 Modifier 时讲过如下与手势检测相关的 Modifier
Modifier.clickable { }
Modifier.combinedClickable { }
Modifier.pointerInput {detectTapGestures { }
}这里对以上内容就不再赘述了直接去讲解更复杂的 Modifier 实现更复杂的触摸反馈效果。
在传统的 View 体系中在自定义触摸反馈的内容时对于 View 我们通常都是重写它的 onTouchEvent()对于 ViewGroup 可能还需要重写 onInterceptTouchEvent()极少数时候会更深入地去重写 dispatchTouchEvent()。当然原生也提供了较为上层的 API 来简化手势检测比如 GestureDetectorCompat 与 ScaleGestureDetectorCompat。
而在 Compose 中情况也是类似的。在 pointerInput() 内调用 awaitEachGesture()在其内部通过 awaitPointerEvent() 可以获得触摸事件
Modifier.pointerInput(Unit) {awaitEachGesture {// 循环调用 awaitPointerEvent() 可获得每一个触摸事件val event awaitPointerEvent()}
}这种用法偏底层Compose 在上层提供了一些类似于 GestureDetectorCompat 的非常完备的 API比如上面提到的 clickable() 与 combinedClickable() 就是点击相关的 API下面我们逐步介绍滑动手势相关的 API。
滑动手势有两个常用的 API scrollable() 与 draggable()后者是前者的底层支撑。
1.1 draggable()
先看 draggable()
/**
* 为单个方向的 UI 元素配置触摸拖动。将拖动距离报告给 DraggableState允许用户根据拖动增量做出反应
* 并更新它们的状态。这个组件的常见用例是当您需要能够在屏幕上的组件内拖动某物并通过一个浮点值表示该
* 状态时。如果您需要控制整个拖动流程请考虑使用 pointerInput配合像 detectDragGestures 这样的
* 辅助函数。如果您正在实现滚动/快速滑动行为请考虑使用 scrollable。
* 参数
* state - DraggableState 可拖动对象的状态。定义了用户端逻辑如何解释拖动事件
* orientation - 拖动的方向
* enabled - 是否启用拖动
* interactionSource - MutableInteractionSource用于在拖动时发出 DragInteraction.Start
* startDragImmediately - 当设置为 true 时可拖动对象将立即开始拖动并阻止其他手势检测器对
* “按下”事件做出反应以阻止组合的基于按压的手势。这旨在允许最终用户通过按压在动画小部件上“捕捉”它。
* 当您拖动的值正在稳定/动画化时设置此选项非常有用
* onDragStarted - 当拖动即将在起始位置开始时将调用的回调允许用户暂停并准备拖动如果需要的话。
* 此挂起函数与可拖动范围一起调用允许进行异步处理如果需要的话
* onDragStopped - 当拖动完成时将调用的回调允许用户根据速度做出反应并处理。此挂起函数与可拖动范围
* 一起调用允许进行异步处理如果需要的话
* reverseDirection - 反转滚动的方向因此从顶部到底部的滚动将表现得像从底部到顶部从左到右的滚动将
* 表现得像从右到左
*/
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
): Modifierdraggable() 有两个必填的参数 state 和 orientation。在 Compose 中所有可操作的组件或 Modifier 都会接收一个 state 参数用于手动操作界面。因为 Compose 是一个严格的声明式 UI 框架开发者是拿不到那些实际的 UI 对象的更无法直接操作它们。但操作不了 UI 对象本身不意味着也操作不了界面。我们可以通过操作 UI 对象依赖的状态对象来实现 UI 的改变。比如说对于 LazyColumn 而言
Composable
fun LazyColumn(modifier: Modifier Modifier,state: LazyListState rememberLazyListState(),contentPadding: PaddingValues PaddingValues(0.dp),reverseLayout: Boolean false,verticalArrangement: Arrangement.Vertical if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,horizontalAlignment: Alignment.Horizontal Alignment.Start,flingBehavior: FlingBehavior ScrollableDefaults.flingBehavior(),userScrollEnabled: Boolean true,content: LazyListScope.() - Unit
)我们可以通过修改它的 state 参数改变 UI 界面。比如
Composable
fun LazyColumnSample() {val listState rememberLazyListState()// animateScrollToItem() 是挂起函数需要协程。scrollToItem() 是瞬间跳到指定 Itemval scope rememberCoroutineScope()Column {LazyColumn(Modifier.weight(1f), listState) {items(List(50) { it 1 }) {Text(Number $it, Modifier.padding(5.dp))}}Button(onClick { scope.launch { listState.animateScrollToItem(20) } },Modifier.height(40.dp)) {Text(修改 LazyColumn 状态)}}
}点击按钮时操作 state 以动画方式让列表滚动到第 21 个列表项 因此修改组件依赖的 state 就是外界控制 UI 变化的一种手段。对于 draggable() 来说它依赖的 state 类型为 DraggableState我们可以通过 rememberDraggableState() 来提供 DraggableState 对象
Composable
fun rememberDraggableState(onDelta: (Float) - Unit): DraggableState {val onDeltaState rememberUpdatedState(onDelta)return remember { DraggableState { onDeltaState.value.invoke(it) } }
}该函数的参数 onDelta 是一个回调函数Float 参数就是这一次拖动在指定方向上产生的位移量指定方向可以是水平或垂直方向在 draggable() 的第二个参数上指定
Box(Modifier.size(50.dp).background(Color.Red).draggable(rememberDraggableState {println(本次拖动距离为$it)}, Orientation.Horizontal)
)向右滑动时输出正数向左滑动时输出负数。
enabled 控制 draggable() 这个 Modifier 是否生效是一个条件性临时的开关符合某些条件时就生效否则就失效。
interactionSource 是交互源对 draggable() 修饰的范围进行触摸相关的状态监控的比如说
setContent {// 创建一个 InteractionSource 对象val interactionSource remember { MutableInteractionSource() }// 监听 InteractionSource 所在的组件的拖拽状态val isDragged by interactionSource.collectIsDraggedAsState()Column {Box(Modifier.size(50.dp).background(Color.Red).draggable(rememberDraggableState {println(本次拖动距离为$it)},Orientation.Horizontal,interactionSource interactionSource))// 根据 Box 的拖拽状态显示不同的文字Text(if (isDragged) 拖动中 else 静止)}
}InteractionSource 可以监听所在组件的交互状态有四个函数可用 分别监听组件的拖拽、聚焦、悬空、按压状态。我们举的例子是监听了组件的拖拽状态效果如下 startDragImmediately 指是否在用户手指按下后立即开始拖动流程如设置为 false 则会在用户手指拖动一小段距离后再开始拖动流程。传统的 ViewGroup 也有这个选项比如用户点击 RecyclerView 中的列表项时可能会有一个很微小的拖动如果 startDragImmediately 设置为 true那么这个微小的拖动会导致列表产生相应的微小位移。但如果为 false则 RecyclerView 会不认为这个点击时产生的微小位移是拖动行为进而不去滑动列表。设置为 false 用户体验会好一些。
onDragStarted 与 onDragStopped 是两个挂起回调函数用于响应在开始拖拽与结束拖拽时的额外需求比如开始拖动时震动一下。
reverseDirection 将手势反向。
写一个简单例子在一个方向上拖动文字。因为拿不到 Text 组件本身因此要通过修改它的位移实现
Composable
fun DraggableText() {var offsetX by remember { mutableStateOf(0f) }Box(Modifier.fillMaxSize()) {Text(Compose,Modifier.offset { IntOffset(offsetX.roundToInt(), 0) }.draggable(rememberDraggableState { offsetX it }, Orientation.Horizontal))}
}1.2 scrollable()
scrollable() 只是一个滑动的监测工具它不具备让组件具有滑动功能的效果。而 verticalScroll() 与 horizontalScroll() 才能让一个组件切实地具备滑动功能就像在传统 View 体系下为一个不具备滑动功能的组件在外面套上了一个 ScrollView。但 verticalScroll() 与 horizontalScroll() 底层是通过 scrollable() 进行滑动监测的。
前面说过 draggable() 是 scrollable() 的底层支撑scrollable() 在 draggable() 的基础上又增加了三个比较重要的功能
惯性滑动嵌套滑动滑动触边效果 overScroll
增加的三个功能是针对滑动布局场景下增加的功能比如对于 ScrollView、RecyclerView 这种布局组件而言在滑动时具备惯性滑动、嵌套滑动在滑动到边缘时展示触边效果才有用。但手指滑动的监测未必都是用于滑动布局比如进度条一般是用不上新增的三个效果的所以对于这类组件只需要 draggable() 提供的基础功能即可。
scrollable() 的用法与 draggable() 相似区别就在于 scrollable() 新增的三个功能都作为参数需要配置
ExperimentalFoundationApi
fun Modifier.scrollable(state: ScrollableState, // 滚动状态包含嵌套滑动orientation: Orientation,overscrollEffect: OverscrollEffect?, // 滚动触边效果enabled: Boolean true,reverseDirection: Boolean false,flingBehavior: FlingBehavior? null, // 惯性滑动interactionSource: MutableInteractionSource? null
)第一个参数 state 的类型是 ScrollableStatescrollable() 支持的嵌套滑动就是通过 ScrollableState 实现的。在指定这个参数时可以通过 rememberScrollableState() 创建一个 ScrollableState 对象但是在该函数的 lambda 表达式中必须返回一个 Float 值表名自己消耗了多少滚动距离
Modifier.scrollable(rememberScrollableState {println(滚动了 $it 个像素)it // 必须把消耗了多少滚动距离返回因为要实现嵌套滑动功能},orientation Orientation.Horizontal,overscrollEffect ScrollableDefaults.overscrollEffect(),
)overscrollEffect 参数用来指定滑动到边缘时的效果该参数可以为空为空时没有效果。可以通过 ScrollableDefaults 提供的 overscrollEffect() 指定一个默认效果。
此外还有 flingBehavior 参数用于指定惯性滑动它可以为空并且默认值就给了 null。但是 scrollable() 的底层实现 pointerScrollable() 会在传入的 flingBehavior 为 null 时给它指定一个默认值 ScrollableDefaults.flingBehavior()
object ScrollableDefaults {/*** Create and remember default [FlingBehavior] that will represent natural fling curve.*/Composablefun flingBehavior(): FlingBehavior {val flingSpec rememberSplineBasedDecayFloat()return remember(flingSpec) {DefaultFlingBehavior(flingSpec)}}/*** Create and remember default [OverscrollEffect] that will be used for showing over scroll* effects.*/ComposableExperimentalFoundationApifun overscrollEffect(): OverscrollEffect {return rememberOverscrollEffect()}
}因此无论是惯性滑动还是触边效果都可以使用 ScrollableDefaults 提供的默认效果即可。
1.3 swipeable()
swipeable() 与 scrollable() 一样都对 draggable() 实现了定制只不过场景不同。scrollable() 是用于横向或纵向的滑动布局组件swipeable() 适用于有明确终点的滑动场景比如滑动删除、侧滑菜单、滑动解锁等。
swipeable() 在 material 和 material2 包中可见在 material3 中被隐藏了。因此只能使用由它实现的组件比如滑动删除组件 SwipeToDismiss。
2、嵌套滑动与 nestedScroll()
在传统的 View 体系中开始是不支持嵌套滑动的像很原始的 ScrollView 与 ListView 都不支持嵌套滑动。后来随着需求的增加Google 以 Jetpack 库的方式开始支持嵌套滑动如 RecyclerView 与 NestedScrollView 等。Compose 作为 Jetpack 库中比较年轻的成员自然会对嵌套滑动有更全面、更完善的支持比如 Modifier 的 scrollable()、LazyColumn/LazyRow 都支持嵌套滑动。
并且Compose 对于很多常见的嵌套滑动需求都提供了实现。比如 Scaffold 配合 LargeTopAppBar 可以实现顶部 AppBar 与页面内容的嵌套滑动。但应用的需求千变万化总会遇到 Compose 没有提供现成实现的嵌套滑动的需求这就是本节课要学习嵌套滑动的目的。
Compose 通过 nestedScroll() 自定义嵌套滑动逻辑在介绍 nestedScroll() 之前先介绍一下 Compose 嵌套滑动的整体逻辑。
Compose 的嵌套滑动由最内层的组件负责触摸事件的处理它的外层组件并不直接负责触摸事件的处理而是只接受它的子滑动组件发送过来的滑动事件的回调通知以实现整体的嵌套滑动。
具体来说每一个组件在进行滑动之前会先去询问它的父组件是否要消费这一段滑动距离如果父组件不消费或者不完全消费剩余的距离才会由自己消费。如果自己没有完全消费掉这段距离会第二次询问父组件是否消费。也就是说子组件在滑动之前与滑动之后会对父组件进行两次询问以应对父组件优先滑动与子组件优先滑动的不同情况。父组件需要开放子组件滑动之前与滑动之后两个回调函数这样子组件在滑动前后会分别调用这两个接口通知父组件子组件要进行滑动了这样父组件可以根据自身需求决定是否在子组件之前或之后滑动。
接下来再看 nestedScroll() 的具体内容
/**
* 修改元素以使其参与嵌套滚动层次结构。
* 有两种参与嵌套滚动的方式作为滚动子元素通过 NestedScrollDispatcher 将滚动事件传递到嵌套滚动链
* 作为嵌套滚动链中的成员提供 NestedScrollConnection当下面的另一个嵌套滚动子元素分派滚动事件时将调用它。
* 在链中以 NestedScrollConnection 的形式参与是强制性的但滚动事件的分派是可选的因为有些情况下元素
* 希望参与嵌套滚动但本身并不是可滚动的。
* 参数
* connection - 与嵌套滚动系统连接以参与事件链接当可滚动的后代正在滚动时接收事件
* dispatcher - 要附加到嵌套滚动系统上的对象可以在其上调用 dispatch* 方法以通知嵌套滚动系统中的
* 祖先发生的滚动
*/
fun Modifier.nestedScroll(connection: NestedScrollConnection,dispatcher: NestedScrollDispatcher? null
): Modifier每次调用 nestedScroll()都会向 Compose 的 UI 树内插入一个嵌套滑动的节点而 nestedScroll() 的两个参数就是为该节点提供的信息。
如果把嵌套滑动看作一个链条为了让这个链条中插入一个新的滑动组件后还能正常运转被插入的滑动组件需要做三件事
作为嵌套滑动的子组件在滑动前和滑动后都去调用一下嵌套滑动父组件的相应的回调函数由 NestedScrollDispatcher 实现作为嵌套滑动的父组件在嵌套滑动子组件滑动前调用父组件的回调函数时做出正确的处理 再向上回调自身的嵌套滑动的父组件的回调函数如果父组件不消费或者没有完全消费则触发自身的滑动逻辑由 NestedScrollConnection 实现
其中第 2 点中的第一条已经由 nestedScroll() 实现了因此自定义嵌套滑动组件时要通过参数实现余下的两件事。
下面我们举个例子来说明如何实现。首先准备一个支持滑动但不支持嵌套滑动的组件 Column然后在该组件内部添加一个 LazyColumn 作为嵌套滑动的内部组件
Composable
fun NestedScrollSample() {var offsetY by remember { mutableStateOf(0f) }Column(Modifier.offset { IntOffset(0, offsetY.roundToInt()) }// draggable() 没支持嵌套滑动.draggable(rememberDraggableState { offsetY it }, Orientation.Vertical)) {for (i in 1..10) {Text(第 $i 项)}LazyColumn(Modifier.height(50.dp).background(Color.Yellow)) {items(5) {Text(内部 List - 第 $it 项)}}}
}然后我们给 Column 的 Modifier 加上 nestedScroll()使其变为一个支持嵌套滑动的组件主要问题在于如何提供 nestedScroll() 的两个参数 NestedScrollConnection 和 NestedScrollDispatcher。
NestedScrollConnection 会让组件作为父组件去响应子组件滑动时父组件应该做哪些事。比如对于我们要实现的例子来说当子组件 LazyColumn 滑动时我们是优先让子组件滑动子组件滑动之后如果有未消费完的距离进行二次询问时我们作为父组件才进行消费因此在实现 NestedScrollConnection 时要重写 onPostScroll() val connection remember {object : NestedScrollConnection {// onPostScroll() 负责子组件滑动之后父组件做出的对应处理如果父组件需要// 在子组件滑动之前进行滑动的话需要重写 onPreScroll()override fun onPostScroll(consumed: Offset,available: Offset,source: NestedScrollSource): Offset {offsetY available.y// 返回消耗了多少滑动距离return available}}// 惯性滑动可以用 ScrollableDefaults.flingBehavior().performFling()}NestedScrollConnection 接口内实际定义了四个函数分别是 onPreScroll()、onPostScroll()、onPreFling() 与 onPostFling()分别用于实现子组件滑动前、子组件滑动后、子组件惯性滑动前、子组件惯性滑动后父组件是否消费以及如何消费滑动距离的逻辑。如果实际需求中需要对惯性滑动也有要求可以使用上节讲过的 ScrollableDefaults.flingBehavior() 获取一个默认行为的 FlingBehavior再调用它的 performFling() 进行惯性滑动。
以上是对 nestedScroll() 所需的第一个参数 NestedScrollConnection 的讲解。对于第二个参数 NestedScrollDispatcher 要做的就是在子组件滑动前与滑动后回调父组件对应的滑动函数将处理权交给父组件并根据父组件的滑动结果做出相应的处理 // 创建一个 NestedScrollDispatcher 对象val dispather remember { NestedScrollDispatcher() }Column(Modifier.offset { IntOffset(0, offsetY.roundToInt()) }.draggable(rememberDraggableState {// 子组件滑动前先询问父组件是否滑动val consumed dispather.dispatchPreScroll(Offset(0f, it), NestedScrollSource.Drag)// 子组件滑动offsetY it - consumed.y// 子组件滑动后再次询问父组件是否滑动dispather.dispatchPostScroll(Offset(0f, it), Offset.Zero, NestedScrollSource.Drag)}, Orientation.Vertical).nestedScroll(connection, dispather))dispatchPreScroll() 有两个参数 available 与 source /*** 触发预滚动传递。这会触发所有祖先的 NestedScrollConnection.onPreScroll使它们有可能在需要时* 预先消费增量。* 参数* available - 从滚动事件中获得的增量* source - 滚动事件的来源* 返回所有祖先在链中预先消耗的总增量。此增量对于此节点不可用因此它应相应地调整消耗。*/fun dispatchPreScroll(available: Offset, source: NestedScrollSource): Offset {return parent?.onPreScroll(available, source) ?: Offset.Zero}available 表示子组件传过来的本次可以滑动的偏移总量在这个嵌套滑动链上的所有祖先本次预滑动的偏移量不能超过这个值。在例子中这个参数传的是 Offset(0f, it)表示把垂直方向上本次可以滑动的所有增量都给了父组件。
source 表示滑动事件的来源常见的来源有 Drag 滑动、Fling 惯性滑动两种。
dispatchPreScroll() 的返回值就是父组件消费了多少距离由于我们的例子中没有让 NestedScrollConnection 重写 onPreScroll()因此 dispatchPreScroll() 就没有消费所以返回 0。
接下来就是子组件滑动这里是用 offsetY it - consumed.y 让子组件消费了所有距离。因为 consumed.y 是 0那么 offsetY 的增量就是本次所有的滑动增量 it。
最后再调用 dispatchPostScroll() 再次询问父组件是否进行滑动它有三个参数第一个是子组件消费了多少距离第二个参数是给父组件剩余的可滑动距离是多少。由于前面已经让子组件消费了所有距离因此第一个参数填子组件消费掉的 Offset(0f, it)第二个参数填剩余可滑动距离实际上是 0也即 Offset.Zero。
完整的代码如下
Composable
fun NestedScrollSample() {var offsetY by remember { mutableStateOf(0f) }val dispather remember { NestedScrollDispatcher() }val connection remember {object : NestedScrollConnection {// onPostScroll() 负责子组件滑动之后父组件做出的对应处理如果父组件需要// 在子组件滑动之前进行滑动的话需要重写 onPreScroll()override fun onPostScroll(consumed: Offset,available: Offset,source: NestedScrollSource): Offset {offsetY available.y// 返回消耗了多少滑动距离return available}}}Column(Modifier.offset { IntOffset(0, offsetY.roundToInt()) }.draggable(rememberDraggableState {val consumed dispather.dispatchPreScroll(Offset(0f, it), NestedScrollSource.Drag)offsetY it - consumed.ydispather.dispatchPostScroll(Offset(0f, it), Offset.Zero, NestedScrollSource.Drag)}, Orientation.Vertical).nestedScroll(connection, dispather)) {for (i in 1..10) {Text(第 $i 项)}LazyColumn(Modifier.height(50.dp).background(Color.Yellow)) {items(5) {Text(内部 List - 第 $it 项)}}}
}效果如下 3、二维滑动监测
Compose 没有直接提供可以进行二维滑动监测的 Modifier 函数但是我们可以用更底层的函数来实现这个功能。
首先调用 Modifier.pointerInput()pointerInput() 这个函数是一个非常底层的函数它可以做最底层的触摸检测拿到最基础的触摸事件从而做最精细的触摸手势的识别与算法的定制。并且它内部也提供了常用的手势识别函数比如与拖拽相关的有如下四种 虽然看起来是 4 组 8 个函数但实际上同名的指向是同一个函数只不过调用方式不同。以 detectDragGestures() 为例
/**
* 等待指针按下和任何方向上的触摸阈值然后对每个拖动事件调用 onDrag 的手势检测器。它遵循
* awaitTouchSlopOrCancellation 的触摸阈值检测但一旦触摸阈值被越过它将自动消耗位置变化。
* 当通过最后已知的指针位置传递触摸阈值时将调用 onDragStart。当所有指针都弹起时将调用 onDragEnd
* 并且如果另一个手势消耗了指针输入则将调用 onDragCancel取消这个手势。
*/
suspend fun PointerInputScope.detectDragGestures(onDragStart: (Offset) - Unit { },onDragEnd: () - Unit { },onDragCancel: () - Unit { },onDrag: (change: PointerInputChange, dragAmount: Offset) - Unit
)它的前三个参数都提供了默认值而最后一个函数参数是唯一必填的参数。因此假如你只想指定 onDrag那么就对应图中的第一种 lambda 的调用方式选择后 AS 会自动将参数填好如果还需要提供其他参数就使用第二种调用方式。
我们重点来看 onDrag 这个回调函数的两个参数
change更底层的类封装了触发这一次滑动事件背后的触摸事件的那根手指相关的信息dragAmount拖拽的偏移量类型是 Offset 可以表示二维的偏移量
Compose 将 Android 原生的触摸事件封装成 Compose 的触摸事件并且对这个触摸事件进行分析分析后将其封装为滑动事件但它底层还是 Compose 的触摸事件所以才称为“滑动事件背后的触摸事件”。然后Compose 也支持多点触控只不过 Compose 的多点触控监控的是最先落下的手指而 Android 原生多点触控监测的是最后落下的手指。造成的不同体验就是原生的可以两个手指轮番滑动而 Compose 只有在先落下的手指抬起后才能由后落下的手指继续滑动。
但不论是 Compose 还是原生它们都只监测正在滑动中的手指而 change 就含有这个手指的信息包括手指的 ID 以及位置信息。因此 dragAmount 可以视为一个便捷的冗余信息它所表示的拖拽的偏移量是可以通过 change 内包含的信息计算出来的。
然后我们再说说这一组函数中的其他三个函数
detectHorizontalDragGestures() 与 detectVerticalDragGestures() 是在水平与垂直方向上的一维滑动监测detectDragGesturesAfterLongPress() 是监测在长按之后的二维滑动手势
最后再来说说 pointerInput() 配合 detectDragGestures() 与 draggable() 的区别。二者都是做滑动监测的所以代码没有本质上的区别甚至在最底层的代码上如拖拽的判定代码使用相同的函数它们的主要区别在于定位不同
draggable() 是较上层、更高级的函数需要实现相同功能时用 draggable() 写起来更方便detectDragGestures() 是较底层、更基础的函数能提供更多底层信息
4、多指手势
多指手势可以分为两类
自定义的多指手势识别自己分析触摸到屏幕上的每一根手指的滑动轨迹然后识别对应的手势利用 API 处理预设好的手势
本节讲解第 2 种下节介绍第 1 种。
Compose 提供了三种多指手势的识别移动、放缩与旋转它们都存在于 detectTransformGestures 函数中该函数也需要在 pointerInput() 内使用。我们先来看该函数的参数
/**
* 一个用于旋转、平移和缩放的手势检测器。一旦达到触摸阈值用户可以使用旋转、平移和缩放手势。
* 当发生旋转、缩放或平移中的任何一种手势时将调用 onGesture传递旋转角度以度为单位、
* 缩放比例因子和像素偏移量。每个改变都是前一次调用和当前手势之间的差异。在触摸阈值之后这将
* 消耗所有位置变化。onGesture 还将提供所有已按下指针的中心点。
*
* 如果 panZoomLock 设置为 true则只有在检测到旋转的触摸阈值之前才允许旋转然后才能进行平移
* 或缩放动作。否则将检测到平移和缩放手势但不会检测到旋转手势。如果 panZoomLock 设置为 false
* 则一旦触摸阈值被触发将检测到所有三种手势。
*/
suspend fun PointerInputScope.detectTransformGestures(panZoomLock: Boolean false,onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) - Unit
)第一个参数 panZoomLock 是一个开关分为两种情况
当它为 false 时三种手势可以同时识别当它为 true 时如果先识别到旋转操作那么就不会再监测滑动和缩放如果先监测到滑动或缩放那么就不会再监测旋转。相当于是把滑动和缩放放在一组旋转单独放在另外一组只监测先触发的那组操作
onGesture 参数是一个回调函数它的参数含义如下
centroid所有按下手指的中心点。这是一个辅助参数需要配合后面三个参数使用pan位移参数表示中心点 centroid 在这一时刻与上一时刻的位置偏移量zoom这一时刻与上一时刻相比的放缩倍数rotation这一时刻与上一时刻相比的旋转角度