Java 面向对象如何落到 Android View 体系

点击一个商品卡片、列表滚动后复用条目、主题切换后重新绘制,这些 UI 场景不是一堆互不相干的 API:它们是 Java 对象协作在 Android 界面层的具体结果。

一句话结论:在 Android UI 中,优先用组合表达界面结构、用接口表达可替换行为、仅在确有"是一个"关系时继承 View 或框架基类,才能把生命周期与职责留在正确的对象手里。

先划清版本与事实边界

本文以当前 Android Framework 与 AndroidX RecyclerView 的公开设计为参照;源码实现和私有调用顺序会随 Android 版本变化。文中的 View、ViewGroup、Activity、ContextWrapper、LayoutInflater、OnClickListener 与 RecyclerView.Adapter 是可追踪的源码锚点,不把内部细节当作稳定 API。标为"简化"的代码只说明对象关系,不能直接替代生产实现。

一个按钮从 XML 到业务回调

设想订单页:Activity 显示一个根布局,布局里有商品列表和"提交订单"按钮。谁创建它们,谁持有它们,谁触发回调,结果交给谁?先沿事件走一遍,再回看 Java 概念。
#mermaid-svg-jgLnGzoDD9fZWX2v{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jgLnGzoDD9fZWX2v .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jgLnGzoDD9fZWX2v .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jgLnGzoDD9fZWX2v .error-icon{fill:#552222;}#mermaid-svg-jgLnGzoDD9fZWX2v .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jgLnGzoDD9fZWX2v .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jgLnGzoDD9fZWX2v .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jgLnGzoDD9fZWX2v .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jgLnGzoDD9fZWX2v .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jgLnGzoDD9fZWX2v .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jgLnGzoDD9fZWX2v .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jgLnGzoDD9fZWX2v .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jgLnGzoDD9fZWX2v .marker.cross{stroke:#333333;}#mermaid-svg-jgLnGzoDD9fZWX2v svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jgLnGzoDD9fZWX2v p{margin:0;}#mermaid-svg-jgLnGzoDD9fZWX2v .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jgLnGzoDD9fZWX2v .cluster-label text{fill:#333;}#mermaid-svg-jgLnGzoDD9fZWX2v .cluster-label span{color:#333;}#mermaid-svg-jgLnGzoDD9fZWX2v .cluster-label span p{background-color:transparent;}#mermaid-svg-jgLnGzoDD9fZWX2v .label text,#mermaid-svg-jgLnGzoDD9fZWX2v span{fill:#333;color:#333;}#mermaid-svg-jgLnGzoDD9fZWX2v .node rect,#mermaid-svg-jgLnGzoDD9fZWX2v .node circle,#mermaid-svg-jgLnGzoDD9fZWX2v .node ellipse,#mermaid-svg-jgLnGzoDD9fZWX2v .node polygon,#mermaid-svg-jgLnGzoDD9fZWX2v .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jgLnGzoDD9fZWX2v .rough-node .label text,#mermaid-svg-jgLnGzoDD9fZWX2v .node .label text,#mermaid-svg-jgLnGzoDD9fZWX2v .image-shape .label,#mermaid-svg-jgLnGzoDD9fZWX2v .icon-shape .label{text-anchor:middle;}#mermaid-svg-jgLnGzoDD9fZWX2v .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jgLnGzoDD9fZWX2v .rough-node .label,#mermaid-svg-jgLnGzoDD9fZWX2v .node .label,#mermaid-svg-jgLnGzoDD9fZWX2v .image-shape .label,#mermaid-svg-jgLnGzoDD9fZWX2v .icon-shape .label{text-align:center;}#mermaid-svg-jgLnGzoDD9fZWX2v .node.clickable{cursor:pointer;}#mermaid-svg-jgLnGzoDD9fZWX2v .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jgLnGzoDD9fZWX2v .arrowheadPath{fill:#333333;}#mermaid-svg-jgLnGzoDD9fZWX2v .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jgLnGzoDD9fZWX2v .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jgLnGzoDD9fZWX2v .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jgLnGzoDD9fZWX2v .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jgLnGzoDD9fZWX2v .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jgLnGzoDD9fZWX2v .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jgLnGzoDD9fZWX2v .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jgLnGzoDD9fZWX2v .cluster text{fill:#333;}#mermaid-svg-jgLnGzoDD9fZWX2v .cluster span{color:#333;}#mermaid-svg-jgLnGzoDD9fZWX2v div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jgLnGzoDD9fZWX2v .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jgLnGzoDD9fZWX2v rect.text{fill:none;stroke-width:0;}#mermaid-svg-jgLnGzoDD9fZWX2v .icon-shape,#mermaid-svg-jgLnGzoDD9fZWX2v .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jgLnGzoDD9fZWX2v .icon-shape p,#mermaid-svg-jgLnGzoDD9fZWX2v .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jgLnGzoDD9fZWX2v .icon-shape .label rect,#mermaid-svg-jgLnGzoDD9fZWX2v .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jgLnGzoDD9fZWX2v .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jgLnGzoDD9fZWX2v .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jgLnGzoDD9fZWX2v :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Activity 创建界面
LayoutInflater 解析布局
ViewGroup 根容器
RecyclerView 列表
Button 提交按钮
商品项 View
商品项 View

  • **谁创建:**系统创建 Activity;应用通常通过 setContentView 或 LayoutInflater.inflate 让 LayoutInflater 按 XML 创建 View 与 ViewGroup 实例。代码直接 new 一个自定义 View 也可以。
  • **谁拥有:**根 ViewGroup 持有子 View;Activity 持有当前窗口所展示的根 View 的引用和界面生命周期。RecyclerView 持有可见子项的 View,Adapter 负责为它提供和绑定数据。
  • **谁触发:**触摸分发、点击判定和绘制由 Framework 驱动。业务代码不应自己调用 onDraw()、onMeasure() 或 Activity 生命周期方法。
  • **结果给谁:**点击最终进入注册的 OnClickListener;列表的绑定结果回到 RecyclerView,绘制结果交给窗口和屏幕,而不是由 Adapter 自己"画到屏幕"。

