想要用RN写个小说APP第四天【左右无限刷新上下章🔖】

2,100 阅读5分钟

我正在参加「掘金·启航计划」

在某天某时,我立下了这个奇怪的flag,关于我和他的爱恨情仇,请看我的第一篇文章
想要用RN写个小说APP第一天
当前已经实现的功能有:

  • 爬取页面内容✅
  • 状态管理以及数据持久化✅
  • 小说内容主题自定义(字体/颜色/...)
  • 爬取后如何进行文本分页✅
  • 分页后如何在用户阅读时,无感加载上/下一章✅(当前)
  • (相关的bug经历我最近都会写出来🧐🧐🧐)
    项目地址,star看我后续更新🌠🌠🌠

今日内容

在上一篇中如何将文章进行分页,我通过读取ReactNative原生组件解决了如何将文章进行分页,并能够将每个页面的内容拆分到数组的单独元素中。今天我们就要解决如何将其渲染分页并可以左右翻页,最后的目标是是在用户没有感知的情况下,将他所阅读的当前章节的上下章缓存下来并进行无缝衔接

先上效果图

纵享丝滑!!!!

19e0bf86a81c10dbffb211658388d51d_raw 00_00_00-00_00_30.gif

初次尝试

最开始翻页切换组件,我找到的是:react-native-pager-view这个包,在一开始只考虑到将当前的章数的前一章以及后面一章请求到并显示时,并没有什么问题,和上面的截图效果是一样的。先贴上项目的大概demo示例代码 (因为原代码太乱,我抽象出来助于理解,非实际代码,只是助于理解)

代码结构

export default function Test({content}: {content: string}) {
  // curIdx记录本次进入时在第几章
  const [curIdx, setCurIdx] = useState(10);
  // useGetCconten大概功能是通过文章url获取到的文本的分页数组
  const content1 = useGetCconten(curIdx - 1);
  const content2 = useGetCconten(curIdx);
  const content3 = useGetCconten(curIdx + 1);
  return (
    <View>
      {[...content1, ...content2, ...content3].map(_ => (
        // PageView翻页插件
        <PageView>
          <Text style={{padding: 30, fontSize: 80}}>
            {content.slice(_ * count, _ * count + count)}
          </Text>
        </PageView>
      ))}
    </View>
  );
}

上面的结构在只显示三章的时候,效果并没有什么问题。而当我在翻页的Change事件中,进行判断用户是否到达了前一章的最后一页,这时候我需要做的是:将用户可能需要的当前所在章的上一章or下一章请求并将其更新到页面渲染的数组中。我很容易的得到了这样的一段代码:

if(isNeedGetContent(index)){
    setCurIdx(curIdx-1);
}

出现问题

当我以为他能够很好的进行工作时,我得到了这样的效果(没有原视频,实现了当前的功能之后,之前的已经跑不起来了哈哈哈哈哈)。

image.png

当我信心满满的的在9.2页setCurIdx(curIdx-1);我得到了这样的效果,页面切换到9.2页时,会在得到更新数组的时候快速的跳回到10.0页,这个时候再继续左滑,就不会出现这个问题,然后继续左滑到9.0页,再左滑,触发请求第八章,又得到了上面的那个效果.....这绝对是大大的问题啊。我在这个问题上花了一天。甚至挣扎着去看了插件中Android端的代码。(好吧没看懂)。最后既没找到原因,也没找到解决本办法。当让....

峰回路转

当我在这个项目的issue中查看着是否有人和我遇到同样的问题时,我看到了这样一条。

image.png image.png

无限滑动

我得到了react-native-infinite-pager这个库,他支持让页面无限滚动。
就那么一下,我感觉我和他对上了眼。最终我也是使用它解决了我的问题。这是他的官网Demo:

import InfinitePager from "react-native-infinite-pager";

const NUM_ITEMS = 50;

function getColor(i: number) {
  const multiplier = 255 / (NUM_ITEMS - 1);
  const colorVal = Math.abs(i) * multiplier;
  return `rgb(${colorVal}, ${Math.abs(128 - colorVal)}, ${255 - colorVal})`;
}

const Page = ({ index }: { index: number }) => {
  return (
    <View
      style={[
        styles.flex,
        {
          alignItems: "center",
          justifyContent: "center",
          backgroundColor: getColor(index),
        },
      ]}
    >
      <Text style={{ color: "white", fontSize: 80 }}>{index}</Text>
    </View>
  );
};

export default function App() {
  return (
    <View style={styles.flex}>
      <InfinitePager
        PageComponent={Page}
        style={styles.flex}
        pageWrapperStyle={styles.flex}
      />
    </View>
  );
}

