尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

Java 面向对象如何落到 Android View 体系 点击一个商品卡片、列表滚动后复用条目、主题切换后重新绘制这些 UI 场景不是一堆互不相干的 API它们是 Java 对象协作在 Android 界面层的具体结果。一句话结论在 Android UI 中优先用组合表达界面结构、用接口表达可替换行为、仅在确有“是一个”关系时继承View或框架基类才能把生命周期与职责留在正确的对象手里。先划清版本与事实边界本文以当前 Android Framework 与 AndroidXRecyclerView的公开设计为参照源码实现和私有调用顺序会随 Android 版本变化。文中的View、ViewGroup、Activity、ContextWrapper、LayoutInflater、OnClickListener与RecyclerView.Adapter是可追踪的源码锚点不把内部细节当作稳定 API。标为“简化”的代码只说明对象关系不能直接替代生产实现。一个按钮从 XML 到业务回调设想订单页Activity 显示一个根布局布局里有商品列表和“提交订单”按钮。谁创建它们谁持有它们谁触发回调结果交给谁先沿事件走一遍再回看 Java 概念。Activity 创建界面LayoutInflater 解析布局ViewGroup 根容器RecyclerView 列表Button 提交按钮商品项 View商品项 View**谁创建**系统创建Activity应用通常通过setContentView或LayoutInflater.inflate让LayoutInflater按 XML 创建View与ViewGroup实例。代码直接new一个自定义View也可以。**谁拥有**根ViewGroup持有子 ViewActivity持有当前窗口所展示的根 View 的引用和界面生命周期。RecyclerView持有可见子项的 ViewAdapter 负责为它提供和绑定数据。**谁触发**触摸分发、点击判定和绘制由 Framework 驱动。业务代码不应自己调用onDraw()、onMeasure()或 Activity 生命周期方法。**结果给谁**点击最终进入注册的OnClickListener列表的绑定结果回到RecyclerView绘制结果交给窗口和屏幕而不是由 Adapter 自己“画到屏幕”。类、对象与组合一棵真实的 UI 树View是所有可见控件的基类负责自身状态、测量、布局位置和绘制入口。TextView、Button是它的具体子类。这里的“类”是蓝图“对象”是运行时真正挂在窗口上的那一个按钮实例。ViewGroup也是View但额外管理子 View。它并不只是“父类复用代码”而是组合的拥有者一个LinearLayout对象包含多个子 View 对象。界面多半应先问“这个对象是否拥有另一个对象”而不是急着设计继承层级。源码锚点回答的问题创建与持有何时工作结果给谁View单个元素如何测量、布局、绘制、接收事件LayoutInflater或业务代码创建父ViewGroup持有Framework 在遍历中调用Canvas 输出交给窗口渲染链路ViewGroup子元素如何组成树布局膨胀或代码创建其父容器持有测量和布局时调度子项子项的位置和绘制顺序由它组织LayoutInflaterXML 如何变成对象树LayoutInflater.from(context)获取并执行调用方接收根 Viewinflate时创建 View把根对象返回给Activity、Fragment 或 AdapterActivity一个界面在哪个生命周期和窗口内系统创建系统任务与窗口体系持有系统调用生命周期Activity 设置内容 View 并把事件结果路由到业务协作对象ContextWrapper为什么 Context 能附加环境能力Activity经由ContextThemeWrapper继承它包装基础 Context资源、主题和系统服务通过 Context 解析这也解释了组合为何常优于继承一个“订单卡片”不必继承RecyclerView才能含有列表它可以是一个ViewGroup内部组合标题、列表和按钮。继承适合稳定的类型身份例如PriceBadgeView is a View组合适合“由什么构成”。接口与回调行为可以换拥有者不用换Java 接口描述能力而不是存储实现。View.OnClickListener就是典型例子按钮不知道提交订单的业务细节只知道在点击时调用一个实现了接口的对象。Activity、Fragment、ViewModel 协调器或独立类都可以接收这个结果只要它实现同一接口或提供等价回调。用户触摸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 各自完成对应绘制。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因此用接口和组合承载变化。// 简化示例可放入 Android 项目后继续完善interfaceRenderPolicy{funcolorFor(value:Int):Int}classThresholdPolicy(privatevalwarningAt:Int):RenderPolicy{overridefuncolorFor(value:Int):Intif(valuewarningAt)Color.REDelseColor.GREEN}classMeterView(context:Context):View(context){varvalue:Int0set(newValue){fieldnewValue.coerceIn(0,100)invalidate()}varrenderPolicy:RenderPolicyThresholdPolicy(warningAt80)set(newPolicy){fieldnewPolicyinvalidate()}privatevalpaintPaint(Paint.ANTI_ALIAS_FLAG)overridefunonDraw(canvas:Canvas){super.onDraw(canvas)paint.colorrenderPolicy.colorFor(value)canvas.drawRect(0f,0f,width*value/100f,height.toFloat(),paint)}}Activity 或 Fragment 创建MeterView后拥有它的配置引用View 自己持有RenderPolicy数据层把新数值交给valueinvalidate()请求 Framework 在下一次合适绘制中回调onDraw。策略返回颜色View 接收结果并负责画到自己的 Canvas。策略没有持有 View 或 Activity因此更容易复用和单测。**刻意省略**示例没有实现onMeasure的最小尺寸与MeasureSpec协商没有contentDescription、无障碍状态和键盘操作没有 XML 属性解析也没有观察者订阅与生命周期清理。生产代码还要处理配置变更、状态保存、动画、主线程更新及画笔对象复用。动手练习让进度条行为可配置在一个空 Activity 中把MeterView加入FrameLayout设置固定高度和MATCH_PARENT宽度。用滑块、按钮或测试数据连续设置meter.value 0..100确认 setter 触发重绘而不是手动调用onDraw。点击“切换规则”时将renderPolicy从ThresholdPolicy(80)换成自己实现的渐变或分段策略。不要修改MeterView.onDraw。给 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 创建 HolderRecyclerView在屏幕上显示和回收它Adapter 在onBindViewHolder里把某个位置的数据绑定到 Holder 持有的 itemView。要区分两种复用View 复用RecyclerView把离屏 itemView 放入缓存或回收池未来交给另一个位置使用。**ViewHolder 复用**Holder 是对一个 itemView 及其子控件引用的包装通常随该 itemView 一起被回收和再次绑定。所以“一个位置永远对应一个 Holder”是错误模型绑定时必须覆盖旧状态、监听器和可见性不能把 position 当成永久身份。Adapter 不应自己持有 Activity 来控制生命周期它应接收数据、回调和必要的短生命周期依赖。四个常见坑**把继承当默认复用。**为了借一个方法而继承View、Activity或 Adapter会同时继承状态与生命周期契约。先抽接口、委托对象或组合组件只有确实是该类型时再继承。**长生命周期对象持有 Activity Context。**单例、缓存、后台任务或静态策略保留 Activity 会阻止页面释放。需要应用级资源时使用 application context需要主题、窗口或页面能力时只在短生命周期范围内使用 Activity。**手动调用生命周期方法。**直接调用onCreate、onDestroy、onMeasure或onDraw会绕开系统状态机。让 Framework 管理生命周期通过公开 API 表达需求。**混淆 View 与 ViewHolder 的复用。**滚动复用不是数据“自动正确”。每次绑定都恢复文本、图片占位、选中状态和监听器异步结果还要确认它仍对应当前数据项。阅读源码时的检查表View的构造函数拿到的是哪一种Context它是否需要主题是否会逃逸到长生命周期对象ViewGroup在何处添加、移除和测量子 View父子关系谁拥有LayoutInflater.inflate的 root 与attachToRoot参数决定了谁接收根 ViewOnClickListener是谁注册、谁持有、何时注销或覆盖结果最终进入哪个状态拥有者RecyclerView.Adapter的onCreateViewHolder与onBindViewHolder分别处理创建和绑定了吗自定义 View 的重绘请求是否使用invalidate或requestLayout而非直接调用回调面试表达与下一站面试表达“我把 Android UI 看成对象树ViewGroup通过组合拥有子 ViewView通过继承提供统一的测量、布局、绘制和事件契约变化的业务行为用接口和策略实现多态。Activity 负责页面生命周期与协作不让长生命周期对象持有它的 ContextRecyclerView 的 Adapter 负责创建与绑定回收由 RecyclerView 管理。”当 UI 对象边界明确后下一步是数据建模把网络 DTO、领域模型和 UI state 分开让 Adapter 和 View 接收稳定、可渲染的数据而不是把所有业务状态塞进控件或 Activity。
返回列表