Android Card Height Layout Issue
日期: 2026-07-08
分支:ai
状态: 待解决 — 改用 300dp 固定高度作为临时方案
问题描述
Android FlexCardView 渲染卡片时,Hippy Yoga 布局引擎计算的 content height 无法传递给 Android View 系统,导致:
- 初始布局高度为 0(WRAP_CONTENT → UNSPEC height → 0)
- Hippy C++
onRuntimeInitialized读取rootView.getHeight()=0→UpdateRootSize(h=0)→CalculateLayout(0,0)→ 全部折叠 - 当前临时方案:固定
minContainerHeightPx兜底,避免 h=0 折叠
关键日志证据
双批次 layout 问题
NativeRenderer: onSizeChanged: rootId=10 w=1142 h=338 → calling updateRootSize
NativeRenderer: onSizeChanged: rootId=10 w=1142 h=0 → calling updateRootSize ← 同一毫秒!
[INFO:root_node.cc(525)] DoAndFlushLayout id=10 rootSize=338.37x100.148
[INFO:dom_node.cc(227)] SetLayoutSize id=10 width=338.37 height=100.148
...updateLayout: id=10 height=338 ← 第一次 layout 正确
[INFO:dom_node.cc(227)] SetLayoutSize id=10 width=338.37 height=0
...updateLayout: id=10 height=0 ← 第二次 layout 折叠为 0
调用链
FlexUIView.onSizeChanged (带 h>0 保护)
→ HippyEngineManagerImpl.onSizeChanged (仅发送 JS event,不调 updateRootSize)
NativeRenderer.onRuntimeInitialized (无 h>0 保护!)
→ rootView.getHeight() → 返回 0(父容器未 layout)
→ onSizeChanged(rootId, w=338, h=0) → updateRootSize(h=0)
→ C++ SetLayoutSize(h=0) → DoLayout → CalculateLayout(0,0) → 折叠
根因分析
1. Android View 系统的 WRAP_CONTENT 测量
FlexUIView (WRAP_CONTENT height)
└─ FrameLayout.onMeasure(EXACT_w, UNSPEC_h=0)
└─ rootView (MATCH_PARENT height) → measuredHeight = 0
→ onRuntimeInitialized 读取 rootView.getHeight() = 0
2. Hippy Yoga 与 Android Layout 不同步
| 平台 | 布局引擎 | 尺寸通信 |
|---|---|---|
| OHOS ArkUI | Yoga (C++) | ArkUI framework 自动读取 Yoga 计算的 content height → 设置 FlexView 高度 |
| Android | Yoga (C++) | Yoga 布局运行在 bridge 线程,Android View 系统运行在 main 线程,无自动同步 |
OHOS 的 FlexView 使用 .width('100%') 无固定高度,ArkUI 自动根据 Yoga 计算结果调整容器高度。Android 的 FlexUIView 使用 onMeasure + setMeasuredDimension,但 Yoga 计算的 content height 无法传回 Android 层。
3. 无法使用 WRAP_CONTENT 动态适配
Hippy 的 root view 不参与 Android measure() 系统(内部用 Yoga),WRAP_CONTENT 会导致 FrameLayout.onMeasure 测量 rootView 为 0。切换为 WRAP_CONTENT 后,第二次 onMeasure 再次触发 h=0 → UpdateRootSize(0) → 布局折叠。
4. CalculateLayout(0, 0) 的 Kernel 行为
// dom_node.cc
layout_node_->CalculateLayout(
is_layout_width_nan_ ? NAN : 0, // 非 NaN 时传 0
is_layout_height_nan_ ? NAN : 0 // 非 NaN 时传 0
);
当 SetLayoutSize(width, height) 被调用后 (height 为非 NaN),DoLayout 传 CalculateLayout(0, 0) → Yoga 以 0x0 父约束计算布局 → 所有子节点折叠为 0。
注:OHOS 上此代码相同,但 OHOS 可能未调用 SetLayoutSize(或传 NaN),让 Yoga 基于内容自由计算。
已尝试的方案
方案 A: FlexUIView.onMeasure 强制 min height ✅ (当前方案)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (measuredHeight < minContainerHeightPx) {
setMeasuredDimension(measuredWidth, minContainerHeightPx)
}
}
- ✅ 解决 h=0 折叠问题
- ❌ 高度固定为 min height (300dp),不是真正的 wrap-content
- ❌ 卡片内容较少时显示多余空白
方案 B: rootView 初始化高度 = minContainerHeightPx ✅ (当前方案)
addView(root, ViewGroup.LayoutParams(MATCH_PARENT, minContainerHeightPx))
- ✅ 防止
onRuntimeInitialized读到 h=0 - ❌ rootView 高度固定,不会随内容动态调整
方案 C: onFirstViewAdded 后切换 WRAP_CONTENT ❌ (失败)
fun onContentReady() {
rootView.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
requestLayout()
}
- ❌ WRAP_CONTENT 再次触发 h=0 → UpdateRootSize(0) → 折叠
- ❌
ClassCastException:需要FrameLayout.LayoutParams而非ViewGroup.LayoutParams
方案 D: NativeRenderer.onSizeChanged 忽略 h=0 ❌ (不可修改 kernel)
修改 renderer/native/android 内核代码被拒绝。
方案 E: C++ DoLayout 使用存储的尺寸 ❌ (不可修改 kernel)
layout_node_->CalculateLayout(
is_layout_width_nan_ ? NAN : layout_node_->GetWidth(),
is_layout_height_nan_ ? NAN : layout_node_->GetHeight()
);
修改 dom/ 内核代码被拒绝。
待探索的方案
- 从 RenderManager 读取 computed height:
RenderManager.batch()已有 RootNode 的 layout 数据 (w=1142 h=338),可以通过 callback 传回 FlexUIView 层 - 改
addView为 WRAP_CONTENT + 自定义onMeasure:重写 rootView 的onMeasure,在 Hippy 布局完成后返回正确高度 - 使用
onSizeChanged反向传递:在NativeRenderer或 RenderManager 中,layout 完成后通过 listener 通知 FlexUIView 更新高度 - Compose 层使用
IntrinsicSize:Modifier.height(IntrinsicSize.Min)可能让 Compose 等待真实内容高度
相关文件
| 文件 | 作用 |
|---|---|
framework/android/.../FlexUIView.kt |
Android 卡片容器,onMeasure + loadCardInternal |
android-modules/agent-runtime/.../FlexCardView.kt |
Compose 包装器,client callbacks + flushEvents |
renderer/native/android/.../NativeRenderer.java |
onRuntimeInitialized 读取 rootView 尺寸 |
dom/src/dom/dom_node.cc |
DoLayout → CalculateLayout 逻辑 |
dom/src/dom/root_node.cc |
DoAndFlushLayout → GetLayoutSize + DoLayout |
renderer/native/android/.../NativeRenderProvider.java |
onSizeChanged → updateRootSize JNI |
renderer/native/android/.../RenderManager.java |
batch() 处理 layout 结果 |
相关 PR/Pitfall
docs/dev-pitfalls.md#23 - @HippyMethod 参数类型 HippyMap vs HippyArraydocs/dev-pitfalls.md#24 - InvocationTargetException 隐藏根因docs/dev-pitfalls.md#25 - callJavaScriptModule 双向链路调试docs/dev-pitfalls.md#29 - McpConfigLoader 未注入 _componentPath