Jetpack Compose中的手势操作和事件处理

高级事件处理 API

Compose 中的手势操作或事件处理全部都是以 Modifier 修饰符的形式提供的,事件处理按照层次可以划分为高级事件处理API和低级事件处理API。

其中高级事件处理API是位于更上层的API,它们都是基于更底层的低级事件处理API实现的,也是开发中比较常用的。高级事件处理API的分类大概如下图所示:

在这里插入图片描述

点击事件

监听点击事件非常简单,使用 clickablecombinedClickable 修饰符即可满足需求:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ClickableExample() {
   
    Column{
   
        Box(Modifier
            .clickable {
    println("clickable") }
            .size(30.dp)
            .background(Color.Red))
        Box(Modifier
            .size(50.dp)
            .background(Color.Blue)
            .combinedClickable(
                onLongClick = {
    println("combinedClickable --> onLongClick") },
                onDoubleClick = {
    println("combinedClickable --> onDoubleClick") },
                onClick = {
    println("combinedClickable --> onClick") }
            ))
    }
}

当点击事件发生时会为被点击的组件施加一个水波纹效果动画的蒙层,这是Material Design中的默认效果,如果不希望点击时有这个效果,可以使用低级别的Api detectTapGestures

另外, clickablecombinedClickable 可以传入一个 enable 参数作为一个可变状态,可以通过该状态来动态控制是否启用点击监听。

Draggable拖动

Draggable可以监听拖动手势偏移量,然后可以根据偏移量定制UI拖动交换效果。但是值得注意的是,Draggable修饰符只支持监听水平方向或垂直方向的偏移,如希望监听任意方向,则可以使用detectDragGestures方法。

使用Draggable至少需要传入2个参数 draggableStateorientation

  • draggableState: 通过它可以获取到拖动手势的偏移量,并且也允许我们动态控制发生偏移行为
  • orientation:监听拖动的方向,只能是水平或垂直

如下代码实现一个简单的滑块拖动效果

@Composable
fun DraggableExample() {
   
    var offsetX by remember {
    mutableStateOf(0f) }
    val boxSlideSize = 50.dp
    val maxLengthPx = with(LocalContext.current) {
   
        resources.displayMetrics.widthPixels - boxSlideSize.toPx()
    }
    // 创建并获取一个DraggableState实例
    val draggableState = rememberDraggableState {
   
        // 使用回调方法回传的参数对状态偏移量进行累加,并限制范围
        offsetX = (offsetX + it).coerceIn(0f, maxLengthPx)
    }
    Box(
        Modifier
            .fillMaxWidth()
            .height(boxSlideSize)
            .background(Color.LightGray)
    ) {
   
        Box(
            Modifier
                .size(boxSlideSize)
                .offset {
    IntOffset(offsetX.roundToInt(), 0) }
                .draggable(
                    orientation = Orientation.Horizontal,
                    state = draggableState
                )
                .background(Color.Red)
        )
    }
}

由于Modifier是链式执行的,因此这里offset修饰符应该放在draggablebackground之前。

运行效果:

在这里插入图片描述

错误示例1(draggable在offset前面):第二次拖动时UI控件拖动只能拖动初始位置才生效,不会跟随UI控件而移动监听,原因是每次拖动时draggable都监听的都是初始位置,不是偏移后位置。
错误示例2(background在offset前面):UI控件不会跟手,原因在于每次绘制时background都在初始位置绘制,不是偏移后位置。

另外,draggable 还有几个参数:

  • enabled:是否启用拖拽,方便动态控制,默认值是true。
  • interactionSource:可以用来收集拖拽的状态,默认值是null。
  • startDragImmediately:是否立即开始拖动,默认值是false,如果设为true可防止其他手势检测器对“down”事件做出反应。这是为了允许最终用户通过按下操作来“捕获”动画组件。当你拖动的值正在设定/动画时,设置它很有用。
  • onDragStarted:一个挂起函数,开始拖动时回调。
  • onDragStopped:一个挂起函数,停止拖动时回调。
  • reverseDirection:是否反方向执行拖拽效果,默认值是false。

其中interactionSource 可以这样使用:

val interactionSource = remember{
    MutableInteractionSource() }
