Android Card Height Layout Issue

日期: 2026-07-08
分支: ai
状态: 待解决 — 改用 300dp 固定高度作为临时方案


问题描述

Android FlexCardView 渲染卡片时,Hippy Yoga 布局引擎计算的 content height 无法传递给 Android View 系统,导致:

  1. 初始布局高度为 0(WRAP_CONTENT → UNSPEC height → 0)
  2. Hippy C++ onRuntimeInitialized 读取 rootView.getHeight()=0 → UpdateRootSize(h=0) → CalculateLayout(0,0) → 全部折叠
  3. 当前临时方案:固定 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/ 内核代码被拒绝。

待探索的方案

  1. 从 RenderManager 读取 computed height:RenderManager.batch() 已有 RootNode 的 layout 数据 (w=1142 h=338),可以通过 callback 传回 FlexUIView 层
  2. 改 addView 为 WRAP_CONTENT + 自定义 onMeasure:重写 rootView 的 onMeasure,在 Hippy 布局完成后返回正确高度
  3. 使用 onSizeChanged 反向传递:在 NativeRenderer 或 RenderManager 中,layout 完成后通过 listener 通知 FlexUIView 更新高度
  4. 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 HippyArray
  • docs/dev-pitfalls.md #24 - InvocationTargetException 隐藏根因
  • docs/dev-pitfalls.md #25 - callJavaScriptModule 双向链路调试
  • docs/dev-pitfalls.md #29 - McpConfigLoader 未注入 _componentPath