背景
这个一直是我想写的一个东西,但实在太麻烦了,而且一直没找到合适的代码适合我参考。正好最近不忙,而且有好多东西要学,又不想学另外一个,就只能写写这个,劳逸结合。
心酸历程
在网上找了大量的文章和代码,搜索的侧重点在仿真翻页,看了他们的实现,发现根本看不进去,太复杂了,计算了手势上的各种位移,而且涉及好多细小的计算,实在头痛,遂放弃。
后来决定先看渲染文字,计算一页显示的字数,初步定了使用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
判断有没有超过显示,具体流程如下
- 则截取一行文字(上面计算的lineCount大小),判断有无换行符
- 如果有换行符,下一页截取最后一个换行符前面的字符串,上一页截取后面的字符串,再次判断是否超过
- 循环往复,如果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 '';
}