
Notes / 前端与移动端
虚拟列表中,为什么组件看得见却仍会被下一项遮挡
从真实内容高度、虚拟列表占位高度和 transform 布局语义,复盘一次只在 iOS 真机出现的模块遮挡问题。
一个看起来像 CSS 的问题
在一次首页模块改版中,我为不同推荐区域拆分了新的展示组件:有的使用横向角色卡片,有的需要上下交错排列,还有的增加了贴近图片底部的装饰元素。
浏览器中的初步效果正常,但放到 iOS 真机后,两个新模块的底部内容会被下一块模块遮挡。其他旧模块没有问题,因此第一反应很容易落到普通 CSS 上:是不是层级不够、父元素裁切了内容,或者安全区没有算对?
这些现象确实存在,但它们不是完整根因。真正的问题是,页面使用了虚拟列表,而虚拟列表理解的“这一项有多高”和组件实际渲染出来的高度已经不一致。
新旧模板先通过布局变体隔离
这次首页后端返回的模块仍然沿用原有 type,但同一个 type 在新版首页中可能对应完全不同的视觉结构。为了不把矩形角色卡、交错横滑和 2×3 网格都塞进旧组件,我在数据进入展示层时补充 _layoutVariant:
for_you_feed -> for_you_showcase
new_feed -> new_arrivals_stagger
trending_feed -> grid_2x3
popular_feed -> grid_2x3
fine_treasures -> fine_treasure_mosaic
模块识别优先使用稳定的 id 或 sourceId,title 只作为测试阶段的兜底。这样既能让后端协议尚未完全稳定时继续真机验证,也不会把展示标题变成长期接口契约。
新版 For You 和 New Arrivals 分别拆成独立组件。旧组件服务于圆形头像横滑和两行跑马灯,如果继续复用,会让同一个文件同时承担两套差异很大的 DOM、尺寸和交互规则。拆分组件不是为了追求文件数量,而是让新旧布局的高度计算互不污染。
虚拟列表看到的是占位高度
普通文档流会根据 DOM 的真实尺寸把后续内容向下推。虚拟列表为了只渲染可见区域,需要提前知道每一项的位置,因此通常依赖配置高度或缓存高度计算下一项的起点。
可以把它理解为两套同时存在的坐标:
- 真实内容高度:浏览器布局后,用户实际看到的组件尺寸;
- 虚拟占位高度:虚拟列表用来计算下一项位置的尺寸。
当新组件的内容超过配置高度时,组件本身仍然可以继续绘制,但下一项已经按照较小的占位高度提前出现。于是视觉上就像“下一块压了上来”。
这也解释了为什么单纯提高 z-index 没有意义:问题不是谁盖在谁上面,而是下一项从一开始就被放在了错误的位置。
几个让高度失真的细节
固定 height 太相信设计稿
设计稿中的固定高度描述的是某个画布上的最终结果,不一定适合直接作为 WebView 中的布局约束。字体渲染、设备宽度、安全区和像素取整都可能让真实内容多出几像素。
新模块最初使用的固定高度过紧,内部稍有变化就会超出容器。将外层改为 min-height 后,组件仍然保留最低视觉尺寸,同时允许内容决定最终高度。
overflow: hidden 把线索藏起来
纵向裁切会让超出部分直接消失,看起来像是子元素尺寸不对。排查阶段将纵向 overflow 调整为可见,可以先确认内容到底有多高,再决定哪里应该裁切。
横向滚动和纵向裁切也不应该被一个笼统的 overflow 同时控制。这个问题让我更习惯按方向明确布局意图。
transform 不参与文档流
上下交错的卡片最初通过 transform: translateY(...) 下移。它在视觉上移动了,但浏览器计算父容器高度时仍然使用变换前的位置。
因此,卡片看起来已经更低,虚拟列表却不知道这段额外距离存在。改用 padding-top 后,下移量进入正常布局,父容器和虚拟占位才有机会得到一致结果。
判断标准很简单:
如果位移应该影响后续内容的位置,就不能只使用 transform。
边框与真机像素取整
半像素边框在高 DPR 屏幕上通常能得到更细的视觉效果,但也可能参与最终尺寸取整。给卡片统一设置 box-sizing: border-box,可以避免边框在预期尺寸之外继续撑大盒子。
单独看这类误差很小,可当固定高度、装饰定位和虚拟列表占位同时偏紧时,几像素就足以暴露问题。
修复不是只改一个数值
最终处理包含两层。
组件内部:
- 固定高度改为最小高度;
- 取消不必要的纵向裁切;
- 使用参与文档流的间距实现交错;
- 装饰元素放回每张卡片自己的视觉容器;
- 统一盒模型,避免边框额外增加尺寸。
虚拟列表层:
- 根据新的展示变体设置对应占位高度;
- 为真机字体和像素取整保留少量安全空间;
- 不影响仍使用旧布局的其他模块。
后续补齐 2×3 网格、1×4 横滑、1+4 组合和交错横滑模板时,我仍然只让这些新变体使用设计稿要求的 2px 模块间距,旧模板继续保留原来的 5px。CSS 间距和 _fixedHeight 必须同步修改,否则组件看起来只有 2px 间隔,虚拟列表却仍按旧高度安排下一项,最终真机结果仍会偏离设计稿。
这两个层次必须一起修改。只放大占位高度会掩盖组件自身的布局问题;只修组件内部,虚拟列表仍可能继续使用旧缓存或旧配置。
真机验证关注什么
完成修改后,我在 iOS 真机上逐项检查:
- 新模块底部不再被下一项遮挡;
- 上下交错仍然成立;
- 装饰元素与图片底边保持对齐;
- 横向滑动和卡片点击正常;
- 后续旧模块的排列没有被新高度污染。
这里不能只依赖桌面浏览器截图,因为虚拟列表、WebView 字体和 DPR 取整共同参与了结果。
留下的排查方法
以后遇到虚拟列表中的重叠、空白或跳动,我会同时检查四个数字:组件真实高度、虚拟占位高度、视觉位移和盒模型额外尺寸。
我也不再把“看起来移动了”等同于“布局知道它移动了”。在虚拟列表中,视觉位置和计算位置一旦分离,后续所有项目都会基于错误坐标继续排列。先让这两套坐标重新一致,比不断补 margin、z-index 或 overflow 更可靠。
Recommended