Compose 修饰符 - 添加交互(手势)

Compose 修饰符 - 添加交互(手势) 官方页面一、概念顶层API是那些自带手势处理的组件高级别API涵盖最常用手势可以向任意组件添加修饰符低级别API提供最大灵活性来自定义。顶层API自带手势处理的组件Button的点击LazyCloumn的拖动。高级别API点按clickable()、combinedClickable滚动verticalScroll()、horizontalScroll()scrollable()、scrollable2D()拖动draggable()、draggable2D()滑动anchoredDraggable()多点触控transformable()低级别API手势监听pointerInput()二、点按 clickable()允许应用检测对该元素的点击。单击fun Modifier.clickable(enabled: Boolean true,onClickLabel: String? null,role: Role? null,onClick: () - Unit)简单使用。fun Modifier.clickable(interactionSource: MutableInteractionSource,indication: Indication?,enabled: Boolean true,onClickLabel: String? null,role: Role? null,onClick: () - Unit)将 indication null, 并将 val interactionSource remember { MutableInteractionSource() } 传入可去除涟漪效果。通过 val isPressed by interactionSource.collectIsPressedAsState() 可以拿到按压/释放状态。长按、双击fun Modifier.combinedClickable(enabled: Boolean true,onClickLabel: String? null,role: Role? null,onLongClickLabel: String? null,onLongClick: (() - Unit)? null,onDoubleClick: (() - Unit)? null,onClick: () - Unit)单击的回调是必须的。Composable fun ClickableSample() { val count remember { mutableStateOf(0) } Text( text count.value.toString(), modifier Modifier.clickable { count.value 1 } ) }2.1 去除点击涟漪封装fun Modifier.clickableNoRipple(onClick: () - Unit) composed { this.then( Modifier.clickable( onClick onClick, interactionSource remember { MutableInteractionSource() }, indication null ) ) }2.2 双击防抖封装fun Modifier.clickableSafety( duration: Long 1000L, onClick: () - Unit ) composed { var lastClick by remember { mutableLongStateOf(0L) } this.then( Modifier.clickable( onClick { val currentTime System.currentTimeMillis() if (currentTime - lastClick duration) { onClick() lastClick currentTime } }, interactionSource remember { MutableInteractionSource() }, indication null ) ) }2.3 双击长按防抖封装提供单击回调是必须的。fun Modifier.clickableWithDoubleClick( onSingleClick: () - Unit, onDoubleClick: () - Unit ) then( Modifier.combinedClickable( onClick onSingleClick, onDoubleClick onDoubleClick ) ) fun Modifier.clickableWithLongClick( onSingleClick: () - Unit, onLongClick: () - Unit ) then( Modifier.combinedClickable( onClick onSingleClick, onLongClick onLongClick ) )2.4 自带点击回调组件的双击防抖封装针对 Button 这种自带点击回调的可组合项。Composable inline fun onClickSafety ( duration: Long 1000L, crossinline onClick: () - Unit ): () - Unit { var lastClick by remember { mutableLongStateOf(0L) } val currentTime System.currentTimeMillis() return if (currentTime - lastClick duration) { lastClick currentTime { onClick() } } else { {} } }2.5 拿到 按压/释放 状态val interactionSource remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Box( modifier Modifier.clickable { interactionSource interactionSource, indication null } ) {}三、滚动FlingBehavior 定义释放后的惯性滑动。3.1 偏移滚动 verticalScroll()、horizontalScroll()类似与 ScrollView可以让内容大于尺寸时滑动里面的内容。借助 ScrollState 还可以更改滚动位置或获取当前状态。verticalScroll()纵向滚动fun Modifier.verticalScroll(state: ScrollState, //滚动状态enabled: Boolean true, //是否启用flingBehavior: FlingBehavior? null,reverseScrolling: Boolean false, //翻转滚动)horizontalScroll()横向滚动fun Modifier.horizontalScroll(state: ScrollState, //滚动状态enabled: Boolean true, //是否启用flingBehavior: FlingBehavior? null,reverseScrolling: Boolean false, //翻转滚动)Composable fun ScrollBoxes() { val scrollState rememberScrollState() //一显示就会自动滚动100px LaunchedEffect(Unit) { scrollState.animateScrollTo(100) } Column( modifier Modifier .background(Color.LightGray) .size(100.dp) .verticalScroll(scrollState) ) { repeat(10) { Text(Item $it, modifier Modifier.padding(2.dp)) } } }3.2 线性可滚动一维 scrollable()只检测手势不偏移内容。构造时需要提供一个 consumeScrollDelta() 函数该函数在每个滚动步骤都会调用以像素为单位必须返回所消耗的距离来确保在嵌套滚动时可以正确传播事件。fun Modifier.scrollable(state: ScrollableState,orientation: Orientation,enabled: Boolean true,reverseDirection: Boolean false,flingBehavior: FlingBehavior? null,interactionSource: MutableInteractionSource? null,): Modifiervar offset by remember { mutableStateOf(0f) } val scrollableState rememberScrollableState { delta - //拿到每次滑动的偏移量delta offset delta delta //必须返回所消耗的距离 } Box( Modifier .size(150.dp) .background(Color.LightGray) .scrollable( orientation Orientation.Vertical, state scrollableState ), contentAlignment Alignment.Center ) { Text(offset.toString()) }3.3 平面可滚动二维scrollable2D()fun Modifier.scrollable2D(state: Scrollable2DState,enabled: Boolean true,overscrollEffect: OverscrollEffect? null,flingBehavior: FlingBehavior? null,interactionSource: MutableInteractionSource? null,)var offset by remember { mutableStateOf(Offset.Zero) } val scrollableState rememberScrollable2DState { delta - offset delta delta } Box( modifier Modifier .size(150.dp) .background(Color.LightGray) .scrollable2D(scrollableState) ) { Text(offset.toString()) }3.4 嵌套滚动3.4.1 自动嵌套滚动对于可滚动组件如 verticalScroll()、horizontalScroll()、scrollable()、Lazy API、TextField简单的嵌套滚动无需额外操作当子元素无法进一步滚动时手势会由父元素处理滚动增量会自动从子元素传播到父容器。//父Box嵌套10个子Box子Box滚动到边界会滚动父Box Composable fun ScrollableSample() { //设置渐变色方便观察子Box滚动(蓝→黄1000级) val gradient Brush.verticalGradient(0f to Color.Blue, 1000f to Color.Yellow) Box( modifier Modifier .background(Color.LightGray) .verticalScroll(rememberScrollState()) .padding(32.dp) ) { Column { repeat(10) { Box( modifier Modifier .height(128.dp) .verticalScroll(rememberScrollState()) ) { Text( text Scroll here, color Color.Red, modifier Modifier .border(12.dp, Color.DarkGray) .background(brush gradient) .padding(24.dp) .height(150.dp) ) } } } } }3.4.2 nestedScroll()对于不可滚动组件如 Box 或自定义组件滚动增量不会在嵌套滚动中传播可使用该修饰符向其它组件提供支持。3.4.3 嵌套滚动互操作性四、拖拽只检测手势不偏移内容需要自行将检测值设置到需要偏移的内容上以像素为单位。4.1 线性拖拽一维draggable()fun Modifier..draggable(state: DraggableState,orientation: Orientation, //拖动方向enabled: Boolean true, //是否启用interactionSource: MutableInteractionSource? null,startDragImmediately: Boolean false,onDragStarted: suspend CoroutineScope.(startedPosition: Offset) - Unit {}, //拖拽开始时的回调onDragStopped: suspend CoroutineScope.(velocity: Float) - Unit {}, //拖拽结束时的回调reverseDirection: Boolean false //反转方向): Modifier//文字横向拖动 var offsetX by remember { mutableFloatStateOf(0f) } val dragState rememberDraggableState { delta - offsetX delta } Text( modifier Modifier .background(Color.White) .offset { x IntOffset(offsetX.roundToInt(), y 0) } .draggable( orientation Orientation.Horizontal, state dragState ), text 横向拖动 )4.2 平面拖拽二维draggable2D()fun Modifier.draggable2D(state: Draggable2DState,enabled: Boolean true, //是否启用interactionSource: MutableInteractionSource? null,startDragImmediately: Boolean false,onDragStarted: (startedPosition: Offset) - Unit NoOpOnDragStart, //拖拽开始时的回调onDragStopped: (velocity: Velocity) - Unit NoOpOnDragStop, //拖拽结束时的回调reverseDirection: Boolean false, //反转方向): Modifiervar offset by remember { mutableStateOf(Offset.Zero) } val dragState rememberDraggable2DState { delta - //delta可以分别拿到xy轴偏移 offset delta } Text( modifier Modifier .size(100.dp) .background(Color.White) .offset { IntOffset(x offset.x.roundToInt(), y offset.y.roundToInt()) } .draggable2D( state dragState ), text 平面拖动 )五、滑动锚点吸附5.1 swipeable() 已过时是一个 Material API在 Compose v1.6-alpha01 中已被 Foundation 的 anchoreDraggable() 取代。只检测手势不偏移内容需要自行将检测值设置到需要偏移的内容上。具有惯性释放后会朝着锚点呈现动画效果常见用途是滑动关闭。OptIn(ExperimentalMaterialApi::class) Composable fun SwipeableSample() { val swipeableState rememberSwipeableState(0) val sizePx with(LocalDensity.current) { squareSize.toPx() } //DP转PX //设置锚点key是像素value是索引 val anchors mapOf(0f to 0, sizePx to 1) Box( modifier Modifier .width(96.dp) .swipeable( state swipeableState, anchors anchors, //阈值超过就会自己滑到底达不到就会滑回来 thresholds { _, _ - FractionalThreshold(0.3f) }, orientation Orientation.Horizontal ) .background(Color.LightGray) ) { Box( Modifier .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) } .size(48.dp) .background(Color.DarkGray) ) } }5.2 anchoredDraggable()官方页面六、多点触控 transformable()只检测手势不转换元素。平移、缩放、旋转。Composable fun TransformableSample() { var scale by remember { mutableStateOf(1f) } //缩放 var rotation by remember { mutableStateOf(0f) } //旋转 var offset by remember { mutableStateOf(Offset.Zero) } //平移 val state rememberTransformableState { zoomChange, offsetChange, rotationChange - scale * zoomChange rotation rotationChange offset offsetChange } Box( Modifier .graphicsLayer( scaleX scale, //等比缩放 scaleY scale, //等比缩放 rotationZ rotation, translationX offset.x, translationY offset.y ) .transformable(state state) .background(Color.Blue) .fillMaxSize() ) }七、自定义手势 pointerInput()手势检测会挂起协程因此在 .pointerInput() 中只能使用一个手势检测第二个是无法执行到的。如果要使用两个再添加一个 .pointerInput()。三种检测方法底层都是基于 awaitEachGesture()。Modifier.pointerInput(key1: Any?, //kay变化会中断处理(不需要就传Unit)。block: suspend PointerInputScope.() - Unit //手势处理都发生在协程中)PointerInputScopeval size: IntSize通过 size 属性可以拿到该组件的宽高如 size.height。//错误示范 modifier Modifier .pointerInput(Unit) { detectTapGestures {} //后面代码永远不会执行到 detectDragGestures {} } //正确使用 modifier Modifier .pointerInput(Unit) { detectTapGestures {} }.pointerInput(Unit) { detectDragGestures {} }7.1 点击检测detectTapGestures()单击、双击、长按suspend fun PointerInputScope.detectTapGestures(onDoubleTap: ((Offset) - Unit)? null, //双击onLongPress: ((Offset) - Unit)? null, //长按onPress: suspend PressGestureScope.(Offset) - Unit NoPressGesture, //按下时按几次触发几次其它三个回调都会触发这里onTap: ((Offset) - Unit)? null //单击)Box( modifier Modifier.size(200.dp).background(Color.Blue) .pointerInput(Unit) { detectTapGestures( onTap {}, onDoubleTap {}, onPress {}, onLongPress {} ) } )7.2 多点触控检测detectTransformGestures()平移、旋转、缩放suspend fun PointerInputScope.detectTransformGestures(panZoomLock: Boolean false,onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) - Unit)参数 panZoomLock 为true时触发阈值才开始旋转为false更灵敏。参数 onGesture手势回调参数值均为当前手势与上一次回调之间的差值变化量。centroid所有按下触点的质心坐标。pan平移单位像素。zoom缩放单位比例因子。rotation旋转单位度。var offset by remember { mutableStateOf(Offset.Zero) } var rotate by remember { mutableFloatStateOf(0f) } var scale by remember { mutableFloatStateOf(1f) } Box( modifier Modifier.size(300.dp).background(Color.White), contentAlignment Alignment.Center ) { Box( modifier Modifier.size(100.dp).background(Color.Blue) .rotate(rotate) .scale(scale) .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) } .pointerInput(Unit) { detectTransformGestures(false) { centroid, pan, zoom, rotation - offset pan scale * zoom rotate rotation } } ) }7.3 拖拽检测参数onDragCancel触发时机多发生于滑动冲突的场景子组件可能最开始是可以获取到拖动事件的当拖动手势事件达到莫个指定条件时可能会被父组件劫持消费这种场景下便会执行该回调。detectDragGestures()拖动suspend fun PointerInputScope.detectDragGestures(onDragStart: (Offset) - Unit { }, //拖动开始时回调onDragEnd: () - Unit { }, //拖动结束时回调onDragCancel: () - Unit { }, //拖动取消时回调onDrag: (change: PointerInputChange, dragAmount: Offset) - Unit //拖动进行时回调)参数 change该组件内的拖动坐标通过 change.position.y 拿到。参数 dragAmount拖动距离通过 dragAmount.x 和 dragAmount.y 拿到各方向上的拖动距离。detectHorizontalDragGestures()水平拖动suspend fun PointerInputScope.detectHorizontalDragGestures(onDragStart: (Offset) - Unit {},onDragEnd: () - Unit {},onDragCancel: () - Unit {},onHorizontalDrag: (change: PointerInputChange, dragAmount: Float) - Unit)detectVerticalDragGestures垂直拖动suspend fun PointerInputScope.detectVerticalDragGestures(onDragStart: (Offset) - Unit {},onDragEnd: () - Unit {},onDragCancel: () - Unit {},onVerticalDrag: (change: PointerInputChange, dragAmount: Float) - Unit)detectDragGesturesAfterLongPress长按后拖动suspend fun PointerInputScope.detectDragGesturesAfterLongPress(onDragStart: (Offset) - Unit {},onDragEnd: () - Unit {},onDragCancel: () - Unit {},onDrag: (change: PointerInputChange, dragAmount: Offset) - Unit,)//父Box中拖动蓝色子Box Box( modifier Modifier.fillMaxSize(), contentAlignment Alignment.Center ) { var offset by remember { mutableStateOf(Offset.Zero) } Box(Modifier .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) } .background(Color.Blue) .size(50.dp) .pointerInput(Unit) { detectDragGestures { change, dragAmount - offset dragAmount } } ) }7.4 自定义检测参考文章写法过于底层基本没有太多场景我们需要使用。7.4.1 开启作用域forEachGesture() 已过时可能丢失手势间的事件换用 awaitEachGesture()。awaitPointerEventScope()suspend fun R awaitPointerEventScope(block: suspend AwaitPointerEventScope.() - R): R单次处理。协程处理完一轮手势交互后便会结束。awaitEachGesture()suspend fun PointerInputScope.awaitEachGesture(block: suspend AwaitPointerEventScope.() - Unit)持续处理。重复调用 block 来持续处理点击事件确保不同手势周期之间的事件传递不会丢失。底层基于 awaitPointerEventScope() 封装。7.4.2 检测方法awaitPointerEvent()suspend fun awaitPointerEvent(pass: PointerEventPass PointerEventPass.Main): PointerEvent会挂起协程等待发生下一次点击事件。awaitFirstDown()第一次按下suspend fun AwaitPointerEventScope.awaitFirstDown(requireUnconsumed: Boolean true,pass: PointerEventPass PointerEventPass.Main,): PointerInputChange拖拽事件suspend fun AwaitPointerEventScope.drag(pointerId: PointerId,onDrag: (PointerInputChange) - Unit,): Booleansuspend fun AwaitPointerEventScope.horizontalDrag(pointerId: PointerId,onDrag: (PointerInputChange) - Unit,): Boolean水平suspend fun AwaitPointerEventScope.verticalDrag(pointerId: PointerId,onDrag: (PointerInputChange) - Unit,): Boolean垂直单次拖拽事件suspend fun AwaitPointerEventScope.awaitDragOrCancellation(pointerId: PointerId): PointerInputChange?suspend fun AwaitPointerEventScope.awaitHorizontalDragOrCancellation(pointerId: PointerId): PointerInputChange?水平suspend fun AwaitPointerEventScope.awaitVerticalDragOrCancellation(pointerId: PointerId): PointerInputChange?垂直有效拖拽事件suspend fun AwaitPointerEventScope.awaitTouchSlopOrCancellation(pointerId: PointerId,onTouchSlopReached: (change: PointerInputChange, overSlop: Offset) - Unit,): PointerInputChange?suspend fun AwaitPointerEventScope.awaitHorizontalTouchSlopOrCancellation(pointerId: PointerId,onTouchSlopReached: (change: PointerInputChange, overSlop: Float) - Unit,)水平suspend fun AwaitPointerEventScope.awaitVerticalTouchSlopOrCancellation(pointerId: PointerId,onTouchSlopReached: (change: PointerInputChange, overSlop: Float) - Unit,)垂直Modifier.pointerInput(Unit) { awaitEachGesture { val downPointer awaitFirstDown() drag(downPointer.id) { change - val y change.position.y } } }