【Jetpack Compose】PointerInput全面解析

在上一篇文章中,我们介绍了手势最基本的点击、拖动和滑动三种事件,看过文章的小伙伴应该知道,拖动和滑动事件都是针对于单个方向的事件,如果我们想实现垂直和水平双向上的滑动事件该如何处理呢?

Compose通过Modifier.pointInput()方法不仅会帮助我们处理双向滑动事件,还提供了多点触控的事件处理机制,下面我们先看下PointInput最基本的用法。

PointInput基本用法

点击和拖动事件除了直接通过Modifier来监听之外,还可以通过Modifier.PonitInput来监听,并且此方式监听的拖动事件可以同时监听到水平和垂直方向的偏移量,接着我们看看具体用法。

less 复制代码
// PointInput实现点击事件
Box(
    modifier = Modifier
        .fillMaxSize()
        .wrapContentSize()
) {
    Box(modifier = Modifier.size(100.dp)
        .background(color = Color.Red)
        .pointerInput(key1 = null) {
            detectTapGestures {
                Log.d(TAG, "PointGestureScreen: TapGesture")
            }
        })
}

点击事件的监听无论是通过Modifier.clickable()还是通过Modifier.pointInput()方法都是比较简单的,pointInput方式只需要在detectTagGestures()内部处理点击的逻辑即可,但是如果只是单纯的处理点击事件,就没有必要使用这种方式了,clickable()更为便捷。

Modifier.pointInput()方法最前面的参数是Key,用于区分当前手势的一个标志位,如果在手势未停止的过程中,key值改变了,那么事件的处理模块会立即取消并重新启动。

下面我们再看下水平和垂直方向上的拖动事件:

scss 复制代码
@Composable
fun PointGestureScreen() {
    var dragOffsetX by remember {
        mutableFloatStateOf(0F)
    }
    var dragOffsetY by remember {
        mutableFloatStateOf(0F)
    }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .wrapContentSize()
    ) {
        Box(modifier = Modifier
            .offset { IntOffset(dragOffsetX.toInt(), dragOffsetY.toInt()) }
						.size(100.dp)
            .background(color = Color.Red)
            .pointerInput(null) {
                detectDragGestures { change, dragAmount ->
                    dragOffsetX += dragAmount.x
                    dragOffsetY += dragAmount.y
                }
            })
    }
}

pointInput中可以通过detectDragGestures()来监听手指的拖动事件,并且可以通过dragAmount参数获取水平和垂直方向的偏移量,上述代码中根据水平和垂直方向上的拖动量对Box进行了offset偏移操作,这样就可以达到Box在屏幕上任意滑动,实现的效果见下图:

PointInput双指缩放和旋转

通过上述的学习之后,点击和滑动事件我们已经掌握了,点击和滑动事件相对来说还是比较简单的,下面我们接着学习双指事件,来实现一个方块的缩放和旋转效果。直接对照着代码学习:

scss 复制代码
@Composable
fun PointGestureScreen() {
    var dragOffsetX by remember {
        mutableFloatStateOf(0F)
    }
    var dragOffsetY by remember {
        mutableFloatStateOf(0F)
    }
    var scale by remember {
        mutableFloatStateOf(1F)
    }
    var rotationAngle by remember {
        mutableFloatStateOf(0F)
    }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .wrapContentSize()
    ) {
        Box(modifier = Modifier
            .size(100.dp)
            .scale(scale)
            .rotate(rotationAngle)
						.offset { IntOffset(dragOffsetX.toInt(), dragOffsetY.toInt()) }
            .background(color = Color.Red)
            .pointerInput(null) {
                detectTransformGestures { centroid, pan, zoom, rotation ->
                    Log.d(
                        TAG,
                        "PointGestureScreen TransformGesture centroid: $centroid, pan: $pan, zoom: $zoom" +
                                ", rotation: $rotation"
                    )
										dragOffsetX += pan.x
                    dragOffsetY += pan.y
                    scale *= zoom
                    rotationAngle += rotation
                }
            })
    }
}

双指变换手势是通过detectTransformGestures来监听,这个方法非常有趣,detectTransformGestures的Lambda参数中提供了四个变量,作用分别为:

  • centroid:中心位置的偏移量,为Offset类型;
  • pan:这个参数乍一看名字不太理解是干啥的,它其实是水平和垂直方向上的偏移量,也是一个Offset类型,可以用参数做组合项移动的事件;
  • zoom:缩放参数,为Float类型,它是以1F为基数,做缩放处理的时候需要累乘;
  • rotation:旋转角度参数,为Float参数,顺时针方向为正数,逆时针方向为负数,做旋转处理的时候需要累加。