类、对象与组合:一棵真实的 UI 树

View 是所有可见控件的基类,负责自身状态、测量、布局位置和绘制入口。TextView、Button 是它的具体子类。这里的"类"是蓝图,"对象"是运行时真正挂在窗口上的那一个按钮实例。

ViewGroup 也是 View,但额外管理子 View。它并不只是"父类复用代码",而是组合的拥有者:一个 LinearLayout 对象包含多个子 View 对象。界面多半应先问"这个对象是否拥有另一个对象",而不是急着设计继承层级。

源码锚点 回答的问题 创建与持有 何时工作,结果给谁
View 单个元素如何测量、布局、绘制、接收事件? LayoutInflater 或业务代码创建;父 ViewGroup 持有 Framework 在遍历中调用;Canvas 输出交给窗口渲染链路
ViewGroup 子元素如何组成树? 布局膨胀或代码创建;其父容器持有 测量和布局时调度子项;子项的位置和绘制顺序由它组织
LayoutInflater XML 如何变成对象树? LayoutInflater.from(context) 获取并执行;调用方接收根 View inflate 时创建 View;把根对象返回给 Activity、Fragment 或 Adapter
Activity 一个界面在哪个生命周期和窗口内? 系统创建;系统任务与窗口体系持有 系统调用生命周期;Activity 设置内容 View 并把事件结果路由到业务协作对象
ContextWrapper 为什么 Context 能附加环境能力? Activity 经由 ContextThemeWrapper 继承它 包装基础 Context;资源、主题和系统服务通过 Context 解析

这也解释了组合为何常优于继承:一个"订单卡片"不必继承 RecyclerView 才能含有列表;它可以是一个 ViewGroup,内部组合标题、列表和按钮。继承适合稳定的类型身份,例如 PriceBadgeView is a View;组合适合"由什么构成"。

接口与回调:行为可以换,拥有者不用换

