一个android本地txt阅读器的思路与实现

news/2024/10/18 7:54:21/

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

一个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;

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


http://www.ppmy.cn/news/191370.html

相关文章

基于Android的Word文档阅读器

随着android系统的发展&#xff0c;android已经得到广泛的认可&#xff0c;作为一名普通的大学生&#xff0c;真的希望自己能在android系统上做一个可以让大家使用、方便大家工作的一个软件&#xff0c;最后决定做一个基于andriod的Word格式阅读器。 经过一些查找工作&#xff…

PDF阅读器使用技巧

期末大考即将来临&#xff0c;不少朋友需要复习做笔记&#xff0c;下面来分享一下使用福昕PDF阅读器的使用技巧吧&#xff0c;希望能够助准备考试的朋友一臂之力。 使用技巧一、添加书签&#xff1b; 在阅读和浏览时&#xff0c;遇到很长的文章&#xff0c;无法在短时间一次性看…

免费离线使用的MarkDown阅读器

自己内网工作环境下&#xff0c;用MarkDown写了功能的帮助文档&#xff0c;要给策划的小伙伴看&#xff0c;苦苦找了好久MarkDown阅读器&#xff0c;终于找到了。 免安装 免费 支持Windows XP/7/8/10/11 不需要安装额外运行环境&#xff0c;下载可用&#xff0c;可离线使用。 …

身份证阅读器二次开发说明

身份证阅读器二次开发现已支持常用的微软系统&#xff0c;如windows系统&#xff0c;同时支持安卓&#xff08;Android&#xff09;平台、Linux平台、单片机等的二次开发应用&#xff0c;支持CS、BS构架。身份证阅读器二次开发接口文件目前常用的开发语言几乎全部支持。 广东东…

android 支持各种格式的阅读器,android txt小说阅读器的实现(完美实现分页阅读,支持常见编码格式)...

前一阵由于项目的需求,需要一个预览txt文件的功能,于是自己写了一个,简单的txt文件阅读器,实现了点击分页切换、滑动分页切换,效果如下: 既然做出来,想整理一下实现的思路,有时间再封装一下,做成一个自己喜欢的风格的阅读器。 实现这个肯定是自定义view啦,将字符一个…

html5小说阅读器源码,文本源码阅读器(NexusTextView)

NexusTextView是一款简单实用的文本/源码阅读器&#xff0c;能够帮助用户更好地阅读程序源码并将其保存为HTML格式&#xff0c;还具有查看二进制文件以及添加自定义语法等功能&#xff0c;功能强大简单实用。有需要的小伙伴欢迎来西西下载体验。 软件简介&#xff1a; 一款小巧…

Ubuntu安装caj阅读器

CAJViewer提供了Linux版本&#xff0c;其中对Ubuntu系统支持16.04及以上版本。 1.下载文件 在https://cajviewer.cnki.net/download.html下载文件 可直接点击https://download.cnki.net/CAJViewer-x86_64-buildubuntu1604-210401.AppImage下载 2.设置文件可执行 右键CAJV…

epub电子书阅读器 EpubViewer

桌面版epub电子文档阅读软件比较少&#xff0c;所以自己编写了一个EpubViwer&#xff0c;直接上图和功能&#xff1a; 功能&#xff1a; 可以同时打开多个Epub文件可以多标签显示Epub电子书不同章节左边栏显示Epub电子书目录可以在目录栏搜索电子书章节可以多标签显示Epub电子…