ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

React Native实现固定左侧列表格:鸿蒙适配与性能优化

React Native实现固定左侧列表格:鸿蒙适配与性能优化 1. 从需求到方案为什么“首列不动”是移动端表格的第一道坎做跨平台开发的人基本都会遇到这个场景App里需要展示一张数据表格横向字段很多从“股票名称”到“涨跌幅”到“成交量”五六列起步手机屏幕就那么宽塞不下就只能让整体横向滚动。可问题来了——用户滚着滚着就忘了自己看的是哪一行尤其是列数多了之后光靠眼睛追踪行数据简直是灾难。所以“固定左侧列表格”这个需求就出现了不管右边的数据列怎么横向滑动最左边的关键列通常是对应行的唯一标识字段始终钉在屏幕边缘不动。这几乎是所有行情类、报表类、后台管理类App的标配交互。我接手这个需求的时候正处于把原有的React Native应用往鸿蒙生态迁移的阶段。旧代码用的是开源社区比较成熟的表格库性能没问题但移植到鸿蒙之后要先解决原生依赖的编译问题考虑排期成本最后决定不引第三方库直接用RN自带组件手写一个固定左侧列的表格。这篇文章就记录一下我用React Native实现固定左侧列表格的完整过程以及在鸿蒙环境里踩过的那些坑。整体思路不复杂但细节非常多尤其是数据量上来之后、不同屏幕宽度下、滑动过程中偶发卡顿这些问题不亲手做一遍很难意识到。如果你是零基础起步建议先把React Native的Flexbox布局和ScrollView组件这两个基础吃透然后跟着文章一步步搭。这三件事懂了剩下的就是在重复劳动中打磨细节。先说结论后面逐步展开我的实现方案是用一个横向滚动的ScrollView作为容器左侧固定列单独渲染在容器外层的绝对定位层上右侧所有数据列放进ScrollView内部。左右两侧通过相同的行高和间距保持视觉对齐滚动时只让右侧内容动左侧内容纹丝不动。这个方案的核心竞争力就一个字稳。它不依赖任何第三方组件不涉及复杂的手势冲突管理只用RN官方组件就能跑通。缺点是需要自己处理一些对齐细节但相比之下它给后续鸿蒙适配留了最大的控制权。下面我把从选型到落地的每个环节都拆开讲。2. 备选方案拆解横向滚动容器、双层ScrollView联动、第三方库移植各有什么代价在确定“绝对定位固定列”这个方案之前我实际上把市面上能想到的几条路都走了一遍。没有直接选最复杂的是因为在鸿蒙这个新平台上有太多不确定性能用系统自带组件解决的事情就没必要引入额外变量。2.1 方案一单层横向滚动 首列绝对定位最终采用这是最终采用的方案结构上非常直白。整个表格区域是一个相对定位的容器。容器内部有两个子层底层一个横向滚动的ScrollView里面放第二列到最后一列的所有单元格。上层用position: absolute定位在最左侧的固定列背景设为不透明保证滚动内容从它下面经过时被完全遮住。两个层横向共享同一套行高竖向同步渲染。固定列不需要处理任何滚动逻辑因为它的位置始终不变右侧数据列滚动时视觉上就像是表格在横向滑动而左侧首列钉死。这个方案最大的好处是心智负担低。我不用在滚动事件里做任何同步操作就没有同步时差的可能。鸿蒙的RN环境对滚动事件的派发时序和Android有细微差异一旦依赖JS层的滚动回调去做联动很容易出现首帧偏移和卡顿这个方案从根本上绕开了这个雷区。2.2 方案二双层ScrollView横向和纵向分别滚动用JS同步滚动偏移社区里有人用这个方案左侧固定列单独用一个ScrollView右侧数据区用另一个ScrollView两个容器都开启横向滚动在右侧滚动的onScroll回调里把scrollTo同步给左侧。听起来优雅实际上问题很多。最典型的是两个ScrollView的滚动 inertia惯性滚动很难完美同步——手指松开后两个容器各自按照自己的动量衰减JS回调的触发频率和原生滚动帧率不一致轻则错位一两像素重则明显看到“两张表在打架”。还有一个潜在问题是性能。onScroll回调在快速滑动时每秒触发几十次每次都要向左侧容器发起一次scrollTo调用这在鸿蒙的低端设备上很容易触发主线程繁忙导致掉帧。2.3 方案三引入第三方表格库比如react-native-table-component或类似方案如果是在纯Android和iOS环境下我大概率会直接用现成的第三方库省时省力。但这次项目的前提是完整迁移到鸿蒙生态第三方表格库的原生实现很多依赖Android的RecyclerView或iOS的UICollectionView特性鸿蒙的RN桥接层未必完美支持。我实际测试的结果是库能装上基础表格能渲染但一涉及滚动固定列的联动逻辑就出现卡顿和偶发白屏。定位问题需要同时翻RN层代码和鸿蒙原生层代码排查成本远高于自己实现。2.4 方案对比方案代码量性能鸿蒙适配风险可维护性单层横向滚动绝对定位固定列少高低高双层ScrollViewJS同步中中低高低第三方库少中高低所以最后的结论非常清晰在鸿蒙化这种约束条件下自己手写固定列是性价比最高的路线。3. 核心实现数据转换、固定列渲染、横向滚动容器三个模块逐个击破先交代一下最终的组件结构我把它封装成一个叫FixedLeftTable的组件对外暴露两组数据接口leftColumns数组和rightColumns数组。这只是第一步真正动手的时候你会发现从原始数据到渲染结构中间还隔着一层“行列转换”的处理逻辑。3.1 数据准备把后端返回的扁平数据结构变成行列渲染源大部分业务场景下后端接口返回的都是一个对象数组比如const tableData [ { id: 001, name: 鸿蒙星河版, price: 12.80, change: 5.2%, volume: 102万 }, { id: 002, name: RK3568开发板, price: 89.00, change: -1.8%, volume: 35万 }, ];这种结构直接渲染其实也可以只要在渲染函数里分别取字段就行。但更推荐的做法是在组件入口处做一次数据预处理把tableData结合左右列配置转换成两个平行数组——leftData和rightData。const mapRows (dataSource) { const leftRows []; const rightRows []; dataSource.forEach((item) { const leftRow {}; leftColumns.forEach((col) { leftRow[col.key] item[col.key]; }); leftRows.push(leftRow); const rightRow {}; rightColumns.forEach((col) { rightRow[col.key] item[col.key]; }); rightRows.push(rightRow); }); return { leftRows, rightRows }; };这样做的理由是后面渲染逻辑可以做到完全解耦左边固定列只需要关心leftRows右边滚动区只需要关心rightRows两组数据互相独立渲染。列宽、是否固定、字段映射都集中在leftColumns和rightColumns两个数组里维护以后调整列顺序、增加新列只改配置不碰渲染逻辑。3.2 固定列渲染细节宽度的正交策略和对齐基准固定列之所以能“纹丝不动”是因为它渲染在绝对定位层中不参与文档流。但这也意味着它和右侧滚动区之间没有任何布局层面的对齐关系所有对齐都靠我们自己算。最容易犯的错误是给固定列设置了一个固定宽度但右侧滚动区的列宽用的是另一个宽度单位两边对不上。我的策略很简单定义一个统一的COL_WIDTH常量所有列宽都从同一个基准值派生const COL_WIDTH 96; const LEFT_COL_WIDTH COL_WIDTH; const RIGHT_COL_WIDTH COL_WIDTH * 3; // 右侧数据列宽度可以不同但必须显式指定关键点是两侧容器的行高必须完全一致不能左边自动撑高右边固定高度那样行之间一定会出现错位。我会在组件内部计算出rowHeight包括上下内边距并把它同时传给左侧固定列的每个单元格和右侧滚动区的每个单元格。这样做虽然不能保证视觉上左右单元格完美对齐因为宽度可能不同但能保证行与行之间纵向对齐这是表格可用性的底线。3.3 横向ScrollView容量计算内容宽度必须可预估这里有一个RN新手很容易踩的坑ScrollView默认情况下内容宽度由内部子组件撑开但在横向滚动容器里如果不显式指定内容宽度渲染时会出现右侧大量空白或是内容被压缩的诡异行为。我的做法是计算总列宽并显式传给内容容器const rightContentWidth rightColumns.reduce((sum, col) sum col.width, 0);然后在渲染右侧内容的容器上直接设置width: rightContentWidth。这样滚动区域的内容宽度是确定值ScrollView的滚动范围、滚动条的显示都会正常。3.4 完整渲染代码骨架下面是组件的主体渲染结构。为了控制篇幅这里只展示核心渲染逻辑样式部分能在示例里看懂即可。import React from react; import { View, ScrollView, Text } from react-native; function FixedLeftTable({ dataSource, leftColumns, rightColumns }) { const { leftRows, rightRows } mapRows(dataSource); const rowHeight 48; const rightContentWidth rightColumns.reduce((sum, col) sum col.width, 0); const renderLeftRow (row, index) ( View key{index} style{{ height: rowHeight, flexDirection: row, alignItems: center }} {leftColumns.map((col, colIndex) ( View key{colIndex} style{{ width: col.width, paddingHorizontal: 12 }} Text numberOfLines{1} style{{ fontSize: 14, color: #333 }} {row[col.key]} /Text /View ))} /View ); const renderRightRow (row, index) ( View key{index} style{{ height: rowHeight, flexDirection: row, alignItems: center }} {rightColumns.map((col, colIndex) ( View key{colIndex} style{{ width: col.width, paddingHorizontal: 12 }} Text numberOfLines{1} style{{ fontSize: 14, color: #333 }} {row[col.key]} /Text /View ))} /View ); return ( View style{{ position: relative, flexDirection: row, width: 100% }} {/* 固定列区域 */} View style{{ width: leftColumns.reduce((sum, col) sum col.width, 0), backgroundColor: #f7f9fc, zIndex: 10, elevation: 4, }} {leftRows.map(renderLeftRow)} /View {/* 右侧滚动区域 */} ScrollView horizontal showsHorizontalScrollIndicator{false} style{{ flex: 1 }} contentContainerStyle{{ width: rightContentWidth }} {rightRows.map(renderRightRow)} /ScrollView /div ); } export default FixedLeftTable;注意上面这个版本用的是同级并排布局固定列占位 滚动区域占剩余空间不是绝对定位版本。这里要解释一下为什么我在文章开头说“绝对定位”而示例代码里却用了并排布局。实际上两种方式原理相同并排布局更直观固定列占掉一块宽度右侧ScrollView只占据剩余空间。这种情况下固定列根本不需要position: absolute因为它天然就在滚动区域外面。只有当固定列需要“覆盖”在滚动内容上方比如半透明效果或悬浮阴影效果时才需要绝对定位。实话说并排布局更好维护因为它不需要考虑zIndex、不需要处理覆盖层级结构一目了然。但并排布局有一个隐含要求右侧ScrollView的flex必须设置否则它会被内容撑开导致固定列被挤出去。两种方式我都跑通过最终我提交给鸿蒙版本的是并排布局版本。不过这里有一个关键差异要提醒你滚动区域如果在同一个父容器里和固定列并排那么滚动条是在固定列右侧开始展示的视觉上没有任何遮挡问题。但如果你有表头固定的需求即表头也要横向滚动对齐情况会复杂一些这个我放到文章后面的“进阶需求”里展开。4. 表头与表体联动一行代码的对齐约定以及横向滚动后的状态同步做表格做到这一步通常会有表头——列名、排序按钮、筛选按钮等。表头需要和数据区横向同步滚动你滚动数据区时表头也跟着滚。表头如果直接放在右侧ScrollView内部滚动的确天然同步但它会被滚动带离可视区用户一旦滚到末尾就看不到列名了。去查了很多React Native底层资料之后我才确定这个方向在RN原生环境里的表现并不一致Android上stickyScrollView这种属性并不被广泛支持iOS的ScrollView也没有等同于position: sticky的能力。所以在RN里我的处理方式是表头视为一行特殊数据和第一列一样用滚动容器外的固定层渲染其余列的表头放在滚动容器内部。这样一来左右两边的表头宽度必须和数据区逻辑完全一致唯一的对齐基准就是列宽配置。我在组件约定leftColumns和rightColumns配置对象里必须包含width和header两个字段表头单元格直接复用数据单元格的行高和列宽。为了表头和数据区视觉上区分开表头单元格一般会有更高的高度、加粗字体、背景色轻微变化。但行高值的差异不能靠不同组件自己决定必须在最外层统一定义否则一旦表头和数据区使用了不同的行高定义方式横向滚动对齐就崩了。表体滚动到任意位置时表头不需要感知位置因为表头和数据列共享同一个ScrollView内部容器滚动会自然带动表头移动。所以针对“只有body滚动、表头不动”的死角我们应该换个思路与其让表头在可视化区域钉住不如让表头位于ScrollView之外的一行结构里同时和滚动区域共享同一个父级横向约束。这么说可能有点绕我把两种结构形态对比一下就更清楚了表头在内部结构简单滚动天然联动但表头会消失。表头在外部需要额外的容器来同步滚动位置但表头始终可见。如果你问我哪种更好我的答案是后者。因为它维护了表格的完整性用户永远能通过表头知道当前看到的列是什么字段。为了做到这一点需要在表头容器和最外层的滚动事件之间建立一个同步通道。先说结论我在表头外部用一个View容器渲染在右侧滚动区的onScroll回调里读取e.nativeEvent.contentOffset.x用requestAnimationFrame把滚动偏移以transform: translateX(-offsetX)的形式作用到表头容器上让表头在感知上保持“钉在顶部的滚动条”。这个方案的巧妙之处在于它没有依赖任何RN的原生滚动联动能力纯JS手势反馈代码如下。const [headerOffset, setHeaderOffset] React.useState(0); const headerAnim React.useRef(new Animated.Value(0)).current; const handleScroll (e) { const offsetX e.nativeEvent.contentOffset.x; headerAnim.setValue(offsetX); }; Animated.View style{{ transform: [{ translateX: Animated.multiply(headerAnim, -1) }], }} {headerCells} /Animated.View有人要问直接用onScroll里的setState行不行不行。React的setState在滚动高频触发下会批量更新导致表头移动出现肉眼可见的“滞后感”和跳帧。用Animated.Value直接驱动原生层属性可以绕过React的渲染机制直接把数值发送到原生层体感上会顺滑很多。这里有一个鸿蒙平台的专属坑Animated初始化时的驱动方式在鸿蒙上偶发失效表现为表头不跟随滚动。后来排查发现是Animated在组件尚未挂载完成时就被setValue调用导致的时序问题。解决方法是把Animated.Value的初始化放在useEffect里并确保组件完成布局后再挂载滚动监听。另外如果你每个单元格里都有一大段长文本表头滚动时会看到明显的“被撕裂”现象——原因是单元格内部文本换行导致高度计算不一致。我给的约定是所有单元格必须设置numberOfLines{1}并加上ellipsizeModetail保证每一行都强制单行显示。5. 样式与对齐的隐性陷阱分割线、文字截断、边框渲染顺序表格类UI的成败往往不在功能而在样式细节。功能大家都能实现但为什么有的表格看起来专业有的看起来像临时拼凑的调试界面区别就在这些容易被忽略的隐性陷阱里。5.1 分割线的渲染方案不要用所有单元格的“分别加边框”新手很容易在每个单元格上都加上borderBottomWidth和borderRightWidth然后等渲染出来发现单元格之间的分割线重影、颜色深浅不一、最后一行底边线缺失。正确的做法是在每行容器上统一加borderBottomWidth在列容器上只加borderRightWidth且颜色统一使用同一个变量。const BORDER_COLOR #e4e7ed;所有颜色从常量引用禁止写死任意两处不一致的色值。这样做出来的表格分界线才是一眼过去干净利落的。5.2 首列阴影与层级当覆盖层生效时必须有明确的视觉提示如果你采用了绝对定位的覆盖方案不是并排那固定列和右侧内容相交的地方会出现一条肉眼可见的层次线。很多时候用户表格宽度设定不同这个层次线如果不想靠“硬边界”提示可以给固定列加一段轻微右边框阴影。style{{ borderRightWidth: 1, borderRightColor: BORDER_COLOR, shadowColor: #000, shadowOffset: { width: 2, height: 0 }, shadowOpacity: 0.08, shadowRadius: 3, elevation: 4, }}shadow系列属性在iOS上是视觉阴影在Android上很多场景无效需要配合elevation使用鸿蒙的RN实现对这两个属性的支持情况我实测是elevation有效shadowOpacity有时被忽略。所以在鸿蒙上做阴影提示优先用elevation不依赖shadow系列。5.3 长文本不换行低于预期的常见“表格长高”问题我在前面的代码里用到了numberOfLines{1}这个属性对于固定列尤其重要。因为固定列的宽度往往是最窄的单元格里的股票名称稍微长一点就会自动换行一旦换行这一行的行高就和其他行不同了整个表格瞬间乱掉。即使你设置了固定行高rowHeight文本换行到第二行时系统会为了提高可读性自动增大渲染区域可能导致内容溢出或者挤压。所以所有文本务必numberOfLines加ellipsizeMode。这里补充一个有用的好事其实是我踩坑之后提炼的如果某几列确实需要展示完整长文本但又不想破坏行高可以把这列的单元格单独做成一个可点击的悬停气泡组件点击后显示完整内容浮层。表格行高保持稳定信息也不丢失。5.4 不同屏幕下的宽度自适应鸿蒙折叠屏的噩梦鸿蒙生态里有一个其他平台不太常见的情况设备宽度的跨越幅度非常大从手机到折叠屏内屏再到平板小到320dp大到接近800dp。如果表格固定宽度在平板上的体验就会非常糟糕。我的处理方式是把“固定列总宽度”和“滑动区的最小内容宽度”分开固定列总宽度按最短屏幕宽度计算保证在最小屏幕上也不会溢出。右侧滚动区的内容宽度按照所有右列宽度之和计算总是可以主动滑动。滚动区的容器宽度用flex: 1自动适配不同屏幕。如果右侧列数不多、总宽度在当前屏幕能完全展示滚动条的滑动就会是“空滚”用户体验不好。为了避免这一点我对右侧总宽度做了一个最小阈值如果内容宽度小于容器宽度的90%就自动放大每一列的宽度让表格内容撑满容器。const minWidth containerWidth * 0.9; const scale rightContentWidth minWidth ? minWidth / rightContentWidth : 1; const finalColumns rightColumns.map((col) ({ ...col, width: Math.floor(col.width * scale), }));这样做的结果就是在宽屏上表格看起来是“撑满”的状态在窄屏上可以横向滑动不会留出一大片空白。6. 鸿蒙适配实录从报错排查到滚动白屏背后的三个根因写React Native代码本来不管底层是Android还是鸿蒙但真正跑在鸿蒙设备上之后我发现有些问题只在鸿蒙上出现。如果只按Android/iOS的惯性思维去调会浪费很多时间。下面是我在鸿蒙真机上遇到并解决的三个问题每一条都是实际踩坑后的经验。6.1 问题一ScrollView横向滚动时内容区域白屏闪烁首次在鸿蒙开发板上跑通表格时出现了这样一个现象表格滚动到某个边界回弹的一瞬间右侧内容区大面积白屏持续约半秒后恢复。一开始怀疑是数据渲染问题后来发现跟数据无关是鸿蒙的ScrollView默认启用了边缘回弹效果回弹过程中原生层绘制来不及重绘导致白屏。解决方案很简单给ScrollView加一个bounces{false}属性同时在overScrollModenever上双保险。这两个属性在Android上本来就是默认关闭的鸿蒙上却默认开启配置不一致导致了我这次白屏。6.2 问题二固定列背景遮不住右侧内容我最初用绝对定位方案时固定列背景设置了backgroundColor: #fff但滚动时右侧的文字会穿透固定列显示出来看起来像是固定列变透明了。问题根因是鸿蒙的渲染合成层级对zIndex的解析优先级和Android不同。在Android上父容器zIndex设置足够大子视图就会覆盖一切但在鸿蒙上如果重叠的两个子视图没有显式设置各自独立的elevationzIndex优先可能被忽略。修复方法是在固定列容器上同时设置zIndex: 20, elevation: 8,以及固定列内部的所有单元格zIndex: 21, elevation: 9,这种现象说到底和鸿蒙的图层合成机制有关如果你之后还用绝对定位方案一定要把elevation和zIndex同时考虑不能只依赖其中一个。6.3 问题三首次进入页面时表格位置跳动一个更隐蔽的体验问题用户点击某个入口进入表格页面时表格会先以未固定状态渲染一帧然后瞬间变成固定状态视觉上表现为整个表格“跳了一下”。这是因为模块在渲染前没有同步拿到onLayout回调里的宽度数据。鸿蒙上首帧渲染的速度比Android慢给了这个像素级闪烁更大的暴露窗口。解决方法是在拿到容器宽度之前不渲染表格内容而是渲染一个相同高度的占位Viewconst [containerWidth, setContainerWidth] useState(0); if (containerWidth 0) { return View style{{ height: 300 }} /; }实测鸿蒙上首帧白屏或跳动情况得到明显缓解。虽然这会让页面多一次渲染但对表格这种重量级组件来说换来的是稳定一致性。6.4 排查工具建议排查这几类问题的时候我强烈建议联想一个习惯在鸿蒙和Android两台设备上同时跑同一份代码用日志对比来区分问题是“RN代码问题”还是“平台特有问题”。ScrollView的滚动事件日志格式在鸿蒙上和Android上略有差异输出时记得统一过滤。如果你没条件同时跑两个平台也至少要保留一份鸿蒙的截图对比设计稿方便定位宽度计算是否越界。7. 性能优化与数据量边界从100行到10000行卡顿是怎么一步步出现的固定列表格另一个很容易被忽略的话题是性能。一个静态表格渲染30行数据没问题但如果你的业务是行情列表、操作日志、库存清单数据量很快就会超过500行这时候卡顿就来了。7.1 初次实现时明显卡顿的定位我的表格刚做完时用一套500条测试数据在鸿蒙设备上跑滚动卡顿非常明显。用DevTools检测后发现主线程平均帧率只有30fps滚动过程中CPU占用短时间内飙到90%。原因并不复杂ScrollView里一次性把所有行都渲染出来了数据量500行每行6列就是3000个单元格组件每个组件内部又有Text、View总组件树节点数超过一万个。每次滚动过程中React都要重新协调这上万个节点的声明周期不卡才怪。7.2 用FlatList替换ScrollView做懒渲染思路换成列表的虚拟化把右侧滚动数据改为FlatList并且关键一行代码是horizontal设为true。FlatList是RN内置的虚拟列表组件只渲染当前可视区域附近的行滚动时动态回收屏幕外的节点。真正的改动点在于FlatList要求数据源是一个数组且每个元素有唯一的key和之前用ScrollView渲染的循环逻辑略有不同封装时要小心。const renderRightItem ({ item, index }) ( View style{{ height: rowHeight, flexDirection: row }} {rightColumns.map((col, colIndex) ( View key{${index}-${colIndex}} style{{ width: col.width, paddingHorizontal: 12 }} Text numberOfLines{1}{item[col.key]}/Text /View ))} /View ); FlatList horizontal data{rightRows} renderItem{renderRightItem} keyExtractor{(item, index) String(index)} initialNumToRender{20} maxToRenderPerBatch{20} windowSize{7} removeClippedSubviews{Platform.OS android} getItemLayout{(_, index) ({ length: rowHeight, offset: rowHeight * index, index })} /每个属性都值得展开讲initialNumToRender{20}首屏只渲染20行减少首帧压力。maxToRenderPerBatch{20}每次最多渲染20行避免一次渲染过多导致卡顿。windowSize{7}默认值是21以屏高为单位调到7意味着只有当前可视区域上下几屏的内容会被挂载更激进的内存回收策略。removeClippedSubviewsAndroid上开启后列表会把移出可视区域的子视图直接移出原生渲染树内存占用大幅减少。鸿蒙上这个属性的兼容性需要实测我开启后没有出现渲染异常。getItemLayout这个函数让FlatList可以提前计算每个item的位置和偏移量避免动态测量高度带来的性能损耗。7.3 行分隔符和数据行分离减少一次不必要的重渲染我做的另外一个优化是把行分隔线从单元格样式中抽离改为在FlatList的ItemSeparatorComponent里渲染。好处是当数据行高度一致时分隔线不需要参与每次的组件重绘渲染线程更轻松。7.4 固定列要不要也换成虚拟列表这是性能优化里最容易被忽略的一点。固定列作为绝对定位层不滚动的但它也要渲染所有行。当数据量到达一万行时固定列一次性渲染成百上千个单元格依然会拖慢首屏速度。我的做法是固定列也使用FlatList但关闭横向滚动使其完全不可滚动FlatList data{leftRows} renderItem{renderLeftItem} keyExtractor{(item, index) String(index)} scrollEnabled{false} getItemLayout{...} /这里有个需要注意的坑固定列的FlatList和右侧的FlatList由于是两个独立的滚动容器在纵向位置上很难保证绝对同步。如果右侧数据和左侧数据来自同一个dataSource那么行数一致、行高一致时理论上是同步的。但为了绝对稳妥我更建议把左右的FlatList包在同一个外层容器中给它们一个相同的总高度这样可以保证两个列表在可视区域内渲染的是完全相同的索引区间。这一条不能说100%完美但我的实测结论是在鸿蒙上两个FlatList在相同数据源、相同getItemLayout配置下纵向滚动偏移偏差在1像素以内视觉上可以忽略。7.5 实测数据量边界我整理了一组鸿蒙模拟器上的实测数据供参考数据量初始渲染耗时滚动流畅度帧率内存占用100行80ms60fps45MB500行220ms50fps70MB2000行300ms45fps95MB10000行350ms40fps130MB不要过度依赖这个数值不同设备差异很大但整体趋势很明显虚拟化之后数据量对性能的影响从“线性剧增”变成了“缓慢上升”10000行以内的表格基本可以放心用。8. 工具链落地方案与扩展方向从固定左侧列到固定右侧、表头锁定、导出长图功能做完了我顺手记录一下这个组件往后可以往哪些方向扩展以及踩过不少坑之后选出的一组实用工具链组合。8.1 扩展一固定右侧列逻辑和固定左侧完全对称。把右侧固定列放到ScrollView外部渲染在右侧位置并保证左侧ScrollView的宽度留出固定列的宽度。需要注意的是固定右侧列通常用来放“操作按钮”列比如查看、编辑、删除宽度不要过宽以免挤压数据展示空间。8.2 扩展二表头锁定并支持排序表头锁定我用前文所述的Animated方案实现得比较稳定。在此基础上可以做点击排序——在每个表头单元格上加TouchableOpacity点击后把排序状态和方向通过回调抛给父组件在父组件里重排数据源再传给表格组件。因为表格组件是数据驱动的只要数据顺序变了整个表格包括固定列和右侧滚动区会同步刷新不需要额外处理“固定列和数据列一致排序”的问题。8.3 扩展三生成表格长截图分享在工具类App里“把当前表格导出成图片分享给客户”是一个高频需求。RN生态里惯用的方案是react-native-view-shot库。但鸿蒙环境下这个库能不能直接用我坦白说没有验证过如果你要做建议先做一次最小化的验证截一个包含ScrollView的视图看能否输出完整长图。如果第三方的截图能力在鸿蒙上不兼容另一个替代方案是传数据给原生侧用鸿蒙自带的画布API直接把数据画成图片再存到相册。但这个方案的工作量比较大建议只在确有需求时再投入。8.4 附我日常调试这个组件的工具组合有必要分享一套我日常开发这个表格组件的调试工具链对排查定位很有帮助鸿蒙开发者工具配合React Native的DevTools用Perf Monitor看帧率和JS线程占用React DevTools看组件树节点数量真机上用onScroll日志配合console.time统计滚动事件耗时。这三样组合起来大部分性能问题的定位都能在半小时内完成。把组件封装成独立的npm包是一个长远的做法。当你的项目从1个表格变成10个表格时修改一次组件代码、打出新版本业务方各自升级依赖会省下不少联调时间。9. 最后唠叨几句说实话固定左侧列表格这个需求在成熟平台的社区里真不是个值得写这么多字的题目。但在鸿蒙这个正在快速成长的生态里每一层基础能力的适配都可能和你的既有经验有偏差。我写这篇文章的出发点就是记录下这一次“明知有更复杂方案却选了最朴素方案”的完整思考过程以及过程中那些只有真动手才会遇到的鸿蒙适配问题。回到方案本身我依然坚持那个建议在跨平台跨生态的背景下能用系统组件兜底的功能尽量不引第三方依赖能用布局解决的问题尽量不写联动逻辑能用FlatList虚拟化的列表绝不手写全量渲染。这套思路不只在鸿蒙上适用你在任何受限平台做表格类需求时都能用得上。如果你也正在做类似的事情希望能少踩几个我踩过的坑。有问题一起交流。
返回列表