rn_for_openharmony狗狗之家app实战-随机狗狗实现
案例开源地址:https://atomgit.com/nutpi/rn_openharmony_dogimg
惊喜感的设计
有时候用户不知道想看什么,只是想随便逛逛。随机狗狗功能就是为这种场景设计的,每次点击都能看到一只不同的狗狗,充满惊喜感。
这种"刷一刷"的交互在很多 App 里都有,比如探探的左右滑、抖音的上下滑。核心都是用随机性制造新鲜感。
页面状态设计
RandomPage 的状态比较简单:
const {favoriteImages, toggleImage} = useStore();
const [loading, setLoading] = useState(true);
const [image, setImage] = useState<DogImage | null>(null);
favoriteImages 和 toggleImage 来自全局状态,处理收藏逻辑。
loading 控制加载状态的显示。
image 存储当前显示的图片,初始值是 null。
为什么用 DogImage | null 而不是 DogImage[]?因为随机狗狗每次只显示一张图片,不需要数组。
加载随机图片
useEffect(() => { loadRandom(); }, []);
const loadRandom = async () => {
setLoading(true);
try {
const res = await api.getImages(1);
if (res.length > 0) setImage(res[0]);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
api.getImages(1) 只请求一张图片。
API 默认返回随机排序的结果,所以每次调用都会得到不同的图片。
setLoading(true) 在请求开始时设置,让用户知道正在加载。
收藏状态的判断
const isFav = image ? favoriteImages.includes(image.id) : false;
这里用了三元表达式处理 image 可能为 null 的情况。
如果 image 存在,检查它的 id 是否在收藏列表里。
如果 image 是 null,直接返回 false。
页面布局结构
return (
<View style={s.container}>
<Header title="🎲 随机狗狗" />
<View style={s.content}>
{loading ? <Loading /> : image && (...)}
</View>
<View style={s.actions}>
...
</View>
</View>
);
三层结构:Header、内容区、操作栏。
内容区根据 loading 状态显示 Loading 组件或图片。
image && (...) 确保 image 存在才渲染内容。
图片和品种信息
<>
<Image source={{uri: image.url}} style={s.img} resizeMode="cover" />
{image.breeds?.[0] && (
<View style={s.info}>
<Text style={s.name}>{image.breeds[0].name}</Text>
{image.breeds[0].temperament && (
<Text style={s.temp} numberOfLines={2}>
{image.breeds[0].temperament}
</Text>
)}
</View>
)}
</>
用 Fragment <>...</> 包裹多个元素。
可选链 image.breeds?.[0] 安全地访问嵌套属性,如果 breeds 不存在或为空数组,不会报错。
temperament 是狗狗的性格描述,用 numberOfLines={2} 限制最多显示两行。
图片样式
img: {
width: width - 32,
height: width - 32,
borderRadius: 16,
backgroundColor: '#eee'
},
宽高都是 width - 32,形成正方形。减去 32 是因为左右各有 16 的 padding。
正方形图片在视觉上更稳定,也方便布局。
backgroundColor: '#eee' 在图片加载前显示灰色背景。
品种信息样式
info: {marginTop: 16, alignItems: 'center', paddingHorizontal: 20},
name: {fontSize: 22, fontWeight: '600', color: '#333'},
temp: {fontSize: 14, color: '#666', marginTop: 6, textAlign: 'center'},
品种名用大号加粗字体,是视觉焦点。
性格描述用小号灰色字体,作为补充信息。
textAlign: 'center' 让多行文字居中对齐。
底部操作栏
<View style={s.actions}>
<TouchableOpacity style={s.action} onPress={loadRandom}>
<Text style={s.actionIcon}>🔄</Text>
<Text style={s.actionText}>换一只</Text>
</TouchableOpacity>
<TouchableOpacity
style={[s.action, s.mainAction]}
onPress={() => image && toggleImage(image.id)}
>
<Text style={s.mainIcon}>{isFav ? '❤️' : '🤍'}</Text>
</TouchableOpacity>
<TouchableOpacity style={s.action}>
<Text style={s.actionIcon}>📤</Text>
<Text style={s.actionText}>分享</Text>
</TouchableOpacity>
</View>
三个按钮:换一只、收藏、分享。
收藏按钮放在中间,用更大的样式突出显示。
onPress={() => image && toggleImage(image.id)} 确保 image 存在才执行。
主按钮的突出设计
mainAction: {
width: 66,
height: 66,
borderRadius: 33,
backgroundColor: '#FFF3E0',
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 16
},
mainIcon: {fontSize: 34},
主按钮是个圆形,直径 66。
borderRadius: 33 是直径的一半,形成正圆。
背景色 #FFF3E0 是浅橙色,和 App 的主题色呼应。
图标用 34 号字,比两侧按钮的 26 号大。
操作栏整体样式
actions: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 20,
paddingBottom: 36,
backgroundColor: '#fff'
},
白色背景和内容区的灰色背景形成对比。
justifyContent: 'center' 让三个按钮居中排列。
paddingBottom: 36 为底部安全区留空间。
换一只的交互
点击"换一只"按钮调用 loadRandom:
<TouchableOpacity style={s.action} onPress={loadRandom}>
这个函数会:
- 设置 loading 为 true,显示加载状态
- 请求新的随机图片
- 更新 image 状态
- 设置 loading 为 false
整个过程用户能看到明确的反馈。
加载状态的体验优化
当前实现在加载时会显示 Loading 组件,图片区域变空。
更好的体验是保留旧图片,在上面显示加载指示器:
const [image, setImage] = useState<DogImage | null>(null);
const [nextImage, setNextImage] = useState<DogImage | null>(null);
const loadRandom = async () => {
setLoading(true);
const res = await api.getImages(1);
if (res.length > 0) {
setNextImage(res[0]);
// 预加载图片
Image.prefetch(res[0].url).then(() => {
setImage(res[0]);
setLoading(false);
});
}
};
用 Image.prefetch 预加载新图片,加载完成后再切换,避免闪烁。
动画效果的添加
切换图片时可以加个淡入淡出效果:
import {Animated} from 'react-native';
const opacity = useRef(new Animated.Value(1)).current;
const loadRandom = async () => {
// 淡出
Animated.timing(opacity, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}).start(async () => {
// 加载新图片
const res = await api.getImages(1);
if (res.length > 0) setImage(res[0]);
// 淡入
Animated.timing(opacity, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}).start();
});
};
先淡出旧图片,加载完成后淡入新图片,过渡更自然。
滑动切换的实现思路
除了点击按钮,还可以支持滑动切换:
左滑:换一只(不喜欢)
右滑:收藏(喜欢)
这种交互类似探探,用户操作更流畅。
实现需要用到 PanResponder 或手势库,代码会复杂不少。
历史记录功能
用户可能想回看之前的狗狗,可以加个历史记录:
const [history, setHistory] = useState<DogImage[]>([]);
const [currentIndex, setCurrentIndex] = useState(-1);
const loadRandom = async () => {
const res = await api.getImages(1);
if (res.length > 0) {
setHistory([...history, res[0]]);
setCurrentIndex(history.length);
setImage(res[0]);
}
};
const goBack = () => {
if (currentIndex > 0) {
setCurrentIndex(currentIndex - 1);
setImage(history[currentIndex - 1]);
}
};
用数组存储浏览过的图片,支持前进后退。
API 的随机机制
api.getImages 的实现:
getImages: (limit = 10, breedId?: number) =>
http.get<DogImage[]>('/images/search', {
limit,
breed_ids: breedId,
order: 'RANDOM',
}),
order: 'RANDOM' 告诉 API 返回随机排序的结果。
The Dog API 的随机是服务端实现的,每次请求都会返回不同的图片。
品种筛选的扩展
可以让用户选择只看某个品种的随机图片:
const [selectedBreed, setSelectedBreed] = useState<number | null>(null);
const loadRandom = async () => {
const res = await api.getImages(1, selectedBreed || undefined);
...
};
传入 breedId 参数,API 会返回该品种的随机图片。
小结
随机狗狗页面的特点:
- 单图展示:每次只显示一张,聚焦用户注意力
- 一键刷新:点击换一只,获取新的随机图片
- 品种信息:显示图片关联的品种名和性格
- 突出主操作:收藏按钮用更大的圆形设计
这种随机浏览的模式很适合休闲场景,用户不需要思考,只需要刷刷刷。
下一篇讲图片上传功能,让用户也能贡献自己的狗狗照片。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)