Box(
    Modifier 
        .draggable(
            orientation = Orientation.Horizontal,
            state = draggableState,
            interactionSource = interactionSource, 
        ).background(Color.Red)
)
val isDragged by interactionSource.collectIsDraggedAsState()
Text(if(isDragged) "正在拖动" else "静止")

Swipeable滑动

使用方式跟Draggable差不多,但是Swipeable可以通过锚点设置吸附效果。

使用Swipeable至少需要传入4个参数:

  • State: 手势状态,通过它可以实时获取当前手势的偏移信息
  • Anchors: 锚点,用于记录不同状态对应数值的映射关系
  • Orientation: 手势方向,只支持水平或垂直
  • thresholds: 不同锚点之间吸附效果的临界阈值,常用的阈值有FixedThreshold(Dp)FractionalThreshold(Float)两种

以下代码使用Swipeable创建一个简单的开关效果:

enum class Status{
    CLOSE, OPEN } // 定义两个枚举项表示开关状态

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SwipeableDemo() {
   
    val blockSize = 48.dp
    val blockSizePx = blockSize.toPx()
    // 创建并获取一个SwipeableState实例
    val swipeableState = rememberSwipeableState(initialValue = Status.CLOSE)
    // 定义锚点,锚点以Pair表示,每个状态对应一个锚点
    val anchors = mapOf(
        0f to Status.CLOSE,
        blockSizePx*2 to Status.OPEN
    )
    Box(
        Modifier
            .size(height = blockSize, width = blockSize * 3)
            .clip(RoundedCornerShape(50))
            .background(Color.Gray)
    ) {
   
        Box(
            Modifier
                .offset {
    IntOffset(swipeableState.offset.value.toInt(), 0) }
                .swipeable(
                    state = swipeableState,
                    anchors = anchors,
                    thresholds = {
    from, to ->
                    	// 从关闭到开启状态时,滑块移动超过30%距离自动吸附到开启状态
                        if (from == Status.CLOSE) {
    
                            FractionalThreshold(0.3f)
                        } else {
    // 从开启状态到关闭状态时,滑块移动超过50%才会自动吸附到关闭状态
                            FractionalThreshold(0.5f)
                        }
                    },
                    orientation = Orientation.Horizontal
                )
                .size(blockSize)
                .clip(RoundedCornerShape(50))
                .background(Color.Red)
        )
    }
}

由于Modifier是链式执行的,因此这里swipeable修饰符应该放在draggablebackground之前。

运行效果:

在这里插入图片描述

注意:Modifier.swipeable 这个修饰符在compose.material中可以正常使用,而在compose.material3库中被隐藏了。但是Cmpose提供了一个 SwipeToDismiss 这个Composable组件来专门做滑动删除的效果。

transformable多点触控

transformable修饰符可以监听双指拖动、缩放或旋转手势

@Composable
fun TransformableExample() {
   
    val boxSize = 200.dp
    var offset by remember {
    mutableStateOf(Offset.Zero) }
    var rotationAngle by remember {
    mutableStateOf(0f) }
    var scale by remember {
    mutableStateOf(1f) }
    // 创建并获取一个TransformableState实例
    val transformableState = rememberTransformableState {
    
    	zoomChange: Float, panChange: Offset, rotationChange: Float ->
        scale *= zoomChange
        offset += panChange
        rotationAngle += rotationChange
    }
    Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
   
        Image(
            painter = painterResource(id = R.drawable.ic_sky),
            contentScale = ContentScale.Crop,
            contentDescription = null,
            modifier = Modifier
                .size(boxSize)
                .rotate(rotationAngle) // 注意rotate的顺序应该先于offset
                .offset {
    IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
                .scale(scale)
                .transformable(
                    state = transformableState,
                    // 该值为true时,发生双指拖动或缩放时,不会同时监听旋转手势信息
                    lockRotationOnZoomPan = true 
                )
        )
    }
}

这里注意rotate的顺序应该先于offset,如果先调用了offset再调用rotate, 则组件会先偏移再旋转,这会导致组件最终位置不可预期。

运行效果:

在这里插入图片描述

Scrollable滚动

主要用于列表场景,结合LazyColumnLazyRow来使用

horizontalScroll 与 verticalScroll

horizontalScroll主要结合Row组件来使用,使其支持水平滚动,horizontalScroll只需要传入一个 scrollState 即可。我们可以使用 rememberScrollState 快速创建一个 scrollState 实例并传入即可。

