一个最简单的小说阅读器,也离不开文本的显示。起初,我以为这是件十分容易完成的事,慢慢的,我才意识到其中的复杂性。很多时候,对于文本的显示,一个文本框便能解决。但是,兼顾着排版与分页等复杂的功能,常用的UI控件就显得力不从心了。为了实现这些较为特殊的功能,就需要通过自定义View来解决。本文将从认识View的概念讲起。

View的概念

  我们对一个应用最直观的印象,就是其使用界面,而界面又由一个或多个控件构成。事实上,我们在手机屏幕上所看到的一切元素,都是View的实例,更本质上讲,都是View所描绘出的一个个像素点。View是以矩形的方式显示在屏幕上,View是用户界面控件的基础。一行文字、一个按钮、一张图片,这些看似整体却又相互独立的元素,可以当作View在屏幕上的展示。

从安卓开发文档上可以看到,View的父类是Object类,而子类则包含了比 悉的ImageView、Button、TextView等等。因此,屏幕上呈现在我们眼前的种种元素,都可以抽象成对象。万物皆对象,而对象就有属性。要想更准确的理解View,就不可避免的直面官方的介绍:

  这个类表示用户界面组件的基本构造模块,一个View 在屏幕上占据了一块矩形区域,并负责绘图和事件处理。View是窗口小部件的基类,用于创建交互式UI组件(按钮、文本字段等)。ViewGroup子类是布局的基类,其是不可见的容器,包含着其他View(或其他ViewGroup),并定义它们的布局属性。

  View的绘制流程是从ViewRoot的performTraversals方法开始的,包含了测量、布局和绘图三个过程,分别是measure、layout和draw。其基本的设计思想是先测量视图的大小,接着设置视图的位置,即视图在屏幕上坐标,最后在所设定的区域描绘出所需的图形。具体的作用如下:

  • measure:判断是否需要重新计算View的大小,需要的话则计算;
  • layout:判断是否需要重新计算View的位置,需要的话则计算;
  • draw:判断是否需要重新绘制View,需要的话则重绘制。

自定义View

  安卓的开发内容各式各样,内置的UI控件往往不能满足我们的需求,正如我们的小说阅读器项目一样,普通的文本框已经无法实现排版和分页的功能,因此,自己定制一个UI控件就成了当务之急。安卓开发也提供可这种方法,允许我们根据自己的需求定义一个UI控件,这便是自定义View。自定义View并不复杂,一个最简单的自定义View需要重写onMeasure()、onDraw()两个函数,onMeasure负责对当前View的尺寸进行测量,onDraw负责把当前这个View绘制出来。完整的自定义Viewch程序还需要写至少写2个构造函数:

public MyView(Context context) {super(context);}public MyView(Context context, AttributeSet attrs) {super(context, attrs); }@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);}

重写onMeasure方法

  为什么要重写onMeasure方法?重写onMeasure方法又有什么用处呢?我刚开始接触的时候也并不是很懂。回想一下,在xml布局文件中,我们在设置控件的layout_width和layout_height属性时,常常使用wrap_content或match_parent作为参数值,而非具体的数值。这是由于要满足不同手机屏幕尺寸的需求,控件的大小不能写死,应具有一定的弹性。wrap_content的作用是强制性地使视图扩展以显示全部内容,布局元素将根据内容更改UI控件的大小。match_parent则强制性地使控件扩展,以填充布局单元内尽可能多的空间。
  当我们设置布局为wrap_content时,自定义控件并不能为我们处理大小,这时就需要重写onMeasure方法,并在该方法中测量控件大小的具体数值。onMeasure方法提供了widthMeasureSpec和 heightMeasureSpec两个参数,除了带有具体的大小数值外,还携带了布局的模式信息,即UNSPECIFIED,AT_MOST,EXACTLY三种模式,分别对应布局中的wrap_content、match_parent和指定数值。在这里,我们主要是处理UNSPECIFIED模式下的大小,即对具体内容的测量。
小说阅读器重写onMeasure方法的具体代码如下:

@SuppressLint("DrawAllocation")@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int widthMode = MeasureSpec.getMode(widthMeasureSpec);int widthSize = MeasureSpec.getSize(widthMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);paddingLeft = getPaddingLeft();paddingTop = getPaddingTop();paddingRight = getPaddingRight();paddingBottom = getPaddingBottom();viewWidth = widthSize;viewHeight = heightSize;readWidth = viewWidth - paddingLeft - paddingRight;readHeight = viewHeight - paddingTop - paddingBottom;setMatrix();getStrData(eBook);int width;int height ;if (widthMode == MeasureSpec.UNSPECIFIED) {width = textWidth;} else {width = widthSize;}if (heightMode == MeasureSpec.UNSPECIFIED) {height = textHeight;} else {height = heightSize;}setMeasuredDimension(width, height);}

