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

相关推荐
2501_9462309825 分钟前
Cordova&OpenHarmony外观主题设置
android·javascript
小韩博1 小时前
小迪之盲注第44课
android·网络安全·adb
夏沫琅琊2 小时前
Android TestDPC 工程详解
android
键来大师2 小时前
Android16 AP热点修改默认密码为12345678
android·framework·rk3576·android16
李坤林2 小时前
Android KGI (Generic Kernel Image)
android
十二测试录2 小时前
Android和iOS测试区别
android·经验分享·ios·职场发展·ab测试
柒许宁安2 小时前
在 Cursor 中运行 Android 项目指南
android·java·个人开发
技术小甜甜3 小时前
【Godot】【入门】GDScript 快速上手(只讲游戏里最常用的 20% 语法)
android·游戏·编辑器·游戏引擎·godot
aqi003 小时前
FFmpeg开发笔记(九十五)国产的开源视频美颜工具VideoEditorForAndroid
android·ffmpeg·音视频·直播·流媒体
sanggou4 小时前
基于Java实现的简易规则引擎(日常开发难点记录)
android·java