案例开源地址: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);

favoriteImagestoggleImage 来自全局状态,处理收藏逻辑。

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}>

这个函数会:

  1. 设置 loading 为 true,显示加载状态
  2. 请求新的随机图片
  3. 更新 image 状态
  4. 设置 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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