重写onDraw方法

  重写onDraw方法比较好理解,我们要在这里把UI控件的内容绘制出来,可以是文本,可以是图形,当然也可以是图片。可以把Canvas当作画布,Paint当作画笔,而我们程序员就是画家,手敲代码就如同手持画笔,双手灵活的在其中作画,灵感所在而随心所欲。在这里我才深切的感受到自定义的真谛,完全可以由需求来定制,不局限于任何限制。
  Canvas提供了几种绘制方法,可以满足大部分的需求:

  • drawLine(s)画直线:前四个参数为直线的起点和终点的 XY 轴坐标。
  • drawRect画矩形:确定矩形四个顶点的位置配上画笔即可。
  • drawText画文本:在 x,y 位置开始画文本其中 y 表示文字的基线(baseline)所在的坐标,而 x坐标就是文字绘制的起始水平坐标,但是每个文字本身两侧都有一定的间隙,故实际文字的位置会比 x 的位置再偏右侧一些。
  • drawBitmap画图片bitmap:要画在画布上的位图,matrix:构建的矩阵作用于将要画出的位图。
  • drawArc画圆弧userCenter 若为true表示此弧会和 RectF 中心相连形成扇形,否则,弧的两头直接相连形成图形。startAngle,负数或大于360则对360模除。sweepAngle,大于360,则画出一圈。角度:以 RectF 中心为坐标中心,中心所在直线为水平线,负角度弧斜上走,正角度弧斜下走,或者说以时钟三点钟为0度,顺时针为正,逆时针为负。
  • drawCircle画圆cx,cy 为所画圆的中心坐标,radius 为圆的半径。当画笔设置了 StrokeWidth 时,圆的半径=内圆的半径+StrokeWidth/2。
  • drawColor,drawRGB画颜色:画整个画布的背景,但若区域受到剪裁,则只绘制剪裁区域的背景。
  • drawOval画椭圆:绘制椭圆,类似drawRect。

  由于小说阅读器要实现页面的排版,根据需要可设置为左对齐、右对齐和两端对齐,我的解决方案是对所有文字进行单独绘制,根据文字的宽度设置对应得坐标,设置对齐方式时,微调其坐标位置即可,而不需要做太大的改动。因为每个文字是单独绘制的,可以十分容易的调整其字间距、行间距以及段与段之间的距离。
  小说阅读器重写onDraw方法的具体代码如下:

@SuppressLint("DrawAllocation")@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);PageModel page = chapterModel.getPageModels().get(chapterModel.getIndex());canvas.drawBitmap(bitmap,matrix,mPaint);for(int i = 0;i<page.getLineModels().size();i++){LineModel line= page.getLineModels().get(i);int num =line.getStringList().size();float spacing;if(num == 0){spacing = 0;}else {spacing = line.getStrDiff()/(float)(num-1);}for (int j=0;j<num;j++){mPaint.setColor(line.getStrColors().get(j));canvas.drawText(line.getStringList().get(j), line.getStrX().get(j)+ paddingLeft + j*spacing,(i + 1) * fontSize * 1.5f + paddingTop - 4, mPaint);}}}

自定义布局属性

  在UI控件的使用过程中,我们通常可以通过改变属性值来改变控件的状态,自定义View也一样,为我们提供了一种自定义布局属性的方法。自定义View的构造函数中,提供了带有布局属性的参数,不过在获取这些参数之前,需要在res目录中的values文件夹下新建一个attrs.xml文件。本例中我定义的attrs.xml文件内容如下所示,包含了颜色、字体大小、文本内容、背景颜色或图片等属性。
  值得注意的是,format指定的参数,具有特殊的含义,具体内容如下,使用时需要一一对应,以免出错:

  • reference: 表示引用,参考某一资源ID;
  • string: 表示字符串;
  • color: 表示颜色值;
  • dimension: 表示尺寸值;
  • boolean: 表示布尔值;
  • integer: 表示整型值;
  • float: 表示浮点值;
  • fraction: 表示百分数;
  • enum: 表示枚举值;
  • flag: 表示位运算。

  本例新建的attrs.xml文件内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="ReadView"><attr name="color" format="color"/><attr name="fontSize" format="dimension"/><attr name="text" format="string"/><attr name="background" format="reference"/><attr name="type" format="enum"><enum name="common" value="1"/><enum name="material" value="2"/></attr><attr name="flag"><flag name="flag1" value="0x01"/><flag name="flag2" value="0x02"/><flag name="flag4" value="0x04"/></attr></declare-styleable>
</resources>

获取属性值代码如下所示:第二个参数是属性的默认值,当在xml文件中不使用该属性时,系统会获取到默认值,做默认处理。

TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ReadView);//获取字体大小,默认大小是24fontSize = (int) ta.getDimension(R.styleable.ReadView_fontSize, 24);//获取文字内容eBook = ta.getString(R.styleable.ReadView_text);//获取文字颜色,默认颜色是BLUEtextColor = ta.getColor(R.styleable.ReadView_color, Color.BLACK);//获取背景background = ta.getResourceId(R.styleable.ReadView_background,R.drawable.paper);ta.recycle();

文本的排版与分页

  文本的排版与分页,是小说阅读器重点解决的问题。排版的引证解释是指按照稿本把铅字、图版等排在一起拼成书报的版子,以供印刷。分页更好理解,是将一本书或者一个章节,按照一定的版面,一张一张的剥离开来。排版与分页看似不同,却基本原理却相差无几。由于这是一个小说阅读器的开发,暂时不考虑图片的显示问题,所有排版均针对文本而言。因此主要体现在三个方面,首行缩进两个字符,自动换行以及文本的两端对齐。至于字间距、行间距、甚至段间距,也可以做相应的调整。
  文本的排版方案,我的思路是首先解决的是文本的自动换行问题,这需要测量字符的宽度,通过累加字符的宽度,然后对比控件宽度,大于时或遇到换行符时就切换下一行。由于每个字符需要单独绘制,这就需要设置每个字符的坐标,这里也比较容易解决,在累加字符宽度时,加入些字间距调整,就是文本在画布上的坐标。至于首行缩进问题,就更简单了,只要判断是段落开始时,加入两个空格符即可。还有一个问题,是解决因为半角全角符号、中英文混排所造成的,文本不对齐现象。我的解决办法是通过计算每行文字的宽度与控件宽度的差值,然后平均加到每个字符的横坐标上作为补充,使得每一行的首尾宽度一致,实现了文本的两端对齐。

分页也一样,通过累加字符的高度值和行间距,然后对比控件的高度值,就可以准确的分出每一个页面来。为了提高效率,控件应该减少对大文件的处理,因此该小说阅读器只针对章节进行排版分页。在绘制文本时,出现了比较明显的锯齿而不清晰,刚开始我并不知道什么问题,最后通过加入mPaint.setAntiAlias(true)解决,该函数是用来防止边缘的锯齿。

private void getStrData(String str){readTool.init();readTool.setStrCaptal(fontSize,textColor);int lineWidth = 2*fontSize;for(int i=0;i<str.length();i++){String subStr;if(i < str.length()-1){subStr  = str.substring(i, i + 1);}else {subStr = str.substring(i);}int fontWidth = (int)mPaint.measureText(subStr);lineWidth = lineWidth + fontWidth;if (subStr.equals("\n")){readTool.addPage(readHeight,fontSize);readTool.addLine(0);readTool.setStrCaptal(fontSize,textColor);lineWidth = 2*fontSize;}else if(lineWidth < readWidth){readTool.addStrArr(subStr,fontWidth,lineWidth-fontWidth,textColor);}else{readTool.addPage(readHeight,fontSize);readTool.addLine(readWidth-lineWidth+fontWidth);lineWidth = fontWidth;readTool.addStrArr(subStr,lineWidth,0,textColor);}}readTool.addEnd(readHeight,fontSize);lineWidth = 0;chapterModel.setPageModels(readTool.getPageModels());lineNum = readTool.getLineModels().size();if (lineNum > 1){textWidth = getWidth();}else {textWidth = lineWidth;}textHeight = lineNum * (fontSize+lineHeight);}

设置背景图片时,由于缩放的缘故,图片十分不清晰。查阅相关资料后,我是通过矩阵Matrix的坐标映射和数值转换来解决。实际上不论2D还是3D,我们要将图形显示在屏幕上,都离不开Matrix,所以说Matrix是一个在背后辛勤工作的劳模。Matrix是一个矩阵,最根本的作用就是坐标转换,其基本原理是:

我们所用到的变换均属于仿射变换,仿射变换是线性变换(缩放,旋转,错切)和平移变换(平移) 的复合。
仿射变换概念:仿射变换其实就是二维坐标到二维坐标的线性变换,保持二维图形的“平直性”(即变换后直线还是直线不会打弯,圆弧还是圆弧)和“平行性”(指保持二维图形间的相对位置关系不变,平行线还是平行线,而直线上点的位置顺序不变),可以通过一系列的原子变换的复合来实现,原子变换就包括:平移、缩放、翻转、旋转和错切。这里除了透视可以改变z轴以外,其他的变换基本都是上述的原子变换,所以,只要最后一行是0,0,1则是仿射矩阵。

 private void setMatrix(){float bitmapWidth = bitmap.getWidth();float bitmapHeight = bitmap.getHeight();float scaleX = viewWidth / bitmapWidth;float scaleY = viewHeight / bitmapHeight;matrix = new Matrix();matrix.postTranslate(0, 0);matrix.preScale(scaleX, scaleY);}

