案例开源地址: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') 返回窗口尺寸,包含 widthheight

还有 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

Logo

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

更多推荐