flutter txt翻页

背景

这个一直是我想写的一个东西,但实在太麻烦了,而且一直没找到合适的代码适合我参考。正好最近不忙,而且有好多东西要学,又不想学另外一个,就只能写写这个,劳逸结合。

心酸历程

在网上找了大量的文章和代码,搜索的侧重点在仿真翻页,看了他们的实现,发现根本看不进去,太复杂了,计算了手势上的各种位移,而且涉及好多细小的计算,实在头痛,遂放弃。

后来决定先看渲染文字,计算一页显示的字数,初步定了使用textPainter渲染,因为它有个属性didExceedMaxLines可以用来判断内容有没有超出显示。(其实一开始我想一次渲染,然后在将所有的按照屏幕高度截成许多页翻页显示,但估计我人菜,翻遍文档也不知道怎么做)

能渲染文字后,就要考虑翻页了,前面仿真翻页那个计算的放弃了,但是我发现swiper滑动也是一种翻页,遂决定使用flutter_swiper,但时候后发现每次翻页都会重新渲染(导致每次显示的都不一样),后来找解决方案,有人说这个好像暂时控制不了,推荐使用PageView自己写,然后就自己写了(发现flutter_swiper对这个作用确实也不大,而且设置loop,反而不如自己控制PageView跳转方便写代码)

于是翻页的组件选好了,渲染文字也可以了,就开始写了。但又发现了好麻烦的事情,因为设计的主要是三个页面轮流显示,两个过渡,使用textPainter的话就相当于需要五个CustomPaint绘制,渲染和文字计算一体(有五个但有两个是相同的),这样就导致代码写的不忍直视,而且又乱七八糟,真的要吐了。。。后来我又想换成三个字符串变量渲染就方便了,正好看textPainter的时候看到RichText,于是心想使用textPainter计算,RichText用来显示。终于可以了!!!

翻页思路

可以看网上写的关于swiper无限循环的原理

永远显示序号1,2,3;0,4序号为过渡(图片和这行字写给作者自己看)

实现

页面初始渲染

Widget buildView(
    BookDetailState state, Dispatch dispatch, ViewService viewService) {
  size = MediaQuery.of(viewService.context).size;
  if (state.detail != '' && state.text2 == '') {
    initVal();
    // 这里的textStartIndex 不是后面直接存的textStartIndex,而是textStateIndex减去下一页和当前页的文本字数后的值
    textStartIndex = state.textStartIndex;
    _getText(state, size, dispatch);
  }
  PageController _controller = PageController(initialPage: 1);
  // const double fontSize = state.fontSize;
  const TextStyle style =
      TextStyle(color: Color(0xff333333), fontSize: 16, height: 1.3);
  painters = [
    // csP,
    RichText(text: TextSpan(text: state.text3, style: style)),
    RichText(text: TextSpan(text: state.text1, style: style)),
    RichText(text: TextSpan(text: state.text2, style: style)),
    RichText(text: TextSpan(text: state.text3, style: style)),
    RichText(text: TextSpan(text: state.text1, style: style)),
  ];
  return Scaffold(
      backgroundColor: const Color(0xffCCE8CF),
      body: Container(
        padding:
            const EdgeInsets.only(left: 12, right: 12, top: 36, bottom: 12),
        child: PageView(
          controller: _controller,
          children: painters,
          onPageChanged: (index) async { // 当页面变化回调
            currentIndex = index;
            // 判断是否能够翻页
            String mixStr = currentSwiperIndex.toString() + '_' + currentIndex.toString();
            Map orderStr = {'1_1':true,'1_2':true,'2_3':true,'3_4':true};
            if(orderStr[mixStr] == null && state.textStartIndex <= 0) return;
            if(orderStr[mixStr] == true && state.textStartIndex >= state.detail.length) return;

            if (index == 0) {
             currentSwiperIndex = painters.length - 2;
             await Future.delayed(const Duration(milliseconds: 400));
               _controller.jumpToPage(currentSwiperIndex);
              realPosition = currentSwiperIndex - 1;
            } else if (index == painters.length - 1) {

              currentSwiperIndex = 1;
              await Future.delayed(const Duration(milliseconds: 400));
              _controller.jumpToPage(currentSwiperIndex);
              realPosition = 0;
            } else {
              pageChangeCount++;
              // 页面变化后 获取下一页(或上一页)的数据
              _getText(state, size, dispatch);
              currentSwiperIndex = index;
              realPosition = index - 1;
              if (realPosition < 0) realPosition = 0;
            }
          },
        ),
      ));
}