理解了四个参数的作用之后,detectTransformGestures使用就变得很简单的了,只需要将对应的参数和我们默认的偏移、缩放和旋转值做相应的累加或者累乘即可。下面运行代码看看实际效果:

Transformable双指缩放和旋转

其实实现双指缩放和旋转除了使用PointInput-detectTransformGestures方式之外,还可以直接使用Modifier.transformable()方式实现,两种方式处理逻辑几乎是一模一样,只是transformable()方式需要传入一个TransformableState,具体的逻辑是在State中处理。

scss 复制代码
val transformState = rememberTransformableState(onTransformation = { zoom, pan, rotation ->
    dragOffsetX += pan.x
    dragOffsetY += pan.y
    scale *= zoom
    rotationAngle += rotation
})

Box(modifier = Modifier
    .size(100.dp)
    .scale(scale)
    .rotate(rotationAngle)
    .offset { IntOffset(dragOffsetX.toInt(), dragOffsetY.toInt()) }
    .background(color = Color.Red)
    .transformable(transformState)
)

具体的使用就是上述代码这样,和PointInput-detectTransformGestures如出一辙,这里就不过多介绍了。

PointInput其余API

Modifier.pointInput除了上面介绍的detectTapGestures、detectDragGestures和detectTransformGestures事件之外,还有其余的几个手势监听,这里就不再过多介绍直接以表格的形式列出,小伙伴们也可以清晰的了解它们的作用。

API 作用
detectDragGesturesAfterLongPress 监听手指长按之后的拖动事件,用法和detectDragGestures一致
detectHorizontalDragGestures 监听手指水平方向的拖动事件
detectVerticalDragGestures 监听手指垂直方向的拖动事件
awaitEachGesture 监听手指每一个手势,直到每一个手指都离开屏幕或者事件被意外取消,此API下面会重点介绍

PointInput.awaitEachGesture

awaitEachGesture是一个比较关键的API,我们可以利用这个API完成一些自定义的事件处理,包括上面点击、长按和拖动事件都可以使用它来完成,细心的小伙伴如果查看了上面detect**一系列API源码就会发现,它们内部都是采用了awaitEachGesture来实现各自的逻辑。

那么接下来我们简单看下awaitEachGesture的源码是如何处理事件的:

kotlin 复制代码
suspend fun PointerInputScope.awaitEachGesture(block: suspend AwaitPointerEventScope.() -> Unit) {
    val currentContext = currentCoroutineContext()
    awaitPointerEventScope {
        while (currentContext.isActive) {
            try {
                block()

                // Wait for all pointers to be up. Gestures start when a finger goes down.
                awaitAllPointersUp()
            } catch (e: CancellationException) {
                if (currentContext.isActive) {
                    // The current gesture was canceled. Wait for all fingers to be "up" before
                    // looping again.
                    awaitAllPointersUp()
                } else {
                    // detectGesture was cancelled externally. Rethrow the cancellation exception to
                    // propagate it upwards.
                    throw e
                }
            }
        }
    }
}
  • 在第2行代码处先获取了当前运行的协程上下文,因为awaitEachGesture是一个挂起函数,需要运行在协程的环境中;
  • 然后在第3行代码处通过awaitPointEventScope()方法将当前代码块挂起,并等待手指事件的输入;
  • 第4行代码比较有趣,直接开启了一个while循环,跳出循环的条件是当前协程不再活跃的状态;
  • 第6行就直接执行block参数,并且用try-catch包裹住,这里主要就是为了捕获协程取消的异常动作;
  • 第9行代码处又是一个挂起函数,awaitAllPointersUp方法是为了等待所有手指都被抬起;
  • 最后在异常处理中判断当前协程环境是否还活跃,如果是活跃状态,继续等待所有手指被抬起。

最后我们通过awaitEachGesture来监听下单指、双指和三指这些手势的判断和相应的逻辑处理。

我们需要实现的功能为单指点击、双指拖动和三指下拉截图,下面我们来进入编码看看如何实现这些功能。

单指点击

单指点击的功能是最容易实现的,我们只需要判断当前按下的手指数量为1即可:

scss 复制代码
Box(modifier = Modifier
    .size(300.dp)
    .offset { averagePosition.value }
    .background(color = Color.Red)
    .pointerInput(null) {
        awaitEachGesture {
            val firstDown = awaitFirstDown()
            do {
                val pointerEvent = awaitPointerEvent()
                val changeList = pointerEvent.changes
                Log.d(TAG, "EachGesture changeList size: ${changeList.size}")
                when (changeList.size) {
                    // 单指
                    1 -> {
                        val singleChange = changeList[0]
                        val position = singleChange.position
                        Log.d(TAG, "EachGesture Single Point: $position")
                      	firstDown.consume()
                    }
                }
            } while (!firstDown.isConsumed)
        }
    }
)

