在这里插入图片描述

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

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

这个页面解决什么问题

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

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

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

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

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

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

相反,网格布局可以:

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

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

代码实现

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

import { getUpcomingAnime } from '../../api/jikan';

const res = await getUpcomingAnime(pageNum);

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

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

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);

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

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 用于控制宽度:

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 的作用是:

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

FlatList 配置,关键是 numColumns:

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

样式代码

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 阶段,先实现基础功能,后续根据用户反馈迭代。

完整代码

把上面的片段组合起来:

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

Logo

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

更多推荐