Jetpack Compose | Modifier手势相关修饰符的使用(二)

单击、双击、长按事件

Compose 中实现点击事件很简单,直接通过Modifier.clickable{} 即可,示例:

kotlin 复制代码
@Composable
fun ClickSample() {
    var count by remember { mutableStateOf(0) }
    Box(
        modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center
    ) {
        Text(fontSize = 20.sp,
            textAlign = TextAlign.Center,
            text = count.toString(),
            modifier = Modifier
                .size(100.dp)
                .clickable { count += 1 }
        )
    }
}

每次点击Text,对应的count 都会进行自增1,上述示例使用的是Text控件,如果是 Button,不用再使用Modifier.clickable{}了,直接使用内部的onClick即可:

kotlin 复制代码
Button(onClick = { ... }) {
    Text(text = "点击Button")
}

detectTapGestures

除了单击事件外,还可以处理双击、长按等事件:

java 复制代码
suspend fun PointerInputScope.detectTapGestures(
    onDoubleTap: ((Offset) -> Unit)? = null,
    onLongPress: ((Offset) -> Unit)? = null,
    onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
    onTap: ((Offset) -> Unit)? = null
){ ... }
  • onPress :每次点击都会回调,相当于View体系中的ACTION_DOWN,其中offset是回调的点击位置(x, y);
  • onTap:轻触时回调;
  • onDoubleTap: 双击时回调;
  • onLongPress:长按时回调,其设定阈值默认是400ms。

使用示例:

kotlin 复制代码
Modifier.pointerInput(Unit) {
    detectTapGestures(
         onPress = { offset -> log("onPress: $offset") },
         onTap = { offset -> log("onTap: $offset") },
         onDoubleTap = { offset -> log("onDoubleTap: $offset") },
         onLongPress = { offset -> log("onLongPress: $offset") },
    )
}

几个场景

1、快速点击并松手:

kotlin 复制代码
17:21:12.117  E  onPress: Offset(28.5, 29.5)
17:21:12.468  E  onTap: Offset(28.5, 29.5)

2、双击:

kotlin 复制代码
17:23:16.060  E  onPress: Offset(31.5, 27.5)
17:23:16.241  E  onPress: Offset(38.5, 25.5)
17:23:16.308  E  onDoubleTap: Offset(38.5, 25.5)

3、长按:

kotlin 复制代码
17:23:41.329  E  onPress: Offset(12.5, 62.5)
17:23:41.634  E  onLongPress: Offset(12.5, 62.5)

Scroll 滚动

  • verticalScrollhorizontalScroll 修饰符让用户在元素内容边界大于最大尺寸约束时滚动元素,跟View体系里的 ScrollViewNestedScrollView 是一样的效果。这里需要注意一点,对于长列表场景,我们可以使用 LazyColumn 与 LazyRow 组件来实现,而对于一般组件,可以通过xxxScroll() 使其具有滚动能力。

来看一个官方示例:

kotlin 复制代码
@Composable
fun ScrollBoxes() {
    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
            .verticalScroll(rememberScrollState())
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier.padding(2.dp))
        }
    }
}

执行效果:

如果想在首次重组时滑动到某个位置上,可以使用rememberScorllState:

kotlin 复制代码
// Smoothly scroll 100px on first composition
val state = rememberScrollState()
LaunchedEffect(Unit) { state.animateScrollTo(100) }
  • scrollable 修饰符与滚动修饰符(verticalScroll、horizontalScroll)不一样,scrollable修饰符只会检测滚动手势,而不会偏移其内容
kotlin 复制代码
@Composable
fun ScrollSample() {
    var offset by remember { mutableStateOf(0f) }
    //指定ScrollableState 每次滚调时会回调,增量delta以px像素为单位
    val scrollState = rememberScrollableState { delta ->
        offset += delta
        delta
    }

    Box(
        contentAlignment = Alignment.Center,
        modifier = Modifier
            .size(150.dp)
            .scrollable(orientation = Orientation.Vertical, state = scrollState)
            .background(Color.LightGray),
    ) {
        Text(text = offset.toString())
    }
}

当竖直滑动时,Text 中一直会展示 offset 偏移值。

drag 拖动

draggable 修饰符是向单一方向(横向or纵向)拖动手势,draggable 与 scrollable类似,仅仅检测手势,如果还需要移动元素,考虑添加offset修饰符。