通过awaitEachGesture监听每一个手势事件,调用awaitFirstDown等待第一个按下事件,然后以firstDown是否被消费为条件开启while循环,循环内部调用awaitPointerEvent方法等待事件的输入,此方法会返回一个PointEvent对象,可以根据PointEvent对象的changes来判断当前事件输入的手指数,只需要判断手指数量为1最终执行点击事件即可。

如果只需要处理点击事件那么可以在处理完之后调用下firstDown.consume()方法将第一个按下事件消费掉,这样就可以跳出while循环。

双指拖动

双指拖动也是比较容易实现,根据PointEvent对象的changes判断手指数量是否等于2,然后计算每个手指的水平和垂直方向的偏移量,再根据双指偏移量计算出平均的偏移量做offset()即可。

scss 复制代码
val averagePosition = remember {
    mutableStateOf(IntOffset.Zero)
}
Box(modifier = Modifier
    .size(300.dp)
    .offset { averagePosition.value }
    .background(color = Color.Red)
    .pointerInput(null) {
        awaitEachGesture {
            val firstDown = awaitFirstDown()
            do {
                val pointerEvent = awaitPointerEvent()
                val changeList = pointerEvent.changes
                Log.d(TAG, "EachGesture changeList size: ${changeList.size}")
                when (changeList.size) {
                    // 双指
                    2 -> {
                        val firstChange = changeList[0]
                        val secChange = changeList[1]
                        Log.d(
                            TAG,
                            "EachGesture Double Point: ${firstChange.position} - ${secChange.position}"
                        )
                        val firstPositionChange = firstChange.positionChange()
                        val secPositionChange = secChange.positionChange()
                        averagePosition.value += IntOffset(
                            ((firstPositionChange.x + secPositionChange.x) / 2).roundToInt(),
                            ((firstPositionChange.y + secPositionChange.y) / 2).roundToInt()
                        )
                    }
                }
            } while (!firstDown.isConsumed)
        }
    }
)

这里需要注意的是每次计算完平均偏移量之后,不可以调用firstDown.consume()方法,需要长期监听双指的偏移量,我们看下实现的效果:

三指下拉截屏

scss 复制代码
var threeFingersY = remember {
    mutableFloatStateOf(0F)
}
Box(modifier = Modifier
    .size(300.dp)
    .offset { averagePosition.value }
    .background(color = Color.Red)
    .pointerInput(null) {
        awaitEachGesture {
            val firstDown = awaitFirstDown()
            do {
                val pointerEvent = awaitPointerEvent()
                val changeList = pointerEvent.changes
                Log.d(TAG, "EachGesture changeList size: ${changeList.size}")
                when (changeList.size) {
                    // 三指
                    3 -> {
                        val firstChange = changeList[0]
                        val secChange = changeList[1]
                        val threeChange = changeList[2]
                        Log.d(
                            TAG,
                            "EachGesture Double Point: ${firstChange.position} - ${secChange.position} - ${threeChange.position}"
                        )
                        val firstPositionChange = firstChange.positionChange()
                        val secPositionChange = secChange.positionChange()
                        val threePositionChange = secChange.positionChange()
                        threeFingersY.floatValue += ((firstPositionChange.y + secPositionChange.y + threePositionChange.y) / 3F)
                        Log.d(TAG, "EachGesture threeFingersY: $threeFingersY")
                        if (threeFingersY.floatValue > 100F) {
                            Log.d(TAG, "三指下拉截屏")
                            firstDown.consume()
                        }
                    }
                }
            } while (!firstDown.isConsumed)
        }
    }
)

这里只是计算了三个手指在Y轴的平均偏移量,然后判断平均偏移量是否大于100F,如果大于了100F那么就执行截屏动作,并且将firstDown消费掉跳出循环,其实实现逻辑还是比较简单和清晰的。

pointerInput-awaitEachGesture远不止可以实现这些简单的功能,我们可以通过awaitEachGesture实现各种各样自定义的手势,感兴趣的小伙伴赶紧上手体验一波吧😄

写在最后

本次关于PointInput的相关知识就介绍到这了,如果小伙伴们对文章有任何的疑问欢迎评论区或者私信交流。

我是Taonce,如果觉得本文对你有所帮助,帮忙关注、赞或者收藏三连一下,谢谢😆😆~

相关推荐
拭心5 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王7 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡8 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道8 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库9 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道9 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe10 小时前
Android Hook - 动态加载so库
android
居居飒10 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He13 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗14 小时前
Android笔试面试题AI答之Android基础(1)
android