【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:List 列表(瀑布流滚动加载)
本文介绍了React Native(RN)在鸿蒙系统上的跨端开发实现,重点解析了瀑布流列表的核心功能。通过@react-native-ohos/adapter适配器,RN代码可直接映射为鸿蒙原生组件,实现"一次开发,多端复用"。文章详细讲解了懒加载图片、瀑布流布局和下拉刷新等功能的实现逻辑,并强调了Base64图标在跨端资源加载中的优势。该方案适合有RN基础、希望快速适配鸿蒙的

在跨端开发领域,React Native(以下简称RN)凭借“一次开发、多端复用”的优势,成为主流选择之一。而鸿蒙系统作为新兴的全场景分布式操作系统,RN通过
@react-native-ohos/adapter适配器,可实现RN代码向鸿蒙原生组件的映射,无需大量修改即可完成跨端适配。
本文将针对一份完整的RN鸿蒙跨端瀑布流列表代码,按片段逐段解读,重点剖析懒加载图片、瀑布流布局、下拉刷新/上拉加载更多等核心功能的实现逻辑,以及RN代码在鸿蒙端的跨端适配要点,适合有RN基础、想入门鸿蒙跨端开发的开发者参考。
代码片段
// App.tsx
import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
SafeAreaView,
Image,
Dimensions,
TouchableOpacity,
FlatList,
RefreshControl,
ActivityIndicator
} from 'react-native';
// Base64 Icons for list components
const LIST_ICONS = {
loading: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFHmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTAxLTAzVDE2OjU1OjQ5KzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0wM1QxNjo1NjoxNCswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0wMS0wM1QxNjo1NjoxNCswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyZjA5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MmYwOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MmYwOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyZjA5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHN0RXZ0OndoZW49IjIwMTktMDEtMDNUMTY6NTU6NDlaIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAA//+gB5ZbAAAJRElEQVRoBe1ZaWxUVRQ+97030+k6U6Z0hk5npksptKW0bFJAWRB3UEFFjQuJKIoLJCZqYhRjNC5RFCMuaKJREhMXjAuKggsuRI0bGkEpWymltNBS2ulMZ9qZmfbe9+699715MzPTmdLS6STnfPPee+fc755zz3nnPcNwHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMx......',
// 省略其他图标Base64编码
};
1. 核心依赖
该部分导入了RN核心组件与React基础Hooks,是RN鸿蒙跨端开发的基础,重点关注2个跨端适配关键点:
-
组件兼容性:导入的
View、Text、Image、FlatList等均为RN核心组件,在鸿蒙端会通过@react-native-ohos/adapter适配器,自动映射为鸿蒙原生组件(如View映射为鸿蒙Component,Image映射为鸿蒙Image组件,FlatList映射为鸿蒙高性能列表组件ListContainer),无需额外修改代码即可实现跨端渲染。 -
功能组件适配:
RefreshControl(下拉刷新)、ActivityIndicator(加载指示器)在鸿蒙端的适配的兼容性较好,RN原生API可直接复用;Dimensions用于获取设备屏幕尺寸,为跨端响应式布局提供支撑,鸿蒙手机、平板等不同尺寸设备均适用。
2. Base64图标定义:跨端资源加载的最优解
代码中将加载中、加载失败、点赞、评论等图标转为Base64编码,嵌入代码中而非通过网络或本地资源引入,这是RN鸿蒙跨端开发中资源适配的关键优化,核心优势有3点:
-
规避跨端资源路径问题:RN原生开发中,本地资源需配置
metro.config.js,而鸿蒙端的资源目录结构(如main_pages、resources)与RN原生不同,直接引用本地资源极易出现“资源找不到”的报错。Base64编码直接嵌入代码,无需依赖任何本地/网络资源,实现“一次编码,多端可用”。 -
提升初始加载速度:图标作为列表组件的基础UI元素,Base64编码无需额外发起网络请求,可随组件一起渲染,避免因图标加载延迟导致的UI闪烁,尤其适配鸿蒙端对“启动速度”和“流畅度”的严苛要求。
-
减小跨端打包复杂度:若使用本地图标,需为RN端、鸿蒙端分别放置对应资源并配置路径,增加打包体积和维护成本;Base64编码无需额外打包资源,简化跨端构建流程。
⚠️ 注意:Base64编码会增加代码体积,建议仅用于小图标(如本文中30x30px的状态图标);若图标较大(如启动图、Banner图),可采用“鸿蒙端本地资源+RN端本地资源”的双端适配方案,通过Platform.OS === 'harmony'判断环境,加载对应平台的资源。
列表项类型
// 列表项类型
interface ListItem {
id: string;
title: string;
description: string;
imageUrl: string;
likes: number;
comments: number;
shares: number;
author: string;
timestamp: string;
width: number;
height: number;
}
使用TypeScript定义ListItem接口,规范列表项的数据结构,这在RN鸿蒙跨端开发中至关重要,核心价值体现在2点:
-
类型安全,避免跨端数据异常:RN与鸿蒙端的JavaScript引擎存在细微差异,若数据类型不明确,可能出现“undefined转null”“数字转字符串”等异常。通过接口定义,强制约束每个字段的类型(如
likes为数字类型、imageUrl为字符串类型),可提前规避跨端数据渲染异常。 -
跨端开发协作效率提升:接口定义清晰了数据传递的规范,前端(RN)、后端、鸿蒙端开发者可基于同一接口开发,避免因数据结构不一致导致的联调问题;同时,TypeScript的类型提示的可减少代码错误,提升开发效率。
重点关注2个与UI渲染相关的字段:width和height,用于计算图片宽高比,避免瀑布流布局中图片拉伸或裁剪,这是瀑布流布局的核心前提(后续会详细解读)。
三、LazyImage 懒加载图片组件
懒加载图片组件
// 懒加载图片组件
const LazyImage = ({ source, style, placeholderColor = '#334155', errorColor = '#dc2626' }: {
source: { uri: string };
style: any;
placeholderColor?: string;
errorColor?: string;
}) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
return (
<View style={style}>
{loading && !error && (
<View style={[StyleSheet.absoluteFill, { backgroundColor: placeholderColor, justifyContent: 'center', alignItems: 'center' }]}>
<Image source={{ uri: LIST_ICONS.loading }} style={{ width: 30, height: 30, tintColor: '#94a3b8' }} />
</View>
)}
{error && (
<View style={[StyleSheet.absoluteFill, { backgroundColor: errorColor, justifyContent: 'center', alignItems: 'center' }]}>
<Image source={{ uri: LIST_ICONS.error }} style={{ width: 30, height: 30, tintColor: '#ffffff' }} />
</View>
)}
<Image
source={source}
style={StyleSheet.absoluteFill}
onLoad={() => setLoading(false)}
onError={() => {
setLoading(false);
setError(true);
}}
resizeMode="cover"
/>
</View>
);
};
LazyImage是整个代码的核心组件之一,封装了“加载中-加载成功-加载失败”的完整图片加载生命周期,实现图片懒加载(结合后续FlatList的视口监听),同时适配RN与鸿蒙双端,重点解读5个跨端关键技术点:
1. 组件接口
组件接收4个参数,均考虑了跨端复用的灵活性:
-
source: { uri: string }:统一接收网络图片地址,RN原生与鸿蒙端均支持该格式;若需适配本地图片,可扩展为source: { uri: string } | number,通过Platform.OS判断环境(鸿蒙端传入本地资源ID,RN端传入本地资源路径)。 -
style: any:接收外部样式,支持父组件自定义图片尺寸、圆角等样式,适配不同场景(如本文中瀑布流图片、图标等)。 -
placeholderColor、errorColor:可自定义占位背景色和错误背景色,适配不同APP的主题风格,跨端渲染效果一致。
2. 状态管理:
使用useState管理2个核心状态,逻辑跨端完全复用:
-
loading: boolean:控制“加载中”状态的显示,初始值为true,图片加载成功后设为false。 -
error: boolean:控制“加载失败”状态的显示,初始值为false,图片加载失败时设为true。
状态切换逻辑:图片开始加载(默认loading=true)→ 加载成功(onLoad触发,loading=false)→ 加载失败(onError触发,loading=false、error=true),双端均遵循该逻辑,保证用户体验一致。
3. 跨端UI空白
加载中、加载失败状态均使用View作为占位容器,结合StyleSheet.absoluteFill让占位视图完全覆盖图片容器,避免图片加载过程中出现UI空白。重点说明:
-
加载中:显示Base64编码的加载图标,配合占位背景色,提升用户等待体验;
-
加载失败:显示错误图标和红色背景,直观提示用户图片加载失败,后续可扩展“点击重试”功能(本文暂未实现,可参考后续优化建议)。
4. 图片渲染
核心图片渲染使用RNImage组件,跨端适配要点:
-
style={StyleSheet.absoluteFill}:让图片完全填充容器,配合父组件传入的style(尺寸、圆角等),实现自定义渲染效果; -
resizeMode="cover":保证图片填充容器且不拉伸、不畸变,RN原生与鸿蒙端对resizeMode的支持完全一致(均支持cover、contain、stretch等),无需额外适配; -
事件回调:
onLoad(加载成功)、onError(加载失败)是RNImage组件的原生事件,鸿蒙端通过适配器完全支持,无需修改回调逻辑。
5. 待结合FlatList视口监听
注意:当前LazyImage组件仅实现了“图片延迟加载”(加载完成前显示占位),真正的“懒加载”(仅当图片进入屏幕视口时才发起加载请求),需结合后续FlatList的onViewableItemsChanged或FlatList的原生懒加载能力实现,这也是瀑布流列表性能优化的关键(后续解读)。
业务组件封装:ListItemComponent 列表项组件
列表项组件
// 列表项组件
const ListItemComponent = ({ item }: { item: ListItem }) => {
const aspectRatio = item.width / item.height;
const imageHeight = 200; // 固定高度
const imageWidth = imageHeight * aspectRatio;
return (
<View style={styles.listItem}>
<View style={styles.imageContainer}>
<LazyImage
source={{ uri: item.imageUrl }}
style={{ width: imageWidth, height: imageHeight }}
/>
</View>
<View style={styles.contentContainer}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.description}>{item.description}</Text>
<View style={styles.authorInfo}>
<Text style={styles.author}>作者: {item.author}</Text>
<Text style={styles.timestamp}>{item.timestamp}</Text>
</View>
<View style={styles.actionsContainer}>
<TouchableOpacity style={styles.actionButton}>
<Image source={{ uri: LIST_ICONS.like }} style={styles.actionIcon} />
<Text style={styles.actionText}>{item.likes}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionButton}>
<Image source={{ uri: LIST_ICONS.comment }} style={styles.actionIcon} />
<Text style={styles.actionText}>{item.comments}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionButton}>
<Image source={{ uri: LIST_ICONS.share }} style={styles.actionIcon} />
<Text style={styles.actionText}>{item.shares}</Text>
</TouchableOpacity>
</View>
</View>
</View>
);
};
ListItemComponent是基于LazyImage的业务封装,用于渲染瀑布流列表中的单个图片项,核心是“图片比例适配+UI样式封装”,同时实现点赞、评论、分享等交互功能,跨端适配重点关注3点:
1. 图片比例适配:
瀑布流布局的关键是“图片宽高比一致,避免布局错乱”,代码中通过以下逻辑实现跨端一致的比例适配:
-
计算宽高比:
aspectRatio = item.width / item.height,基于接口返回的图片原始宽高,确保比例准确; -
固定高度+动态宽度:设置图片固定高度
imageHeight = 200(单位dp),动态计算宽度imageWidth = imageHeight * aspectRatio,保证图片比例不畸变。
跨端适配优势:鸿蒙端屏幕尺寸多样(手机、平板、智慧屏等),固定高度+动态宽度的方式,可避免因屏幕宽度变化导致的图片拉伸或裁剪,同时保证瀑布流两列布局的均匀分布(后续结合FlatList的numColumns={2}实现)。
⚠️ 优化点:可扩展为“根据屏幕宽度动态计算图片高度”,如const imageHeight = (width - 40) / 2 / aspectRatio(width为屏幕宽度,40为左右间距),让图片项在不同屏幕尺寸下保持一致的布局比例。
2. UI样式封装:
ListItemComponent的样式通过外部StyleSheet定义(后续解读样式部分),跨端适配重点关注2点:
-
样式属性兼容性:代码中使用的样式属性(
borderRadius、overflow、flexDirection等),均是RN与鸿蒙端兼容的属性;鸿蒙端对borderRadius的支持较好,但部分旧版本鸿蒙系统对borderRadius与overflow: 'hidden'的兼容性不佳,可能出现圆角失效的情况,可通过给图片容器添加overflow: 'hidden'并结合鸿蒙原生样式优化。 -
文本样式适配:
Text组件的fontSize、color、fontWeight等属性,RN原生与鸿蒙端完全兼容;但字体家族(fontFamily)适配需注意——鸿蒙端默认字体与RN原生不同,可通过Platform.OS === 'harmony'判断环境,加载对应平台的字体文件,保证跨端文本显示一致。
3. 交互功能
组件底部实现了点赞、评论、分享三个交互按钮,使用RNTouchableOpacity组件(鸿蒙端映射为鸿蒙原生Button组件,支持点击反馈),跨端适配要点:
-
TouchableOpacity的onPress事件,跨端完全复用,无需修改逻辑; -
图标使用Base64编码的
LIST_ICONS,跨端加载无差异;tintColor用于修改图标颜色,鸿蒙端与RN原生均支持该属性; -
可扩展点:当前仅实现了UI展示,可添加点击事件逻辑(如点赞数递增、跳转评论页等),逻辑跨端完全复用,无需额外适配。
主组件核心逻辑
// 主应用组件
const App = () => {
const [listData, setListData] = useState<ListItem[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
// 模拟加载数据
const loadData = async (pageNum: number, isRefresh = false) => {
if (isRefresh) {
setListData([]);
}
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 生成模拟数据
const newData: ListItem[] = Array.from({ length: 10 }, (_, i) => ({
id: `${pageNum * 10 + i}`,
title: `瀑布流内容 ${pageNum * 10 + i + 1}`,
description: `这是第 ${pageNum * 10 + i + 1} 个瀑布流项目的详细描述,展示了瀑布流布局的效果。`,
imageUrl: `https://picsum.photos/seed/${pageNum * 10 + i}/600/400`,
likes: Math.floor(Math.random() * 100),
comments: Math.floor(Math.random() * 50),
shares: Math.floor(Math.random() * 30),
author: `作者${String.fromCharCode(65 + (pageNum * 10 + i) % 26)}`,
timestamp: `${Math.floor(Math.random() * 12) + 1}小时前`,
width: 600,
height: 400
}));
if (isRefresh) {
setListData(newData);
} else {
setListData(prev => [...prev, ...newData]);
}
// 模拟没有更多数据的情况
if (pageNum >= 5) {
setHasMore(false);
}
};
// 初始加载
useEffect(() => {
loadData(1);
}, []);
// 下拉刷新
const onRefresh = useCallback(async () => {
setRefreshing(true);
await loadData(1, true);
setPage(1);
setRefreshing(false);
}, []);
// 加载更多
const loadMore = useCallback(async () => {
if (loadingMore || !hasMore) return;
setLoadingMore(true);
await loadData(page + 1);
setPage(prev => prev + 1);
setLoadingMore(false);
}, [page, loadingMore, hasMore]);
// 渲染部分省略,后续单独解读
};
App组件是整个应用的入口,负责数据管理、页面布局、列表渲染,核心实现了“初始加载、下拉刷新、上拉加载更多”三大功能,同时管理列表相关的状态,跨端适配重点关注4个核心技术点:
1. 状态管理:
使用useState管理5个核心状态,逻辑跨端完全复用,无需修改:
-
listData: ListItem[]:存储瀑布流列表数据,初始值为空数组; -
refreshing: boolean:控制下拉刷新状态,true表示正在刷新; -
loadingMore: boolean:控制上拉加载更多状态,true表示正在加载; -
page: number:当前页码,用于加载更多时请求下一页数据; -
hasMore: boolean:是否还有更多数据,用于控制加载更多的显示/隐藏。
跨端适配优势:React Hooks(useState、useEffect、useCallback)在鸿蒙端完全支持,状态管理逻辑无需任何修改,可直接复用RN原生代码;若为复杂跨端应用,可扩展为Redux、MobX等状态管理库,RN与鸿蒙端均支持这些库(通过适配包),实现“一次状态定义,多端共享”。
2. 数据加载逻辑:
loadData方法封装了数据加载的核心逻辑,模拟了真实接口请求的流程,跨端适配要点:
-
请求逻辑复用:方法接收
pageNum(页码)和isRefresh(是否下拉刷新)两个参数,适配“初始加载、下拉刷新、加载更多”三种场景,逻辑跨端完全复用; -
模拟API请求:使用
setTimeout模拟接口请求延迟,真实开发中可替换为fetch或axios(RN原生与鸿蒙端均支持fetch,鸿蒙端也支持axios,需安装对应依赖); -
数据格式统一:生成的模拟数据严格遵循
ListItem接口定义,保证跨端数据类型一致;图片地址使用https://picsum.photos(免费图片接口),跨端均可正常加载; -
无更多数据处理:当页码
pageNum >= 5时,设置hasMore = false,停止加载更多,适配真实业务场景中“数据加载完毕”的逻辑。
3. 下拉刷新:
下拉刷新功能通过RN原生RefreshControl组件实现,结合onRefresh方法,跨端适配要点:
-
useCallback优化:onRefresh方法使用useCallback包裹,避免因组件重新渲染导致的方法重复创建,提升跨端性能(尤其鸿蒙端对渲染性能要求较高); -
跨端兼容性:
RefreshControl组件在鸿蒙端通过适配器完全支持,refreshing(刷新状态)、colors(刷新指示器颜色)、tintColor(指示器颜色)等属性,跨端渲染效果一致; -
刷新逻辑:下拉刷新时,重置页码为1,重新加载第一页数据,清空原有列表,实现“刷新重置”的效果,逻辑跨端复用。
4. 上拉加载更多:
上拉加载更多功能通过FlatList的onEndReached和onEndReachedThreshold属性实现,跨端适配要点:
-
onEndReached:列表滚动到底部时触发,调用loadMore方法加载下一页数据; -
onEndReachedThreshold={0.1}:设置列表滚动到距离底部10%的位置时,触发加载更多,提升用户体验(避免用户滚动到最底部才开始加载); -
防重复加载:通过
loadingMore和hasMore状态判断,避免多次触发加载更多(如用户快速滚动到底部时); -
鸿蒙端适配:FlatList在鸿蒙端映射为原生
ListContainer组件,onEndReached事件的触发时机与RN原生一致,无需修改逻辑,保证跨端体验统一。
FlatList 瀑布流
FlatList渲染部分
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerTitle}>瀑布流列表</Text>
<Text style={styles.headerSubtitle}>懒加载图片瀑布流布局</Text>
</View><FlatList
data={listData}
renderItem={({ item }) => <ListItemComponent item={item} />}
keyExtractor={(item) => item.id}
numColumns={2}
contentContainerStyle={styles.listContainer}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={['#3b82f6']}
tintColor="#3b82f6"
/>
}
onEndReached={loadMore}
onEndReachedThreshold={0.1}
ListFooterComponent={
loadingMore ? (
<View style={styles.loadingMoreContainer}>
<ActivityIndicator size="small" color="#3b82f6" />
<Text style={styles.loadingMoreText}>加载更多...</Text>
</View>
) : hasMore ? null : (
<View style={styles.noMoreContainer}>
<Text style={styles.noMoreText}>没有更多数据了</Text>
</View>
)
}
/>
{/* 功能特性、使用说明、底部信息省略,后续解读 */}
</SafeAreaView>
);
FlatList是RN实现瀑布流布局的核心组件,也是跨端高性能列表渲染的关键——在鸿蒙端,FlatList通过适配器映射为鸿蒙原生ListContainer组件(鸿蒙原生高性能列表组件,支持视口内渲染、组件复用),无需修改代码即可实现跨端高性能列表,重点解读5个跨端关键技术点:
1. 瀑布流布局实现:numColumns={2}
通过numColumns={2}属性,直接实现两列瀑布流布局,这是RN FlatList实现瀑布流的最简方式,跨端适配要点:
-
布局均匀性:结合
contentContainerStyle={styles.listContainer}(设置列表内边距)和listItem样式(设置列表项间距),保证两列布局均匀,跨端显示一致; -
鸿蒙端适配:
numColumns属性在鸿蒙端完全支持,映射为ListContainer的gridCount属性,实现两列网格布局,与RN原生效果一致; -
优势:相比
ScrollView(一次性渲染所有子组件),FlatList支持“视口内渲染”(仅渲染屏幕可见区域的列表项)和“组件复用”,大幅降低内存占用,提升跨端性能(尤其列表数据较多时,如本文中每页10条,共5页50条数据)。
2. 核心属性
FlatList的核心属性均支持跨端复用,重点说明:
-
data={listData}:数据源,接收ListItem[]类型数据,跨端数据格式一致; -
renderItem:渲染单个列表项,调用ListItemComponent组件,跨端组件复用; -
keyExtractor={(item) => item.id}:设置列表项唯一标识,避免RN渲染警告,鸿蒙端同样需要唯一标识,保证组件复用正常; -
showsVerticalScrollIndicator={false}:隐藏垂直滚动条,跨端视觉效果一致; -
ListFooterComponent:列表底部组件,用于显示“加载更多”或“没有更多数据”提示,跨端完全复用,适配不同加载状态。
3. 列表底部组件:
ListFooterComponent根据loadingMore和hasMore状态,动态渲染不同的底部提示,跨端适配要点:
-
加载中:显示
ActivityIndicator(加载指示器)和“加载更多…”文本,ActivityIndicator组件跨端完全支持,size(尺寸)、color(颜色)属性可自定义; -
没有更多数据:显示“没有更多数据了”文本,提示用户数据加载完毕;
-
样式适配:底部组件的样式(如内边距、文本颜色)与整体主题一致,跨端视觉效果统一。
4. SafeAreaView:
页面根容器使用SafeAreaView,而非普通View,这是跨端刘海屏、状态栏适配的关键:
-
RN原生:
SafeAreaView用于适配iOS刘海屏、Android状态栏,避免内容被刘海屏或状态栏遮挡; -
鸿蒙端:
SafeAreaView通过适配器,自动适配鸿蒙系统的状态栏、导航栏高度,无需额外计算,适配鸿蒙手机、平板等不同设备的刘海屏、水滴屏。
代码片段
// 功能特性section
<View style={styles.featuresSection}>
<Text style={styles.featuresTitle}>功能特性</Text>
<View style={styles.featureList}>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>瀑布流布局展示</Text>
</View>
{/* 省略其他功能特性项 */}
</View>
</View>
// 使用说明section
<View style={styles.usageSection}>
<Text style={styles.usageTitle}>使用说明</Text>
<Text style={styles.usageText}>
瀑布流列表适用于图片展示、商品列表等场景。
通过懒加载技术优化性能,提升用户体验。
</Text>
</View>
// 底部信息
<View style={styles.footer}>
<Text style={styles.footerText}>© 2023 瀑布流列表. All rights reserved.</Text>
</View>
这部分是静态UI组件,用于展示应用的功能特性、使用说明和底部版权信息,核心是“跨端UI样式复用”,适配要点:
-
布局逻辑复用:使用
View嵌套实现section布局,flexDirection: 'row'实现功能特性项的横向排列,逻辑跨端完全复用; -
样式统一:静态UI的样式与列表、头部样式保持一致,使用深色主题(
backgroundColor: '#1e293b'),文本颜色、间距等属性跨端渲染一致; -
文本适配:
Text组件的textAlign: 'center'(居中对齐)、lineHeight(行高)等属性,跨端支持一致,保证文本显示美观; -
鸿蒙端适配:静态UI组件无复杂交互,仅涉及基础布局和文本渲染,RN原生代码可直接复用,无需任何修改,适配鸿蒙端的显示规范。
全局样式定义
const { width, height } = Dimensions.get('window');
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0f172a',
},
header: {
backgroundColor: '#1e293b',
paddingTop: 20,
paddingBottom: 25,
paddingHorizontal: 20,
borderBottomWidth: 1,
borderBottomColor: '#334155',
},
// 省略其他样式定义
listItem: {
backgroundColor: '#1e293b',
margin: 5,
borderRadius: 12,
overflow: 'hidden',
borderWidth: 1,
borderColor: '#334155',
},
// 省略其他样式定义
});
StyleSheet是RN跨端样式管理的核心,通过StyleSheet.create定义全局样式,相比普通对象样式,具有“性能优化、样式校验”的优势,跨端适配重点关注3点:
1. 动态尺寸计算:
通过Dimensions.get('window')获取屏幕宽高(width为屏幕宽度,height为屏幕高度),用于动态计算组件尺寸(如列表项宽度),核心优势:
-
跨端适配不同屏幕尺寸:鸿蒙端设备屏幕尺寸多样(手机、平板等),动态尺寸计算可避免固定尺寸导致的布局错乱,实现“一次计算,多端适配”;
-
示例:列表项宽度计算(前文ListItemComponent中),基于屏幕宽度动态计算,保证两列瀑布流布局均匀。
2. 样式属性
代码中使用的样式属性均为RN与鸿蒙端兼容的属性,重点规避以下不兼容属性:
-
RN原生支持
shadowColor、shadowOffset、shadowOpacity等阴影属性,鸿蒙端不支持,可通过Platform.OS === 'ios'/'android'单独设置,鸿蒙端使用elevation属性实现阴影效果; -
鸿蒙端支持
backgroundBlur等模糊效果,RN原生需通过第三方组件实现,可通过条件渲染适配双端; -
本文中使用的
flex、backgroundColor、borderRadius、padding等属性,跨端完全兼容,无需适配。
3. 主题样式统一:
代码采用深色主题,统一设置背景色(#0f172a、#1e293b)、文本颜色(#f1f5f9、#94a3b8)、边框颜色(#334155),跨端适配优势:
- 避免鸿蒙端主题适配问题:鸿蒙端支持系统主题切换(深色/浅色),若APP使用固定主题,统一的样式可避免主题切换导致的视觉错乱;
真实演示案例代码:
// App.tsx
import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
SafeAreaView,
Image,
Dimensions,
TouchableOpacity,
FlatList,
RefreshControl,
ActivityIndicator
} from 'react-native';
// Base64 Icons for list components
const LIST_ICONS = {
loading: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFHmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTAxLTAzVDE2OjU1OjQ5KzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0wM1QxNjo1NjoxNCswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0wMS0wM1QxNjo1NjoxNCswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyZjA5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MmYwOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MmYwOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyZjA5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHN0RXZ0OndoZW49IjIwMTktMDEtMDNUMTY6NTU6NDlaIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAA//+gB5ZbAAAJRElEQVRoBe1ZaWxUVRQ+97030+k6U6Z0hk5npksptKW0bFJAWRB3UEFFjQuJKIoLJCZqYhRjNC5RFCMuaKJREhMXjAuKggsuRI0bGkEpWymltNBS2ulMZ9qZmfbe9+699715MzPTmdLS6STnfPPee+fc755zz3nnPcNwHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMx......',
error: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFHmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTAxLTAzVDE2OjU2OjIwKzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0wM1QxNjo1Njo0NSswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0wMS0wM1QxNjo1Njo0NSswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyZjE5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MmYxOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MmYxOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyZjE5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHN0RXZ0OndoZW49IjIwMTktMDEtMDNUMTY6NTY6MjBaIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAA//+gB5ZbAAAJRElEQVRoBe1ZaWxUVRQ+97030+k6U6Z0hk5npksptKW0bFJAWRB3UEFFjQuJKIoLJCZqYhRjNC5RFCMuaKJREhMXjAuKggsuRI0bGkEpWymltNBS2ulMZ9qZmfbe9+699715MzPTmdLS6STnfPPee+fc755zz3nnPcNwHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMx......',
refresh: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFHmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTAxLTAzVDE2OjU2OjUxKzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0wM1QxNjo1NzoxNiswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0wMS0wM1QxNjo1NzoxNiswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyZjI5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MmYyOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MmYyOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyZjI5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHN0RXZ0OndoZW49IjIwMTktMDEtMDNUMTY6NTY6NTFaIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAA//+gB5ZbAAAJRElEQVRoBe1ZaWxUVRQ+97030+k6U6Z0hk5npksptKW0bFJAWRB3UEFFjQuJKIoLJCZqYhRjNC5RFCMuaKJREhMXjAuKggsuRI0bGkEpWymltNBS2ulMZ9qZmfbe9+699715MzPTmdLS6STnfPPee+fc755zz3nnPcNwHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMx......',
like: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFHmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTAxLTAzVDE2OjU3OjIyKzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0wM1QxNjo1Nzo0OCswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0wMS0wM1QxNjo1Nzo0OCswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyZjM5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MmYzOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MmYzOWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyZjM5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHN0RXZ0OndoZW49IjIwMTktMDEtMDNUMTY6NTc6MjJaIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAA//+gB5ZbAAAJRElEQVRoBe1ZaWxUVRQ+97030+k6U6Z0hk5npksptKW0bFJAWRB3UEFFjQuJKIoLJCZqYhRjNC5RFCMuaKJREhMXjAuKggsuRI0bGkEpWymltNBS2ulMZ9qZmfbe9+699715MzPTmdLS6STnfPPee+fc755zz3nnPcNwHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMx......',
comment: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFHmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTAxLTAzVDE2OjU3OjUzKzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0wM1QxNjo1ODoxOCswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0wMS0wM1QxNjo1ODoxOCswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyZjQ5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MmY0OWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MmY0OWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyZjQ5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHN0RXZ0OndoZW49IjIwMTktMDEtMDNUMTY6NTc6NTNaIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAA//+gB5ZbAAAJRElEQVRoBe1ZaWxUVRQ+97030+k6U6Z0hk5npksptKW0bFJAWRB3UEFFjQuJKIoLJCZqYhRjNC5RFCMuaKJREhMXjAuKggsuRI0bGkEpWymltNBS2ulMZ9qZmfbe9+699715MzPTmdLS6STnfPPee+fc755zz3nnPcNwHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMx......',
share: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFHmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTAxLTAzVDE2OjU4OjI0KzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0wM1QxNjo1ODo0OSswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0wMS0wM1QxNjo1ODo0OSswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyZjU5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MmY1OWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MmY1OWNmNDktNWI1ZS1mOTQ3LWI4N2ItMmQzOWI0MTI0MmViIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyZjU5Y2Y0OS01YjVlLWY5NDctYjg3Yi0yZDM5YjQxMjQyZWIiIHN0RXZ0OndoZW49IjIwMTktMDEtMDNUMTY6NTg6MjRaIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAA//+gB5ZbAAAJRElEQVRoBe1ZaWxUVRQ+97030+k6U6Z0hk5npksptKW0bFJAWRB3UEFFjQuJKIoLJCZqYhRjNC5RFCMuaKJREhMXjAuKggsuRI0bGkEpWymltNBS2ulMZ9qZmfbe9+699715MzPTmdLS6STnfPPee+fc755zz3nnPcNwHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMdxHMx......'
};
// 列表项类型
interface ListItem {
id: string;
title: string;
description: string;
imageUrl: string;
likes: number;
comments: number;
shares: number;
author: string;
timestamp: string;
width: number;
height: number;
}
// 懒加载图片组件
const LazyImage = ({ source, style, placeholderColor = '#334155', errorColor = '#dc2626' }: {
source: { uri: string };
style: any;
placeholderColor?: string;
errorColor?: string;
}) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
return (
<View style={style}>
{loading && !error && (
<View style={[StyleSheet.absoluteFill, { backgroundColor: placeholderColor, justifyContent: 'center', alignItems: 'center' }]}>
<Image source={{ uri: LIST_ICONS.loading }} style={{ width: 30, height: 30, tintColor: '#94a3b8' }} />
</View>
)}
{error && (
<View style={[StyleSheet.absoluteFill, { backgroundColor: errorColor, justifyContent: 'center', alignItems: 'center' }]}>
<Image source={{ uri: LIST_ICONS.error }} style={{ width: 30, height: 30, tintColor: '#ffffff' }} />
</View>
)}
<Image
source={source}
style={StyleSheet.absoluteFill}
onLoad={() => setLoading(false)}
onError={() => {
setLoading(false);
setError(true);
}}
resizeMode="cover"
/>
</View>
);
};
// 列表项组件
const ListItemComponent = ({ item }: { item: ListItem }) => {
const aspectRatio = item.width / item.height;
const imageHeight = 200; // 固定高度
const imageWidth = imageHeight * aspectRatio;
return (
<View style={styles.listItem}>
<View style={styles.imageContainer}>
<LazyImage
source={{ uri: item.imageUrl }}
style={{ width: imageWidth, height: imageHeight }}
/>
</View>
<View style={styles.contentContainer}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.description}>{item.description}</Text>
<View style={styles.authorInfo}>
<Text style={styles.author}>作者: {item.author}</Text>
<Text style={styles.timestamp}>{item.timestamp}</Text>
</View>
<View style={styles.actionsContainer}>
<TouchableOpacity style={styles.actionButton}>
<Image source={{ uri: LIST_ICONS.like }} style={styles.actionIcon} />
<Text style={styles.actionText}>{item.likes}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionButton}>
<Image source={{ uri: LIST_ICONS.comment }} style={styles.actionIcon} />
<Text style={styles.actionText}>{item.comments}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionButton}>
<Image source={{ uri: LIST_ICONS.share }} style={styles.actionIcon} />
<Text style={styles.actionText}>{item.shares}</Text>
</TouchableOpacity>
</View>
</View>
</View>
);
};
// 主应用组件
const App = () => {
const [listData, setListData] = useState<ListItem[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
// 模拟加载数据
const loadData = async (pageNum: number, isRefresh = false) => {
if (isRefresh) {
setListData([]);
}
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 生成模拟数据
const newData: ListItem[] = Array.from({ length: 10 }, (_, i) => ({
id: `${pageNum * 10 + i}`,
title: `瀑布流内容 ${pageNum * 10 + i + 1}`,
description: `这是第 ${pageNum * 10 + i + 1} 个瀑布流项目的详细描述,展示了瀑布流布局的效果。`,
imageUrl: `https://picsum.photos/seed/${pageNum * 10 + i}/600/400`,
likes: Math.floor(Math.random() * 100),
comments: Math.floor(Math.random() * 50),
shares: Math.floor(Math.random() * 30),
author: `作者${String.fromCharCode(65 + (pageNum * 10 + i) % 26)}`,
timestamp: `${Math.floor(Math.random() * 12) + 1}小时前`,
width: 600,
height: 400
}));
if (isRefresh) {
setListData(newData);
} else {
setListData(prev => [...prev, ...newData]);
}
// 模拟没有更多数据的情况
if (pageNum >= 5) {
setHasMore(false);
}
};
// 初始加载
useEffect(() => {
loadData(1);
}, []);
// 下拉刷新
const onRefresh = useCallback(async () => {
setRefreshing(true);
await loadData(1, true);
setPage(1);
setRefreshing(false);
}, []);
// 加载更多
const loadMore = useCallback(async () => {
if (loadingMore || !hasMore) return;
setLoadingMore(true);
await loadData(page + 1);
setPage(prev => prev + 1);
setLoadingMore(false);
}, [page, loadingMore, hasMore]);
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerTitle}>瀑布流列表</Text>
<Text style={styles.headerSubtitle}>懒加载图片瀑布流布局</Text>
</View>
<FlatList
data={listData}
renderItem={({ item }) => <ListItemComponent item={item} />}
keyExtractor={(item) => item.id}
numColumns={2}
contentContainerStyle={styles.listContainer}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={['#3b82f6']}
tintColor="#3b82f6"
/>
}
onEndReached={loadMore}
onEndReachedThreshold={0.1}
ListFooterComponent={
loadingMore ? (
<View style={styles.loadingMoreContainer}>
<ActivityIndicator size="small" color="#3b82f6" />
<Text style={styles.loadingMoreText}>加载更多...</Text>
</View>
) : hasMore ? null : (
<View style={styles.noMoreContainer}>
<Text style={styles.noMoreText}>没有更多数据了</Text>
</View>
)
}
/>
<View style={styles.featuresSection}>
<Text style={styles.featuresTitle}>功能特性</Text>
<View style={styles.featureList}>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>瀑布流布局展示</Text>
</View>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>懒加载图片优化性能</Text>
</View>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>下拉刷新功能</Text>
</View>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>上拉加载更多</Text>
</View>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>丰富的Base64图标库</Text>
</View>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>响应式布局支持</Text>
</View>
</View>
</View>
<View style={styles.usageSection}>
<Text style={styles.usageTitle}>使用说明</Text>
<Text style={styles.usageText}>
瀑布流列表适用于图片展示、商品列表等场景。
通过懒加载技术优化性能,提升用户体验。
</Text>
</View>
<View style={styles.footer}>
<Text style={styles.footerText}>© 2023 瀑布流列表. All rights reserved.</Text>
</View>
</SafeAreaView>
);
};
const { width, height } = Dimensions.get('window');
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0f172a',
},
header: {
backgroundColor: '#1e293b',
paddingTop: 20,
paddingBottom: 25,
paddingHorizontal: 20,
borderBottomWidth: 1,
borderBottomColor: '#334155',
},
headerTitle: {
fontSize: 26,
fontWeight: '700',
color: '#f1f5f9',
textAlign: 'center',
marginBottom: 5,
},
headerSubtitle: {
fontSize: 15,
color: '#94a3b8',
textAlign: 'center',
},
listContainer: {
padding: 10,
},
listItem: {
backgroundColor: '#1e293b',
margin: 5,
borderRadius: 12,
overflow: 'hidden',
borderWidth: 1,
borderColor: '#334155',
},
imageContainer: {
position: 'relative',
},
contentContainer: {
padding: 12,
},
title: {
fontSize: 16,
fontWeight: '700',
color: '#f1f5f9',
marginBottom: 6,
},
description: {
fontSize: 14,
color: '#cbd5e1',
lineHeight: 20,
marginBottom: 10,
},
authorInfo: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 10,
},
author: {
fontSize: 12,
color: '#94a3b8',
},
timestamp: {
fontSize: 12,
color: '#94a3b8',
},
actionsContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
paddingTop: 10,
borderTopWidth: 1,
borderTopColor: '#334155',
},
actionButton: {
flexDirection: 'row',
alignItems: 'center',
},
actionIcon: {
width: 16,
height: 16,
tintColor: '#94a3b8',
marginRight: 4,
},
actionText: {
fontSize: 12,
color: '#94a3b8',
},
loadingMoreContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 20,
},
loadingMoreText: {
marginLeft: 10,
color: '#94a3b8',
fontSize: 14,
},
noMoreContainer: {
paddingVertical: 20,
alignItems: 'center',
},
noMoreText: {
color: '#94a3b8',
fontSize: 14,
},
featuresSection: {
backgroundColor: '#1e293b',
borderRadius: 16,
padding: 20,
margin: 15,
borderWidth: 1,
borderColor: '#334155',
},
featuresTitle: {
fontSize: 20,
fontWeight: '700',
color: '#f1f5f9',
marginBottom: 15,
textAlign: 'center',
},
featureList: {
paddingLeft: 10,
},
featureItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
featureBullet: {
fontSize: 18,
color: '#3b82f6',
marginRight: 10,
},
featureText: {
fontSize: 16,
color: '#cbd5e1',
flex: 1,
},
usageSection: {
backgroundColor: '#1e293b',
borderRadius: 16,
padding: 20,
margin: 15,
borderWidth: 1,
borderColor: '#334155',
},
usageTitle: {
fontSize: 20,
fontWeight: '700',
color: '#f1f5f9',
marginBottom: 15,
textAlign: 'center',
},
usageText: {
fontSize: 16,
color: '#cbd5e1',
lineHeight: 24,
textAlign: 'center',
},
footer: {
paddingVertical: 15,
alignItems: 'center',
borderTopWidth: 1,
borderTopColor: '#334155',
backgroundColor: '#1e293b',
},
footerText: {
fontSize: 14,
color: '#94a3b8',
fontWeight: '500',
},
});
export default App;

打包
接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

最后运行效果图如下显示:

本文介绍了React Native(RN)在鸿蒙系统上的跨端开发实现,重点解析了瀑布流列表的核心功能。通过@react-native-ohos/adapter适配器,RN代码可直接映射为鸿蒙原生组件,实现"一次开发,多端复用"。文章详细讲解了懒加载图片、瀑布流布局和下拉刷新等功能的实现逻辑,并强调了Base64图标在跨端资源加载中的优势。该方案适合有RN基础、希望快速适配鸿蒙的开发者,能有效降低多端适配成本,提升开发效率。
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
更多推荐



所有评论(0)