Compose 实践与探索十五 —— 自定义触摸
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
): Modifier
draggable() 有两个必填的参数 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 的类型是 ScrollableState,scrollable() 支持的嵌套滑动就是通过 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 = rememberSplineBasedDecay<Float>()return remember(flingSpec) {DefaultFlingBehavior(flingSpec)}}/*** Create and remember default [OverscrollEffect] that will be used for showing over scroll* effects.*/@Composable@ExperimentalFoundationApifun 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:这一时刻与上一时刻相比的旋转角度
相关文章:
Compose 实践与探索十五 —— 自定义触摸
1、自定义触摸与一维滑动监测 之前我们在讲 Modifier 时讲过如下与手势检测相关的 Modifier: Modifier.clickable { } Modifier.combinedClickable { } Modifier.pointerInput {detectTapGestures { } }这里对以上内容就不再赘述了,直接去讲解更复杂的…...
第P8周:YOLOv5-C3模块实现
🍨 本文为🔗365天深度学习训练营中的学习记录博客 🍖 原作者:K同学啊 1. 模块组成 C3 模块由 卷积层(Convolutional layers)、激活函数(Activation functions) 和 残差连接&#…...
知识蒸馏:让大模型“瘦身“而不失智慧的魔术
引言:当AI模型需要"减肥" 在人工智能领域,一个有趣的悖论正在上演:大模型的参数规模每年以10倍速度增长,而移动设备的算力却始终受限。GPT-4的1750亿参数需要价值500万美元的GPU集群运行,但现实中的智能设备…...
`docker commit`和`docker tag`
1.docker commit docker commit是一个 Docker 命令,用于将一个正在运行的容器(Container)的状态提交为一个新的镜像(Image)。这类似于在版本控制系统中提交更改。 作用 • 当你对一个容器进行了修改(例如安…...
构建下一代AI Agent:自动化开发与行业落地全解析
1. 下一代AI Agent:概念与核心能力 核心能力描述技术支撑应用价值自主性独立规划与执行任务,无需持续人工干预决策树、强化学习、目标导向规划减少人工干预,提高任务执行效率决策能力评估多种方案并选择最优解决方案贝叶斯决策、多目标优化、…...
项目篇:模拟实现高并发内存池(2)
1.整体框架的设计 首先我们要来大概的梳理一下我们的高并发内存池的整体框架设计 在现代很多开发环境其实都是多核多线程,在申请内存的情况下,就必然会存在激烈的锁竞争问题。如果我们需要要实现内存池,必须要考虑以下几方面的问题。 1.性…...
PostgreSQL用SQL实现俄罗斯方块
📢📢📢📣📣📣 作者:IT邦德 中国DBA联盟(ACDU)成员,10余年DBA工作经验 Oracle、PostgreSQL ACE CSDN博客专家及B站知名UP主,全网粉丝10万 擅长主流Oracle、MySQL、PG、高斯…...
强大的AI网站推荐(第二集)—— V0.dev
网站:V0.dev 号称:前端开发神器,专为开发人员和设计师设计,能够使用 AI 生成 React 代码 博主评价:生成的UI效果太强大了,适合需要快速创建UI原型的设计师和开发者 推荐指数:🌟&…...
田间机器人幼苗视觉检测与护苗施肥装置研究(大纲)
田间机器人幼苗视觉检测与护苗施肥装置研究 基于多光谱视觉与精准施肥的农业机器人系统设计 第一章 绪论 1.1 研究背景与意义 农业智能化需求: 传统幼苗检测依赖人工,效率低且易遗漏弱苗/病苗施肥不精准导致资源浪费和环境污染 技术挑战:…...
vLLM 同时部署多个模型及调用
目录 一、单例加载多模型 (一) 原生多模型支持(vLLM ≥0.3.0) (二) 针对 vLLM 单实例部署多模型时 只有最后一个模型生效 的问题,结合实际测试和源码分析,以下是具体原因和解决方…...
LeetCode 2680.最大或值:位运算
【LetMeFly】2680.最大或值:位运算 力扣题目链接:https://leetcode.cn/problems/maximum-or/ 给你一个下标从 0 开始长度为 n 的整数数组 nums 和一个整数 k 。每一次操作中,你可以选择一个数并将它乘 2 。 你最多可以进行 k 次操作&#…...
Python——MySQL数据库编程
MySQL 是现在最流行的关系型数据库管理系统,在 WEB 开发中,MySQL 是最好的 RDBMS 应用软件之一。接下来,让我们快速掌握 python 使用 MySQL 的相关知识,并轻松使用 MySQL 数据库。 第1关:python数据库编程之创建数据库…...
AI 如何重塑数据湖的未来
在生成式 AI 与大模型技术飞速发展的今天,数据湖技术正迎来前所未有的挑战与机遇。海量非结构化数据的存储与处理、实时性与计算效率的平衡、高效存储的需求,已成为数据平台的核心难题。如何突破传统架构的局限,构建支持 AI 驱动的高效数据湖…...
C++ - 从零实现Json-Rpc框架-2(服务端模块 客户端模块 框架设计)
项⽬设计 本质上来讲,我们要实现的rpc(远端调⽤)思想上并不复杂,甚⾄可以说是简单,其实就是客⼾端想要完成某个任务的处理,但是这个处理的过程并不⾃⼰来完成,⽽是,将请求发送到服务…...
课程5. 迁移学习
课程5. 迁移学习 卷积神经网络架构ImageNet神经网络架构实践从 torchvision 加载模型在一个图像上测试预先训练的网络 迁移学习网络训练冻结层实践准备数据替换网络的最后一层冻结层网络训练获取测试样本的质量指标 课程计划: 流行的神经网络架构迁移学习 卷积神经…...
SATA(Serial Advanced Technology Attachment)详解
一、SATA的定义与核心特性 SATA(串行高级技术附件)是一种 用于连接存储设备(如硬盘、固态硬盘、光驱)的高速串行接口标准,取代了早期的PATA(并行ATA)。其核心特性包括: 高速传输&am…...
常用的 MyBatis 标签及其作用
MyBatis 是一个优秀的持久层框架,它通过 XML 或注解的方式将 Java 对象与数据库操作进行映射。在 MyBatis 的 XML 映射文件中,可以使用多种标签来定义 SQL 语句、参数映射、结果映射等。以下是一些常用的 MyBatis 标签及其作用: 1. 基本标签 …...
Blender配置渲染设置并输出动画
在Blender中,渲染设置和渲染动画的选项位于不同的面板中。以下是具体步骤: 渲染设置 渲染设置用于配置输出格式、分辨率、帧率等参数。 打开右侧的 属性面板(按 N 键可切换显示)。 点击 “输出属性” 选项卡(图标是…...
网络故障排查指南:分治法与排除法结合的分层诊断手册
目录 一、排查方法论:分治法与排除法的结合 1. 分治法(Divide and Conquer) 2. 排除法(Elimination) 二、分层诊断实战手册 1. 物理层排查(设备与线路) 硬件检测三板斧 运维经验 2. 网络…...
【万字总结】前端全方位性能优化指南(三)——GPU渲染加速、WebGPU、OffscreenCanvas多线程渲染
theme: condensed-night-purple 前言 当每秒60帧的流畅渲染遭遇百万级多边形场景,传统CPU绘图如同单车道上的赛车——即便引擎轰鸣,依然难逃卡顿困局。现代GPU加速技术将渲染任务从「单车道」扩展到「八车道」,本章以分层爆破、API革命、线程联邦为技术支柱,拆解如何通过G…...
报错 - redis - Unit redis.service could not be found.
报错: Unit redis.service could not be found.Could not connect to Redis at 127.0.0.1:6379: Connection refused解决方法: 检查状态、有必要的话 重新安装 Linux 上查看状态 systemctl status redis显示以下内容,代表正常服务 出现下面…...
Windows系统本地部署OpenManus对接Ollama调用本地AI大模型
文章目录 前言1. 环境准备1.1 安装Python1.2. 安装conda 2. 本地部署OpenManus2.1 创建一个新conda环境2.2 克隆存储库2.3 安装依赖环境 3. 安装Ollama4. 安装QwQ 32B模型5. 修改OpenManus配置文件6. 运行OpenManus7.通过网页使用OpenManus8. 安装内网穿透8.1 配置随机公网地址…...
【递归,搜索与回溯算法篇】- 名词解释
一. 递归 1. 什么是递归? 定义: 函数自己调用自己的情况关键点: ➀终止条件: 必须明确递归出口,避免无限递归 ➁子问题拆分: 问题需能分解成结构相同的更小的子问题缺点: ➀栈溢出风险&#x…...
【设计模式】装饰模式
六、装饰模式 装饰(Decorator) 模式也称为装饰器模式/包装模式,是一种结构型模式。这是一个非常有趣和值得学习的设计模式,该模式展现出了运行时的一种扩展能力,以及比继承更强大和灵活的设计视角和设计能力,甚至在有些场合下&am…...
c库、POSIX库、C++库、boost库之间的区别和联系
文章目录 一、区别1. 定义和来源2. 功能范围3. 可移植性4. 语言支持5. 维护和更新 二、联系1. 相互补充2. 部分功能重叠3. 共同促进编程发展4. 代码兼容性 三、总结 一、区别 1. 定义和来源 C 库函数:由 ANSI C 和 ISO C 标准定义,是 C 语言编程的基础…...
算法及数据结构系列 - 树
系列文章目录 算法及数据结构系列 - 二分查找 算法及数据结构系列 - BFS算法 算法及数据结构系列 - 动态规划 算法及数据结构系列 - 双指针 算法及数据结构系列 - 回溯算法 文章目录 树框架树遍历框架N叉树遍历框架 经典题型124.二叉树的最大路径和105.从前序与中序遍历序列构造…...
go安装lazydocker
安装 先安装go环境 https://blog.csdn.net/Yqha1/article/details/146430281?fromshareblogdetail&sharetypeblogdetail&sharerId146430281&sharereferPC&sharesourceYqha1&sharefromfrom_link 安装lazydocker go install github.com/jesseduffield/laz…...
《深度学习》——YOLOv3详解
文章目录 YOLOv3简介YOLOv3核心原理YOLOv3改进YOLOv3网络结构 YOLOv3简介 YOLOv3(You Only Look Once, version 3)是一种先进的实时目标检测算法,由 Joseph Redmon 和 Ali Farhadi 开发。它在目标检测领域表现出色,具有速度快、精…...
使用spring-ai-ollama访问本地化部署DeepSeek
创建SpringBoot工程,引入依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"htt…...
Kafka消息自定义序列化
文章目录 1. 默认序列化2.自定义序列化3.示例4.自定义解序列化器 1. 默认序列化 在网络中发送数据都是以字节的方式,Kafka也不例外。Apache Kafka支持用户给broker发送各种类型的消息。它可以是一个字符串、一个整数、一个数组或是其他任意的对象类型。序列化器(se…...
使用Systemd管理ES服务进程
Centos中的Systemd介绍 CentOS 中的 Systemd 详细介绍 Systemd 是 Linux 系统的初始化系统和服务管理器,自 CentOS 7 起取代了传统的 SysVinit,成为默认的初始化工具。它负责系统启动、服务管理、日志记录等核心功能,显著提升了系统的启动速…...
编程语言选择分析:C#、Rust、Go 与 TypeScript 编译器优化
编程语言选择分析:C#、Rust、Go 与 TypeScript 编译器优化 在讨论编程语言的选择时,特别是针对微软的 C# 和 Rust,以及谷歌的 Go 语言,以及微软试图通过 Go 来拯救 TypeScript 编译器的问题,我们可以从多个角度来分析和…...
使用粘贴控件
HarmonyOS 5.0.3(15) 版本的配套文档,该版本API能力级别为API 15 Release 文章目录 约束与限制开发步骤 粘贴控件是一种特殊的系统安全控件,它允许应用在用户的授权下无提示地读取剪贴板数据。 在应用集成粘贴控件后,用户点击该控件…...
MySQL 客户端连不上(1045 错误)原因全解析
MySQL 客户端连不上(1045 错误)原因全解析 在我们学习 MySQL 或从事 MySQL DBA 工作期间,时常会遇到:“我尝试连接到 MySQL 并且收到1045 错误,但我确定我的用户和密码都没问题”。 不管你现在是否是高手还是高高手,都不可避免曾经在初学的时候犯过一些很初级的错误,例…...
麒麟系列Linux发行版探秘
以下内容摘自《银河麒麟操作系统进阶应用》一书。 银河麒麟操作系统(Kylin) 银河麒麟(Kylin)操作系统是中国自主研发的一款基于Linux内核的操作系统。它的发展历程可以追溯到2002年,最初由国防科技大学主导研发&…...
刘强东突然发声:不该用算法压榨最底层兄弟!东哥,真正的人民企业家
今天忙了一天,很累,准备睡觉的时候,看到网上盛传的刘强东的朋友圈,东哥又在朋友圈发文了。 说实话,看完之后,感动,真的感动。 尤其是当我看到这两句话的时候。 1、我们所学的知识、商业模式、技…...
信息收集与问答系统流程分析与改进建议
现有系统的问题与局限 1. 资源管理问题 二元决策机制过于简化:当前系统仅在令牌预算耗尽时才进入Beast Mode,缺乏渐进式资源分配策略缺少早期预算规划:没有基于问题复杂度的初始资源分配机制缺乏优先级资源分配:所有问题和策略消…...
【人工智能】如何理解transformer中的token?
如何理解transformer中的token? **一、Token在Transformer中的作用****二、文本分词的常见方法****1. 基于词典的分词(Dictionary-based Tokenization)****2. 子词分词(Subword Tokenization)****(1) WordPiece算法****(2) BPE&a…...
Spring Boot 集成 Kafka 消息发送方案
一、引言 在 Spring Boot 项目中,Kafka 是常用的消息队列,可实现高效的消息传递。本文介绍三种在 Spring Boot 中使用 Kafka 发送消息的方式,分析各自优缺点,并给出对应的 pom.xml 依赖。 二、依赖引入 在 pom.xml 中添加以下依赖: <dependencies><!-- Sprin…...
Hadoop•HDFS的Java API操作
听说这是目录哦 上传文件到HDFS🌈一、下载Windows版本的JDK和Hadoop二、配置物理机环境变量三、创建项目四 、添加依赖五、新建java类六、创建文件七、打开集群八、选中、运行 从HDFS下载文件🪐一、写代码二、HDFS要个文件三、物理机要个文件夹ÿ…...
电脑如何设置几分钟后自动关机
摘要:本文提供Windows、macOS和Linux系统设置定时自动关机的详细方法。 目录 一、Windows系统设置方法 设置定时关机 取消关机计划 二、macOS系统设置方法 设置定时关机取消关机计划 三、Linux系统设置方法 设置定时关机 取消关机计划 四、注意事项五、扩展&#x…...
固定公网 IP
固定公网 IP 是指为用户分配一个长期不变且可从互联网直接访问的 IP 地址,具有以下重要作用: 1. 搭建服务器 网站托管:可用于托管网站、博客或电子商务平台。 应用服务器:支持运行邮件服务器、游戏服务器、数据库等。 远程访问&…...
Linux安装go环境
安装一个lazydocker,根据文档需要先安装go环境 https://github.com/jesseduffield/lazydocker 官方文档解析 https://go.dev/doc/install 文档内容如下,一共三步 1.删除先前安装的go,解压下载的go压缩包到/usr/local目录 2.添加环境变量&…...
Git的基本使用
Git的基本使用 前言 :为什么使用GitGit基本操作1. 初始化2. Git分区3. 认识.git目录4. git基本操作 Git分支管理1. 基本操作2. Git分支设计规范 Git 标签管理1. Git标签的使用2. 标签使用规范3. Git标签与分支的区别 分离头指针问题1. 分离头指针问题的风险2. 分离头…...
鸿蒙Flutter开发故事:不,你不需要鸿蒙化
在华为牵头下,Flutter 鸿蒙化如火如荼进行,当第一次看到一份上百个插件的Excel 列表时,我也感到震惊,排名前 100 的插件赫然在列,这无疑是一次大规模的军团作战。 然后,参战团队鱼龙混杂,难免有…...
Mysql:关于命名
1. 命名的对象:库名、表名、列名、索引名 2. 用反引号包裹的情况下,命名时不允许使用空白字符、反引号,其它字符均可 3. 无反引号包裹的情况下,命名时仅允许使用:$、_、数字、大小写字母、中文字符(已知win系统支持)…...
JAVA————十五万字汇总
JAVA语言概述 JAVA语句结构 JAVA面向对象程序设计(一) JAVA面向对象程序设计(二) JAVA面向对象程序设计(三)工具类的实现 JAVA面向对象程序设计(四)录入异常处理 JAVA图形用户界面设…...
Chrome-Edge-IDEA-Win 常用插件-工具包
Chrome-Edge-IDEA-Win 常用插件-工具包 Chrome-Edge-IDEA-Win 常用插件-工具包谷歌插件chropathJSONViewOctotree - GitHub code treeXPath Helper书签侧边栏篡改猴Print Edit WEEdge浏览器插件IDEA插件CodeGlance Pro 代码迷你缩放图插件Alibaba Cloud ToolkitAlibaba Java Co…...
DeepSeek-R1论文深度解析:纯强化学习如何引爆LLM推理革命?
技术突破:从“无监督”到“自主进化”的跨越 paper :https://arxiv.org/pdf/2501.12948目录 技术突破:从“无监督”到“自主进化”的跨越1 DeepSeek-R1-Zero: RLnoSFT1.1 R1-Zero: GRPO(Group Relative Po…...
最新!Ubuntu Docker 安装教程
源自: AINLPer(每日干货分享!!) 编辑: ShuYini 校稿: ShuYini 时间: 2025-3-1 更多:>>>>大模型/AIGC、学术前沿的知识分享! 看到很多部署大模型的时候,都是基于docker安装部署的。…...