文本的排版与分页效果图:

小说阅读器开发(二)文本的排版与分页相关推荐

  1. 小说阅读器开发笔记(二)文本的排版与分页

      一个最简单的小说阅读器,也离不开文本的显示.起初,我以为这是件十分容易完成的事,慢慢的,我才意识到其中的复杂性.很多时候,对于文本的显示,一个文本框便能解决.但是,兼顾着排版与分页等复杂的功能,常 ...

  2. 基于python简易小说阅读器(二)

    基于python简易小说阅读器(二)   在基于python简易小说阅读器(一)中,用requests模块和beautifulsoup模块完成了阅读器的后台,实现了下载小说内容的功能,现在用tkint ...

  3. ❤️一文掌握HTML+CSS+JS开发小说阅读器❤️

    上周<让CSS3中Transform属性带你一文实现炫酷的转盘抽奖效果>博文中说到这周出一篇介绍小说阅读器开发的博文,可能是离职不上班的原因,在家变得也懒散了一些,本来是打算上周三时候动手 ...

  4. json阅读器_Flutter小说阅读器系列一:使用Bloc模式获取起点小说关键字提示

    Bloc模式下的小说关键字提示效果图 最近难得有些闲暇时间,所以我又打算做一个小说阅读器,以前倒是用RN+Golang写了一个,不过当时太过放飞自我导致自己看起来都很费力,这次我准备换成Flutter ...

  5. 套路继续, .txt 小说阅读器功能开发

    1, 解决一个 bug 正文结尾 (最后一行最后一个字)跟右边界, 有多余的空白间隔 Core Text 的渲染流程,就是富文本绘制 从流程上看, 感觉这一页的文字分配少了,给他加点字,就满了 // ...

  6. 免费小说阅读器(Android版本)全站开源

    此小说阅读器只追求两项 极简(无广告,无添加) 丰富(内容丰富,只有你想不到的,没有它没有的) 漫品客户端 全站开源 开源地址: https://github.com/AnyMarvel/ManPin ...

  7. Web阅读器开发系列教程(入门篇)

    作者:Sam 前言 最近我在慕课网发布了两门关于Web阅读应用开发的课程,采用Vue全家桶开发.免费课是入门级课程,初步实现了一个阅读器.实战课是进阶课程,实现了一个高性能的互联网阅读应用.两个项目都 ...

  8. 基于Python(Tkinter)实现(图形界面)小说阅读器【100010450】

    计算机网络 Project-小说阅读器 一.概述 本文为 2019 秋计算机网络课程 Socket 编程实验报告,我选择了小说阅读器作为实现对象.本节主要阐述任务要求.项目概述及文章框架. 1.1 任 ...

  9. reader小说阅读器 v1.9.2.0

    Duang~天空一声巨响,我就是那传说中的reader小说阅读器,我乃吾爱破解沈大场开发的简单易于使用的小说阅读器,支持缩小界面,可以让你上班摸鱼看小说,有多种按键,有助于用户更好的浏览阅读,用户只需 ...

最新文章

  1. JVM: G1和CMS的区别
  2. B2B 企业如何高效获客增长?
  3. OpenCV watershed分水岭分割算法的实例(附完整代码)
  4. 后台审核管理 ergo_Kogito,ergo规则—第2部分:规则的全面执行模型
  5. 制造业数据分析存在哪些问题
  6. [编程题]手机屏幕解锁模式
  7. [转]Http Message结构学习总结
  8. 线性基——数集压缩自动机
  9. getComputedStyle
  10. Tent-Logistic-Cosine混沌映射(提供参考文献及Matlab代码)
  11. 安装Node.js,系统提示User installations are disabled via policy on the machine
  12. 余承东吐槽iPhone X长的丑体验差;雷军称小米明年要进世界500强;特斯拉股价被指太荒唐丨价值早报
  13. 北大韦神等十人获奖,均分1000万元,达摩院2021青橙奖出炉
  14. 多媒体——视频——利用视频视图VideoView播放视频
  15. principal argument cannot be null
  16. 查询出每个分组中的 top n 条记录
  17. php 鲜为人知的函数
  18. 前端面试 | JavaScript知识点 | 课程笔记
  19. 英语计算机Internet主题PPT,internet英语课件.ppt
  20. Windows系统下载地址;office下载地址;visio下载地址

热门文章

  1. oracle 无metalink账号补丁下载方法
  2. 记一次记忆深刻的springcloudgateway网关调优
  3. 搭建Eureka高可用集群
  4. CSS3动画之二:Animations功能
  5. winbox基础应用教程
  6. 单片机和cpu的区别
  7. 如何使用GHO镜像安装器安装系统
  8. 飞鸽传书2007 优化的房子原理
  9. C++ 实现小型图书管理系统
  10. Object Detection in 20 Years A Survey-论文翻译(阅读笔记)