Java 接口描述能力,而不是存储实现。View.OnClickListener 就是典型例子:按钮不知道提交订单的业务细节,只知道在点击时调用一个实现了接口的对象。Activity、Fragment、ViewModel 协调器或独立类都可以接收这个结果,只要它实现同一接口或提供等价回调。
#mermaid-svg-BXpJjOAdLYiQvOnA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BXpJjOAdLYiQvOnA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BXpJjOAdLYiQvOnA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BXpJjOAdLYiQvOnA .error-icon{fill:#552222;}#mermaid-svg-BXpJjOAdLYiQvOnA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BXpJjOAdLYiQvOnA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BXpJjOAdLYiQvOnA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BXpJjOAdLYiQvOnA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BXpJjOAdLYiQvOnA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BXpJjOAdLYiQvOnA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BXpJjOAdLYiQvOnA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BXpJjOAdLYiQvOnA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BXpJjOAdLYiQvOnA .marker.cross{stroke:#333333;}#mermaid-svg-BXpJjOAdLYiQvOnA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BXpJjOAdLYiQvOnA p{margin:0;}#mermaid-svg-BXpJjOAdLYiQvOnA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BXpJjOAdLYiQvOnA .cluster-label text{fill:#333;}#mermaid-svg-BXpJjOAdLYiQvOnA .cluster-label span{color:#333;}#mermaid-svg-BXpJjOAdLYiQvOnA .cluster-label span p{background-color:transparent;}#mermaid-svg-BXpJjOAdLYiQvOnA .label text,#mermaid-svg-BXpJjOAdLYiQvOnA span{fill:#333;color:#333;}#mermaid-svg-BXpJjOAdLYiQvOnA .node rect,#mermaid-svg-BXpJjOAdLYiQvOnA .node circle,#mermaid-svg-BXpJjOAdLYiQvOnA .node ellipse,#mermaid-svg-BXpJjOAdLYiQvOnA .node polygon,#mermaid-svg-BXpJjOAdLYiQvOnA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BXpJjOAdLYiQvOnA .rough-node .label text,#mermaid-svg-BXpJjOAdLYiQvOnA .node .label text,#mermaid-svg-BXpJjOAdLYiQvOnA .image-shape .label,#mermaid-svg-BXpJjOAdLYiQvOnA .icon-shape .label{text-anchor:middle;}#mermaid-svg-BXpJjOAdLYiQvOnA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BXpJjOAdLYiQvOnA .rough-node .label,#mermaid-svg-BXpJjOAdLYiQvOnA .node .label,#mermaid-svg-BXpJjOAdLYiQvOnA .image-shape .label,#mermaid-svg-BXpJjOAdLYiQvOnA .icon-shape .label{text-align:center;}#mermaid-svg-BXpJjOAdLYiQvOnA .node.clickable{cursor:pointer;}#mermaid-svg-BXpJjOAdLYiQvOnA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BXpJjOAdLYiQvOnA .arrowheadPath{fill:#333333;}#mermaid-svg-BXpJjOAdLYiQvOnA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BXpJjOAdLYiQvOnA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BXpJjOAdLYiQvOnA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BXpJjOAdLYiQvOnA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BXpJjOAdLYiQvOnA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BXpJjOAdLYiQvOnA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BXpJjOAdLYiQvOnA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BXpJjOAdLYiQvOnA .cluster text{fill:#333;}#mermaid-svg-BXpJjOAdLYiQvOnA .cluster span{color:#333;}#mermaid-svg-BXpJjOAdLYiQvOnA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BXpJjOAdLYiQvOnA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BXpJjOAdLYiQvOnA rect.text{fill:none;stroke-width:0;}#mermaid-svg-BXpJjOAdLYiQvOnA .icon-shape,#mermaid-svg-BXpJjOAdLYiQvOnA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BXpJjOAdLYiQvOnA .icon-shape p,#mermaid-svg-BXpJjOAdLYiQvOnA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BXpJjOAdLYiQvOnA .icon-shape .label rect,#mermaid-svg-BXpJjOAdLYiQvOnA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BXpJjOAdLYiQvOnA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BXpJjOAdLYiQvOnA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BXpJjOAdLYiQvOnA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户触摸
View 事件分发
View 点击判定
OnClickListener 回调
业务处理对象
状态更新
界面请求刷新

