赞
踩
一个最简单的小说阅读器,也离不开文本的显示。起初,我以为这是件十分容易完成的事,慢慢的,我才意识到其中的复杂性。很多时候,对于文本的显示,一个文本框便能解决。但是,兼顾着排版与分页等复杂的功能,常用的UI控件就显得力不从心了。为了实现这些较为特殊的功能,就需要通过自定义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。其基本的设计思想是先测量视图的大小,接着设置视图的位置,即视图在屏幕上坐标,最后在所设定的区域描绘出所需的图形。具体的作用如下:
安卓的开发内容各式各样,内置的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);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- }

为什么要重写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")
- @Override
- protected 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方法比较好理解,我们要在这里把UI控件的内容绘制出来,可以是文本,可以是图形,当然也可以是图片。可以把Canvas当作画布,Paint当作画笔,而我们程序员就是画家,手敲代码就如同手持画笔,双手灵活的在其中作画,灵感所在而随心所欲。在这里我才深切的感受到自定义的真谛,完全可以由需求来定制,不局限于任何限制。
Canvas提供了几种绘制方法,可以满足大部分的需求:
由于小说阅读器要实现页面的排版,根据需要可设置为左对齐、右对齐和两端对齐,我的解决方案是对所有文字进行单独绘制,根据文字的宽度设置对应得坐标,设置对齐方式时,微调其坐标位置即可,而不需要做太大的改动。因为每个文字是单独绘制的,可以十分容易的调整其字间距、行间距以及段与段之间的距离。
小说阅读器重写onDraw方法的具体代码如下:
- @SuppressLint("DrawAllocation")
- @Override
- protected 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指定的参数,具有特殊的含义,具体内容如下,使用时需要一一对应,以免出错:
本例新建的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);
- //获取字体大小,默认大小是24
- fontSize = (int) ta.getDimension(R.styleable.ReadView_fontSize, 24);
- //获取文字内容
- eBook = ta.getString(R.styleable.ReadView_text);
- //获取文字颜色,默认颜色是BLUE
- textColor = 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);
- }
文本的排版与分页效果图:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。