RN for OpenHarmony AnimeHub项目实战:即将上映页面开发

案例开源地址:https://atomgit.com/nutpi/Rn_openharmony_AnimeHub

每个季度开始前,动漫迷们都会关注一个问题:下季度有什么新番?即将上映页面就是用来展示那些已经公布但还没开播的动漫作品。

这个页面解决什么问题

动漫行业有个特点:新番信息会提前几个月甚至半年公布。制作公司会先放出预告片、主视觉图、声优阵容等信息来预热。对于动漫迷来说,提前了解这些信息可以:

  • 规划追番计划,避免同一天太多番要追
  • 提前了解感兴趣的作品,开播时不会错过
  • 参与社区讨论,和其他粉丝交流期待

即将上映页面把这些"预告中"的动漫集中展示,方便用户浏览和收藏。

页面设计选择:网格 vs 列表

这个页面选择了网格布局,和正在热播页面的列表布局不同。为什么?

即将上映的动漫有个特点:还没有评分。因为还没开播,没人看过,自然没有评分数据。既然没有评分,列表布局的优势(显示排名和评分)就不存在了。

相反,网格布局可以:

  • 一屏显示更多作品
  • 封面图更大,视觉冲击力更强
  • 用户可以通过封面快速判断画风是否喜欢

所以这个页面用两列网格,每个卡片显示封面和标题。

代码实现

先看 API 调用。获取即将上映的动漫用的是专门的接口:

typescript 复制代码
import { getUpcomingAnime } from '../../api/jikan';

const res = await getUpcomingAnime(pageNum);

这个接口返回的是按预计开播时间排序的动漫列表,最快开播的排在前面。

状态管理和其他分页列表一样,五件套:

typescript 复制代码
const [animeList, setAnimeList] = useState<Anime[]>([]);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);

数据加载函数,标准模板:

typescript 复制代码
const loadData = async (pageNum: number, append = false) => {
  try {
    if (pageNum === 1) setLoading(true);
    else setLoadingMore(true);
    
    const res = await getUpcomingAnime(pageNum);
    const newData = res.data || [];
    
    if (append) {
      setAnimeList(prev => [...prev, ...newData]);
    } else {
      setAnimeList(newData);
    }
    setHasMore(res.pagination?.has_next_page || false);
  } catch (error) {
    console.error('Load error:', error);
  } finally {
    setLoading(false);
    setLoadingMore(false);
  }
};

渲染函数是这个页面的特色。注意外面包了一层 View 用于控制宽度:

typescript 复制代码
const renderItem = ({ item }: { item: Anime }) => (
  <View style={styles.cardWrapper}>
    <AnimeCard
      anime={item}
      onPress={() => navigation.navigate('AnimeDetail', { animeId: item.mal_id })}
    />
  </View>
);

为什么需要 cardWrapper?因为 FlatList 的 numColumns 只是把列表分成多列,但不会自动处理每个卡片的宽度和间距。cardWrapper 的作用是:

typescript 复制代码
cardWrapper: {
  flex: 1,
  maxWidth: '50%',
  padding: Spacing.xs,
},
  • flex: 1 让卡片填充可用空间
  • maxWidth: '50%' 确保每行最多两个
  • padding 创造卡片之间的间距

FlatList 配置,关键是 numColumns:

typescript 复制代码
<FlatList
  data={animeList}
  renderItem={renderItem}
  keyExtractor={item => item.mal_id.toString()}
  numColumns={2}
  contentContainerStyle={styles.list}
  showsVerticalScrollIndicator={false}
  onEndReached={handleLoadMore}
  onEndReachedThreshold={0.5}
  ListFooterComponent={loadingMore ? <Loading text="加载更多..." /> : null}
  ListEmptyComponent={<EmptyState icon="rocket" title="暂无数据" />}
/>

numColumns={2} 告诉 FlatList 把数据分成两列显示。FlatList 会自动处理布局,我们只需要确保每个 item 的宽度正确。

空状态用了火箭图标(rocket),暗示"即将发射"的意思,和"即将上映"的主题呼应。

AnimeCard vs AnimeListItem

项目中有两个展示动漫的组件:

AnimeCard 用于网格布局:

  • 显示大封面图
  • 标题在图片下方
  • 适合浏览、发现场景

AnimeListItem 用于列表布局:

  • 显示小缩略图
  • 标题和详情在右侧
  • 可以显示排名
  • 适合排行榜、搜索结果场景

选择哪个组件取决于页面的目的。即将上映页面的目的是"发现新作品",用户主要通过封面来判断是否感兴趣,所以用 AnimeCard。

样式代码

typescript 复制代码
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: Colors.background,
  },
  list: {
    padding: Spacing.sm,
  },
  cardWrapper: {
    flex: 1,
    maxWidth: '50%',
    padding: Spacing.xs,
  },
});

样式很简洁。container 是标准的全屏容器,list 设置列表的内边距,cardWrapper 控制每个卡片的布局。