kotlin 复制代码
//1、控制横向or纵向drag拖拽
//注:draggable 与 scrollable类似,仅仅检测手势,如果还需要移动元素,考虑添加offset修饰符
var offsetX by remember { mutableStateOf(0f) }
val draggableState = rememberDraggableState(
    onDelta = { delta -> offsetX += delta }
)
Text(text = "Drag me!", modifier = Modifier
     .fillMaxWidth()
     .background(Color.Gray)
     .offset { IntOffset(offsetX.roundToInt(), 0) }
     .draggable(
         orientation = Orientation.Horizontal, state = draggableState
     ))

因为添加了Modifier.offset 修饰符,此时拖动Text ,Text 文字会在内部横向进行滑动。

detectDragGestures

如果需要控制整个拖动手势,可以通过 Modifier.pointerInput来检测:

kotlin 复制代码
Box(modifier = Modifier.fillMaxSize()) {
    var offsetX by remember { mutableStateOf(0f) }
    var offsetY by remember { mutableStateOf(0f) }

    Box(
        Modifier
            .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
            .background(Color.Blue)
            .size(50.dp)
            .pointerInput(Unit) {
                detectDragGestures { change, dragAmount ->
                    change.consumeAllChanges()
                    offsetX += dragAmount.x
                    offsetY += dragAmount.y
                }
            }
    )
}

//detectDragGestures
suspend fun PointerInputScope.detectDragGestures(
    onDragStart: (Offset) -> Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
) { ... }

执行结果:

detectDragGestures扩展函数内部通过onDrag(PointerInputChange, Offset)实现拖拽回调,得到x、y轴的差值,进而通过offset 移动元素。

swipe 滑动

swipe 与 drag 很相似,不同的是,swipe支持设置锚点和阈值。当滑动到阈值时,控件可以自行滑动到目标终点。swipeable 修饰符中的参数如下:

kotlin 复制代码
fun <T> Modifier.swipeable(
    state: SwipeableState<T>,
    anchors: Map<Float, T>,
    orientation: Orientation,
    enabled: Boolean = true,
    reverseDirection: Boolean = false,
    interactionSource: MutableInteractionSource? = null,
    thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) },
    resistance: ResistanceConfig? = resistanceConfig(anchors.keys),
    velocityThreshold: Dp = VelocityThreshold
){ ... }
  • state: SwipeableState<T>:用于跟踪滑动的状态。SwipeableState 包含有关当前滑动位置、速度等信息的状态。
  • anchors: Map<Float, T>:定义了滑动的锚点。Map 的键是锚点的位置,key表示偏移量(单位是Px),value是状态(T 类型)。
  • orientation: Orientation:指定滑动的方向,可以是 Orientation.Horizontal 或 Orientation.Vertical。

上面三个参数是必须要设置的。

  • thresholds: (from: T, to: T) -> ThresholdConfig:用于定义滑动的阈值配置。默认情况下,使用固定的阈值(FixedThreshold(56.dp))。可以根据 from 和 to 的状态值来自定义阈值配置。

ThresholdConfig阈值有两个具体实现: 1、FixedThreshold(private val offset: Dp)来设置具体偏移值,thresholds默认就是 { _, _ -> FixedThreshold(56.dp) }; 2、FractionalThreshold(fraction: Float)来设置偏移比例, 其中fraction的取值范围是[0.0, 1.0]。 当滑动超过阈值时,松手,滑块也会自动吸附到目标状态。如下设置中,默认是CLOSE状态,当滑动超过20%时会自动滑动到OPEN状态;反之,当前是OPEN状态,需要滑动超过30%时才会自动滑动到CLOSE状态。

  • enabled: Boolean:指定是否启用滑动手势。默认为 true。
  • reverseDirection: Boolean:指定是否允许反向滑动。默认为 false,即只能在指定方向上滑动。
  • interactionSource: MutableInteractionSource?:用于指定提供交互事件的 MutableInteractionSource 实例。
  • resistance: ResistanceConfig?:用于定义滑动的阻力配置。默认情况下,使用锚点的位置来设置阻力。
  • velocityThreshold: Dp:用于指定触发滑动的速度阈值。默认为 VelocityThreshold,是一个常量,表示触发滑动的默认速度阈值。

上面介绍了各个参数的意义,其中state、anchors锚点、orientation方向、thresholds阈值是通常要设置的,来看使用示例:

kotlin 复制代码
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SwipeableSample() {
    val width = 144.dp
    val squareSize = 48.dp

    val swipeableState = rememberSwipeableState(Switch.CLOSE)
    //LocalDensity.current获取当前组合中的像素密度,进而进行dp->px的转换 ,px=dp*density
    val sizePx = with(LocalDensity.current) { squareSize.toPx() }
    //每个状态都对应一个锚点,锚点以键值对进行表示:key表示偏移量(单位是Px),value是状态
    //如下设置:偏移量为0f时表示的是CLOSE状态,而偏移96dp时表示的是OPEN状态
    val anchors =
        mapOf(0f to Switch.CLOSE, sizePx * 2 to Switch.OPEN) // Maps anchor points (in px) to states

    Box(
        modifier = Modifier
            .width(width)
            .swipeable(
                state = swipeableState,
                anchors = anchors,
                thresholds = { from, to ->
                    //from、to都表示的是anchors中设置的状态,这里表示的是CLOSE/OPEN状态。
                    if (from == Switch.CLOSE) {
                        FractionalThreshold(0.2f)
                    } else {
                        FractionalThreshold(0.3f)
                    }
                },
                orientation = Orientation.Horizontal
            )
            .background(Color.LightGray)
    ) {
        Box(
            Modifier
                .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
                .size(squareSize)
                .background(Color.Red)
        )
    }
}

注:在Compose中,LocalDensity.current 是一个 CompositionLocal 对象,用于获取当前组合(Composition)中的屏幕像素密度(density)。屏幕像素密度通常以"DPI"(每英寸点数)为单位。

LocalDensity.current 的主要作用是提供当前组合中的屏幕像素密度,以便在编写UI时进行适当的尺寸和布局调整,以适应不同的屏幕密度。这可以确保应用在不同设备上有一致的外观和布局。以下两种写法都可以将dp转换为px:

kotlin 复制代码
val squareSize = 48.dp
//方式一:直接调用Density.toPx()方法
val sizePx = with(LocalDensity.current) { squareSize.toPx() }

//方式二:获取当前屏幕像素密度,然后自行计算
val density = LocalDensity.current.density
val sizePx = squareSize * density

多点触控

transformer 修饰符用于检测平移、缩放和旋转等多触控手势transformer 本身不会旋转元素,只会检测手势。

kotlin 复制代码
@Composable
fun rememberTransformableState(
    onTransformation: (zoomChange: Float, panChange: Offset, rotationChange: Float) -> Unit
): TransformableState { ... }
}

rememberTransformableState() 可以获取 TransformableState 实例,通过lambda回调 获取到双指拖动、缩放、旋转等手势信息,进而进行相应的操作即可。

使用示例:

kotlin 复制代码
@Composable
fun TransformableSample() {
    var scale by remember { mutableStateOf(1f) }
    var rotationAngle by remember { mutableStateOf(0f) }
    var offset by remember { mutableStateOf(Offset.Zero) }

    val state = rememberTransformableState(onTransformation = { zoomChange, offsetChange, rotationChange ->
            scale *= zoomChange
            offset += offsetChange
            rotationAngle += rotationChange
        })
        
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        Box(
            modifier = Modifier
                .size(150.dp)
                .rotate(rotationAngle)
                .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
                .scale(scale)
                .transformable(state = state)
                .background(Color.Blue)
        )
    }
}

detectTransformGestures

上述的 transformable() 可以使用detectTransformGestures 进行替换,使用如下:

java 复制代码
Modifier.pointerInput(Unit) {
      detectTransformGestures(
         //如果panZoomLock为true,则只有在平移或缩放运动之前检测到旋转的触摸倾斜时才允许旋转。否则,将检测平移和缩放手势,但不会检测旋转手势。
         //如果panZoomLock为false,所有三种手势都被检测,默认是false。
         panZoomLock = false,
         onGesture = { centroid: Offset, pan: Offset, zoom: Float, rotation: Float ->
             offset += pan
             scale *= zoom
             rotationAngle += rotation
       })
}

总结

下面几个修饰符只负责检测手势,如果还需要移动元素,考虑添加offset 修饰符:

  • scrollable 修饰符只检测滚动手势,不会偏移其内容。
  • draggable 修饰符是向单一方向(横向or纵向)拖动手势,仅仅检测手势。
  • swipeable 修饰符是像一个方向滑动,此修饰符不会移动元素,而只检测手势。

资料

【1】Compose 点击、滑动、拖动、多点触控等:https://developer.android.com/jetpack/compose/touch-input/gestures?hl=zh-cn

相关推荐
Estar.Lee17 分钟前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
温辉_xh39 分钟前
uiautomator案例
android
工业甲酰苯胺2 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做3432 小时前
Android 不同情况下使用 runOnUiThread
android·java
Estar.Lee3 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯4 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey5 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!7 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟8 小时前
Android音频采集
android·音视频
小白也想学C9 小时前
Android 功耗分析(底层篇)
android·功耗