一页显示字数多少的计算方法

先预设该页不包含换行等符号,皆为文字和标点符号等可见的字符串。
1. 计算一行文字的高度 textHeight = (字号 * 行高).ceil()
2. 计算一页能显示的行数 lines = ((屏幕高度 – paddingTop – paddingBottom) / textHeight).floor()
3. 计算一行能显示的文字个数 lineCount = ((屏幕宽度-paddingLeft – paddingRight) / 字号).floor() 假如有设置文字间隔也要计算进去
4. 计算该页最多能显示的字数 textCount = lineCount * lines

文字最多显示的字数已经计算好了,但实际上很多文章是有段落,说明有很多换行符,它们会占据一些空白位置,所以一页显示不了这么多文字。于是在这个程序中,通过didExceedMaxLines判断有没有超过显示,具体流程如下

  1. 则截取一行文字(上面计算的lineCount大小),判断有无换行符
  2. 如果有换行符,下一页截取最后一个换行符前面的字符串,上一页截取后面的字符串,再次判断是否超过
  3. 循环往复,如果1步骤中无换行符,则直接截去lineCount个字符(这个不是很精确,会导致有时候一页还差几个字符未填满,往前翻的页面可能和之前翻的也差几个字符,但不是很影响使用,可以优化)

使用TextPainter计算下一页的文本

String getTextPainter(
    String text, style, Size size, int textLine, int lineCount) {
  TextPainter textPainter = TextPainter(
      text: TextSpan(text: text, style: style),
      textAlign: TextAlign.justify,
      maxLines: textLine,
      textDirection: TextDirection.ltr)
    ..layout(
      maxWidth: size.width - 12.0 - 12.0,
    );
  if (textPainter.didExceedMaxLines) {
    // 判断截取掉的字符是否含换行符,如果包含则,去掉换行符后面的文字;否则则去掉这一行
    int textEndIndex = text.length - lineCount;
    String ctext = text.substring(text.length - lineCount);
    int rCount = ctext.lastIndexOf("\r\n");
    if (rCount < 0) {
      text = text.substring(0, textEndIndex);
    } else {
      text = text.substring(0, textEndIndex + rCount);
    }

    return getTextPainter(text, style, size, textLine, lineCount);
  } else {
    textStartIndex += text.length;
    return text;
  }
}

使用TextPainter计算上一页的文本

String getPreText(String text, style, Size size, int textLine, int lineCount) {
  TextPainter textPainter = TextPainter(
      text: TextSpan(text: text, style: style),
      textAlign: TextAlign.justify,
      maxLines: textLine,
      textDirection: TextDirection.ltr)
    ..layout(
      maxWidth: size.width - 12.0 - 12.0,
    );
  if (textPainter.didExceedMaxLines) {
    // 判断截取掉的字符是否含换行符,如果包含则,去掉换行符后面的文字;否则则去掉这一行
    int start = lineCount;
    String ctext = text.substring(0, start);
    int rCount = ctext.indexOf("\r\n");
    if (rCount < 0) {
      text = text.substring(start);
    } else {
      text = text.substring(rCount + 2);
    }
    return getPreText(text, style, size, textLine, lineCount);
  } else {
    return text;
  }
}

_getText方法,页面切换触发