@Composable
fun HorizontalScrollExample() {
   
    val scrollState = rememberScrollState()
    Row(
        Modifier
            .padding(10.dp)
            .border(BorderStroke(1.dp, Color.Blue))
            .height(50.dp)
            .horizontalScroll(scrollState)
    ) {
   
        repeat(50) {
   
            Text("item $it", Modifier.padding(10.dp))
            Divider(Modifier.width(1.dp).fillMaxHeight())
        }
    }
}

verticalScrollhorizontalScroll使用类似,主要结合Column组件使用。

@Composable
fun VerticalScrollExample() {
   
    val scrollState = rememberScrollState()
    Column(
        Modifier
            .height(300.dp)
            .verticalScroll(scrollState)
    ) {
   
        repeat(50) {
   
            Text("item $it", Modifier.padding(10.dp))
            Divider()
        }
    }
}

verticalScrollhorizontalScroll 这两个修饰符除了在RowColumn组件上使用外,在其他组件上也可以应用,如 Box 组件等。

低级别 scrollable 修饰符

horizontalScrollverticalScroll都是基于scrollable实现的, scrollable修饰符除了传入一个scrollState外,还需要传入Orientation(水平或垂直)

以下代码通过 scrollable 修饰符的滚动监听能力,自己来定制实现类似 horizontalScroll 修饰符的功能:

@Composable
fun ScrollableExample1() {
   
    Column(Modifier.padding(10.dp)) {
   
        val scrollState = rememberScrollState()
        Row(
            Modifier
                .border(BorderStroke(1.dp, Color.Blue))
                .height(50.dp)
                .offset(x = -scrollState.value.toDp()) // 滚动位置增大时应该向左偏移,所以这里设为负数
                .scrollable(scrollState, Orientation.Horizontal, reverseDirection = true)
        ) {
   
            repeat(50) {
   
                Text("item $it", Modifier.padding(10.dp))
                Divider(Modifier.width(1.dp).fillMaxHeight())
            }
        }
        Text(text = "scrollState.value: ${
     scrollState.value}")
    }
}

注意: scrollable的滚动位置范围为0~MAX_VALUE, 默认当手指在组件上向右滑动时,滚动位置会增大向左滑动时,滚动位置会减小,直到减小到0。 由于滚动位置默认初始值为0,所以默认我们只能向右滑来增大滚动位置。如果将reverseDirection参数设置为true时,那么此时手指向左滑滚动位置会增大,向右滑滚动位置会减小。

因此这里将reverseDirection设为true允许我们从初始位置向左滑以查看Row组件右侧超出屏幕的内容部分。

补充提示: 在使用 rememberScrollState 创建 ScrollState 实例时我们是可以通过 initial 参数来指定组件初始滚动位置的

class ScrollState(initial: Int) : ScrollableState {
   
  var value: Int by mutableStateOf(initial, structuralEqualityPolicy())
  	private set
  suspend fun animateScrollTo(...)
  suspend fun scrollTo(...)
  ...
}

上面的代码运行后我们会发现,当进行左滑时,原本位于屏幕外的内容进入屏幕时右边出现一片空白,这是因为Row组件的默认测量策略导致超出屏幕的子组件宽度测量结果为零。

在这里插入图片描述

此时需要使用layout修饰符来自定义布局,我们需要创建一个新的约束,用于测量组件的真实宽度,主动设置组件应有的宽高尺寸,并根据组件的滚动偏移量来摆放组件内容。

@Composable
fun ScrollableExample2() {
   
    Column(Modifier.padding(10.dp)) {
   
        val scrollState = rememberScrollState()
        Row(
            Modifier
                .border(BorderStroke(1.dp, Color.Blue))
                .height(50.dp)
                .clipScrollableContainer(Orientation.Horizontal) // 留出父组件设置的padding空间
                .scrollable(scrollState, Orientation.Horizontal, reverseDirection = true)
                .layout {
    measurable, constraints ->
                    println("constraints: $constraints")
                    // 约束中默认最大宽度为父组件所允许的最大宽度,此处为屏幕宽度
                    // 将最大宽度设置为无限大
                    val childConstraints = constraints.copy(
                        maxWidth = Constraints.Infinity
                    )
                    println("childConstraints: $childConstraints")
                    val placeable = measurab
  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值