失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > Android 电子书功能实现 长按选中 高亮显示。 TXT

Android 电子书功能实现 长按选中 高亮显示。 TXT

时间:2018-08-27 20:00:17

相关推荐

Android  电子书功能实现 长按选中 高亮显示。  TXT

近期公司有一个电子书需求的开发,功能除了电子书的基本功能之外,还有长按选中,可以滑动高亮显示等等。最初是准备使用FBReader,但是发现不太优化,之前用过FBReader。然后就网上找demo,发现对于选中高亮显示真的是有点尴尬。之后就参考一些博客,然后就自己搞了一下。

效果图如下:

功能主要包含:

1.长按选中高亮显示

2.滑动绘制高亮显示

3.滑动绘制之后弹窗

4.句或者段落后面出现标识

5.绘制虚线

电子书解析绘制翻页主线功能我用的是/spuermax/WeYueReadergithub上的项目。然后其实他的功能就自己改里面的东西,添加新的业务需求代码。总之,收益很大。在此建议不要为完成功能而写代码。

涉及到的知识:

Canvas(drawText drawRect drawLine )、 Path (moveTo lintTo)、Point (存字符的位置信息)、Rect(绘制高亮) 、 事件分发 View刷新(postInvalidate、invalidate)。

绘制电子书的核心实现(分页、绘制、动画等等)不做太多的解释,可以自己了解一下现有的Demo。总体的说有几个难点

第一个是分页的逻辑,我看过几个项目,有一个是对每个字进行计算,然后用屏幕宽高加上分辨率来计算一行所需的字符个数,如果是单个字遍历,就直接追加就可以,如果是字符串使用截取,我用的是第二种;计算完行所需的数量,在根据屏幕高计算页所需的行数;段跟段之间的分割,大部分用的是"\n",或者是用一些特殊字符,比如“\u00”等等。

第二个是缓存,无论是页缓存或者章节缓存,这里的页缓存不是Android的PageCache,是绘制电子书的每一页,可以按照ViewPage的缓存三页,当前页和上一下和下一页;章节缓存可以使用File文件,对于章节缓存,如果单纯的电子书显示,建议采用章节缓存(可以使用RxJava)

例如:

Observable.concat(chapterContentBeans).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(resultMessage -> {ChapterContentModel model = new Gson().fromJson(new Gson().toJson(resultMessage.getData()), ChapterContentModel.class);StringBuffer stringBuffer = converGson(model);BookSaveUtils.getInstance().saveChapterInfo(model.getBookCode(), model.getSentence(), stringBuffer.toString());getView().chapterContent();title = titles.poll();}, throwable -> Log.i("Flowable", "throwable = " + throwable.getMessage()), () -> {}, disposable -> {});

第三个是View的事件处理,在自定义显示View中,一个屏幕被划分为,中间区域和上一页下一页区域;如果在加上绘制标注和高亮绘制,会更加麻烦,不过只要了解事件的分发流程,这些都是代码量的问题。

总体思路:

无论是长按选中高亮显示,还是滑动绘制高亮显示,最关键是位置信息(x,y)值。我们需要将一个章节划分页,页里面划分行,行里面划分每一个字符,字符里面包含各自的位置信息。 OK。然后思路基本就出来了。 在你进行滑动绘制高亮的时候,使用MOVE事件的X、Y值,每次刷新FirstShowChar 和LastShowChar ,调用自定义View的postInvalidate,刷新onDraw方法。

滑动绘制的四种模式:

public enum Mode {Normal,PressSelectText,//按下滑动模式SelectMoveForward,//向前滑动模式SelectMoveBack//向后滑动模式}

四种模式分别对应在滑动绘制的时候的不同状态,长按进行绘制单个字符的高亮,在单个字符高亮显示之后,按下左右两个Icon滑动,分别是向前滑动和向后滑动模式。

单个字符的Model:

public class ShowChar {public char charData;public boolean isSelected;public Point TopLeftPosition = null;public Point TopRightPosition = null;public Point BottomLeftPosition = null;public Point BottomRightPosition = null;public float charWidth = 0;}

单行的Model:

public class ShowLine {public List<ShowChar> CharsData = null;public String getLineData() {String linedata = "";if (CharsData == null || CharsData.size() == 0) return linedata;for (ShowChar c : CharsData) {linedata = linedata + c.charData;}return linedata;}public float lintHeight;}

每页的Model:

public class TxtPage {public int position;public String title;public int titleLines; //当前 lines 中为 title 的行数。public List<String> lines;public List<String> linesChange;public List<ShowLine> showLines;// 当前页的行数public List<NotationBean> notationList;// 页面标注的信息public String sentence;//章节第一句}

上述的字、行、页的Model,贯穿绘制显示的电子书页面和一些别的扩展功能。

在这里姑且认为你已经开完电子书页面绘制的逻辑,直接开怼,其实绘制高亮的逻辑,跟绘制电子书的逻辑车没有太大的关联。

在复杂的功能也是一步一步走流程的。

1.长按绘制单个字的高亮

在Down事件中自定你长按事件,得到按下的X、Y值,根据xy值去查找对应的区域坐标,drawPath。

自定义事件

timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {((Activity) getContext()).runOnUiThread(new Runnable() {@Overridepublic void run() {if (currentMode == Mode.Normal) {isLongClick = true;currentMode = Mode.PressSelectText;mPageLoader.setMode(Mode.PressSelectText);// 设置ModemPageLoader.setDown_x(x);mPageLoader.setDown_y(y);postInvalidate();}}});}}, LONG_CLICK_DURATION);

主要是赋值点下DOWN事件的X、Y值,然后执行postInvalidate()。之后在onDraw方法里,绘制高亮。

private void drawSelectText() {if (mCurrentMode == PageView.Mode.PressSelectText) {drawPressSelectText();} else if (mCurrentMode == PageView.Mode.SelectMoveForward) {drawMoveSelectText();} else if (mCurrentMode == PageView.Mode.SelectMoveBack) {drawMoveSelectText();}}

分三种模式,长按高亮和向前滑动和向后滑动。先来看绘制高亮

private void drawPressSelectText() {ShowChar showChar = searchPressShowChar(Down_x, Down_y);if (showChar != null) {FirstSelectShowChar = LastSelectShowChar = showChar;mSelectTextPath.reset();mSelectTextPath.moveTo(showChar.TopLeftPosition.x, showChar.TopLeftPosition.y);mSelectTextPath.lineTo(showChar.TopRightPosition.x, showChar.TopRightPosition.y);mSelectTextPath.lineTo(showChar.BottomRightPosition.x, showChar.BottomRightPosition.y + 10);mSelectTextPath.lineTo(showChar.BottomLeftPosition.x, showChar.BottomLeftPosition.y + 10);canvas.drawPath(mSelectTextPath, mSelectBgPaint);//绘制两个IcondrawBorderPoint();Down_x = -1;Down_y = -1;}}

根据DOWN事件的XY值,来确定所选定的字,定位一页内容的字。

public ShowChar searchPressShowChar(float down_X2, float down_Y2) {TxtPage curPage = getCurPage(getPagePos());List<ShowLine> showLines = curPage.showLines;for (ShowLine l : showLines) {for (ShowChar showChar : l.CharsData) {if (down_Y2 > showChar.BottomLeftPosition.y) {break;// 说明是在下一行}if (down_Y2 <= showChar.BottomLeftPosition.y && down_X2 >= showChar.BottomLeftPosition.x && down_X2 <= showChar.BottomRightPosition.x) {return showChar;}}}return null;}

可能会有疑问,怎么拿到每个字的位置。在绘制的当前的页的内容时候,是每一行,每一行绘制上去的,也就是drawText。

for (int n = 0; n < str.length(); n++) {ShowChar showChar = new ShowChar();showChar.charData = str.charAt(n);showChar.id = i;showChar.x = w;showChar.y = top + 10;//--------------------------保存位置--------------------------------rightPosition = leftPosition + mTextPaint.measureText(str) / str.length();Point topLeftPoint = new Point();showChar.TopLeftPosition = topLeftPoint;topLeftPoint.x = (int) leftPosition;topLeftPoint.y = (int) (bottomPosition - mTextPaint.getTextSize());Point bottomLeftPoint = new Point();showChar.BottomLeftPosition = bottomLeftPoint;bottomLeftPoint.x = (int) leftPosition;bottomLeftPoint.y = (int) bottomPosition;Point topRightPoint = new Point();showChar.TopRightPosition = topRightPoint;topRightPoint.x = (int) rightPosition;topRightPoint.y = (int) (bottomPosition - mTextPaint.getTextSize());Point bottomRightPoint = new Point();showChar.BottomRightPosition = bottomRightPoint;bottomRightPoint.x = (int) rightPosition;bottomRightPoint.y = (int) bottomPosition;leftPosition = rightPosition;showCharList.add(showChar);}

在drawContent中其实很多逻辑,这里只是把每个字赋值位置单独拿出来。使用的是Point。每个字,你可以把它当做一个矩形,矩形的四个点上下左右划分四个Point,保存在ShowChar中。

OK,回到上一步的定位到字之后绘制单个字的高亮,其中有两个FirstSelectShowChar和LastSelectShowChar字段,比较重要,只要理解他的作用,滑动绘制基本就很随意了。

重点解释一下这两个含义:

具体的业务需求是:长按选中对应坐标下的字,背景绘制为高亮,左右两边绘制ICON,接下来的按下操作如果是在左右两边的ICON区域内,左边的话,对应的是FirstSelectShowChar,在判断是否向上区域滑动;右边的话,对应的是LastSelectShowChar,在判断是否是向下区域滑动,否则就去下高亮绘制。需求不太明白的可以用掌阅或者书旗试一下。

我们第一次是长按,然后选中的是一个字,这时候,FirstSelectShowChar=LastSelectShowChar = 当前选中的字,也就是“,”;当前的Mode是PressSelectText。 然后看上面的黄色区域,和下面的绿色区域,和逗号两边的蓝色小框框。第二次点击,需要判断一下是否在左右的两边蓝色小框框,左边的话更新Mode模式为SelectMoveForward,右边的话更新模式为SelectMoveBack。

判断是否在左右两边的小框框:

public boolean checkIfSelectRegionMove(float x, float y) {if (FirstSelectShowChar == null && LastSelectShowChar == null) {return false;}float flx, frx, fty, fby;flx = FirstSelectShowChar.TopLeftPosition.x - 40;frx = FirstSelectShowChar.TopLeftPosition.x + 10;fty = FirstSelectShowChar.TopLeftPosition.y;fby = FirstSelectShowChar.BottomLeftPosition.y + 20;float llx, lrx, lty, lby;llx = LastSelectShowChar.TopRightPosition.x - 10;lrx = LastSelectShowChar.TopRightPosition.x + 40;lty = LastSelectShowChar.TopRightPosition.y;lby = LastSelectShowChar.BottomRightPosition.y + 20;if ((x >= flx && x <= frx) && (y >= fty && y <= fby)) {mCurrentMode = PageView.Mode.SelectMoveForward;return true;}if ((x >= llx && x <= lrx) && (y >= lty && y < lby)) {mCurrentMode = PageView.Mode.SelectMoveBack;return true;}return false;}

更新完当前的Mode后,在进行下一轮滑动方向区域判断,是否向前滑动,是否想后滑动:

向前滑动判断:

public boolean isCanMoveForward(float down_x, float down_y) {Path p = new Path();p.moveTo(LastSelectShowChar.TopRightPosition.x, LastSelectShowChar.TopRightPosition.y);p.lineTo(mPageView.getWidth(), LastSelectShowChar.TopRightPosition.y);p.lineTo(mPageView.getWidth(), 0);p.lineTo(0, 0);p.lineTo(0, LastSelectShowChar.BottomRightPosition.y);p.lineTo(LastSelectShowChar.BottomRightPosition.x, LastSelectShowChar.BottomRightPosition.y);p.lineTo(LastSelectShowChar.TopRightPosition.x, LastSelectShowChar.TopRightPosition.y);return computeRegion(p).contains((int) down_x, (int) down_y);}

向后滑动判断:

public boolean isCanMoveBack(float down_x, float down_y) {Path p = new Path();p.moveTo(FirstSelectShowChar.TopLeftPosition.x, FirstSelectShowChar.TopLeftPosition.y);p.lineTo(mPageView.getWidth(), FirstSelectShowChar.TopLeftPosition.y);p.lineTo(mPageView.getWidth(), mPageView.getHeight());p.lineTo(0, mPageView.getHeight());p.lineTo(0, FirstSelectShowChar.BottomLeftPosition.y);p.lineTo(FirstSelectShowChar.BottomLeftPosition.x, FirstSelectShowChar.BottomLeftPosition.y);p.lineTo(FirstSelectShowChar.TopLeftPosition.x, FirstSelectShowChar.TopLeftPosition.y);return computeRegion(p).contains((int) down_x, (int) down_y);}

关于Path的解释:

你仔细看,就会发现,这些Path连成的区域其实就是上面的黄色区域和蓝色区域。

接下来是确认滑动方向后,进行的取值,简单来说就是不停的去赋值第一个字符或者是最后一个字符。

public void checkSelectForwardText(float down_x, float down_y) {ShowChar moveToChar = searchPressShowChar(down_x, down_y);Log.i("PageView", "moveToChar --" + moveToChar);if (LastSelectShowChar != null && moveToChar != null) {if (moveToChar.BottomLeftPosition.x < LastSelectShowChar.BottomLeftPosition.x|| (moveToChar.BottomLeftPosition.x == LastSelectShowChar.BottomLeftPosition.x&& moveToChar.TopRightPosition.y <= LastSelectShowChar.TopRightPosition.y)) {Log.i("PageView", "我是checkSelectForwardText ------------ ");FirstSelectShowChar = moveToChar;checkSelectText();}}}

public void checkSelectBackText(float down_x, float down_y) {ShowChar moveToChar = searchPressShowChar(down_x, down_y);if (FirstSelectShowChar != null && moveToChar != null) {if (moveToChar.BottomRightPosition.x > FirstSelectShowChar.BottomRightPosition.x|| (moveToChar.BottomRightPosition.x == FirstSelectShowChar.BottomRightPosition.x&& moveToChar.TopRightPosition.y >= FirstSelectShowChar.TopRightPosition.y)) {Log.i("PageView", "我是checkSelectBackText ------------ ");LastSelectShowChar = moveToChar;checkSelectText();}}}

在这里其实有一个问题,如果是单行的话,这个代码逻辑没毛病,如果是多行的话,就会有小瑕疵。

private synchronized void checkSelectText() {Boolean Started = false;Boolean Ended = false;//清空之前滑动的数据mSelectLines.clear();TxtPage curPage = getCurPage(getPagePos());//当前页面没有数据或者没有选择或者已经释放了长按选择事件,不执行if (curPage == null || FirstSelectShowChar == null || LastSelectShowChar == null) {return;}//获取当前页面行数据List<ShowLine> lines = curPage.showLines;// 找到选择的字符数据,转化为选择的行,然后将行选择背景画出来。for (ShowLine line : lines) {ShowLine selectLine = new ShowLine();selectLine.CharsData = new ArrayList<>();for (ShowChar c : line.CharsData) {if (!Started) {// 定位到行中的字,然后转换成行。 主要是分行if (c.TopLeftPosition.x == FirstSelectShowChar.TopLeftPosition.x && c.TopLeftPosition.y == FirstSelectShowChar.TopLeftPosition.y) {Started = true;selectLine.CharsData.add(c);if (c.TopLeftPosition.x == LastSelectShowChar.TopLeftPosition.x && c.TopLeftPosition.y == LastSelectShowChar.TopLeftPosition.y) {Ended = true;break;}}} else {if (c.TopLeftPosition.x == LastSelectShowChar.TopLeftPosition.x && c.TopLeftPosition.y == LastSelectShowChar.TopLeftPosition.y) {Ended = true;if (selectLine.CharsData != null || !selectLine.CharsData.contains(c)) {selectLine.CharsData.add(c);}break;} else {selectLine.CharsData.add(c);}}}if (selectLine != null) {mSelectLines.add(selectLine);}Log.i("PageLoaderSelect", "选择字体是 --- " + mSelectLines);if (Started && Ended) {return;}}}

选择完数据之后,主要是把分行数据进行区分。

private void drawMoveSelectText() {if (mSelectLines != null && mSelectLines.size() > 0) {for (ShowLine line : mSelectLines) {Path path = new Path();if (line.CharsData.size() > 0) {Log.i("PageLoaderSelect", "draw-------------move------------select------------text");ShowChar firstChar = line.CharsData.get(0);ShowChar lastChar = line.CharsData.get(line.CharsData.size() - 1);path.moveTo(firstChar.TopLeftPosition.x, firstChar.TopLeftPosition.y);path.lineTo(lastChar.TopRightPosition.x, lastChar.TopRightPosition.y);path.lineTo(lastChar.BottomRightPosition.x, lastChar.BottomRightPosition.y + 10);path.lineTo(firstChar.BottomLeftPosition.x, firstChar.BottomLeftPosition.y + 10);path.lineTo(firstChar.TopLeftPosition.x, firstChar.TopLeftPosition.y);canvas.drawPath(path, mSelectBgPaint);drawBorderPoint();}}}}

OK。

有几个小问题

1.事件处理的逻辑没有写太多

2.Mode的变化刷新onDraw

3.电子书的数据在线数据的格式,以及对应的业务

其实第三个问题,真的是把我给搞崩溃了,其实现在看来,如果要做批注或者笔记的功能,单纯的文本TXT根本不适合,非要做的话,只能写很多很多逻辑代码。 以后可以考虑一下用HTML,就像Epub一样的格式,对批注这些功能是比较友好的。

PS:如果有什么问题,欢迎文章下面补充 。

微信公众号:SuperMaxs

如果感觉文章对您有帮助 ,可以关注我的公众号 SuperMaxs (如果有技术问题可以通过公众号加私人微信)。

星球了解:/yJ2fq3z

--------------------------------------------------------------------------------------------------

04月13日 。不好意思,我又来还债了(有好几个老哥加我好友请教源码 ,很尴尬给大家带来不便请谅解) 。

之前一直没有放源码 。

PS:附上源码链接 (真正自己用的话需要自己在优化 ,因为已经没有项目源码 ,换工作了,所以我也是看着这篇文章写的)。

/spuermax/SuperTest

给大家带来不便请谅解。

如果觉得《Android 电子书功能实现 长按选中 高亮显示。 TXT》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。