button.setOnClickListener(listener) 的调用方创建并注册行为对象,View 保存这个接口引用,Framework 在合适的触摸时机触发,业务处理对象接收点击结果。这里实现了多态 :调用点面对的是 OnClickListener,运行时实际执行的可以是不同的实现。

不要把"接口"误解为只能写匿名类。一个命名策略对象通常更容易测试,也能避免让 Activity 承担所有业务。Kotlin 的 lambda 只是把同样的回调接口写得更紧凑。

继承、抽象类与多态分发

继承让子类获得父类契约和部分实现。ViewGroup extends View 是合理的框架层次:容器首先也是一个可绘制、可测量的 View。抽象类则用于"流程相同、留一个步骤给子类填"的场景。许多 View 绘制扩展点可理解为这个模式:Framework 控制遍历时机,具体 View 在自己的绘制入口补充内容。

多态不是"到处 instanceof"。它是让调用者通过共同父类或接口发消息,由运行时对象选择实现。布局遍历与绘制分发必须分开理解:前者确定每个子 View 的尺寸和位置,后者才把内容交给 Canvas。绘制时,容器只需把子项当作 View 调度;不同的 TextView、图片 View、自定义 View 各自完成对应绘制。
#mermaid-svg-gL0ps5vSJb3Z80rn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gL0ps5vSJb3Z80rn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gL0ps5vSJb3Z80rn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gL0ps5vSJb3Z80rn .error-icon{fill:#552222;}#mermaid-svg-gL0ps5vSJb3Z80rn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gL0ps5vSJb3Z80rn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gL0ps5vSJb3Z80rn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gL0ps5vSJb3Z80rn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gL0ps5vSJb3Z80rn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gL0ps5vSJb3Z80rn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gL0ps5vSJb3Z80rn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gL0ps5vSJb3Z80rn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gL0ps5vSJb3Z80rn .marker.cross{stroke:#333333;}#mermaid-svg-gL0ps5vSJb3Z80rn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gL0ps5vSJb3Z80rn p{margin:0;}#mermaid-svg-gL0ps5vSJb3Z80rn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gL0ps5vSJb3Z80rn .cluster-label text{fill:#333;}#mermaid-svg-gL0ps5vSJb3Z80rn .cluster-label span{color:#333;}#mermaid-svg-gL0ps5vSJb3Z80rn .cluster-label span p{background-color:transparent;}#mermaid-svg-gL0ps5vSJb3Z80rn .label text,#mermaid-svg-gL0ps5vSJb3Z80rn span{fill:#333;color:#333;}#mermaid-svg-gL0ps5vSJb3Z80rn .node rect,#mermaid-svg-gL0ps5vSJb3Z80rn .node circle,#mermaid-svg-gL0ps5vSJb3Z80rn .node ellipse,#mermaid-svg-gL0ps5vSJb3Z80rn .node polygon,#mermaid-svg-gL0ps5vSJb3Z80rn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gL0ps5vSJb3Z80rn .rough-node .label text,#mermaid-svg-gL0ps5vSJb3Z80rn .node .label text,#mermaid-svg-gL0ps5vSJb3Z80rn .image-shape .label,#mermaid-svg-gL0ps5vSJb3Z80rn .icon-shape .label{text-anchor:middle;}#mermaid-svg-gL0ps5vSJb3Z80rn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gL0ps5vSJb3Z80rn .rough-node .label,#mermaid-svg-gL0ps5vSJb3Z80rn .node .label,#mermaid-svg-gL0ps5vSJb3Z80rn .image-shape .label,#mermaid-svg-gL0ps5vSJb3Z80rn .icon-shape .label{text-align:center;}#mermaid-svg-gL0ps5vSJb3Z80rn .node.clickable{cursor:pointer;}#mermaid-svg-gL0ps5vSJb3Z80rn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gL0ps5vSJb3Z80rn .arrowheadPath{fill:#333333;}#mermaid-svg-gL0ps5vSJb3Z80rn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gL0ps5vSJb3Z80rn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gL0ps5vSJb3Z80rn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gL0ps5vSJb3Z80rn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gL0ps5vSJb3Z80rn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gL0ps5vSJb3Z80rn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gL0ps5vSJb3Z80rn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gL0ps5vSJb3Z80rn .cluster text{fill:#333;}#mermaid-svg-gL0ps5vSJb3Z80rn .cluster span{color:#333;}#mermaid-svg-gL0ps5vSJb3Z80rn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gL0ps5vSJb3Z80rn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gL0ps5vSJb3Z80rn rect.text{fill:none;stroke-width:0;}#mermaid-svg-gL0ps5vSJb3Z80rn .icon-shape,#mermaid-svg-gL0ps5vSJb3Z80rn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gL0ps5vSJb3Z80rn .icon-shape p,#mermaid-svg-gL0ps5vSJb3Z80rn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gL0ps5vSJb3Z80rn .icon-shape .label rect,#mermaid-svg-gL0ps5vSJb3Z80rn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gL0ps5vSJb3Z80rn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gL0ps5vSJb3Z80rn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gL0ps5vSJb3Z80rn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ViewRootImpl 遍历入口
测量与布局阶段
绘制阶段
ViewGroup 绘制分发
View 子项引用
TextView 绘制实现
ImageView 绘制实现
PriceBadgeView 绘制实现
Canvas 输出

