本节摘要:Android 的事件处理建立在监听器模式上——现在注册、将来回调。本节拆开 setOnClickListener 看清接口本质,追踪一次点击从触控硬件到回调代码的全链路,并讲清返回值"消费"语义与内存泄漏这两个原理级话题。
前几章已经写过好几遍 setOnClickListener { doGuess() },现在是拆开它的时机。去掉语法糖,完整的写法长这样:
binding.btnGuess.setOnClickListener(object : View.OnClickListener { override fun onClick(v: View) { doGuess() } })
看清三件事:OnClickListener 是一个只含一个方法的接口;setOnClickListener 把这个接口的实现交给按钮保管;你的 onClick 方法此刻不执行,等用户真点下去才被调用。lambda 只是这段样板的地道缩写。这就是监听器模式的两拍:注册在先(你说"有事叫我"),回调在后(系统在事件发生时叫你)。想通"花括号里的代码是未来时",异步编程的大门就开了一条缝——协程、网络回调、生命周期观察者,全是这一拍一拍的变奏。
View 身上挂着一整面监听器注册表,各管一类事件:
binding.etGuess.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence?, a: Int, b: Int, c: Int) {} override fun onTextChanged(s: CharSequence?, a: Int, b: Int, c: Int) { binding.btnGuess.isEnabled = !s.isNullOrBlank() } override fun afterTextChanged(s: Editable?) {} })
输入框一有风吹草动,"提交"按钮随之亮起或变灰——这个"空输入禁用按钮"的细节,是表单可用性的基本功。注意 TextWatcher 有三个方法且接口不止一个方法,Kotlin 的 lambda 单接口缩写不再适用,得老实写 object,空方法留着不删。
手指触下到 doGuess 执行,中间隔着一整条流水线:触控驱动把物理按压变成事件流,经系统服务投递到应用进程的队列,ViewRootImpl 沿视图树自上而下派发,最终到达目标按钮;按钮内置的手势识别器把"按下、抬起、没怎么移动"判定为单击,才轮到你的 onClick。理解这条链的收益在排错时刻兑现——回调里的代码每执行一次,背后都对应一次真实的派发,如果你发现 onClick 被连调两次,嫌疑不在链上,而多半是监听器被重复注册(比如在会多次执行的回调里又 set 了一遍)。
派发顺序还有一条实用规则:onTouch 先于 onClick。这直接引出返回值语义:
binding.ivFace.setOnTouchListener { v, event -> when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { v.performClick(); true } else -> false } }
返回 true 表示"事件到此为止,我消费了",后续的单击识别不会再发生;返回 false 则事件继续流动,交给按钮内置的点击识别。5.2 节会把这层"谁消费谁负责"展开成完整规则。

监听器是匿名对象,天然持有创建它的外部引用。Activity 里注册的监听器被一个比 Activity 活得更久的东西保管时(比如放在单例或全局缓存里的对象),Activity 销毁后监听器还拽着它不放,内存泄漏就发生了。入门阶段的守则简单:监听器在哪个组件里注册,就随哪个组件生灭——在 Activity 的 onCreate 里 set 的,不交给外部长期保管,就不会出事。反向场景更常见:在监听器里启动协程或注册广播接收器,组件销毁时记得反注册,第 8 章调试内存问题时会再遇到这笔账。
监听器能不能动态换? 能,setOnClickListener 后调再传 null 就是注销,新监听器直接覆盖旧的。这个特性在"按钮状态机"场景很实用:提交按钮在游戏进行中执行猜测、结束后执行重开,同一个控件按状态切换监听器,比在同一个回调里写一堆 if 分支清晰。
一个事件真的只发给一个控件吗? 不一定,这才有了"事件分发"这个词的分量。触摸事件先沿视图树自上而下"询问"(onInterceptTouchEvent 钩子就在这条路上),再自下而上"上报",父子控件可以协商谁来处理——ScrollView 之所以能拦截子控件的手势开始滚动,靠的就是这条协商链。入门阶段不必深挖源码,但知道"事件是一段可协商的旅程"而非"一枚直达的邮件",遇到列表里按钮点不动这类嵌套冲突时,你才知道该往哪个方向找答案。
补一个训练方法:把本章所有写过的监听器收进一张表,列出控件、事件类型、注册方法、回调签名、返回值语义五列。表格填完你会发现所谓"事件处理"就是这张表的反复实例化——往后遇到新控件,第一反应是查它有哪些监听器可注册,而不是从头发明交互。