rn_for_openharmony狗狗之家app实战-图库实现
本文介绍了宠物类App图库页面的实现方案,主要包含以下内容: 页面结构采用ScrollView+RefreshControl实现下拉刷新,配合ImageCard组件展示图片卡片 状态管理包括: 全局收藏状态(favoriteImages) 加载状态(loading/refreshing) 图片数据(images) 核心功能实现: 通过useEffect和loadData函数获取API数据 灵活的A
案例开源地址:https://atomgit.com/nutpi/rn_openharmony_dogimg
图片是最好的内容
做宠物类 App,图片绝对是核心。用户打开狗狗之家,第一眼看到的就是各种可爱的狗狗照片。图库页面作为图片模块的入口,承担着展示、浏览、收藏等多重职责。
这篇文章从实际代码出发,讲讲图库页面的实现思路。涉及到瀑布流布局、图片卡片组件、收藏状态管理等内容。
页面整体结构
先看 GalleryPage 的导入部分:
import React, {useState, useEffect} from 'react';
import {View, ScrollView, StyleSheet, RefreshControl} from 'react-native';
import {Header, Loading, Empty, ImageCard} from '../../components';
import {useNavigation, useStore} from '../../hooks';
import {api} from '../../api';
import type {DogImage} from '../../types';
这里用到了几个关键模块:
- ScrollView + RefreshControl:实现下拉刷新的滚动容器
- ImageCard:图片卡片组件,负责单张图片的展示
- useStore:全局状态管理,处理收藏逻辑
- api:封装好的接口调用
状态定义
页面需要管理的状态不多,但每个都有明确的用途:
const {navigate} = useNavigation();
const {favoriteImages, toggleImage} = useStore();
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [images, setImages] = useState<DogImage[]>([]);
navigate 用于跳转到图片详情页。
favoriteImages 是收藏的图片 ID 列表,从全局状态获取。
toggleImage 是切换收藏状态的方法。
loading 控制首次加载的 Loading 显示。
refreshing 控制下拉刷新的状态。
images 存储从 API 获取的图片数据。
数据加载函数
useEffect(() => { loadData(); }, []);
const loadData = async () => {
try {
const res = await api.getImages(20);
setImages(res);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
setRefreshing(false);
}
};
useEffect 在组件挂载时触发一次数据加载。
loadData 是个 async 函数,调用 api.getImages(20) 获取 20 张随机图片。
finally 块很重要,不管成功还是失败都要把 loading 状态关掉,否则用户会一直看到加载中。
API 接口设计
看看 api.getImages 的实现:
getImages: (limit = 10, breedId?: number) =>
http.get<DogImage[]>('/images/search', {
limit,
breed_ids: breedId,
order: 'RANDOM',
}),
参数说明:
- limit:返回图片数量,默认 10 张
- breedId:可选,指定品种 ID 筛选
- order: ‘RANDOM’:随机排序,每次刷新看到不同的图片
这个接口设计得很灵活,图库页面用它获取随机图片,品种详情页也能用它获取特定品种的图片。
页面渲染结构
return (
<View style={s.container}>
<Header title="📷 狗狗图库" showBack={false} />
{loading ? <Loading full /> : (
<ScrollView
style={s.content}
contentContainerStyle={s.grid}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={loadData} />
}
>
{images.length > 0 ? images.map(img => (
<ImageCard
key={img.id}
image={img}
liked={favoriteImages.includes(img.id)}
onLike={() => toggleImage(img.id)}
onPress={() => navigate('ImageView', {image: img})}
/>
)) : <Empty icon="📷" title="暂无图片" />}
</ScrollView>
)}
</View>
);
结构分三层:
Header:顶部标题栏,showBack={false} 因为这是底部 Tab 页面,不需要返回按钮。
Loading:首次加载时显示全屏 Loading。
ScrollView:加载完成后显示图片列表。
下拉刷新的实现
<RefreshControl refreshing={refreshing} onRefresh={loadData} />
RefreshControl 是 React Native 内置的下拉刷新组件。
refreshing 控制刷新指示器的显示。
onRefresh 是下拉触发的回调,这里直接复用 loadData 函数。
但有个问题,loadData 里没有设置 setRefreshing(true)。正确的做法应该是:
const onRefresh = () => {
setRefreshing(true);
loadData();
};
然后把 onRefresh 传给 RefreshControl。不过当前代码也能工作,因为 loadData 的 finally 里会把 refreshing 设为 false。
图片卡片组件
ImageCard 是图库的核心组件,看看它的实现:
import React from 'react';
import {Image, TouchableOpacity, Text, StyleSheet, Dimensions} from 'react-native';
import type {DogImage} from '../types';
const W = (Dimensions.get('window').width - 48) / 2;
W 是卡片宽度的计算。屏幕宽度减去 48(左右各 16 的 padding,中间 16 的间距),再除以 2,得到两列布局下每张卡片的宽度。
卡片的 Props 定义
interface Props {
image: DogImage;
liked?: boolean;
onPress?: () => void;
onLike?: () => void;
}
- image:图片数据,必传
- liked:是否已收藏,可选
- onPress:点击卡片的回调
- onLike:点击收藏按钮的回调
把 liked 和 onLike 设为可选,是因为有些场景可能不需要收藏功能。
卡片渲染逻辑
export function ImageCard({image, liked, onPress, onLike}: Props) {
return (
<TouchableOpacity style={s.card} onPress={onPress} activeOpacity={0.9}>
<Image source={{uri: image.url}} style={s.img} />
{onLike && (
<TouchableOpacity style={s.like} onPress={onLike}>
<Text style={s.heart}>{liked ? '❤️' : '🤍'}</Text>
</TouchableOpacity>
)}
</TouchableOpacity>
);
}
外层 TouchableOpacity 让整个卡片可点击。
activeOpacity={0.9} 让点击时的透明度变化不那么明显,视觉上更舒服。
收藏按钮用条件渲染 {onLike && ...},只有传了 onLike 才显示。
收藏图标用 emoji 实现,❤️ 表示已收藏,🤍 表示未收藏。简单直观。
卡片样式详解
const s = StyleSheet.create({
card: {
width: W,
height: W * 1.2,
borderRadius: 12,
overflow: 'hidden',
marginBottom: 12
},
img: {
width: '100%',
height: '100%',
backgroundColor: '#f0f0f0'
},
like: {
position: 'absolute',
bottom: 8,
right: 8,
backgroundColor: 'rgba(0,0,0,0.3)',
borderRadius: 16,
padding: 6
},
heart: {fontSize: 18},
});
card 样式:
- 宽度是计算出来的 W
- 高度是宽度的 1.2 倍,形成竖向的卡片比例
overflow: 'hidden'配合 borderRadius 实现圆角裁剪
img 样式:
- 宽高都是 100%,填满卡片
- 背景色
#f0f0f0在图片加载前显示,避免空白
like 样式:
position: 'absolute'让按钮浮在图片上- 半透明黑色背景让按钮在任何图片上都清晰可见
收藏功能的状态管理
收藏状态存在全局 store 里,看看 useStore 的实现:
export function useStore(): AppState & {
toggleBreed: (id: number) => void;
toggleImage: (id: string) => void;
} {
const [state, setState] = useState(appStore.getState());
useEffect(() => {
const unsubscribe = appStore.subscribe(setState);
return unsubscribe;
}, []);
useStore 返回当前状态和操作方法。
通过 appStore.subscribe 订阅状态变化,状态更新时自动触发组件重渲染。
toggleImage 的实现
const toggleImage = (id: string) => {
const list = state.favoriteImages;
appStore.setState({
favoriteImages: list.includes(id)
? list.filter(x => x !== id)
: [...list, id],
});
};
逻辑很简单:
- 如果已经在收藏列表里,就过滤掉(取消收藏)
- 如果不在列表里,就添加进去(添加收藏)
用三元表达式一行搞定,简洁明了。
网格布局的实现
const s = StyleSheet.create({
container: {flex: 1, backgroundColor: '#f5f5f5'},
content: {flex: 1},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
padding: 16,
justifyContent: 'space-between'
},
});
grid 样式是关键:
flexDirection: 'row'让子元素横向排列flexWrap: 'wrap'允许换行justifyContent: 'space-between'让两列之间自动分配间距
这样就实现了两列的网格布局,不需要额外的布局库。
点击跳转详情
onPress={() => navigate('ImageView', {image: img})}
点击卡片时,跳转到 ImageView 页面,并把整个 image 对象作为参数传递。
这样详情页就能直接使用图片数据,不需要再请求一次接口。
空状态处理
{images.length > 0 ? images.map(...) : <Empty icon="📷" title="暂无图片" />}
如果图片列表为空,显示 Empty 组件。
虽然正常情况下 API 总会返回图片,但做好空状态处理是个好习惯。网络异常、接口变更都可能导致空数据。
性能优化思考
当前实现用的是 ScrollView,对于大量图片可能有性能问题。优化方向:
FlatList 替代 ScrollView:FlatList 有虚拟列表功能,只渲染可见区域的元素。
<FlatList
data={images}
numColumns={2}
renderItem={({item}) => <ImageCard image={item} ... />}
keyExtractor={item => item.id}
/>
图片懒加载:只有进入可视区域才开始加载图片。
图片缓存:避免重复下载相同的图片。
不过对于 20 张图片的场景,ScrollView 完全够用。过早优化是万恶之源。
图片类型定义
interface DogImage {
id: string;
url: string;
width?: number;
height?: number;
breeds?: Breed[];
}
The Dog API 返回的图片数据结构。
id 是唯一标识,用于收藏功能和列表渲染的 key。
url 是图片地址。
breeds 是可选的品种信息,有些图片会带上对应的品种数据。
小结
图库页面虽然代码不多,但涉及的知识点挺丰富:
- Dimensions API:获取屏幕尺寸,计算响应式布局
- Flexbox 网格:用 flexWrap 实现多列布局
- 全局状态:收藏功能的跨组件状态共享
- 条件渲染:根据状态显示不同的 UI
- 组件复用:ImageCard 可以在多个页面使用
下一篇讲图片查看页面,会涉及到手势操作和图片缩放。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐




所有评论(0)