TXT分页

小说阅读

TextView字数

小说分页

Textview显示字数

关于使用TextView阅读TXT分页的探讨,之所以这么个标题,是因为最终实现的效果也不是100%精确分页(精确分页就是说,不管怎么上一页下一页,每页的字符都保持不变,不会出现上一页少一行多一行少字符多字符之类的情况),所以不敢妄称实现,只是一些心得。做的时候发现同样问题的人从古至今非常多,但是没发现简单的实现,因此发出来,希望可以共同探讨一下,至于100%精确分页,还得请诸位实现了的大佬指点。

首先明确一点,这里说的100%精确分页,是建立在用多少读取多少,不是使用大量内存来读取、分割、保存、中转等,至于为什么这么苛刻,还是解释一下吧,最初这个需求是为了给一款石器时代的墨水屏电纸书用的,系统为Android2.x,内存少的可怜,加载多了可能就会死机,因此我只能尽量节省内存。而且这个古董不支持触摸,就算支持触摸,墨水屏显示滚动效果很不好,刷新效率低,滚动的话估计会糊成一片,所以只能翻页,刷新整个页面效果多少还好点。因此,最简单的方式就是TextView加载一段内容显示。

当然,除了TextView之外,你可以自绘实现,只是这样感觉比较麻烦,没有对这个进行深入的研究,所以不在本文的讨论之内。

本身TextView显示一段文本,非常简单,加载TXT,无非也是显示其中一段文本,但是,要做到分页控制,才会发现这个很难实现。搜索资料时发现有人说TextView是最复杂的View,因为涉及的东西太多了,比如左右顺序,换行控制等等。是不是最复杂的我不知道,但是感觉它提供的API还是太少。

目前这些能见到的100%精确分页的,都搞的非常非常麻烦,甚至还有搬出来数据库的……我只是想TextView加载一个片段而已,怎么就这么复杂呢?

其实还是怪TextView没有提供几个重要的API,比如,这个TextView最多能显示多少个字符,比如这个TextView当前可见的字符有多少个。如果有了这俩API,那剩下的就很好办了。

为什么我会纠结这个TextView最多显示多少个字符呢?

两个原因,1是内存很紧张,加载多了可能就死机;2是所找到的代码资料里,每次加载最多多少个字符,全都是写死的,比如给个2000,肯定够用了,但是实际上只显示几百个字符,我加载那么多干啥,地主家有余粮,可我这不够啊。或者给个小一点的值,但是改变字体大小后,显示的字符数量又比设定值要多,会留白怎么办?因此我必须要知道TextView最多能显示的字符数量,尽量减少无用的内存占用,还得不能不够显示的。另外就是,可能有人说,直接算好了这个设备的屏幕能显示多少字符,然后也写死算了……代码里硬编码一些这样的值,是不规范的,我个人也非常不喜欢,因为明显感觉有逻辑漏洞,万一代码放到其他设备上呢?

为什么我需要TextView当前可见的字符有多少个呢?

因为要分页啊!我不知道当前显示了多少个字符,就不能定位到底阅读到哪里,翻页就没法实现了。总不能只显示一页吧。

没有这俩API,只能自己想办法造了。自己造,思路是有,但是真的去做,发现非常困难,为什么呢?比如获取最多能显示多少个字符,纯中文,好处理,View宽度÷每个字符宽度,可以得到一行多少个字,然后×多少行。但是当全英文或者中英混排的时候就不行了,因为英文字母、标点宽度都是不同的!只能退而求其次,假设全是英文字符“a”,然后算出来这个TextView最多能显示多少个“a”。为什么用“a”呢?因为“a”的宽度比较适中吧,可能有人问,为什么不用标点什么的呢?这是因为我个人感觉,就算用“a”拿到的最大显示数量,也远比实际显示出来的字符数量要多的多,当然,你要是高兴或者内存足够,就想用啥用啥。

开始处理最大显示数量的问题,找了一圈,发现有个办法可以实现:

获取单个字符的宽度高度,然后用View的面积÷单个字符的面积=可显示的字符数量

因此可以写成:

/**
 *
获取大概最多的可显示的数量
 * 只是个大概的数量,不能当真。为了减少每次读取好几千个字符的尴尬。是根据控件大小计算得来的,
 * 真实显示情况肯定比这个少,因为还有行距什么的。
 *
 * @return
数量
 */
private int getMaxTotalWordsCouldShow(boolean chinese) {
    String text = chinese ? "" : "a";
    Paint paint = getPaint();
    paint.setTextSize(getTextSize());
    int textWidth = (int) paint.measureText(text.toString());
    Rect rect = new Rect();
    paint.getTextBounds(text, 0, text.length(), rect);
    int w = rect.width();
    int h = rect.height();
    int width = textWidth < w ? textWidth : w;

int wSpace = (getWidth() == 0 ? getMeasuredWidth() : getWidth()) - getPaddingLeft() - getPaddingRight();
    int hSpace = (getHeight() == 0 ? getMeasuredHeight() : getHeight()) - getPaddingTop() - getPaddingBottom();

int textSpace = width * h;
    int showSpace = wSpace * hSpace;
    return showSpace / textSpace;
}

上面的方法是放到继承自TextView的自定义View中用的。

第一个问题算是处理完了,虽然不够精确完美,但是至少算是解决了问题。

然后第二个问题:当前页面显示的字符数量

通过搜索发现:

/*** 获取当前页总字数*/public int getCurrentTotalCharCount() {try {return getLayout().getLineEnd(getCurrentTotalLineCount());} catch (Exception ignore) {}return 0;}/*** 获取当前页总行数*/public int getCurrentTotalLineCount() {try {Layout layout = getLayout();int topOfLastLine = getHeight() - getPaddingTop() - getPaddingBottom() - getLineHeight();return layout.getLineForVertical(topOfLastLine);} catch (Exception ignore) {}return 0;}

这个可以返回页面总字数,但是不知道是否足够精确。貌似也找不到更精确的方法了。需要注意的是这俩方法只能在渲染完毕之后调用。

接下来处理第三个问题:分页

先说说分页是怎么分:一大段文本,阅读到当前页,阅读的位置应该是当前页面左上角第一个字符;下一页比较好理解,就是当前页左上角第一个字符+当前页一共显示了多少个字符;上一页就比较麻烦了,应该是当前页左上角第一个字符 减去 上一页一共可以看到多少个字符才能定位到上一页的第一个字符的位置。这是个难点哦,我找到的demo里,只是实现了下一页,上一页估计原作者也没想到该怎么办,所以没有提供代码,也没提怎么处理。解决这个问题,其实我是用了个小技巧:从当前页左上角第一个字符的位置(已知)然后往前读取x个字符(TextView最多能显示的字符数量),然后将读取出来的字符翻转,就是尾巴变成头,然后赋到TextView里,就能拿到当前页显示了多少个字符了,对吧?但是这时候显示出来的字符,都是倒序的,因为我们前面做了翻转,所以这时候要再次把第一次读取出来的字符取指定长度,这时候就是上一页的真正的我们要的内容了,可以赋到TextView了,阅读的定位指针,就可以真正改变了。但是要注意的是,这期间给TextView两次赋值,会造成两次刷新,因此我在第一次赋值,也就是赋翻转之后的内容的时候,给TextView设置了setWillNotDraw(true),来屏蔽更新(虽然我不确定是不是真的有效,真的没有刷新),然后第二次赋值的时候才允许Draw。至此,第三个问题应该也处理完了。

 
虽然上面的做法理论上讲得通,实际做也可以,但是你会发现,不断的上一页下一页之后,页面行数会有误差,也就是不够精确,为什么呢?我没有详细验证,我觉得可能是1比如一个汉字,占两个字节,读取的时候刚好读了一半(可能看到乱码?),然后这种误差不断累积等;2统计当前页面显示了多少字符的方法所返回的结果与读取文件时字符的计算误差,比如统计字符的方法返回1个字符,但是读取文件时可能2个字节(我只是举例哈,具体的我也没验证,比如换行符、空格,到底是读取了几个字节或者统计成几个字符,我也不确定。欢迎大佬们验证)诸如此类,这些误差可能累计下来就导致分页不精确了。
好啦,原理上就是这些了。代码没多少,简单放一下吧。
自定义TextView,用来显示阅读的:
 
/*** Copyright (C), 2000-2019** @date 2019-12-08 19:09* History:* <author> <time> <version> <desc>* 2019-12-08 19:09 1  描述(简述该类的作用目的等)*/public class ReadingTextView extends TextView {public ReadingTextView(Context context) {super(context);}public ReadingTextView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}public ReadingTextView(Context context, AttributeSet attrs) {super(context, attrs);}private int maxChineseWordsCount = 0;private int maxEnglishWordsCount = 0;/*** 获取大概最多的可显示的数量* 只是个大概的数量,不能当真。为了减少每次读取好几千个字符的尴尬。是根据控件大小计算得来的,* 真实显示情况肯定比这个少,因为还有行距什么的。** @return 数量*/private int getMaxTotalWordsCouldShow(boolean chinese) {String text = chinese ? "测" : "a";Paint paint = getPaint();paint.setTextSize(getTextSize());int textWidth = (int) paint.measureText(text.toString());Rect rect = new Rect();paint.getTextBounds(text, 0, text.length(), rect);int w = rect.width();int h = rect.height();int width = textWidth < w ? textWidth : w;int wSpace = (getWidth() == 0 ? getMeasuredWidth() : getWidth()) - getPaddingLeft() - getPaddingRight();int hSpace = (getHeight() == 0 ? getMeasuredHeight() : getHeight()) - getPaddingTop() - getPaddingBottom();int textSpace = width * h;int showSpace = wSpace * hSpace;return showSpace / textSpace;}@Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) {super.onLayout(changed, left, top, right, bottom);resize();maxChineseWordsCount = getMaxTotalWordsCouldShow(true);maxEnglishWordsCount = getMaxTotalWordsCouldShow(false);}public int getMaxChineseWordsCount() {return maxChineseWordsCount;}public int getMaxEnglishWordsCount() {return maxEnglishWordsCount;}/*** 大小改变时,重新设置内容,返回的是有多少个字没有被显示。** @return 去掉的字数*/public int resize() {CharSequence oldContent = getText();CharSequence newContent = oldContent.subSequence(0, getCurrentTotalCharCount());setText(newContent);return oldContent.length() - newContent.length();}/*** 获取当前页总字数*/public int getCurrentTotalCharCount() {try {return getLayout().getLineEnd(getCurrentTotalLineCount());} catch (Exception ignore) {}return 0;}/*** 获取当前页总行数*/public int getCurrentTotalLineCount() {try {Layout layout = getLayout();int topOfLastLine = getHeight() - getPaddingTop() - getPaddingBottom() - getLineHeight();return layout.getLineForVertical(topOfLastLine);} catch (Exception ignore) {}return 0;}}
 
然后是外部调用的时候,上一页下一页:
private void reloadCurrentPage() {findViewById(R.id.reading_bottom_loading_iv).setVisibility(View.VISIBLE);long readAt = mConfig.getReadingPosition();if (readAt < mConfig.getTotalWordsCount()) {ProjectUtils.readFile(mFileNovel, mCurrentTextEncoding, getLoadMaxWordsCount(), readAt, false, new MyFileReadCallback() {@Overridepublic void onFileRead(boolean isSuccess, int errorCode, String desc, String content) {if (isSuccess) {reading_main_rtv.setText(content);//不会改变指针,无需处理updateReadingProcess();} else {showToast(content);}loading.setVisibility(View.GONE);findViewById(R.id.reading_bottom_loading_iv).setVisibility(View.GONE);}});}}//重新读取当前页private void loadNextPage() {Log.d(TAG, "loadNextPage: ===========最多中文:" + reading_main_rtv.getMaxChineseWordsCount());Log.d(TAG, "loadNextPage: ===========最多英文:" + reading_main_rtv.getMaxEnglishWordsCount());下一页的起始位置应当是当前页左上角!!!因为一旦改变字体,右下角的位置是会改变的!long readAt = mConfig.getReadingPosition();int showCount = reading_main_rtv.getCurrentTotalCharCount();final long startAt = readAt + showCount;if (startAt < mConfig.getTotalWordsCount()) {findViewById(R.id.reading_bottom_loading_iv).setVisibility(View.VISIBLE);ProjectUtils.readFile(mFileNovel, mCurrentTextEncoding, getLoadMaxWordsCount(), startAt, false, new MyFileReadCallback() {@Overridepublic void onFileRead(boolean isSuccess, int errorCode, String desc, String content) {if (isSuccess) {reading_main_rtv.setText(content);mConfig.setReadingPosition(startAt);updateAndSaveConfig();updateReadingProcess();} else {showToast(content);}loading.setVisibility(View.GONE);findViewById(R.id.reading_bottom_loading_iv).setVisibility(View.GONE);}});}else{showToast("已到达最后一页!");}}//下一页private void loadPreviousPage() {//上一页:readAt是当前页左上角第一个字符的位置,上一页应该是该位置开始//然后减去要读取的长度(获得到skip的位置,但是不能为负数)//然后查询长度不能超过skip。读出来剩余的字符,但是这些字符远大于当前页面可显示的字符//所以下面又进行了一个字符串翻转的骚操作,是为了拿到从后往前可显示的字符数。//所以要两次setText,但是不能让第一次的产生绘制,所以setWillNotDraw屏蔽一下。//至此,解决所有问题。final long readAt = mConfig.getReadingPosition();if (readAt > 0) {findViewById(R.id.reading_bottom_loading_iv).setVisibility(View.VISIBLE);ProjectUtils.readFile(mFileNovel, mCurrentTextEncoding, getLoadMaxWordsCount(), readAt, true, new MyFileReadCallback() {@Overridepublic void onFileRead(boolean isSuccess, int errorCode, String desc, String content) {if (isSuccess) {//获取到内容后进行翻转,然后赋值,用于拿到该页面能显示的字符数量,然后再切割原始字符//可能会有些字符的偏差,具体原因没深入研究。String reverse = ProjectUtils.reverse(content);reading_main_rtv.setWillNotDraw(true);//禁止更新reading_main_rtv.setText(reverse);//翻转后的,用来拿到数量int showCount = reading_main_rtv.getCurrentTotalCharCount();content = content.substring(content.length() - showCount);//切割能显示的数量reading_main_rtv.setText(content);reading_main_rtv.setWillNotDraw(false);//可以更新if (readAt - showCount < 0) {//更新指针位置mConfig.setReadingPosition(0);} else {mConfig.setReadingPosition(readAt - showCount);}updateAndSaveConfig();updateReadingProcess();} else {showToast(content);}loading.setVisibility(View.GONE);findViewById(R.id.reading_bottom_loading_iv).setVisibility(View.GONE);}});}else{showToast("已到达第一页!");}}//上一页
只要有了阅读的位置指针,读取文件就好办了,有好多种方式,这里就不写了。
其实我挺懒的写文章的。。。
有更好的方式,欢迎指点,如果有纰漏,也欢迎斧正。
 
参考资料:
https://my.oschina.net/gotax/blog/136860
https://blog.csdn.net/f409031mn/article/details/88778108
https://blog.csdn.net/jdsjlzx/article/details/84958289
https://blog.csdn.net/gaoanchen/article/details/50437111 里面的评论在讨论分页的问题

还有这个:https://blog.csdn.net/knock/article/details/5436177 这个感觉很有意思,为指定设备写的固件,按理说知道每页显示多少字符,计算分页应该容易多了,但是回想以前用MP3、电子词典看电子书什么的,确实很慢。。不知道那些古董代码逻辑是怎么写的。

还有些找不到了。。。就这样吧
 

关于使用TextView阅读TXT分页的探讨相关推荐

  1. ViewPager + TextView 小说阅读器分页

    先看效果 原理就是TextView里面的几个API /*** Get the line number corresponding to the specified vertical position. ...

  2. 简单方法实现Android阅读器分页

    写了一个非常小的阅读器.在实现分页功能时,一直没有思路.后来想了一个非常特别的方法.经过测试可以完美的实现分页功能. 主要思路: 1.将文本内容填充到TextView中,调用setText一句搞定. ...

  3. 怎么在MAC上阅读txt小说,小说阅读器推荐

    epub.txt是常见的电子书格式,我们在网上下载小说时经常会遇到.Mac电脑由于系统的"挑剔性",想必平时大家通常会遇到自己使用的小说阅读器不能在Mac系统上兼容的问题,今天小编 ...

  4. 怎么在电脑上阅读txt小说,小说阅读器推荐

    txt是一种使用广泛的文档.电子书格式,因为工作的原因,小编接触到很多不同的阅读器,今天小编将为大家推荐市面上最好的3个txt阅读器. Top1:Neat Reader 这款在小编用过的阅读器中绝对排 ...

  5. 怎么在Windows电脑上阅读txt小说,小说阅读器推荐

    txt是使用广泛的小说电子书格式,想必平时大家通常会遇到自己使用的小说阅读器不能在Windows系统上兼容的问题,因为工作的原因,小编接触到很多不同的阅读器,今天小编将为大家推荐Windows电脑上最 ...

  6. mysql 高效分页查询_PostgreSQL、MySQL高效分页方法探讨

    对于数据库相关的业务,逃不过的数据分页场景,无论是前台分页浏览还是划到页面底部自动加载.对于分页需求,各数据库也提供了成熟的SQL支持,类似于Hibernate等ORM框架也集成了相关的方法.但是基于 ...

  7. IPOD Touch阅读TXT电子书解决方案

    熊猫看书是我现在NOKIA手机上用的,感觉还不错,也有IPHONE版本,就直接下载并装上了. 但是如何将TXT文件上传到IPOD Touch却成了难题,APPLE封的实在是太死了. 网上一搜,发现没什 ...

  8. 怎么在IOS上阅读txt小说,小说阅读器推荐

    小编在通勤的时候,经常会看到身边的人在拿着手机看小说,看来喜欢使用手机阅读的人真的越来越多了.今天小编就为大家推荐几款良心的IOS手机阅读器,一起来看看把! 第一款:neat reader 这款阅读器 ...

  9. 关于阅读文章技巧的探讨

    引子:这会闲着没事,随手拿起一本<程序员>杂志来看.这是第6期的,内容早已陆续看完了.但是当我翻了翻里面的内容,我惊住了:里面的内容已级没有一点印象了.就像从来就没看过一样!等于说以前看的 ...

最新文章

  1. Python-TXT文本操作
  2. python之抽象一
  3. E-UTRA channel bandwidths per operating band (36.101)
  4. SIFT特征及特征匹配:SIFT and feature matching
  5. 机器学习从入门到精通50讲(一)-大数据平台下的数据质量管理
  6. esp8266接7735_基于8266的ESPEASY固件接入HASS的教程(可无脑接入各类传感...
  7. 台积电CEO魏哲家:3nm工艺按计划推进 明年一季度将看到营收
  8. 使用go来做系统,如何比java node php 更 简单
  9. 使用plt *.log
  10. JAVA里面==和euqals的区别
  11. java系统排序_java各种排序实现
  12. Windows 关于Robocopy的使用详解
  13. 第二十期 在Android中修改GPS定位数据的完整方案《手机就是开发板》
  14. dart语言hello world
  15. w ndows 10关机快捷键,windows关机快捷键winuu
  16. 医疗对话摘要论文阅读笔记
  17. 关于canvas生成图片的方法
  18. 基于Citespace和vosviewer文献计量学可视化SCI论文高效写作方法--开启从小白到精通的基于文献计量学论文写作之旅
  19. 【HTML】Canvas(3)-绘制图片
  20. 如何查看自己电脑的并口端口号?

热门文章

  1. 【闲来无聊写个几个小特效——五角星,小光圈,探照灯】
  2. 年终奖多发 1 块,税后反而少 3W?
  3. android fqrouter2,fqrouter2
  4. 衣服的褶皱怎么画?怎样才能画好衣服的褶皱?
  5. iPhone4激活出错,菜鸟试验!!!!!
  6. 6.824-lecture11
  7. 我该选择留下,还是离开?
  8. 【application Extension 之TodayExtension】扩展与宿主App之间共享数据有两种方式
  9. 集成显卡可以运行cad吗_CAD用独显还是集显好?
  10. 问题解决——Android模拟器模拟缩放等双指操作