rn_for_openharmony狗狗之家app实战-品种图集实现
本文介绍了一个宠物图片展示页面的实现方案,核心是通过响应式网格布局展示品种相关图片。主要技术点包括: 接口设计:通过breedId参数筛选特定品种图片,支持随机排序和数量限制 网格布局:使用flex布局实现两列自适应网格,通过Dimensions计算精确宽度 图片卡片组件:封装ImageCard组件实现图片展示、收藏和点击查看大图功能 状态管理:处理加载、刷新和空状态,集成全局收藏功能 该方案通过
案例开源地址:https://atomgit.com/nutpi/rn_openharmony_dogimg
图片是核心吸引力
做宠物类 App,图片绝对是最吸引用户的内容。品种详情页只展示了一张图,用户想看更多怎么办?品种图集页就是为此而生的。
这个页面从 API 获取指定品种的多张图片,用网格布局展示,支持收藏和查看大图。涉及到的技术点包括:按品种筛选图片、响应式网格布局、图片卡片组件封装。
接口分析
先看获取图片的接口:
getImages: (limit = 10, breedId?: number) =>
http.get<DogImage[]>('/images/search', {
limit,
breed_ids: breedId,
order: 'RANDOM',
}),
三个参数:
- limit:返回数量,默认 10
- breedId:品种 ID,可选,传了就只返回该品种的图片
- order:排序方式,RANDOM 表示随机
品种图集页会传入品种 ID,获取该品种的图片。
页面组件引入
import React, {useState, useEffect} from 'react';
import {View, ScrollView, StyleSheet, RefreshControl} from 'react-native';
import {Header, Loading, Empty, ImageCard} from '../../components';
import {useNavigation, useRoute, useStore} from '../../hooks';
import {api} from '../../api';
import type {Breed, DogImage} from '../../types';
引入了 ImageCard 组件,这是专门用来展示图片的卡片,后面会详细讲。
Empty 组件用于没有图片时的空状态展示。
状态和数据获取
export function BreedImagesPage() {
const {navigate} = useNavigation();
const {params} = useRoute<{breed: Breed}>();
const {favoriteImages, toggleImage} = useStore();
从路由参数拿到品种信息,从全局状态拿到图片收藏相关的数据和方法。
状态定义:
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [images, setImages] = useState<DogImage[]>([]);
和品种列表页类似,三个状态:首次加载、下拉刷新、图片数据。
数据加载:
useEffect(() => { loadData(); }, []);
const loadData = async () => {
try {
const res = await api.getImages(20, params.breed.id);
setImages(res);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
setRefreshing(false);
}
};
调用 api.getImages,传入 20 和品种 ID。一次获取 20 张图片,数量适中。
params.breed.id 是从详情页传过来的品种 ID,用于筛选图片。
页面渲染结构
return (
<View style={s.container}>
<Header title={`${params.breed.name} 图片`} />
Header 标题动态显示品种名,比如"拉布拉多 图片"。
条件渲染主体内容:
{loading ? <Loading full /> : (
<ScrollView
style={s.content}
contentContainerStyle={s.grid}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={loadData} />}
>
contentContainerStyle 是 ScrollView 内容容器的样式,用于设置网格布局。和 style 的区别是:style 作用于 ScrollView 本身,contentContainerStyle 作用于内部内容。
图片列表渲染:
{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>
);
}
有图片就渲染 ImageCard 列表,没有就显示 Empty 组件。
每个 ImageCard 传入四个属性:
- image:图片数据
- liked:是否已收藏
- onLike:点击收藏的回调
- onPress:点击图片的回调,跳转到大图页面
网格布局的实现
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'两端对齐,中间自动留间距
padding: 16 给整体留边距。
ImageCard 组件
这是图片展示的核心组件,看看它的实现:
import React from 'react';
import {Image, TouchableOpacity, Text, StyleSheet, Dimensions} from 'react-native';
import type {DogImage} from '../types';
引入了 Dimensions,用于获取屏幕尺寸,实现响应式布局。
计算卡片宽度:
const W = (Dimensions.get('window').width - 48) / 2;
这行代码很关键。屏幕宽度减去 48(左右各 16 边距 + 中间 16 间距),再除以 2,得到每个卡片的宽度。
这样不管什么屏幕尺寸,两列布局都能正好撑满。
ImageCard 的 Props
interface Props {
image: DogImage;
liked?: boolean;
onPress?: () => void;
onLike?: () => void;
}
四个属性:
- image:必传,图片数据
- liked:可选,是否已收藏
- onPress:可选,点击卡片的回调
- onLike:可选,点击收藏的回调
onLike 设为可选是因为有些场景不需要收藏功能。
ImageCard 的渲染
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} />
外层是可点击的容器,activeOpacity={0.9} 让点击时透明度变化小一点,不会太闪。
图片撑满整个卡片。
收藏按钮:
{onLike && (
<TouchableOpacity style={s.like} onPress={onLike}>
<Text style={s.heart}>{liked ? '❤️' : '🤍'}</Text>
</TouchableOpacity>
)}
</TouchableOpacity>
);
}
条件渲染:只有传了 onLike 才显示收藏按钮。
按钮用绝对定位放在右下角,半透明黑色背景让图标在任何图片上都能看清。
ImageCard 的样式
const s = StyleSheet.create({
card: {width: W, height: W * 1.2, borderRadius: 12, overflow: 'hidden', marginBottom: 12},
img: {width: '100%', height: '100%', backgroundColor: '#f0f0f0'},
卡片宽度是计算出来的 W,高度是宽度的 1.2 倍,形成竖向的矩形。
overflow: 'hidden' 配合 borderRadius 让图片也有圆角。
收藏按钮样式:
like: {position: 'absolute', bottom: 8, right: 8, backgroundColor: 'rgba(0,0,0,0.3)', borderRadius: 16, padding: 6},
heart: {fontSize: 18},
});
position: 'absolute'绝对定位bottom: 8, right: 8距离右下角 8 像素rgba(0,0,0,0.3)半透明黑色背景borderRadius: 16圆形按钮
图片查看页面
点击图片跳转到 ImageViewPage,看看它的实现:
import React from 'react';
import {View, Image, TouchableOpacity, Text, StyleSheet, Dimensions} from 'react-native';
import {Header} from '../../components';
import {useRoute, useStore} from '../../hooks';
import type {DogImage} from '../../types';
const {width, height} = Dimensions.get('window');
获取屏幕宽高,用于设置图片尺寸。
数据获取:
export function ImageViewPage() {
const {params} = useRoute<{image: DogImage}>();
const {favoriteImages, toggleImage} = useStore();
const img = params.image;
const isFav = favoriteImages.includes(img.id);
从路由参数拿图片数据,从全局状态拿收藏信息。
图片查看页的结构
return (
<View style={s.container}>
<Header title="图片详情" right={
<TouchableOpacity onPress={() => toggleImage(img.id)}>
<Text style={s.fav}>{isFav ? '❤️' : '🤍'}</Text>
</TouchableOpacity>
} />
Header 右侧有收藏按钮,和品种详情页一样的设计。
图片展示区:
<View style={s.imgBox}>
<Image source={{uri: img.url}} style={s.img} resizeMode="contain" />
</View>
resizeMode="contain" 让图片完整显示,不裁切,保持比例。
底部操作栏:
<View style={s.actions}>
<TouchableOpacity style={s.action} onPress={() => toggleImage(img.id)}>
<Text style={s.actionIcon}>{isFav ? '❤️' : '🤍'}</Text>
<Text style={s.actionText}>{isFav ? '已收藏' : '收藏'}</Text>
</TouchableOpacity>
<TouchableOpacity style={s.action}>
<Text style={s.actionIcon}>📤</Text>
<Text style={s.actionText}>分享</Text>
</TouchableOpacity>
<TouchableOpacity style={s.action}>
<Text style={s.actionIcon}>💾</Text>
<Text style={s.actionText}>保存</Text>
</TouchableOpacity>
</View>
</View>
);
}
三个操作按钮:收藏、分享、保存。收藏功能已实现,分享和保存目前只是占位。
图片查看页的样式
const s = StyleSheet.create({
container: {flex: 1, backgroundColor: '#000'},
fav: {fontSize: 22},
背景用纯黑色,让图片更突出。
图片区域:
imgBox: {flex: 1, justifyContent: 'center', alignItems: 'center'},
img: {width, height: height * 0.6},
图片容器撑满中间区域,内容居中。
图片宽度等于屏幕宽度,高度是屏幕高度的 60%。这样上下留有空间给 Header 和操作栏。
操作栏:
actions: {flexDirection: 'row', backgroundColor: '#1a1a1a', paddingVertical: 20, paddingBottom: 36},
action: {flex: 1, alignItems: 'center'},
actionIcon: {fontSize: 26, marginBottom: 4},
actionText: {fontSize: 12, color: '#fff'},
});
深灰色背景,和纯黑区分开。
paddingBottom: 36 多留一些底部空间,避免被手机底部的安全区遮挡。
Dimensions 的使用
Dimensions 是 RN 提供的获取屏幕尺寸的 API:
const {width, height} = Dimensions.get('window');
get('window') 返回窗口尺寸,包含 width 和 height。
还有 get('screen') 返回屏幕尺寸,在有状态栏的设备上会比 window 大一点。
一般用 window 就够了。
响应式布局的思路
ImageCard 的宽度计算:
const W = (Dimensions.get('window').width - 48) / 2;
这种计算方式的好处是自适应。不管屏幕多宽,两列布局都能正好撑满,不会出现多余的空白或者溢出。
48 是怎么来的?
- 左边距 16
- 右边距 16
- 中间间距 16(两列之间)
总共 48。
如果想做三列布局,公式就是:
const W = (Dimensions.get('window').width - 64) / 3;
// 64 = 16 + 16 + 16 + 16(左、右、两个间距)
图片收藏的状态同步
图片收藏用的是全局状态,所以在图集页收藏的图片,在图片查看页也能看到收藏状态。
// 图集页
const {favoriteImages, toggleImage} = useStore();
liked={favoriteImages.includes(img.id)}
// 查看页
const {favoriteImages, toggleImage} = useStore();
const isFav = favoriteImages.includes(img.id);
两个页面用同一个 favoriteImages 数组,状态自动同步。
在查看页点击收藏,返回图集页,对应的图片心形图标也会变化。这就是全局状态管理的好处。
空状态处理
{images.length > 0 ? images.map(...) : <Empty icon="📷" title="暂无图片" />}
如果 API 返回空数组,显示 Empty 组件。
虽然 The Dog API 的品种一般都有图片,但做好空状态处理是个好习惯。万一网络问题或者 API 变化,页面不会显示空白。
可以优化的地方
图片懒加载:当前所有图片同时加载,如果数量多可能会卡。可以用 FlatList 的懒加载特性。
图片缓存:同一张图片多次显示会重复请求。可以用 react-native-fast-image 这类库做缓存。
手势操作:图片查看页可以加双指缩放、滑动切换等手势。
加载更多:当前固定获取 20 张,可以做分页加载更多。
这些后续可以优化,先保证核心功能。
小结
品种图集页涉及的知识点:
- 按品种筛选图片:API 传入 breedId 参数
- 响应式网格布局:Dimensions 计算卡片宽度
- ImageCard 组件:封装图片展示和收藏功能
- 图片查看页:全屏展示、resizeMode 控制缩放
- 状态同步:全局状态让多个页面的收藏状态一致
下一篇讲品种分组页面,会涉及到数据的分组处理和分组列表的展示。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)