思考

这里面最重点的就是PageComponent属性,它允许我们传入一个页面组件,而我们得到的参数是一个当前页面的序号,初始为0,随着我们的左滑与右滑,序号随之增减(左滑后为-1,右滑后为1)。
那现在的唯一问题就是我要如何通过每一次得到的序号去映射到用户应该渲染的对应页面。

image.png

我直接贴出我最后的解决方案,联想到我这是一个关于0的中心对称结构。我每一次进入页面时定义了一个这样的结构,在这里面,LEFT用于存储在0左边相关的页面数据,RIGHT相反,contents存储页面对应的内容文本,pageCount是随着章节的新增请求时,会记录所请求章节的总页数:

const DATA = useRef<IData>({
    LEFT: {contents: [], pagesCount: []},
    RIGHT: {contents: [], pagesCount: []},
  });

大概是这样的,下面的contents三章存储着三章的内容,pageCount有着对应的总页数:

1666864854028.jpg

当我定义好这个结构后,那我需要解决的就是,在index需要大于0时,让组件到RIGHT分支取数据渲染,小于0是到LEFT分支取数据。

书写代码

代码如下: 在将LEFT分支,要将页面数据推入数组时,我们需要将数组翻转再推入,那我们就可以通过-index-1(-1是因为数组下标从0开始,而-index最低是1),然后访问到对应的页面内容:

const renderPage = useCallback(({index}: {index: number}) => {
    if (index < 0) {
      return (
        <View style={{padding}}>
          <Text style={style.title}>
            {DATA.current.LEFT.contents[-index - 1]?.title ?? `${index}`}
          </Text>
          <Text style={textStyle}>
            {DATA.current.LEFT.contents[-index - 1]?.content ?? ''}
          </Text>
        </View>
      );
    } else {
      return (
        <View style={{padding}}>
          <Text style={style.title}>
            {DATA.current.RIGHT.contents[index]?.title ?? `${index}`}
          </Text>
          <Text style={textStyle}>
            {DATA.current.RIGHT.contents[index]?.content ?? ''}
          </Text>
        </View>
      );
    }
  }, []);

渲染数据的问题解决了,下一步解决何时去请求上下章的问题。 在onPageChange事件中,拿到当前进入的页面序号。
index小于0时,这时我们需要用到的是我们之前记录在pageCount中的章节各自的总页数,对除去数组最后一个的元素(记录的是数组最后一章节的页数)然后进行求和,将其与-index对比,相等则说明我们到了页面数组中的最后一章。这时发起请求去得到再上一章的内容,然后更新LEFT分支contentspageCount数组内容:

onPageChange判断逻辑

onPageChange={index => {
              const copyDATA = DATA.current;
              if (
                index === -sum(copyDATA.LEFT.pagesCount.slice(0, -1)) - 1 &&
                !bool
              ) {
                type = 'pre';
                const idx = curIdx - copyDATA.LEFT.pagesCount.length;
                getChapterForFun(ChapterList[idx - 1]).then(
                  ({content, title}) => {
                    setContent({content, title});
                  },
                );
              } else if (
                index === sum(copyDATA.RIGHT.pagesCount.slice(0, -1))
              ) {
                type = 'nex';
                getChapterForFun(
                  ChapterList[curIdx + copyDATA.RIGHT.pagesCount.length],
                ).then(({content, title}) => {
                  setContent({content, title});
                });
              }
              console.log('getRealPage', getRealPage(curIdx, index, copyDATA));
              storage.set(
                'book-address',
                JSON.stringify(getRealPage(curIdx, index, copyDATA)),
              );
            }}

更新分支

const addPreContent = useCallback((arr: any) => {
    if (type === 'pre') {
      console.log('preDddPreContent', arr.length);
      DATA.current.LEFT.pagesCount.push(arr.length);
      DATA.current.LEFT.contents.push(...arr.reverse());
      type = '';
    } else if (type === 'nex') {
      console.log('nexDddPreContent', arr.length);
      DATA.current.RIGHT.pagesCount.push(arr.length);
      DATA.current.RIGHT.contents.push(...arr);
      type = '';
    }
  }, []);

结语

随着到这,无感加载上下章的代码也就内容了,当然还有着一些细节,在触发页面改变的事件时,去计算得到当前的章节序号以及该章节所在第几页。并将其记录下来。用于用户下次进入小说内容页时,能够恢复到对应的页数

后续计划更新(打勾的已经更新了)的内容有: (下面计划的功能都是已经大致完成了💨)

项目地址,star看我后续更新🌠🌠🌠