实际 Framework 调用涉及 measure、layout、draw 等多个阶段。ViewRootImpl 的遍历中,测量和布局阶段先由容器协商子项尺寸、写入位置;随后绘制阶段从根 View 开始,ViewGroup 再通过自己的绘制分发把子项作为 View 交给具体实现。上图只把后半段展开为多态关系,不能读成"布局遍历直接调用子项绘制"。不要在业务中直接调用生命周期式回调来强行刷新。数据变更后,按 View API 请求重新布局或重绘,例如视情况调用 requestLayout()、invalidate(),让 Framework 选择正确时机。

一个可替换的绘制策略:接口 + 组合 + 自定义 View

下面是可直接改造成项目代码的 Kotlin 练习。MeterView 是 View 的子类,因此继承只承担"它是一个 View"的身份;颜色计算是可替换的 RenderPolicy,因此用接口和组合承载变化。

kotlin 复制代码
// 简化示例:可放入 Android 项目后继续完善
interface RenderPolicy {
    fun colorFor(value: Int): Int
}

class ThresholdPolicy(private val warningAt: Int) : RenderPolicy {
    override fun colorFor(value: Int): Int =
        if (value >= warningAt) Color.RED else Color.GREEN
}

class MeterView(context: Context) : View(context) {
    var value: Int = 0
        set(newValue) {
            field = newValue.coerceIn(0, 100)
            invalidate()
        }

    var renderPolicy: RenderPolicy = ThresholdPolicy(warningAt = 80)
        set(newPolicy) {
            field = newPolicy
            invalidate()
        }

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        paint.color = renderPolicy.colorFor(value)
        canvas.drawRect(0f, 0f, width * value / 100f, height.toFloat(), paint)
    }
}

Activity 或 Fragment 创建 MeterView 后拥有它的配置引用;View 自己持有 RenderPolicy;数据层把新数值交给 value;invalidate() 请求 Framework 在下一次合适绘制中回调 onDraw。策略返回颜色,View 接收结果并负责画到自己的 Canvas。策略没有持有 View 或 Activity,因此更容易复用和单测。

**刻意省略:**示例没有实现 onMeasure 的最小尺寸与 MeasureSpec 协商,没有 contentDescription、无障碍状态和键盘操作,没有 XML 属性解析,也没有观察者订阅与生命周期清理。生产代码还要处理配置变更、状态保存、动画、主线程更新及画笔对象复用。

动手练习:让进度条行为可配置

  1. 在一个空 Activity 中把 MeterView 加入 FrameLayout,设置固定高度和 MATCH_PARENT 宽度。
  2. 用滑块、按钮或测试数据连续设置 meter.value = 0..100,确认 setter 触发重绘而不是手动调用 onDraw。
  3. 点击"切换规则"时,将 renderPolicy 从 ThresholdPolicy(80) 换成自己实现的渐变或分段策略。不要修改 MeterView.onDraw。
  4. 给 View 提供内容描述,并为极小或零尺寸编写 onMeasure 策略;验证旋转屏幕后规则和数值的恢复方案。

