摘要:Android自带的文本显示控件TextView往往难以满足排版要求。以两端对齐排版要求为例,实现能够两端对齐的文本显示控件ExTextView,为扩展TextView功能提供了方法和思路。
关键词:Android;文本显示;控件TextView;ExTextView
DOIDOI:10.11907/rjdk.143658
中图分类号:TP301
文献标识码:A 文章编号文章
编号:16727800(2015)001003303
基金项目基金项目:
作者简介作者简介:朱明东(1975-),男,硕士,国防信息学院一系讲师,研究方向为数据处理、数据分析、数据管理。
0 引言
TextView控件在Android开发中应用比较广泛,只要有文本显示要求时,通常都会用到它。但是TexwView控件并不十分完美,它在显示文本时,特别是有中西文混合文本时,往往显得参差不齐、不够工整,影响了排版效果,不能满足“两端对齐”这一中文显示的基本要求。Android实现文本两端对齐显示的基本方法有3种:①将文本转换为html格式,用WebView控件显示;②弃用TexwView控件,重新实现一个具有两端对齐功能的新控件;③在TexwView控件的基础上,扩展实现两端对齐功能。第一种方法需要在文本显示前把文本转换为html格式,这需要程序员对html格式相当熟悉;第二种方法是重新实现一个文本显示控件,需要对Android控件的实现机理有深入研究[1],对程序员的能力要求比较高;第三种方法相对简单,只需在现有控件的基础上,覆盖文本输出方法。本文采用第三种方法,即扩展TextView控件功能。
1 TextView控件机理
要扩展TextView控件功能,首先要对TextView控件的实现机理有一定了解。Android的每一个控件虽然实现起来相当复杂,但除了具体实现细节外,几乎所有的可视控件都包含两个要素:一个是与用户的交互界面(UI),另一个是与用户交互的用户输入事件。TextView作为Android的一个基础控件也不例外,用户界面是通过在画布上绘制UI,用户主要是键盘输入以及触摸屏输入。因此,分析TextView控件的机理就是要搞清控件界面的绘制框架及其输入过程。本文主要关注TextView控件的界面绘制步骤。控件的UI绘制操作通常分为3步,分别是测量、布局和绘制。
1.1 测量
对于一个可视控件,必须确定其所占空间的大小,所以TextView要重写父类View的成员函数onMeasure。该函数有两个参数,分别是用来描述宽度测量规范的widthMeasureSpec和高度测量规范的heightMeasureSpec。测量规范使用1个int值来表示,这个int值包含了2个分量。第1个是mode分量,使用最高2位来表示。测量模式有3种,分别是MeasureSpec.UNSPECIFIED(0)、MeasureSpec.EXACTLY(1)和MeasureSpec.AT_MOST(2);第2个是size分量,使用低30位来表示。当mode分量等于MeasureSpec.EXACTLY时,size分量的值就是父视图设置的宽度或者高度;当mode分量等于MeasureSpec.AT_MOST时,size分量的值就是父视图限定当前控件设置的最大宽度或者高度;当mode分量等于MeasureSpec.UNSPECIFIED时,父视图不限定当前控件所设置的宽度或者高度,这时候当前控件就按照实际需求来设置宽度和高度。
1.2 布局
通过测量后确定了控件的大小,但是控件的位置还未确定。控件的位置是通过布局这个操作来完成的。Android可视控件是按照树形结构组织在一起的,其中,子控件的位置由父控件来设置,也就是说,只有容器类控件才执行布局操作,通过重写父类View的成员函数onLayout来实现。由于TextView控件不是容器类控件,因此,它可以不重写父类View的成员函数onLayout。
1.3 绘制
经过测量和布局操作后,就确定了控件TextView的大小和位置,接下来绘制UI。控件为了绘制UI,必须重写父类View的成员函数onDraw。该函数只有一个参数canvas,canvas描述的是一块画布,控件的UI就是绘制在这块画布上的。画布提供了丰富的接口来绘制UI,例如画线(drawLine)、画圆(drawCircle)、输出文字(drawText)和贴图(drawBitmap)等等。有了这些UI画图接口之后,就可以随心所欲地绘制控件的UI了。
通过分析TextView控件的机理,不难发现,TextView对文本的显示是通过在画布(canvas)上输出文本实现的。因此要在TextView的基础上实现文本的两端对齐,关键是要重新安排每一行的字符数,控制字间距,在TextView的画布(canvas)上精确地输出每一个字符,从而确保每一行的第一个字符和最后一个字符是对齐的。
2 扩展TextView设计
2.1 两端对齐显示的基本要求
要实现文本的两端对齐,表面上是每行的最后一个字符在纵坐标上保持一致,其实还要考虑文本在显示格式上的要求,特别是每一行的第一个字符(行首)和最后一个字符(行尾)是否符合格式规范。需要考虑的格式规范要求有:①行首字符不能是以下字符:句号(。.)、问号(??)、叹号(!!)、逗号(,,)、冒号(::)和分号(;;)和引号(””)、括号())]})、书名号(》>)的后一半等;②行尾字符不能是以下字符:引号(“”)、括号((([{)、书名号(《<)的前一半。
2.2 ExTextView设计[2]
ExTextView继承了Android的TextView,其继承关系如图1所示,ExTextView的类图如图2所示。
ExTextView主要的属性包括文本高度(m_iTextHeight)、文本宽度(m_iTextWidth)、画笔(mPaint)、文本(text)、行间距(LineSpace)、左边距(left_Margin)、右边距(right_Margin)、上边距(top_Margin)、下边距(bottom_Margin)、字体高度(m_iFontHeight)、所有行属性(strings)。
其中,描述行属性的内部类Tlineattr包含3个成员,分别是该行所包含的字符串(linetext)、该行的字间距(extrawidth)和该行每一个字符的输出宽度(widths)。
ExTextView的机理是:确定每一行字符属性的IniLines()方法和覆盖父类的OnDraw()方法。IniLines()方法主要是确定第一行所包含的字符,保证行首字符和行尾字符符合格式规范的要求,其判断逻辑如图3所示。OnDraw()方法主要是根据每一行所包含的字符,确定字间距,保证所有行首字符的水平坐标值一致,所有行尾字符的水平坐标值一致,操作流程如图4所示。
3 扩展TextView实现
3.1 主要代码
ExTextView要能够实现两端对齐,其核心是先要分配每一行的字符,然后重写父类的成员函数onDraw ()。
分配字符的方法IniLines()实现如下:
private void IniLines() {
strings.clear();
String Text = text;//得到要输出的文本
intm_LineWidth = m_iTextWidth - left_Margin - right_Margin; //可输出的画布宽度
float[] widths = new float[Text.length()]; //保存每个字符所占宽度的数组
mPaint.getTextWidths(Text, widths); //得到每个字符输出时的宽度
final String Laststr = "((《“{[<"; //不能是行尾的字符
final String Firststr = "、,。;:)?”,.;:)》?/-]}>"; //不能是行首的字符
float curwidth = 0; //保存当前行所含字符的总的宽度
intstartindex = 0; //行开始的字符位置
intendindex = 0; //行结束的字符位置
String lastch = ""; //上一个字符
Boolean IsCalExtraWidth = false;//是否需要计算字间距
for (inti = 0; i Boolean lineclosed = false; //当前行是否结束 String ch = Text.substring(i, i + 1); //得到当前字符 curwidth = curwidth + widths[i]; //当前行已输出字符的宽度 if (curwidth>m_LineWidth) { //超出了行宽 lineclosed = true;//当前行结束 IsCalExtraWidth = true;//需要计算字间距 if (Firststr.contains(ch)) { //如果当前字符不能为行首,则当前字符为该行的行尾 endindex = i; //当前行结束的字字符位置 curwidth = 0; //下一行的行宽 } else { //当前字符可以为行首 if (Laststr.contains(lastch)) { //上一个字符不能为行尾,则上一个字符为下一行的行首 endindex = i - 2;//当前行的行尾为当前字符的前两个字符 curwidth = widths[i - 1] + widths[i]; //下一行的行宽 } else { //上一个字符可以成为行尾 endindex = i - 1; //当前行的行尾为上一个字符 curwidth = widths[i]; //下一行的行宽 } } lastch = ch; } if (!lineclosed) { if (ch.equals("n") || i == Text.length() - 1) { //或当前字符为换行符或文本最后一个字符,则当前行结束 lineclosed = true; endindex = i; IsCalExtraWidth = false;//不需要计算字间距 curwidth = 0; } } if (lineclosed) { //如果当前行结束 intlen = endindex - startindex + 1;//当前行字符个数 float[] linewidths = new float[len]; //当前行每一个字符的宽度
float linewidth = 0; //当前行所有字符输出宽度之和
for (int j = startindex; j linewidths[j - startindex] = widths[j]; linewidth = linewidth + widths[j]; } float extrawidth = 0;//调整字间距 if (IsCalExtraWidth) { //需要计算调整字间距 extrawidth = (m_LineWidth - linewidth) / len; } stringlist.add(Text.substring(startindex, endindex + 1)); //当前行所包含的字符 strings.add(new Tlineattr(Text.substring(startindex, endindex + 1), extrawidth, linewidths)); startindex = endindex + 1; //下一行的起始字符的位置 } }//结束对字符的历遍 if (endindex != (Text.length() - 1)) { //若上一行的行尾不是文本的最后一个字符,则还剩最后一行 endindex = Text.length() - 1; intlen = endindex - startindex + 1;//当前行字符个数 float[] linewidths = new float[len]; //当前行每一个字符的宽度 float linewidth = 0; //当前行所有字符输出宽度之和 for (int j = startindex; j linewidths[j - startindex] = widths[j]; linewidth = linewidth + widths[j]; } stringlist.add(Text.substring(startindex, endindex + 1)); //当前行所包含的字符 strings.add(new Tlineattr(Text.substring(startindex, endindex + 1), 0, linewidths)); } } 成员函数onDraw()的实现代码如下。 @Override protected void onDraw(Canvas canvas) { float drawx=0; //绘制字符的横坐标 float drawy=m_iFontHeight; //绘制字符的纵坐标 for(inti=0;i drawx = left_Margin; for (int j=0;j String ch=strings.get(i).linetext.substring(j,j+1); //取一个字符 canvas.drawText(ch,drawx,drawy,mPaint); // 绘制当前字符 drawx=drawx+ strings.get(i).extrawidth+strings.get(i).widths[j]; //下一个字符的横坐标 }//结束当前行的循环 drawy=drawy+m_iFontHeight; //下一行字符的纵坐标 }//结束所有行的循环 } 3.2 应用实例 ExTextView控件的使用与TextView控件的使用是一样的,这里不作详细介绍。对于同一段文本,图5是Android自带的TextView显示效果,图6是ExTextView的显示效果。 4 结语 具有两端对齐功能的ExTextView控件,还不十分完善。比如,为防止英文单词被截断,需要加上中英文混合分词技术,等等。本文只是提供了扩展TextView功能一种思路,读者可以沿着这种思路不断扩展,实现所需功能。