注意 list 用的是 Spacing.sm(小间距),cardWrapper 用的是 Spacing.xs(超小间距)。这样整体边距是 sm,卡片之间的间距是 xs * 2 = sm,视觉上比较协调。

即将上映数据的特点

即将上映的动漫数据有一些特殊之处:

没有评分:score 字段通常是 null 或 0。AnimeCard 组件需要处理这种情况,不显示评分或显示"暂无评分"。

没有集数:episodes 字段可能是 null,因为还没确定总集数。有些作品会显示预计集数,有些则完全未知。

有预计开播时间:aired.from 字段包含预计开播日期。可以用这个信息显示"X月开播"或倒计时。

信息可能不完整:简介、角色、制作人员等信息可能还没公布,详情页可能比较空。

和季度动漫页的区别

即将上映页面和季度动漫页面有什么区别?

季度动漫页

  • 显示特定季度(如 2024 年春季)的动漫
  • 包括已开播和未开播的
  • 用户需要先选择年份和季度

即将上映页

  • 只显示还没开播的动漫
  • 不限于某个季度,可能跨越多个季度
  • 直接显示,不需要选择

简单说,季度动漫页是"按时间分类",即将上映页是"按状态筛选"。

可以添加的功能

当前实现比较基础,可以考虑添加:

开播倒计时:显示距离开播还有多少天。这个信息对用户很有价值,可以帮助他们规划。

按开播时间排序:当前是按热度排序,也可以提供按开播时间排序的选项。

筛选功能:按类型(TV、电影、OVA)、按季度(2024春、2024夏)筛选。

提醒功能:用户可以设置开播提醒,到时候推送通知。

这些功能会让页面更实用,但也会增加复杂度。在 MVP 阶段,先实现基础功能,后续根据用户反馈迭代。

完整代码

把上面的片段组合起来:

typescript 复制代码
import React, { useEffect, useState, useCallback } from 'react';
import { View, FlatList, StyleSheet } from 'react-native';
import { Colors, Spacing } from '../../theme';
import { Anime } from '../../types';
import { getUpcomingAnime } from '../../api/jikan';
import { AnimeCard } from '../../components/anime';
import { Header, Loading, EmptyState } from '../../components/common';

export const UpcomingAnimeScreen = ({ navigation }: any) => {
  const [animeList, setAnimeList] = useState<Anime[]>([]);
  const [loading, setLoading] = useState(true);
  const [loadingMore, setLoadingMore] = useState(false);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);

  // loadData, useEffect, handleLoadMore, renderItem...
  // 省略,和前面讲的一样

  if (loading) {
    return (
      <View style={styles.container}>
        <Header title="即将上映" showBack onBack={() => navigation.goBack()} />
        <Loading fullScreen text="加载中..." />
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Header title="即将上映" showBack onBack={() => navigation.goBack()} />
      <FlatList
        data={animeList}
        renderItem={renderItem}
        keyExtractor={item => item.mal_id.toString()}
        numColumns={2}
        contentContainerStyle={styles.list}
        showsVerticalScrollIndicator={false}
        onEndReached={handleLoadMore}
        onEndReachedThreshold={0.5}
        ListFooterComponent={loadingMore ? <Loading text="加载更多..." /> : null}
        ListEmptyComponent={<EmptyState icon="rocket" title="暂无数据" />}
      />
    </View>
  );
};

小结

即将上映页面展示还没开播的动漫,帮助用户提前了解和规划追番。页面使用网格布局,因为即将上映的动漫没有评分数据,网格布局可以更好地展示封面。

实现上使用 FlatList 的 numColumns 属性创建两列网格,cardWrapper 控制每个卡片的宽度和间距。AnimeCard 组件负责渲染单个卡片,封装了封面图和标题的显示逻辑。

即将上映的数据有其特殊性:没有评分、集数可能未知、信息可能不完整。组件需要优雅地处理这些情况,不能因为数据缺失就崩溃或显示异常。

下一篇讲人气排行页面,展示按人气(而非评分)排序的动漫。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
墨狂之逸才1 天前
React Native 状态管理大比拼:Event Bus 还是 Context?小白一看就懂!
react native
爱滑雪的码农1 天前
React Native 完整开发全流程(从零到上线)
javascript·react native·react.js
沐言人生1 天前
ReactNative 源码分析12——Native View创建流程onBatchComplete
android·react native
沐言人生3 天前
ReactNative 源码分析11——Native View创建流程setChildren和manageChildren
android·react native
沐言人生4 天前
ReactNative 源码分析10——Native View创建流程createView
android·react native
坏小虎4 天前
【聊天列表组件选型建议】FlashList、FlatList、LegendList三种列表组件
javascript·react native·react.js
sealaugh325 天前
react native(学习笔记第五课) 英语打卡微应用(4)- frontend的列表展示
笔记·学习·react native
沐言人生6 天前
ReactNative 源码分析9——Native View初始化
android·react native
接着奏乐接着舞6 天前
react native expo打包
javascript·react native·react.js
jxm_csdn7 天前
Expo Go 本地命令行编译 apk(Ubutnu22.04)
react native