这项练习的验收点是:新增一种颜色规则只新增一个 RenderPolicy 实现,View 的绘制流程与 Activity 的生命周期代码都不需要复制。它把接口、多态、组合与 View 继承边界放在同一个可观察结果里。

Inner class 的位置:靠近宿主,不是躲开职责

内部类适合表达与宿主紧密相关、不会单独复用的协作。例如自定义 View 的私有触摸状态或一个短生命周期的点击监听器。它可以直接访问外部类成员,但这份便利也意味着引用关系更紧。

RecyclerView.Adapter 的 ViewHolder 在 Kotlin 中应默认写成非 inner 的嵌套类,例如 class ProductHolder(itemView: View) : RecyclerView.ViewHolder(itemView);这与 Java 的静态嵌套类一样,不会隐式保留 Adapter 引用。只有 Holder 确实需要访问 Adapter 成员时才使用 inner,并优先考虑把绑定所需的数据或回调显式传入。Adapter 创建 Holder,RecyclerView 在屏幕上显示和回收它,Adapter 在 onBindViewHolder 里把某个位置的数据绑定到 Holder 持有的 itemView。要区分两种复用:

  • View 复用: RecyclerView 把离屏 itemView 放入缓存或回收池,未来交给另一个位置使用。
  • **ViewHolder 复用:**Holder 是对一个 itemView 及其子控件引用的包装,通常随该 itemView 一起被回收和再次绑定。

所以"一个位置永远对应一个 Holder"是错误模型;绑定时必须覆盖旧状态、监听器和可见性,不能把 position 当成永久身份。Adapter 不应自己持有 Activity 来控制生命周期;它应接收数据、回调和必要的短生命周期依赖。

四个常见坑

  1. **把继承当默认复用。**为了借一个方法而继承 View、Activity 或 Adapter,会同时继承状态与生命周期契约。先抽接口、委托对象或组合组件;只有确实是该类型时再继承。
  2. **长生命周期对象持有 Activity Context。**单例、缓存、后台任务或静态策略保留 Activity 会阻止页面释放。需要应用级资源时使用 application context;需要主题、窗口或页面能力时只在短生命周期范围内使用 Activity。
  3. **手动调用生命周期方法。**直接调用 onCreate、onDestroy、onMeasure 或 onDraw 会绕开系统状态机。让 Framework 管理生命周期;通过公开 API 表达需求。
  4. **混淆 View 与 ViewHolder 的复用。**滚动复用不是数据"自动正确"。每次绑定都恢复文本、图片占位、选中状态和监听器,异步结果还要确认它仍对应当前数据项。

阅读源码时的检查表

  • View 的构造函数拿到的是哪一种 Context?它是否需要主题,是否会逃逸到长生命周期对象?
  • ViewGroup 在何处添加、移除和测量子 View?父子关系谁拥有?
  • LayoutInflater.inflate 的 root 与 attachToRoot 参数决定了谁接收根 View?
  • OnClickListener 是谁注册、谁持有、何时注销或覆盖?结果最终进入哪个状态拥有者?
  • RecyclerView.Adapter 的 onCreateViewHolder 与 onBindViewHolder 分别处理创建和绑定了吗?
  • 自定义 View 的重绘请求是否使用 invalidate 或 requestLayout,而非直接调用回调?

面试表达与下一站

面试表达: "我把 Android UI 看成对象树:ViewGroup 通过组合拥有子 View,View 通过继承提供统一的测量、布局、绘制和事件契约;变化的业务行为用接口和策略实现多态。Activity 负责页面生命周期与协作,不让长生命周期对象持有它的 Context;RecyclerView 的 Adapter 负责创建与绑定,回收由 RecyclerView 管理。"

当 UI 对象边界明确后,下一步是数据建模:把网络 DTO、领域模型和 UI state 分开,让 Adapter 和 View 接收稳定、可渲染的数据,而不是把所有业务状态塞进控件或 Activity。

相关推荐
三少爷的鞋10 小时前
AI 时代,我们都将成为通才型开发者:只懂 Android,已经不够了
android
Dovis(誓平步青云)14 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码16 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程19 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程20 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程21 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程21 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓21 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程21 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer