文中的项目已废弃,请移步新项目

一个Android本地阅读器的核心功能实现,kotlin+jetpack+mvvm版本

在我刚学习Android的时候,就想着要做一个本地阅读器,后来我的确做了一个,简单实现了功能就匆匆上架市场,之后便再无维护。

现在回头来看,界面简陋不说,性能也很差,决定重做一下。
先上图:

项目github地址:https://github.com/YuanWenHai/IReader


###核心功能

因为准备实现的阅读器属于简易版,功能上需要实现的并不算多,核心功能大致有如下几条:
1,保存阅读位置;这个是必须的,总不能每次打开一本书都在开头处。
2,调整字体大小;不同的人有不同的阅读习惯,调整字体大小也是很有必要的功能。
3,书籍搜索、添加;将本地储存的小说文件添加到阅读器中。
4,章节目录;从文件中索引出章节,并可以导向指定章节。


###文件添加

决定需求之后我们来考虑整个软件的实现。
按照我们使用软件时的流程,首先,我们应该能看到自己的书籍列表,即,添加本地书籍到列表中。
这个的实现还是比较容易的,简单一点我们可以通过调用系统的文件选择,但这样的机制对于一个阅读器而言,或许不是特别适合,所以我写了一个简单的文件搜索工具,界面大概是这样的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7cXG33rU-1608950463716)(https://camo.githubusercontent.com/6ba5bfb982fad426f2a21c804180c5b4e5dc4ff4/687474703a2f2f692e6d616b65616769662e636f6d2f6d656469612f31312d31352d323031362f6975393145482e676966)]
FileSearcher on github:https://github.com/YuanWenHai/FileSearcher
所以我们搞定了文件添加。


###列表
添加文件之后我们要考虑书籍列表的持久化,我们得保存这个书籍列表啊。
先来看看我们需要保存的都有什么:
1,名称
2,位置
3,访问时间
4,文字编码
访问时间用于排序,将用户最近访问的文件放在第一位。
这么多条,sharedPreferences这种键值储存显然是不行的,而且也无法保证顺序,所以我们这里需要用到数据库。
ok至目前为止我们的代码逻辑是这样的,打开书架——读取数据库——无书籍——打开文件选择器——选择书籍——在书架展示刚刚选择的书籍并将被选中的条目写入数据库。
这里需要提及的是,在添加书籍的过程中可能会和已有的条目重复,我们需要做一下过滤。
于是我们有了一个书籍列表。


###阅读界面
接下来的操作应该是打开书籍开始阅读,在这里我们用一个自定义view来完成:

class PageView extends View{Bitmap bit;@Overrideprotected void onDraw(Canvas canvas){super.onDraw(canvas);canvas.save();//在这里将我们传入的bitmap绘制出来canvas.drawBitmap(bit,0,0,null);canvas.restore();}
public void setBitmap(Bitmap bitmap){bit = bitmap;}}

在bitmap上绘制想要的内容:

Bitmap bitmap = Bitmap.createBitmap(screenWidth,screenHeight,Bitmap.Config.ARGB_8888);
mView.setBitmap(bitmap);
mCanvas = new Canvas(bitmap);
//通过canvas绘制
mCanvas.drawColor...
mCanvas.drawText..
//最后invalidate View
mView.invalidate();

为自定义view设定bitmap,我们在这个bitmap上绘制阅读界面,然后invalidate这个view就可以展示。
内容的获取:
1,因为我们的阅读要从上次停止的位置开始,也就是说,我们在打开文件后要跳转到某个位置然后开始读取,我使用了RandomAccessFile。
2,要读取多少;通过对屏幕尺寸,字体大小,偏移量的计算,我们得出一页需要的行数,以及每行的字数。
3,按段落读取,用0x0a识别二进制文件中的换行符,读取到0x0a停止。
4,将读取到的bytes转化为String,这里就有个绕不过的问题,编码;不同的书籍有不同的编码,有gbk,有utf8,utf16等等诸多,这里用一个EncodingDetector类库来完成识别,并将结果写入数据库。
5,当本页行数已经达到限制时,若已读取到的段落中尚有文字,我们将读取时的指针后退/前进相应的位置。
6,用SharedPreferences保存上次阅读位置。

public class PageFactory {private int screenHeight, screenWidth;//实际屏幕尺寸private int pageHeight,pageWidth;//文字排版页面尺寸private int lineNumber;//行数private int lineSpace = Util.getPXWithDP(5);private int fileLength;//Book的字节数private int fontSize ;private static final int margin = Util.getPXWithDP(5);//文字显示距离屏幕实际尺寸的偏移量private Paint mPaint;private int begin;//当前阅读的字节数_开始private int end;//当前阅读的字节数_结束private MappedByteBuffer mappedFile;//映射到内存中的文件private RandomAccessFile randomFile;//关闭Random流时使用private String encoding;//编码private Context mContext;private SPHelper spHelper = SPHelper.getInstance();private PageView mView;private Canvas mCanvas;private ArrayList<String> content = new ArrayList<>();private Book book;public PageFactory(PageView view){DisplayMetrics metrics = new DisplayMetrics();mContext = view.getContext();mView = view;((Activity)mContext).getWindowManager().getDefaultDisplay().getMetrics(metrics);screenHeight = metrics.heightPixels;screenWidth = metrics.widthPixels;fontSize = spHelper.getFontSize();pageHeight = screenHeight - margin*2 - fontSize;pageWidth = screenWidth -margin*2;lineNumber = pageHeight/(fontSize+lineSpace);mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);mPaint.setTextSize(fontSize);mPaint.setColor(mContext.getResources().getColor(R.color.dayModeTextColor));//设置bitmapBitmap bitmap = Bitmap.createBitmap(screenWidth,screenHeight, Bitmap.Config.ARGB_8888);mView.setBitmap(bitmap);mCanvas = new Canvas(bitmap);}//打开书籍public void openBook(final Book book){this.book = book;encoding = book.getEncoding();begin = spHelper.getBookmarkStart(book.getBookName());end = spHelper.getBookmarkEnd(book.getBookName());File file = new File(book.getPath());fileLength = (int) file.length();try {randomFile = new RandomAccessFile(file, "r");mappedFile = randomFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, (long) fileLength);} catch (Exception e) {e.printStackTrace();Util.makeToast("打开失败!");}}//下一页public void nextPage(){if(end >= fileLength){return;}else{content.clear();begin = end;pageDown();}printPage();}//上一页public void prePage(){if(begin <= 0){return;}else{content.clear();pageUp();end = begin;pageDown();}printPage();}//向后读取一个段落,返回bytesprivate byte[] readParagraphForward(int end){byte b0;int i = end;while(i < fileLength){b0 = mappedFile.get(i);if(b0 == 10) {break;}i++;}i = Math.min(fileLength-1,i);int nParaSize = i - end + 1 ;byte[] buf = new byte[nParaSize];for (i = 0; i < nParaSize; i++) {buf[i] =  mappedFile.get(end + i);}return buf;}//向前读取一个段落private byte[] readParagraphBack(int begin){byte b0 ;int i = begin -1 ;while(i > 0){b0 = mappedFile.get(i);if(b0 == 0x0a && i != begin -1 ){i++;break;}i--;}int nParaSize = begin -i ;byte[] buf = new byte[nParaSize];for (int j = 0; j < nParaSize; j++) {buf[j] = mappedFile.get(i + j);}return buf;}//获取后一页的内容
private void pageDown(){String strParagraph = "";while((content.size()<lineNumber) && (end< fileLength)){byte[] byteTemp = readParagraphForward(end);end += byteTemp.length;try{strParagraph = new String(byteTemp, encoding);}catch(Exception e){e.printStackTrace();}strParagraph = strParagraph.replaceAll("\r\n","  ");strParagraph = strParagraph.replaceAll("\n", " ");//计算每行需要的字数,切断string放入list中while(strParagraph.length() >  0){int size = mPaint.breakText(strParagraph,true,pageWidth,null);content.add(strParagraph.substring(0,size));strParagraph = strParagraph.substring(size);if(content.size() >= lineNumber){break;}}//如有剩余,则将指针回退if(strParagraph.length()>0){try{end -= (strParagraph).getBytes(encoding).length;}catch(Exception e){e.printStackTrace();}}}
}//读取前一页的内容private  void pageUp(){String strParagraph = "";List<String> tempList = new ArrayList<>();while(tempList.size()<lineNumber && begin>0){byte[] byteTemp = readParagraphBack(begin);begin -= byteTemp.length;try{strParagraph = new String(byteTemp, encoding);}catch(UnsupportedEncodingException e){e.printStackTrace();}strParagraph = strParagraph.replaceAll("\r\n","  ");strParagraph = strParagraph.replaceAll("\n","  ");while(strParagraph.length() > 0){int size = mPaint.breakText(strParagraph,true,pageWidth,null);tempList.add(strParagraph.substring(0, size));strParagraph = strParagraph.substring(size);if(tempList.size() >= lineNumber){break;}}if(strParagraph.length() > 0){try{begin+= strParagraph.getBytes(encoding).length;}catch (UnsupportedEncodingException u){u.printStackTrace();}}}}SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm", Locale.CHINA);//将获取到的内容绘制到view上public void printPage(){if(content.size()>0){int y = margin;mCanvas.drawColor(mContext.getResources().getColor(R.color.dayModeBackgroundColor));for(String line : content){y += fontSize+lineSpace;mCanvas.drawText(line,margin,y, mPaint);}float percent = (float) begin / fileLength *100;DecimalFormat format = new DecimalFormat("#0.00");String readingProgress = format.format(percent)+"%";int length = (int ) mPaint.measureText(readingProgress);mCanvas.drawText(readingProgress, (screenWidth - length) / 2, screenHeight - margin, mPaint);//显示时间String time = simpleDateFormat.format(new Date(System.currentTimeMillis()));mCanvas.drawText("时间:"+time,margin, screenHeight -margin, mPaint);//显示电量String batteryLevel = getBatteryLevel();float[] widths = new float[batteryLevel.length()];float batteryLevelStringWidth = 0;mPaint.getTextWidths(batteryLevel, widths);for(float f : widths){batteryLevelStringWidth += f;}mCanvas.drawText(batteryLevel, screenWidth - margin - batteryLevelStringWidth, screenHeight - margin, mPaint);mView.invalidate();}}private String getBatteryLevel(){Intent batteryIntent = mContext.registerReceiver(null,new IntentFilter(Intent.ACTION_BATTERY_CHANGED));int scaledLevel = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL,-1);int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);return "电量:"+String.valueOf(scaledLevel*100/scale);}public void saveBookmark(){SPHelper.getInstance().setBookmarkEnd(book.getBookName(),begin);SPHelper.getInstance().setBookmarkStart(book.getBookName(),begin);}public void setFontSize(int size){if(size < 15){return;}fontSize = size;mPaint.setTextSize(fontSize);pageHeight =  screenHeight - margin*2 - fontSize;lineNumber = pageHeight/(fontSize+lineSpace);end = begin;nextPage();SPHelper.getInstance().setFontSize(size);}
}

现在我们已经可以在阅读界面上看到书籍内容,并可以翻页。


###章节目录
1,获取章节;这个的实现方式有很多,比如正则,比如在读取整个txt文件的readLine循环中做单句关键字判定。
2,跳转到章节,这个有点意思,我们的阅读界面是通过字节读取展示的,而我们获取目录是在string文件中,两者之间的关系难以直接转换,即,虽然我知道第X章在第X个字的位置,但我无法准确得知这个字在byte文件中的位置,思前想后,决定用段落作为标记。因为换行符在byte中是可以被读取到的。
这就又回到了第一条,如果我们使用正则读取,那么将无法得到当前章节的段落数,readLine是个不错的选择,当获取到符合筛选条件的条目时,我们将其段落数也记录下来。
只有章节的段落数位置还不够,我们需要记录下在byte文件中每个0x0a出现的位置,然后用章节的段落位置去拿byte段落中的位置,这样我们就得到了每个段落在文件中的位置。

private List<Chapter> findChapterParagraphPosition(){List<Chapter> list = new ArrayList<>();int i = 0;try {InputStreamReader isr = new InputStreamReader(new FileInputStream(new File(book.getPath())), encoding);BufferedReader reader = new BufferedReader(isr);String temp;Chapter chapter;while ((temp = reader.readLine()) != null) {//这里关键字可以是章,也可以是其他的什么if(temp.contains("第")&&temp.contains(keyword)){chapter = new Chapter();chapter.setChapterName(temp);chapter.setBookName(book.getBookName());chapter.setChapterParagraphPosition(i);list.add(chapter);}i++;}} catch (FileNotFoundException f) {f.printStackTrace();Util.makeToast("未发现" + book.getBookName() + "文件");} catch (IOException e) {e.printStackTrace();}return list;}private List<Integer> findParagraphInBytePosition(){List<Integer> list = new ArrayList<>();byte[] fileBytes = new byte[mappedFileLength];mappedByteBuffer.get(fileBytes);mappedByteBuffer.position(0);for(int i=0;i<mappedFileLength;i++){if(fileBytes[i] == 0x0a){//i的位置为句尾list.add(i+1);}}return list;}private void insert(){for(Chapter chapter : findChapterParagraphPosition()){chapter.setChapterBytePosition(findParagraphInBytePosition().get(chapter.getChapterParagraphPosition()));}}

需要注意的是,如上代码段应该新开一个线程去执行,否则很容易ANR。

随后展示,并写入数据库。
3,定位,将目录列表的位置定位到当前阅读章节,这个我们用一个二分查找逻辑来实现。

private int getChapterNumber(int position,List<Chapter> list){position -= 2;//因为在获取章节位置时往前了一字节,同时position指向的是下一未读字节,故这里回退两个字节int begin = 0;int end = list.size()-1;while (begin <= end){int middle = begin + (end-begin)/2;if(middle == 0 && list.get(middle).getChapterBytePosition() >= position){return 0;}if(middle == list.size()-1 && list.get(list.size()-1).getChapterBytePosition() <= position){return list.size()-1;}if(list.get(middle).getChapterBytePosition() <= position  && list.get(middle+1).getChapterBytePosition() > position){return middle;}else if (list.get(middle).getChapterBytePosition() > position && list.get(middle-1).getChapterBytePosition() <= position){return middle -1;}else if(list.get(middle).getChapterBytePosition() < position && list.get(middle+1).getChapterBytePosition() < position){begin = middle+1;}else if(list.get(middle).getChapterBytePosition() > position && list.get(middle-1).getChapterBytePosition() > position){end = middle-1;}}return 0;

阅读器比较核心的部分基本就是这样,接下来说说这其中的坑。

一个android本地txt阅读器的思路与实现相关推荐

  1. Android本地小说阅读器(仿真、覆盖、滑动翻页,支持大文件)

    项目地址:https://github.com/PeachBlossom/treader 分享下之前写的小说阅读器,项目结构是传统mvc这样来做,欢迎大家star. 如风小说阅读器,添加书签.目录跳转 ...

  2. 基于Android的本地电子书阅读器的设计与实现Ebook(1)

    基于Android的本地电子书阅读器的设计与实现Ebook(1) 学习Android时间不久,试着做了一个本地电子书阅读器APP,因为知识浅薄并不能像其他大佬一样实现各种繁杂的功能,但可以实现基本的阅 ...

  3. 基于android的电子书阅读器app

    基于android的电子书阅读器app 基于Android平台的电子书阅读器的设计与实现主要通过Eclipse开发工具, Java语言与Sqlite数据库来完成的.本阅读器实现了本地阅读,手动翻页,书 ...

  4. 评测三款最流行的txt阅读器(ios手机适用)

    IOS上有哪些好用的txt阅读器?小编找了三款网络上呼声最高的进行试用,分别是 ,发现各有特点.一起来看看吧. 1 neat reader Neat Reader是一款比较轻量级的阅读器,可以跨平台使 ...

  5. 豆豆TXT阅读器1.0发布

    前后共花了一个多月时间,完成了个人的第一个android应用--豆豆TXT阅读器. 此应用主要用于阅读android设备本地TXT文件.具有以下功能: 1.支持手动进入sd卡目录并选择TXT文件导入书 ...

  6. Android平台epub阅读器推荐

    Android平台epub阅读器推荐 书籍是人类进步的阶梯.人们通过书籍来了解事物的种种,了解伟人给我们的启发和教育.随着时间的推移,电子书的出现逐渐使纸质书的地位发生动摇.固然使用书本来学习和阅读有 ...

  7. 评测三款高颜值的txt阅读器(ios手机适用)

    IOS上有哪些好用的.能解析txt格式的.颜值还高的阅读器?小编找了三款设计比较出色的进行试用,一起来看看吧. 1 neat reader 相信很多人都用过它,软件如其名,在前端设计上是费了心思的. ...

  8. android rss_Android RSS阅读器应用程序

    android rss In this tutorial, we'll be discussing Android RSS Reader and develop an RSS Feed Reader ...

  9. 评测3款高颜值的安卓txt阅读器

    txt阅读器作为一种特殊文件的解析软件,把文件解析得精美.吸引人观看,且配套的标注.笔记功能齐全,才能称之为比较合格的txt阅读器.以下是三款适配安卓系统的高颜值txt评测结果:  1.Neat Re ...

  10. 安卓手机上最好的3个txt阅读器

    txt格式是一款非常常见的电子书格式,很多手机由于自身不能直接打开txt格式文件或者软件使用感较差而给我们阅读带来困扰.今天小编就为大家介绍3款可以在安卓手机上使用的txt阅读器. 第一款:neat ...

最新文章

  1. Java并发基础构建模块简介
  2. Javascript 两种 function 定义的区别
  3. 《高质量java程序设计》读书笔记之----异常处理(1)
  4. 在线英汉词典 智能纠错的设计
  5. 学用MVC4做网站二:2.2添加用户组
  6. Android+Java中使用RSA加密实现接口调用时的校验功能
  7. [CentOS] CentOS 6 IPv6 关闭方法
  8. 剑指offer之机器人的运动轨迹
  9. 16 岁赚到 20 万美元,我的编程之路始于对代码的热爱
  10. 微软称伊朗国家黑客攻击美国国防技术公司
  11. [六省联考2017]分手是祝愿
  12. 图像增强之直方图均衡化
  13. fanuc系统服务器连接,FANUC IO LINK i地址分配操作方法
  14. 北大青鸟汉字注释机内码_汉字与机内码相互转换程序
  15. 金字塔原理——表达的逻辑
  16. (三)Lucene中Index.ANALYZED分词相关
  17. OpenStack Queen 版本变更概述
  18. 终端运行npm install @tinymce/tinymce-vue -S报错的解决办法
  19. 机器学习 (十五) 关联分析之Apriori算法
  20. 统计学第一篇,均值、中位数、众数

热门文章

  1. EasyFlash | 让 Flash 成为小型 KV 数据库
  2. 工程项目经济评价的基本方法
  3. 谷歌ai人工智能叫什么_为Google产品提供动力的人工智能
  4. web调用qq临时会话
  5. 推荐软件——total commander(善用佳软)
  6. ctfmon是什么启动项_我MSCONFIG启动项里面没有ctfmon怎么处理?
  7. 句柄详解,什么是句柄?句柄有什么用?
  8. 用java怎么开发图片标注工具,一些好用的图片标注工具
  9. html和css命名标准,CSS命名规则和命名方法
  10. 易语言64位进程注入DLL