☰
处理用户输入
2026/10/5 16:18:25 网站建设 项目流程

看下面的代码:

@ComposablefunPointerInputEvent(){Box(modifier=Modifier.requiredSize(200.dp).background(Color.Blue).pointerInput(Unit){awaitPointerEventScope{while(true){valevent=awaitPointerEvent()Log.d("PointerInputEvent","event:${event.type}")}}})}

当你在 Box 所在的蓝色区域内按下拖动然后抬起鼠标,会收到如下打印:

2026-04-19 21:49:34.607 26743-26743 PointerInputEvent com.example.composetest D event: Press 2026-04-19 21:49:34.772 26743-26743 PointerInputEvent com.example.composetest D event: Move 2026-04-19 21:49:35.023 26743-26743 PointerInputEvent com.example.composetest D event: Move 2026-04-19 21:49:35.286 26743-26743 PointerInputEvent com.example.composetest D event: Release

这里封装了一个 Box,在 Box 里面调用了 Modifier.pointerInput,代码如下:

funModifier.pointerInput(key1:Any?,block:PointerInputEventHandler):Modifier=thisthenSuspendPointerInputElement(key1=key1,pointerInputEventHandler=block)

pointerInput 函数的作用是:在当前组件区域内处理 Pointer Input(指针输入)事件,它可以传入一个 key1 参数,当 key1 改变时,block 会被取消,并重新执行(重新开始监听)。

block 是一个 PointerInputEventHandler, PointerInputEventHandler 代码如下:

funinterfacePointerInputEventHandler{// ↑ 函数式接口:只有一个抽象方法,可以直接用 lambda 简写suspendoperatorfunPointerInputScope.invoke()// ↑ ↑ ↑ ↑ ↑// 挂起函数 运算符重载 扩展函数 接收者类型 方法名}

它是一个函数式接口(也称为 SAM 接口,即 Single Abstract Method),函数式接口是只有一个抽象方法的接口,可以使用 Lambda 表达式来实例化它。

Modifier.pointerInput(Unit){...}// lambda 写法

invoke 是 kotlin 内置 “魔法函数名”,任何类或接口只要实现了 operator fun invoke(),它的实例就能直接当函数用。比如,原来你要这样写:

// 定义valhandler=PointerInputEventHandler{...}// 使用handler.invoke()

现在只需要这样写就可以了:

// 定义valhandler=PointerInputEventHandler{...}// 使用handler()

PointerInputScope.invoke() 是扩展函数接收者,意思是 lambda 内部可以直接访问 PointerInputScope 的成员:

Modifier.pointerInput(Unit){// 这里的 this 是 PointerInputScope,可以直接用它的成员size// 组件尺寸awaitPointerEvent()// 等待指针事件awaitFirstDown()// 等待按下currentEvent// 当前事件}

PointerInputScope 代码如下:

interfacePointerInputScope:Density{valsize:IntSizevaldepth:Int...suspendfun<R>awaitPointerEventScope(block:suspendAwaitPointerEventScope.()->R):R}

这里 awaitPointerEventScope 会挂起并等待指针事件。

同一个 PointerInputScope 里可以用 launch 并发跑多个 awaitPointerEventScope:

Modifier.pointerInput(Unit){launch{awaitPointerEventScope{while(true){valevent=awaitPointerEvent()// 处理拖动}}}launch{awaitPointerEventScope{while(true){valevent=awaitPointerEvent()// 处理点击}}}}

多个 block 按安装的顺序进行事件派发。

awaitPointerEvent 也是一个挂起函数,代码如下:

suspend fun awaitPointerEvent(pass: PointerEventPass = PointerEventPass.Main): PointerEvent

它会挂起直到一个 PointerEvent 被上报给 pass(阶段),pass 默认是 PointerEventPass.Main,什么是 PointerEventPass(三个阶段),一个指针事件在组件树里会按三个 pass 依次传递:

Pass方向含义
Initial祖先 → 后代(自顶向下)最外层先看到
Main后代 → 祖先(自底向上)最内层先处理,默认
Final后代 → 祖先(自底向上)只看未被消费的事件

pass 参数可以用于选你在哪个阶段接收事件。

在等下一个事件之前,你可以修改返回的 PointerEvent 来 “消费” 事件:

while(true){valevent=awaitPointerEvent()event.changes.forEach{change->if(change.pressed){change.consume()// ← 消费:标记事件已被处理}}// 消费后,下一阶段(Final pass)的其他 handler 就收不到这个事件了}

消费后,下一阶段(Final pass)的其他 handler 就收不到这个事件了。消费(consume)机制图解

事件到达 │ ▼ Initial pass(祖先先看) │ ▼ Main pass(后代处理,默认这里) │ ← 你在这里 change.consume() ▼ Final pass(只收到未消费的事件) │ ← 被消费后,这里收不到 ▼ 结束

经典用法:

Modifier.pointerInput(Unit){awaitPointerEventScope{while(true){valevent=awaitPointerEvent()// 等事件(默认 Main)event.changes.forEach{change->if(change.pressed){change.consume()// 消费,防止泄漏给父组件}}}}}

使控件可点击、滚动、拖拽

使用 pointerInput 函数来处理用户输入是比较偏底层的,就像是在 View 系统中处理 TouchEvent 一样。

事实上,我们很多时候并不需要使用这么底层的 API。Modifier 提供了很多的上层 API 来处理诸如点击、滚动、拖拽等用户输入事件。

首先看点击。事实上,有些控件默认就是可以点击的,如 Button。而有些则不能,如 Box。

让一个默认不能点击的控件变得可以点击,并不一定非要使用 pointerInput 函数,clickable 函数也能做到,并且代码会更加简洁。

@ComposablefunHighLevelCompose(){valcontext=LocalContext.currentBox(modifier=Modifier.requiredSize(200.dp).background(Color.Blue).clickable{Toast.makeText(context,"Box is clicked",Toast.LENGTH_SHORT).show()})}

再来看拖拽。draggable 函数允许让一个控件在水平或垂直方向上拖拽,并可以监听用户的拖拽距离,我们再根据返回的拖拽距离对控件进行相应的偏移,就可以实现拖拽效果了。

@ComposablefunHighLevelCompose(){varoffsetXbyremember{mutableStateOf(0f)}Box(modifier=Modifier.offset{IntOffset(offsetX.roundToInt(),0)}.requiredSize(200.dp).background(Color.Blue).draggable(orientation=Orientation.Horizontal,state=rememberDraggableState{delta->offsetX+=delta}))}

不过 draggable 函数有一个弊端,它只能允许控件在水平或垂直方向上拖拽,不可以同时在水平和垂直方向上拖拽。所以如果你有这种特殊需求的话,那么就可以使用更加底层的 pointerInput 函数来实现:

@ComposablefunHighLevelCompose(){varoffsetXbyremember{mutableStateOf(0f)}varoffsetYbyremember{mutableStateOf(0f)}Box(modifier=Modifier.offset{IntOffset(offsetX.roundToInt(),offsetY.roundToInt())}.requiredSize(200.dp).background(Color.Blue).pointerInput(Unit){detectDragGestures{change,dragAmount->change.consume()offsetX+=dragAmount.x offsetY+=dragAmount.y}})}

在 pointerInput 函数内部,我们调用 detectDragGestures 来监听用户的拖拽手势,这样就可以同步获得用户在水平和垂直方向上的拖拽距离,并对控件进行相应的偏移了。

另外要记得,由于这是底层API,所以很多事情要自己做,比如事件处理完了,要记得调用 consume 函数将它消费掉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询