ViewCompose Architecture
1. 文档定位
本文档是 ViewCompose 的当前架构规范版,用于定义:
- 模块职责边界
- 核心调用链
- 新增代码的落点规则
- 变更时必须遵守的约束
如果实现要偏离本文档,必须先更新文档,再改代码。
历史长版快照见:
2. 当前基线(2026-07)
- 技术基线:Kotlin + Android View System
- SDK:
minSdk 24、compileSdk 36 - 当前模块:
:viewcompose-runtime、:viewcompose-text-core、:viewcompose-ui-contract、:viewcompose-navigation-core、:viewcompose-navigation(特性分支孵化中)、:viewcompose-animation-core、:viewcompose-animation、:viewcompose-gesture-core、:viewcompose-gesture、:viewcompose-graphics-core、:viewcompose-graphics、:viewcompose-shadow-android、:viewcompose-widget-core、:viewcompose-widget-constraintlayout、:viewcompose-renderer、:viewcompose-host-android、:viewcompose-overlay-android、:viewcompose-image-coil、:viewcompose-lifecycle、:viewcompose-viewmodel、:viewcompose-preview-core、:viewcompose-preview-runner、:viewcompose-preview、:viewcompose-benchmark、:app
2.1 模块职责
| 模块 | 职责 | 约束 |
|---|---|---|
viewcompose-runtime | 状态与读依赖观察(state/observation) | 纯 Kotlin/JVM 模块;主源码禁止 android.* / androidx.*,构建不引入 AndroidX 依赖 |
viewcompose-text-core | 完整纯文本编辑状态(text/selection/composition)、EditingBuffer、输入变换、撤销/重做 | 纯 Kotlin/JVM;禁止 Android 类型;偏移统一使用 UTF-16 以匹配平台编辑协议 |
viewcompose-ui-contract | 纯 Kotlin UI 契约层(Modifier、VNode/NodeSpec、layout 枚举、collection/state 协议) | 主源码禁止 android.* / androidx.* |
viewcompose-navigation-core | 系统导航内核(route/back stack/two-phase transaction/page lifecycle planning) | 纯 Kotlin/JVM;禁止 Android/AndroidX 类型;页面 Session 与平台 back 适配不得进入此模块 |
viewcompose-navigation | Android 系统导航集成(destination owner/page session/NavHost/back adapter) | 依赖 navigation-core 与 host-android;稳定前不向 app 默认入口注入,不允许 host-android 反向依赖 |
viewcompose-animation-core | 动画内核(AnimationSpec/Easing/Converter/Engine/TransitionCore) | 纯 Kotlin/JVM;禁止引入 Android 依赖 |
viewcompose-animation | 动画 DSL 集成层(animate*AsState/Animatable/Transition/AnimatedVisibility/Content) | 调用层 API;运行时驱动统一使用 MonotonicFrameClock + coroutine;不直接依赖 Android View 动画实现 |
viewcompose-gesture-core | 手势策略内核(axis lock、transform slop、swipe settle) | 纯 Kotlin/JVM;禁止引入 Android 依赖;renderer 只做事件适配并调用该内核 |
viewcompose-gesture | 平台无关手势 DSL 入口层(pointerInput、combinedClickable、draggable/anchoredDraggable/transformable) | 仅定义手势 modifier 与状态入口;不承载策略判定实现 |
viewcompose-graphics-core | 图形绘制内核(geometry/path/brush/draw command/draw cache) | 纯 Kotlin/JVM;禁止引入 Android 依赖;仅定义平台无关图形模型 |
viewcompose-graphics | 图形 DSL 集成层(Canvas、drawBehind、drawWithContent、drawWithCache) | 仅定义业务 API 与契约映射;不直接依赖 Android Canvas 实现 |
viewcompose-shadow-android | 可选高级阴影后端、缓存与 Android 绘制实现 | 依赖 renderer 的最小 Decoration SPI;renderer/host 不依赖该模块;通过 ServiceLoader 或显式安装接入 |
viewcompose-widget-core | DSL、Theme/Defaults、Local 与 overlay 声明契约 | 不依赖 viewcompose-renderer;不放 Android 宿主入口 API |
viewcompose-widget-constraintlayout | ConstraintLayout 组件 DSL(ConstraintLayout/createRef(s)/constrainAs/constrain/constraintSet) | 仅承载约束布局 DSL 与 scope;平台渲染实现仍在 viewcompose-renderer |
viewcompose-renderer | Android View 渲染实现(reconcile、binder、patch、container) | 只消费 ui-contract,不承载业务 DSL |
viewcompose-host-android | Android 宿主运行时与入口(setUiContent/renderInto/RenderSession、AndroidView/nativeView、宿主 Local 注入) | 只做平台执行与注入,不承载业务 DSL |
viewcompose-overlay-android | Android overlay host/presenter(Dialog/Popup/ModalBottomSheet/Snackbar/Toast) | 只做平台实现,不依赖 renderer 资源 |
viewcompose-image-coil | 远程图片加载桥接(RemoteImageLoader Android 实现) | 通过平台无关 target 契约接入,不回流核心渲染逻辑 |
viewcompose-lifecycle | 生命周期感知的状态收集 API(collectAsStateWithLifecycle)与生命周期 Local 对外入口 | 不承载 Android 视图实现;不新增宿主注入逻辑 |
viewcompose-viewmodel | ViewModel/SavedStateHandle 协作 API(viewModel、savedStateHandle)与 ViewModel Local 对外入口 | 不承载 Android 视图实现;不新增宿主注入逻辑 |
viewcompose-preview-core | Preview 注解、确定性配置和跨进程请求/结果协议 | 纯 Kotlin/JVM;禁止 Android、Compose 与 IDE SDK 依赖 |
viewcompose-preview-runner | 隔离进程内的原生 View 静态渲染、图片导出和结构化诊断 | 允许 Android/Layoutlib;禁止 Compose 与 IDE SDK 依赖 |
viewcompose-preview | 开发预览与截图回归(Compose Preview bridge、PreviewCatalog、Paparazzi) | 仅开发态能力;不参与 app 运行时入口;禁止依赖 :app |
viewcompose-benchmark | 宏基准入口与性能回归数据采集 | 不承载业务 demo 与框架语义逻辑 |
app | demo、manual verification、ui tests 入口 | 不承载框架核心实现 |
2.1.1 模块依赖方向硬边界
模块依赖固定为“基础层 -> 可选能力层 -> 工具层 -> Demo 应用”的单向消费关系;箭头表示后者可以消费前者,禁止基础层为了复用某个可选实现而产生反向依赖。
- 基础层包括 runtime、纯 Kotlin 内核、UI contract、widget-core、renderer、lifecycle/viewmodel 与 host-android。每个基础模块的 Gradle project 依赖使用显式白名单;新增依赖必须先证明属于稳定基础契约。
- navigation、animation、gesture、graphics、shadow、constraintlayout、overlay 与 image-coil 属于可选能力。它们可以依赖基础层,但基础层禁止依赖这些模块;未引入任一可选能力时,核心渲染链仍必须可独立编译运行。
- preview、preview worker/runner/Gradle plugin 与 benchmark 属于工具层。运行时基础模块和可选能力模块均禁止依赖工具层;所有框架模块禁止依赖
app。 - 新增
viewcompose-*模块时,必须在同一提交中登记为“基础 / 可选能力 / 工具”之一。未分类模块、基础模块白名单外依赖、可选能力到工具层的依赖都会由verifyModuleDependencyBoundaries阻断。 qaQuick固定执行该边界测试。不能以 Demo 可以编译、依赖当前恰好存在或 code review 已确认作为跳过理由。
2.2 当前架构判断
当前架构是可维护的 View-based 声明式 v1:
- 主树更新模型:
SlotTable Lite节点组脏区重组 + 根树引用复用 - 列表/分页等复用容器:独立 session 刷新路径
- overlay:声明契约与平台实现已分层
- 节点语义已完成
NodeSpec-only收口(无Props双轨) - 生命周期与 ViewModel 协作 API 已从
widget-core拆分到独立模块,宿主自动注入能力保持不变 - 动画与手势已形成“内核 + DSL + Android interop 扩展”分层模型(animation-core + animation、gesture-core + gesture、host interop)
- graphics 已形成“内核 + DSL + renderer + host interop”分层模型(graphics-core + graphics + renderer draw pipeline + host-android AndroidGraphicsInterop)
- ConstraintLayout 已按“widget DSL 模块 + renderer 平台映射”分层落地,支持 anchors/dimension/bias/baseline/baselineToTop/baselineToBottom/circle/guideline/barrier/chain(+weights)/Flow/Group/Layer/Placeholder/decoupled constraintSet,以及 match-constraint 进阶参数(min/max/percent/constrained)
- Theme token 已进入“消费闭环”阶段:新增 token 必须进入 defaults/composite 默认值,或明确登记为 reserved semantic palette
- 文本输入已硬切到
TextFieldState单一状态主权:纯 Kotlin 编辑内核负责值、选区、组合区与历史;renderer 的ViewComposeEditText只负责 AndroidEditable/InputConnection适配 - 系统导航在独立特性分支孵化:先稳定纯 Kotlin 返回栈事务与页面生命周期内核,再接入 Android 页面 Session;稳定前不改变现有 Activity/Fragment 宿主入口
2.3 app 目录落位基线
app 模块采用“入口与演示分层”:
app/src/main/java/com/viewcompose/activity/entry- 根入口 Activity(如
MainActivity、渲染宿主入口)
- 根入口 Activity(如
app/src/main/java/com/viewcompose/activity/demo/pages/<domain>- demo 页面 Activity 路由入口,按页面域分层(
core/interaction/advanced/quality)
- demo 页面 Activity 路由入口,按页面域分层(
app/src/main/java/com/viewcompose/activity/demo/sandbox- 非核心页面实验入口(动画/手势/图形等)
app/src/main/java/com/viewcompose/demo/core- demo 全局骨架与共享能力(catalog、theme session、test tags、section helpers)
app/src/main/java/com/viewcompose/demo/pages/<feature>- 按功能页归档的 demo 实现(foundations/layouts/input/feedback/...)
app/src/androidTest/java/com/viewcompose- demo/UI 回归测试
2.4 viewcompose-renderer 目录落位基线
renderer 侧避免“单目录平铺”,按职责拆到二级目录:
NodeType/VNode/NodeSpec及其子类型只允许定义在viewcompose-ui-contract;renderer 禁止新增com.viewcompose.renderer.node镜像契约。viewcompose-renderer/src/main/java/.../view/container/{core,layout,collection,navigation,input}- Android View 容器映射层,按控件族群分类
viewcompose-renderer/src/main/java/.../view/tree/binder/core- 绑定流程核心(factory/differ/plan/registry/modifier)
NodeBinderDescriptors是 bind/patch/diff 元数据单源注册表(禁止并行映射)- descriptor 源文件固定收敛在
core/descriptor/,禁止回流平铺到core/根目录 ViewModifierApplier仅作 facade,具体职责拆到core/modifier子模块- 容器策略(reuse/motion/focus follow)由 widget DSL 写入
NodeSpec,binder 直接读 spec 应用,不再走 modifier 策略提取
viewcompose-renderer/src/main/java/.../view/tree/binder/widget- 分控件 binder 实现(content/input/media/feedback/collection 等)
- TextField 固定通过
ViewComposeEditText + AndroidTextFieldController同步完整编辑快照;禁止在普通重组补丁中无条件setText()或把光标移动到末尾
viewcompose-renderer/src/main/java/.../view/lazy/{adapter,focus,layout,reuse,session,state}- 延迟容器子系统按能力拆分(适配器、焦点跟随、间距布局、复用策略、session、状态)
LazyListState由 RecyclerView scroll/layout/adapter observer 推送不可变布局快照;绑定同一 RecyclerView 时禁止重置 anchor- item key/contentType/span/sticky kind 属于
ui-contract,Android 侧分别映射 stable ID、view type、SpanSizeLookup 与 pinned header decoration
3. 核心调用链
4. 强约束边界
4.1 平台实现边界
- Android
Dialog/PopupWindow/Toast/Snackbar宿主实现只放viewcompose-overlay-android。 viewcompose-widget-core只保留平台无关声明契约与 runtime 组合能力。- demo 专用逻辑不回流到框架模块。
4.2 Modifier / NodeSpec / Theme 边界
Modifier:通用修饰与 scoped parent-data。- 组件语义参数:走组件 DSL 参数 +
NodeSpec。 - 主题默认值:走
Theme -> Defaults,不把主题直接做成通用 modifier。 AndroidThemeBridge固定走“snapshot reader + token mapper”两层实现:reader 只读 Android / AppCompat / Material 平台字段,mapper 只做语义映射与 fallback。AndroidThemeBridge当前允许 best-effort 映射surfaceTint与统一圆角shapeAppearance*Component;不允许为了表面覆盖率在 bridge 中猜测非统一四角 shape 或控件三档尺寸。controls继续保持 framework-owned defaults,除非 Android 原主题系统存在稳定且统一的来源;禁止把零散 widget style 强行提升为全局 token 真相源。viewcompose-ui-contract的Modifier文件只承载“全局稳定语义”的元素声明与 builder;仅特定容器生效的策略必须进入容器 DSL 参数与NodeSpec。- 禁止新增
Props/TypedPropKeys/PropKeys/node.props动态语义路径。 - 约束 parent-data(
layoutId/constrainAs/constrain)仅允许用于ConstraintLayout子节点;错误宿主必须输出 validator 警告。 - 复合组件内部文本样式必须通过
NodeSpec全量传递(fontSize/fontWeight/fontFamily/letterSpacing/lineHeight/includeFontPadding),禁止重新退回“只传textSizeSp”。
对应规范:
4.3 宿主接入边界
com.viewcompose.host.android.ComponentActivity.setUiContent(...)不暴露内部RenderSession给页面调用方,并由宿主自动管理dispose。com.viewcompose.host.android.Fragment.setUiContent(...)是官方入口:不暴露内部RenderSession,并在viewLifecycleOwner销毁时自动dispose。setUiContent的默认overlayHostFactory走OverlayHostDefaults.androidOrNoOp(...):优先通过OverlayHostFactoryProvider(ServiceLoader)发现 Android 实现;缺失时回退 no-op 并输出提示。overlay-android必须通过META-INF/services注册OverlayHostFactoryProvider,禁止回退字符串反射装配(Class.forName)。- host 对外回调
onRenderStats/onRenderResult只能暴露 core 自有诊断类型(com.viewcompose.widget.core.RenderStats/RenderTreeResult),renderer 诊断类型仅允许出现在 host 内部适配层。 - system bars insets 走组件侧
Modifier.systemBarsInsetsPadding(...),不绑死 Activity 全局参数。 viewcompose-host-android必须通过installRenderSessionPlatform(...)一次性原子注册渲染引擎、帧调度 runtime 与组合协程上下文;RenderSession创建时固定使用同一平台快照,缺失或重复安装立即失败,禁止 no-op/immediate/empty-context 分段降级。
4.4 延迟 session 容器边界
只要容器满足“延迟创建 + holder/session 复用”,就必须视为一级架构对象,必须具备:
- 结构稳定时的可见内容刷新路径
- 空 diff 刷新保障
- recycle/dispose 与生命周期一致性
- framework 托管的
RecyclerView/ViewPager2容器默认保持“本地池 + 系统动画器”;可通过容器参数reusePolicy/motionPolicy对单个容器启用共享池与动画策略,并通过垂直容器参数focusFollowKeyboard控制键盘跟随。
专项清单:
4.5 Environment 边界
- 宿主入口(
com.viewcompose.host.android.setUiContent(...))默认自动注入UiEnvironment(androidContext = root.context)。 - 业务层允许在局部子树使用
UiEnvironment(values = ...)做覆盖;默认注入不阻断局部覆写。 viewcompose-renderer不依赖viewcompose-widget-core/context/Environment,只消费 renderer 已解析的NodeSpec与平台参数。viewcompose-renderer中的 dp/sp 尺寸换算统一走内部工具(viewcompose-renderer/view/DimensionUtils.kt),容器类禁止私有density/dpToPx重复实现。- Android 平台环境提取入口固定为
AndroidEnvironmentBridge,新增 Android 环境字段时必须先扩展该桥接,再进入UiEnvironmentValues。
4.6 Local 扩展边界
- 业务侧自定义 token 必须通过统一 Local API:
uiLocalOf、UiLocals.current、ProvideLocal、ProvideLocals。 viewcompose-widget-core内置 Local 也统一走上述 API,不再新增专用ProvideXxx调用范式。viewcompose-renderer不新增 Local 语义入口;只消费 reconcile 后的NodeSpec。- Local 的 snapshot/restore 必须与延迟容器、overlay 场景一致传播,不允许能力回退。
- 生命周期与 ViewModel 相关 Local 的对外包名固定为
com.viewcompose.lifecycle与com.viewcompose.viewmodel;默认注入由viewcompose-host-android的AndroidHostBridge完成。
4.7 SlotTable Lite 重组边界
ComposerLite是唯一组合内核,RenderSession仅负责“首帧 compose + 后续增量 recompose”的调度,不再走 session 级全树读依赖观察;失效重绘调度固定走Choreographer帧对齐路径。- 组边界由
UiTreeBuilder.emit(...)建立;未脏组直接复用上次VNode引用,dirty 组才重建。 - 组级失效来源固定为两类:状态读依赖失效、
emit输入(spec/modifier)变化;两者都进入InvalidationQueue去重合并。 - 结构漂移(同层 group key/顺序不一致)必须回退到最近稳定祖先子树重组,并只打印一次告警,禁止 silent corruption。
LocalContext必须按组 snapshot/restore,保证局部重组下 Local 读取一致。remember、key、DisposableEffect、SideEffect、LaunchedEffect、rememberCoroutineScope等组合 API 只允许在活动的ComposerLite组合中调用;禁止维护备用 slot/effect store 或在组合外静默降级。
4.8 文本编辑边界
viewcompose-text-core是文本、方向选区、IME 组合区、编辑事务和撤销历史的唯一平台无关真相源。TextField/TextArea/SearchBar公开 API 只接受稳定的TextFieldState;禁止重新增加String + onValueChange双状态入口。- Android renderer 必须保留原生
AppCompatEditText的输入法、无障碍、硬件键盘和系统选择能力,不实现自有文本布局或完整InputConnection。 - 原生输入在
InputConnection/batch edit 边界内合并后同步到状态;状态回写必须使用最小Editable.replace()并恢复 selection/composition。 InputTransformation只处理用户输入,程序调用TextFieldState.edit不经过输入过滤。- 保存恢复只持久化 text 与 selection;IME composition 和 undo/redo history 属于当前编辑会话,不跨进程恢复。
- 富文本 span、inline attachment 与统一 receive-content 属于独立文档模型能力,不允许通过把 Android
Spannable放入 core 契约来实现。
4.9 State Snapshot 边界
MutableState必须通过 snapshot 事务写入,不允许绕过SnapshotRuntime直接改值。mutableStateOf的去抖/冲突语义由SnapshotMutationPolicy定义;默认structuralEqualityPolicy。- 并发
MutableSnapshot.apply()冲突处理固定为:先判等、再 merge、merge 失败即失败返回。 ComposerLite每轮 compose 必须运行在一致性读快照中,保证同一轮读取不漂移。DerivedState缓存失效必须感知 snapshot 读版本,禁止仅靠全局 dirty 布尔。rememberUpdatedState只保证“重组后可见”,不保证“同一组合阶段 effect 立即读取到最新值”。ComposerLite.prepareRoot()只生成候选组合;slot、观察订阅、RememberObserver与 Effect 生命周期必须在 renderer 成功后提交,失败时统一 abort。DisposableEffect、SideEffect与RememberObserver.onRemembered只允许在提交阶段执行;失败候选中的 remembered value 必须收到onAbandoned。RenderSession是组合协程树的唯一根 owner:根使用SupervisorJob隔离子任务,Session 销毁必须取消全部后代。LaunchedEffect的启动/Key 重启/遗忘取消必须由RememberObserver提交生命周期驱动,失败组合不得启动任务。produceState固定为 suspend producer,并通过awaitDispose清理;collectAsState*与动画不得创建独立根 Job。- 传给
rememberCoroutineScope、collectAsState*与动画的附加CoroutineContext不得包含Job,防止脱离组合父任务。 - 组合阶段若先写 snapshot-backed mirror state 再立刻读回,该读值可能仍是旧快照;控制流判定必须基于实时内核值,不得依赖同帧 mirror 回读。
- 组合事务保证 slot/观察/Effect/VNode 提交一致性;组合体内主动写入的全局 snapshot state 仍遵循 snapshot 自身事务,不承诺与 Android View patch 跨系统原子回滚。
- 组合事务使用 touched-scope journal:只有本轮实际执行或输入变化的 Scope 才复制回滚状态,禁止恢复为每帧全 SlotTree checkpoint。
- 相同帧到达同一 Scope 的重复失效必须合并;组合进行中的失效仍须递增版本,保证本轮结束后保留下一次重组。
- 脏 Scope 若生成 type/key/spec/modifier/children 引用均等价的 VNode,必须沿用旧 VNode 引用,为 renderer 提供 O(1)
SkipSubtree。 - 无编译器自动 restart group;跨多个兄弟 VNode 的业务组件应按需使用无原生节点的
RecomposeBoundary,普通捕获值显式声明为 inputs。
4.10 Render 调度边界
RenderSession.render()保持立即执行语义(首帧与显式调用同步渲染)。- 状态失效触发的重绘必须通过
FrameAlignedRenderDispatcher合帧调度,禁止回退到container.post。 - 同一帧内多次 invalidation 只能触发一次
RenderSession渲染提交。 dispose()必须取消未执行帧回调,禁止 session 销毁后延迟渲染。- lazy item session 与 overlay surface session 继续复用
RenderSession.render()的立即语义,避免首显空白。 - renderer 的递归 patch 必须共享一次 apply transaction;删除资源只能在整棵树成功后释放。
- patch 失败必须尽力恢复旧
VNode、mounted children、布局参数与 View 顺序,并释放本轮新建节点。 AndroidView.update/onReset/nativeView仅允许可重放的 View 内配置;不可重放的外部动作必须放入事务成功后才发布的onCommit。- renderer transaction 使用 mutation journal,只记录实际绑定、移动、插入或删除的 MountedNode/ViewGroup;稳定子树不得进入回滚快照。
AnimatedSizeNodeWrapper必须保留未变化 VNode/List 的引用,且整帧只转换一次;禁止无动画节点的递归 copy。NodeBindingDiffer必须先于 Modifier/LayoutParams 解析执行;SkipSubtree不得解析或重复 preflight。- 结构深度统计与逐 NodeType 绑定统计只在 debug/诊断回调启用时收集。
- 所有可恢复失败必须通过
RenderFailure(phase/recovery/frameId/operation/nodeKey)上报;日志不是可观测性 API。
4.11 Renderer 绑定复杂度边界
NodeViewBinderRegistry与NodeBindingDiffer的 bind/patch/diff 映射必须从NodeBinderDescriptors单源派生,禁止新增并行手工 map。- 新增
NodeType或新增NodeViewPatch时,只允许修改 descriptor 源;不得同时改 registry/differ 的独立映射分支。 - descriptor 源文件必须落在
view/tree/binder/core/descriptor/,core/根目录禁止新增NodeBinder*.kt平铺文件。 ViewModifierApplier仅负责编排,不承载具体细节实现;样式/交互/insets/容器策略必须分别落在core/modifier子职责对象。- 任何绕过 descriptor 的快速修复都视为架构违规,必须在同一迭代回补为单源注册。
4.12 模块单包根边界
- 每个模块只允许一个包根前缀,且必须与模块职责对应(允许该前缀下的子包分层)。
- 约束范围覆盖
src/main、src/test、src/androidTest,测试源码不允许例外包根。 - Android 模块
namespace必须与该模块包根一致(viewcompose-ui-contract作为 Kotlin/JVM 模块例外)。 - lifecycle/viewmodel 的 Local 对外 API 包名固定为
com.viewcompose.lifecycle与com.viewcompose.viewmodel,并且源码归属必须落在对应模块,不得回流widget-core。
4.13 开发预览边界
- 平台无关 Preview 注解、确定性配置和进程协议集中在
:viewcompose-preview-core,禁止 Android、Compose 与 IDE SDK 依赖。 - 原生静态挂载、measure/layout/draw 与诊断导出集中在
:viewcompose-preview-runner;禁止 Compose 和 IDE SDK 依赖。 - Compose Preview 适配器、
PreviewCatalog与 Paparazzi 资产集中在:viewcompose-preview,不允许回流app或核心运行时模块。 - Android Studio Preview 与 Paparazzi 必须共享
PreviewCatalog单源,禁止双份示例维护。 - preview worker 与 IDE 插件必须通过带版本的结构化协议通信,业务渲染代码禁止运行在 IDE 进程内。
- overlay 在 preview 场景仅允许静态内容模拟;真实窗口行为继续由 instrumentation 覆盖。
- 新增组件(或关键复合组件)必须同轮补
PreviewSpec与 Paparazzi 快照基线。
4.14 动画与手势边界
- 动画分层固定为
viewcompose-animation-core+viewcompose-animation;手势分层固定为viewcompose-gesture-core+viewcompose-gesture。 graphicsLayer是主链动画承载能力;与alpha/offset/elevation/zIndex冲突时,以graphicsLayer同语义字段优先。- Android 高阶动画(
TransitionManager/MotionLayout/Animator)只能通过viewcompose-host-android的 interop 入口接入,禁止回流到平台无关主链。 - renderer 手势消费规则固定为“手势先消费,未消费再回落 clickable”,并维持方向锁 + slop + priority 的冲突策略。
- 列表/分页动画默认 opt-in(
motionPolicy),并与reusePolicy兼容,不改变未启用容器行为。 AnimatedVisibility语义固定为 Compose 对齐:默认fadeIn+expandIn/shrinkOut+fadeOut,并通过NodeType.AnimatedVisibilityHost参与父布局尺寸动画;exit 全部动画完成后才移除 subtree。- 手势仲裁顺序固定为
pointerInput -> transform/drag/swipe -> combinedClickable;当pointerInput返回Consumed时,必须强短路后续链路。 - transform 激活必须经过 slop 门槛:
panMotion、abs(1 - zoomChange) * centroidSize、abs(rotationRadians) * centroidSize任一超过touchSlop才进入 active 状态。 - anchored settle 语义固定为“速度优先 + 距离次之 + 最近 anchor 兜底”:先比较
minimumFlingVelocity,再比较max(touchSlop * 2, segmentSpan * 0.35),否则回最近锚点。 - transform active 后每帧只派发一次合并 delta(zoom/pan/rotation),并在 active 时再请求
requestDisallowInterceptTouchEvent(true),避免提前抢占父容器。 Modifier.animateContentSize(...)通过 renderer 侧AnimatedSizeHost结构包装落地,执行真实测量尺寸插值并参与父布局重排(非graphicsLayer缩放假象);AnimationSpec的 easing/spring/keyframes/repeat/reverse 语义必须在执行层保持一致。Animatable默认通过rememberAnimatable(...)绑定 frame clock,animateTo(...)不再要求调用侧显式传frameClock;非组合场景可通过构造参数显式绑定。AnimatedSizeHost收起路径必须避免“子节点先跳到末端尺寸”;子节点布局需跟随 host 当前动画尺寸,保证展开/收起两方向的视觉连续性。- 手势策略新增或修改(axis lock/slop/swipe settle)必须下沉到
viewcompose-gesture-core;renderer 禁止新增并行策略分支。 combinedClickable只有在enabled=true且至少提供一个回调(click/double/long)时才参与仲裁;无回调场景必须视为 no-op 且不消费触摸流。
4.15 Graphics 边界
- graphics 分层固定为
viewcompose-graphics-core(平台无关图形内核)+viewcompose-graphics(业务 DSL)+ renderer(Android Canvas 执行)+viewcompose-host-androidinterop(Android 特有高阶能力)。 viewcompose-graphics-core主源码禁止android.*/androidx.*import;纯度由verifyGraphicsCorePurity硬门禁。- draw modifier 绘制顺序固定:
drawBehind在内容前,drawWithContent显式控制内容绘制,多 draw modifier 按 modifier 链顺序稳定执行。 drawWithCache语义固定为“依赖变化才重建缓存命令”;禁止每帧重建缓存对象掩盖性能问题。- Android 专属高阶图形能力(
RenderEffect、RuntimeShader、Drawable/Canvasbridge)只能经host-android的AndroidGraphicsInterop暴露,禁止回流平台无关主链。 DrawRoundRect必须遵守四角半径语义:四角一致时走drawRoundRect快路径,非一致时走Path.addRoundRect,禁止退回“只读 topLeft”实现。DrawImage的Drawable分支必须应用DrawPaint组合语义(alpha/blend/colorFilter/imageFilter),并在绘制后恢复原始bounds。ImageFilterModel.Chain必须在执行层可生效;当前Blur + Chain路径采用递归合并半径(高斯方差累加)后下发到平台滤镜,禁止直接忽略Chain。
4.15.1 高级阴影装饰边界
- 平台无关契约固定在
viewcompose-ui-contract:UiShadow与dropShadow(s)/innerShadow(s)只保存有序、不可变的逻辑单位规格。 - renderer 只拥有
AndroidViewDecorationBackend最小协议、通用宿主、父级活跃装饰索引和独立的zIndex排序;viewcompose-renderer与viewcompose-host-android禁止依赖具体阴影模块。 - Android 栅格化、缓存、后端选择和诊断固定在可选
viewcompose-shadow-android;它通过META-INF/services注册后端,也允许应用启动时显式调用ShadowDecorationLayer.install()。 - 后端缺失时
dropShadow(s)/innerShadow(s)必须稳定降级为 no-op,核心渲染、Lazy、Pager、Tab、预览和宿主仍可独立编译运行。 setUiContent与静态预览的必要根容器必须保持普通FrameLayout;只有顶层节点确实需要装饰或非零zIndex、且现有容器不具备协议时,renderInto才按需增加通用宿主。嵌套装饰由最近的框架布局容器直接绘制。- 没有活跃装饰 child 时,容器
drawChild只能执行一次父级布尔快速判断后直达原生绘制,不得逐 child 查询阴影 tag 或调用具体后端;存在活跃装饰时,每个 child 最多查询一次父级身份索引并复用于前后绘制平面;没有非零zIndex时必须关闭自定义 child drawing order,交回 Android 原生顺序。 - 框架容器在 child 内容前绘制外阴影、在 child 完整内容与 foreground 后绘制内阴影;不得为每层阴影创建额外业务 View。
- 高级阴影不参与 measure/layout、hit test、焦点或无障碍;
zIndex、Materialelevation与精确阴影保持三套独立语义。 - 多层阴影严格保留声明顺序;外阴影可超出 child bounds,但仍受最近 viewport/显式 clip chain 约束;内阴影必须裁切在 shape 内。
- 静态栅格缓存 key 必须覆盖尺寸、density、layout direction、shape 与完整规格;仅平移、缩放、旋转或 alpha 变化不得重建栅格。
ShadowRenderPolicy.Auto的当前默认后端是ExactBitmap。RenderNodeDisplayList只作为 API 29+ 显式实验策略;没有同设备发布态数据证明稳定收益前不得切换默认值。- Lazy 回收、节点移除、事务回滚与 RenderSession dispose 必须同步移除阴影规格;父级索引不得通过全局强引用持有 View,进程级缓存只能保存不可变栅格。
- 公开使用规则、限制与验证入口见 shadows.md。
4.16 Semantics 与无障碍边界
- 无障碍声明统一使用
Modifier.semantics { ... }与SemanticsConfiguration;contentDescription只是该结构化契约的便捷入口,禁止新增平行单字段 Modifier。 - 平台无关契约必须覆盖描述、状态、role、heading、live region、选中/勾选/启用、错误、进度、pane title、点击标签、合并后代与隐藏子树。
- renderer 必须通过 Android 原生 View 属性与
AccessibilityNodeInfoCompat映射语义,不建立自有无障碍树。 - 同一 View 被 patch 或复用时,移除 semantics 必须恢复该 View 原有的 content/state/delegate/heading/live-region/importance,禁止把上一节点语义泄漏到下一节点。
- TextField、列表、滑块等原生控件的内建语义优先保留;结构化 semantics 只覆盖显式声明的属性。
4.17 系统导航边界
- 返回栈、路由值、导航事务与页面生命周期规划固定落在纯 Kotlin/JVM 的
viewcompose-navigation-core。 - AndroidX
LifecycleOwner/ViewModelStoreOwner/SavedStateRegistryOwner、系统返回分发和页面 View 容器只能进入后续 Android 导航集成模块。 - 一个 destination 对应一个独立页面
RenderSession;禁止把完整返回栈作为根 Session 中普通条件分支实现。 - 导航必须走 prepare/commit/rollback 两阶段事务;候选页面首次渲染成功前不得发布新返回栈或暂停当前页面。
- 被隐藏但仍在栈中的页面保持
CREATED并保留状态所有权;自适应窗格场景中的多个可交互页面可以同时为RESUMED;永久移除且退出转场完成后才进入DESTROYED并释放资源。 - Activity/Window 仅是根平台宿主,不作为 destination;导航稳定前不得改变现有 Activity/Fragment 宿主入口。
- 候选 destination 必须先在未挂载容器中同步完成首帧,再以隐藏状态 staged;回滚必须同时释放页面 Session 与 entry owner。
- 已提交 destination 复用页面 Session 时,必须在显式刷新路径更新最新
UiLocalSnapshot与内容闭包,禁止复用首次创建时的旧环境。 pop发布新返回栈前必须先刷新即将重新显示的已有页面;刷新失败时保留原返回栈、当前可见页和生命周期。- 导航执行期间产生的重入命令必须进入主线程串行队列;失败候选渲染期间产生的命令随候选一起丢弃,禁止作用到旧返回栈。
- 返回栈提交后的 effect 应用若发生不可恢复异常,协调器必须进入
Failed并拒绝后续命令,禁止在部分提交状态继续运行。 - 自适应多窗格只能改变同一已提交返回栈的可见集合与原生 View 布局;禁止建立平行导航状态、重建可见 entry owner,或让窗格策略引用活动栈外 entry。
详细规范见 navigation.md。
5. 当前热点与风险
ViewTreeRenderer仍是复杂度热点,新增能力优先拆辅助对象,不继续堆主类。- 当前是“节点组级重组 + 根级遍历调度”模型;后续优化重点是提升组键稳定性诊断与更细粒度跳过命中率。
viewcompose-widget-core已解除对renderer的直依赖;后续演进优先维持runtime/ui-contract/widget-core/renderer/host-android分层,不回流耦合。- 延迟 session 容器专项回归已覆盖
LazyVerticalGrid/HorizontalPager/VerticalPager;Lazy P1 已补齐结构化 item DSL、完整可观察 layout state、sticky headers、contentType/span、预取和边界能力。 AndroidHostBridge已迁至viewcompose-host-android;若后续目标扩展到跨平台,下一步重点是进一步收口widget-core内 Android 专属 bridge(theme/environment)边界。
6. 变更落地清单(必须执行)
任何架构相关改动,至少完成:
- 模块/目录归属审查
- 文档同步(本文档 + 相关规范文档)
- 单元测试或 instrumentation 回归(按能力类型选择)
- demo 验证路径补齐
执行流程规则见:
7. 关联文档
- 统一能力路线图:roadmap.md
- 性能主线:performance.md
- 状态快照规范:state-snapshots.md
- 文档入口:docs/README.md
- 系统导航规范:navigation.md