单击、双击、长按事件
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 滚动
verticalScroll
和horizontalScroll
修饰符让用户在元素内容边界大于最大尺寸约束时滚动元素,跟View体系里的ScrollView
或NestedScrollView
是一样的效果。这里需要注意一点,对于长列表场景,我们可以使用 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