String _getText(BookDetailState state, Size size, Dispatch dispatch) {
  const TextStyle style =
      TextStyle(color: Color(0xff333333), fontSize: 16, height: 1.3);
  int height = (state.fontSize * state.lineHeight).ceil();
  int lines = ((size.height - 36 - 12) / height).floor();
  // textLine = lines;
  int lineCount = ((size.width - 12 - 12) / state.fontSize).floor();
  int textCount = lineCount * lines;
// orderStr 为下一页顺序组合值 下面为顺序
  Map orderStr = {
    "1_2": true,
    "2_3": true,
    '3_4': true,
    '1_1': true
  };
  String mix = currentSwiperIndex.toString() + '_' + currentIndex.toString();
  // currentIndex为当前序号 preText,currentText,nextText分别时PageView序号1,2,3
  // textStartIndex 为当前页的下一页的下一页的第一个字符的顺序号
  // 下一页
  if (orderStr[mix] != null) {
    if (pageChangeCount == 0) {
      // pageChangeCount 为页面初始化时
      if (textStartIndex > 0) {
        // 假如起始位置不是0,则取前一页
        int start = textStartIndex - textCount ;
        String text =
            state.detail.substring(start > 0 ?start : 0, textStartIndex);
        nextText = getPreText(text, style, size, lines, lineCount);
      }
      String text =
          state.detail.substring(textStartIndex, textStartIndex + textCount);
      preText = getTextPainter(text, style, size, lines, lineCount);
      String text2 =
          state.detail.substring(textStartIndex, textStartIndex + textCount);
      currentText = getTextPainter(text2, style, size, lines, lineCount);
      stateStartIndex = textStartIndex - nextText.length - currentText.length;
    } else {
      if (currentIndex == 3) {
        String text =
            state.detail.substring(textStartIndex, textStartIndex + textCount);
        preText = getTextPainter(text, style, size, lines, lineCount);
        stateStartIndex = textStartIndex - preText.length - nextText.length;
      } else if (currentIndex == 2) {
        String text =
            state.detail.substring(textStartIndex, textStartIndex + textCount);
        nextText = getTextPainter(text, style, size, lines, lineCount);
        stateStartIndex = textStartIndex - currentText.length - nextText.length;
      } else if (currentIndex == 1) {
        String text =
            state.detail.substring(textStartIndex, textStartIndex + textCount);
        currentText = getTextPainter(text, style, size, lines, lineCount);
        stateStartIndex = textStartIndex - preText.length - currentText.length;
      }
    }
  } else {
    // 上一页
    if (pageChangeCount == 0 && currentIndex == 0) return '';
    int end = textStartIndex - currentText.length - nextText.length - preText.length;
    if(end <= 0) {
      textStartIndex = 0;
      end = 0;
      dispatch(BookDetailActionCreator.onIncreateTextCount(0));
      return '';
    }
    int start = end - textCount > 0 ? end-textCount : 0 ;

    String text = '';
    if (currentIndex == 1) {
      text = state.detail.substring(start, end);
      nextText = getPreText(text, style, size, lines, lineCount);
      textStartIndex = end + preText.length + currentText.length;
    } else if (currentIndex == 2) {
      text = state.detail.substring(start, end);
      preText = getPreText(text, style, size, lines, lineCount);
      textStartIndex = end + nextText.length + currentText.length;
    } else if (currentIndex == 3) {
      text = state.detail.substring(start, end);
      currentText = getPreText(text, style, size, lines, lineCount);
      textStartIndex = end + preText.length + nextText.length;
    }
    if(end - textCount <= 0) {
      stateStartIndex = 0;
    } else {
      stateStartIndex = end + text.length;
    }

  }
  // 更新text1,text2,text3 渲染页面
  dispatch(
      BookDetailActionCreator.onUpdateText(preText, currentText, nextText));
  // 记录当前的阅读记录
  dispatch(BookDetailActionCreator.onIncreateTextCount(stateStartIndex));
  return '';
}