【移动应用开发技术】Android怎样写一款书籍阅读器_第1页
【移动应用开发技术】Android怎样写一款书籍阅读器_第2页
【移动应用开发技术】Android怎样写一款书籍阅读器_第3页
【移动应用开发技术】Android怎样写一款书籍阅读器_第4页
【移动应用开发技术】Android怎样写一款书籍阅读器_第5页
已阅读5页,还剩18页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

【移动应用开发技术】Android怎样写一款书籍阅读器

在下给大家分享一下Android怎样写一款书籍阅读器,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!一款书籍阅读器,需要以下功能才能说的上比较完整:文字页面展示,即书页;页面之间的跳转动画,即翻页动作;能够在每一页上记录阅读进度,即书签;能够自由选择文字并标注,即笔记;能够设置一些属性,如屏幕亮度,字体大小,主体颜色等,即个性化设置。书籍阅读器这篇文章带来的就是如何打造这么一款阅读器。(由于整体代码量比较大,所以我只能说说我的实现思路再加上部分的核心代码来说明,不会有太多的代码展示。)翻页动作——搭建整个阅读器的框架在阅读器上的翻页动作无外乎仿真和平移这两种动画,翻页时需要准备两张页面,一张是当前页,另一张是需要翻转的下一页。翻页的过程就是对这两个页面的剪辑。这里就不赘述翻页的原理了(仿真翻页可以由贝塞尔曲线计算坐标绘制实现,平移翻页则是简单坐标平移变化),这里提供一些参考链接。Github上的PageFlip库现在要做的就是将翻页动作与View结合起来,我们新建一个PageAnimController内部实现翻页动画和动画切换,同时设置PageCarver来监听翻页动作,目的是为了能够让view检测到翻页动作。

public

interface

PageCarver

{

void

drawPage(Canvas

canvas,

int

index);//绘制页内容

Integer

requestPrePage();//请求翻到上一页

Integer

requestNextPage();//请求翻到下一页

void

requestInvalidate();//刷新界面

Integer

getCurrentPageIndex();//获取当前页

/**

*

开始动画的回调

*

*

@param

isCancel

是否是取消动画

*/

void

onStartAnim(boolean

isCancel);

/**

*

结束动画的回调

*

*

@param

isCancel

是否是取消动画

*/

void

onStopAnim(boolean

isCancel);

}新建BaseReaderView作为阅读器的基础视图,两者结合以便控制阅读器的翻页效果。public

abstract

class

BaseReaderView

extends

View

implements

PageAnimController.PageCarver{

/**

*

将View的绘制事件传送给

PageAnimController

实现动画绘制过程中

*

@param

canvas

*

@return

*/

@Override

protected

void

onDraw(Canvas

canvas)

{

if

(pageAnimController

==

null

||

!pageAnimController.dispatchDrawPage(canvas,

this))

{

drawPage(canvas,

currentPageIndex);

}

}

/**

*

将View的触摸事件传送给

PageAnimController

以便实现翻页动画

*

@param

event

*

@return

*/

@Override

public

boolean

onTouchEvent(MotionEvent

event)

{

pageAnimController.dispatchTouchEvent(event,

this);

return

true;

}

}但是在翻页动画中是需要无数次的调用drawPage来绘制界面的,为了减少界面计算的开支必须要有一个Bitmap缓存来降低消耗。复用时可以直接使用已经生成的bitmap./**

*

<p>

*

页面快照,用来存储阅读器每一页的内容

*

*

@author

cpacm

2017/10/9

*/

public

class

PageSnapshot

{

private

int

pageIndex;

private

Bitmap

mBitmap;

private

Canvas

mCanvas;

public

Canvas

beginRecording(int

width,

int

height)

{

if

(mBitmap

==

null)

{

mBitmap

=

Bitmap.createBitmap(width,

height,

Bitmap.Config.ARGB_4444);

mCanvas

=

new

Canvas(mBitmap);

}

else

{

mCanvas.drawColor(Color.TRANSPARENT,

PorterDuff.Mode.CLEAR);

}

return

mCanvas;

}

public

void

draw(Canvas

canvas)

{

if

(null

!=

mBitmap)

{

canvas.drawBitmap(mBitmap,

0,

0,

null);

}

}

public

void

destroy()

{

if

(mBitmap

!=

null

&&

!mBitmap.isRecycled())

{

mBitmap.recycle();

mBitmap

=

null;

}

}

}基础模型如下图所示:页面切换模型现在我们来总结一下,这一部分我们搭建了阅读器最基础的框架,包括(1)翻页动画与阅读器视图的结合,能够确保在View中正确监听翻页动作,保证整个翻页动作的准确性。(2)利用Bitmap缓存优化绘图流程,保证翻页动画的流畅性。而后包括文字,图片等元素的显示都是绘制在这个Bitmap上的。书页——组合模式,保证阅读器高度可定制化阅读器模块图一般来说,阅读器获取数据都是一章一章来的,不管是从网络上还是本地。而获取过来的数据阅读器要进行分页才能展示。如上图所示,书页展示由PageElement模块负责,该模块接收从BookReaderView传入的章节数据,然后再经底下的4个模块计算来分页。分页模块PageElement,分页模块:功能包括将传入的章节数据分成数个PageData(生成的PageData个数即为该章节页数,PageData记录了每一页开头文字在章节的位置,同时包含该页面HeaderData,LineData,HeadrData和FooterData数据等。各个Data里面记录了相应的文字信息,可以快速的定位到章节内容中。);绘制页面;缓存章节数据以便无缝切换章节。HeaderElement,页头部分:显示章节的标题;绘制每一页的头部。LineElement,文字行部分:测量一行文字需要的字数;测量行高;绘制行文字;绘制笔记内容;测量每一个字在屏幕中的位置,用于笔记功能;ImageElement,图片部分:测量图片的宽高;绘制图片。FooterElement,页尾部分:绘制每一页的页尾,包括进度,时间和电量。

//摘自

PageElement

onDraw

方法

@Override

public

void

draw(Canvas

canvas)

{

int

index

=

drawPageIndex

-

startPageIndex;

if

(index

<

0

||

index

>=

pages.size())

return;

BookPageData

bookPageData

=

pages.get(index);

int

offsetX

=

bookSettingParams.paddingLeft;

int

offsetY

=

bookSettingParams.paddingTop;

if

(bookPageData

==

null)

return;

canvas.drawColor(bookSettingParams.getBgColor());

bookHeaderElement.setChapterTitle(bookPageData.getChapterName());

bookHeaderElement.setX(offsetX);

bookHeaderElement.setY(offsetY);

if

(bookPageData.isChapterFirstPage())

{

bookHeaderElement.drawFirstPage(canvas);

}

else

{

bookHeaderElement.draw(canvas);

}

bookFooterElement.setProgress(bookPageData.getPageIndex(),

bookPageData.getPageNums());

bookFooterElement.setX(offsetX);

bookFooterElement.setY(offsetY

+

getHeight()

-

bookFooterElement.getHeight());

bookFooterElement.draw(canvas);

for

(int

i

=

0;

i

<

bookPageData.getDataList().size();

i++)

{

BookData

bookData

=

bookPageData.getDataList().get(i);

if

(bookData

instanceof

BookLineData)

{

BookLineData

bookLineData

=

(BookLineData)

bookData;

bookLineElement.setLineText(bookLineData.getContent());

bookLineElement.setX(bookLineData.getPosition().x);

bookLineElement.setY(bookLineData.getPosition().y);

bookLineElement.drawWithDigests(canvas,

bookLineData,

bookReaderView.getCurrentDigests(index));

//bookLineElement.draw(canvas);

}

else

if

(bookData

instanceof

BookImageData)

{

BookImageData

bookImageData

=

(BookImageData)

bookData;

bookImageElement.setX(bookImageData.getPosition().x);

bookImageElement.setY(bookImageData.getPosition().y);

bookImageElement.syncDrawWithinBitmap(canvas,

bookImageData,

bookReaderView.getCacheBitmap(drawPageIndex));

}

}

}将书页分成几部分组合起来可以有效的减少代码的耦合,而且可以自由的控制每一部分的修改,添加和移除。比如当以后我想要加个批注的功能,可以再添加一个新的Element,再复写其测量方法和绘制方法,就可以很方便的使用了。总结一下:(1)PageElement利用各个Element模块将章节数据进行测量分页,每一页PageData记录着LineData,ImageData,HeaderData和FooterData信息。绘图时需要将各个信息填入Element中(2)绘图时调用PageElement的draw方法,其draw方法再调用各个Element的draw方法以完成整个绘图流程。另外还需要提到的一点是阅读器内部维护了一个书页的队列,该队列缓存了由三个章节数据转化而来的书页列表。比如说你正在阅读第六章,那么队列里面缓存的就是第五章,第六章和第七章的数据,这样就能实现上下章翻页的无缝切换而不需要在翻至下一章时因为等待新的章节数据加载而中断整个阅读体验。/**

*

<p>

*

章节缓存构成方案如下:

*

|

-6,-5,-4,-3,-2,-1,0

|

1,2,3,4,5,6,7,8,9

|

10,11,12,13,14,15

|

=

pages

*

|

cacheChapter1

|

cacheChapter2

|

cacheChapter3

|

*

startPageIndex

=

pageIndex:-6

endPageIndex

=

pageIndex:16

*

currentChapterStartIndex

=>

pageIndex:1

=>

pages[7]

*

currentChapterEndIndex

=>

pageIndex:10

=>

pages[16]

*

</p>

*/书签,笔记——记录阅读进度书签书签的本质就是记录当前页的第一个文字在整章文本的位置,然后再加上书籍的id,章节的id(或序号)就能准确定位。笔记要记录笔记就需要文字选择器来选择文字,这个时候就需要知道每一个字在当前的坐标位置(之前用LineElement测量文字时已经生成每个文字的位置)。为了达到上图的效果,就必须要处理在当前页的触摸事件:文字选择流程有些细节的处理没有放到流程中,但大致意思是能明白的//

TextSelectorElement

上的触摸分发方法

public

boolean

dispatchTouchEvent(final

MotionEvent

ev)

{

int

key

=

ev.getAction();

currentTouchPoint.set(ev.getX(),

ev.getY());

switch

(key)

{

case

MotionEvent.ACTION_DOWN:

isPressInvalid

=

false;

hasConsume

=

true;

isDown

=

true;

mTouchDownPoint.set(ev.getX(),

ev.getY());

//

该方法中会记录isBookDigestDown的值

checkIsPressDigests(ev.getX(),

ev.getY());

//判断是否处于选择模式

if

(!isSelect)

{

if

(isBookDigestDown

==

0)

{

postLongClickPerform(0);//提交长按时间

}

}

else

{

//

判断是否触摸到选择光标上,若是则可以拖动光标移动

checkCurrentMoveCursor(ev);

}

break;

case

MotionEvent.ACTION_MOVE:

float

move

=

PointF.length(ev.getX()

-

mTouchDownPoint.x,

ev.getY()

-

mTouchDownPoint.y);

if

(move

>

moveSlop)

{

isPressInvalid

=

true;

}

if

(isPressInvalid)

{

removeLongPressPerform();

if

(isSelect)

{

//

关闭弹窗(包括笔记编辑框等)

onCloseView();

//

移动光标

onMove(ev);

}

else

{

//未处于选择模式下,相当于一个普通的点击事件

onPress(ev);

}

}

break;

case

MotionEvent.ACTION_UP:

hasConsume

=

false;

removeLongPressPerform();

if

(isSelect)

{

//

-1

表示为未触摸到光标

if

(moveCursor

==

-1)

{

//

取消选择模式

setSelect(false);

hasConsume

=

true;

}

else

{

//停止移动时,会打开笔记生成弹框

onOpenDigestsView();

}

moveCursor

=

-1;

}

else

{

if

(isBookDigestDown

==

1)

{

onOpenNoteView();

hasConsume

=

true;

}

else

if

(isBookDigestDown

==

2)

{

onOpenEditView();

hasConsume

=

true;

}

else

{

//

模拟成一个普通的点击事件,会取消当前的选择模式

onPress(ev);

}

}

invalidate();

break;

case

MotionEvent.ACTION_CANCEL:

hasConsume

=

false;

removeLongPressPerform();

break;

default:

break;

}

//

判断选择器是否消耗了当前事件

return

hasConsume

||

isSelect;

}当然,笔记也要记录当前选择的书籍id,章节id(或序号),文字在章节中的位置这些信息,方便定点跳转。设置——为阅读器添砖加瓦阅读器设置界面阅读器的设置一般包括:界面亮度的调整,字体大小的调整,上下章的跳转,书籍目录笔记和书签的展示,翻页动画的更改,日夜主题的更改。当一些设置需要阅读器能够在参数变化时及时响应,就得需要在设置变化时能及时更新BookReaderView下的各个Element模块。这里我是通过一个辅助类贯穿整个阅读器来帮助更新各个模块,该类记录了阅读器内部所有可设置的属性,当各个模块被通知需要更新时重新从该类中读取参数并设置(比如画笔的颜色,页面的间距,字体的大小等)。//

摘自

PageElement

下的设置属性变化方法

//

BookSettingParams

即为记录阅读器设置属性的辅助类

@Override

public

void

update(ReaderSettingParams

params)

{

bookSettingParams

=

(BookSettingParams)

params;

bookHeaderElement.update(bookSettingParams);

bookFooterElement.update(bookSettingParams);

bookLineElement.update(bookSettingParams);

bookImageElement.update(bookSettingParams);

initPageElement();

}语音朗读——为阅读器添加辅助功能语音朗读此处的语音朗读使用的是讯飞的TTS引擎。如何使用引入TTS我这里就不具体描述了,重要的是在TTS的onSpeakProgress(intprogress,intbeginPos,intendPos)方法中可以获取当前句子的朗读进度。当我们传入一章文字时,TTS会自动帮助我们分段(会以,。等标点符号切割整篇文字),然后按段落来进行朗读。上面progress代表该段落在整篇文字的进度,beginPos代表该段落的起始字符在整篇文字的位置,endPos代表该段落的末尾字符在整篇文字的位置。既然能够知道朗读的位置,那就能知道朗读时文字在屏幕的位置了(之前有说过LineData记录了每个字符在屏幕中的位置),那剩下的就是怎么绘制的问题了。/**

*

<p>

*

听书tts播放模组

*

*

@author

cpacm

2017/12/13

*/

public

class

BookSpeechElement

extends

ResElement

implements

SynthesizerListener

{

//

省略部分代码

//

从每一页数据

PageData

中的

LineData

列表中获取要绘制的区域

private

void

updateDrawRect(int

startPos,

int

endPos)

{

if

(endPos

<=

offsetPosition

||

endPos

==

this.endPos)

return;

this.endPos

=

endPos;

this.tempPos

=

startPos;

int

s

=

this.startPos

+

startPos

+

bookPageData.getStartPos()

-

offsetPosition;

int

e

=

this.startPos

+

endPos

+

bookPageData.getStartPos()

-

offsetPosition;

drawRect.clear();

for

(BookLineData

line

:

lineData)

{

if

(line.startPos

>

e

||

line.endPos

<=

s)

continue;

if

(line.startPos

<=

s

&&

line.endPos

<=

e)

{

Rect

startRect

=

line.getCharArea().get(s);

Rect

endRect

=

line.getCharArea().get(line.endPos

-

1);

Rect

rect

=

new

Rect(startRect.left,

startRect.top,

endRect.right,

endRect.bottom);

drawRect.add(rect);

}

if

(line.startPos

>

s

&&

line.endPos

<=

e)

{

Rect

startRect

=

line.getCharArea().get(line.startPos);

Rect

endRect

=

line.getCharArea().get(line.endPos

-

1);

Rect

rect

=

new

Rect(startRect.left,

startRect.top,

endRect.right,

endRect.bottom);

drawRect.add(rect);

}

if

(line.startPos

>

s

&&

line.endPos

>

e)

{

Rect

startRect

=

line.getCharArea().get(line.startPos);

Rect

endRect

=

line.getCharArea().get(e);

Rect

rect

=

new

Rect(startRect.left,

startRect.top,

endRect.right,

endRect.bottom);

drawRect.add(rect);

}

if

(line.startPos

<=

s

&&

line.endPos

>

e)

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论