摘要:Android自帶的文本顯示控件TextView往往難以滿足排版要求。以兩端對齊排版要求為例,實(shí)現(xiàn)能夠兩端對齊的文本顯示控件ExTextView,為擴(kuò)展TextView功能提供了方法和思路。
關(guān)鍵詞:Android;文本顯示;控件TextView;ExTextView
DOIDOI:10.11907/rjdk.143658
中圖分類號:TP301
文獻(xiàn)標(biāo)識碼:A 文章編號文章
編號:16727800(2015)001003303
基金項(xiàng)目基金項(xiàng)目:
作者簡介作者簡介:朱明東(1975-),男,碩士,國防信息學(xué)院一系講師,研究方向?yàn)閿?shù)據(jù)處理、數(shù)據(jù)分析、數(shù)據(jù)管理。
0 引言
TextView控件在Android開發(fā)中應(yīng)用比較廣泛,只要有文本顯示要求時,通常都會用到它。但是TexwView控件并不十分完美,它在顯示文本時,特別是有中西文混合文本時,往往顯得參差不齊、不夠工整,影響了排版效果,不能滿足“兩端對齊”這一中文顯示的基本要求。Android實(shí)現(xiàn)文本兩端對齊顯示的基本方法有3種:①將文本轉(zhuǎn)換為html格式,用WebView控件顯示;②棄用TexwView控件,重新實(shí)現(xiàn)一個具有兩端對齊功能的新控件;③在TexwView控件的基礎(chǔ)上,擴(kuò)展實(shí)現(xiàn)兩端對齊功能。第一種方法需要在文本顯示前把文本轉(zhuǎn)換為html格式,這需要程序員對html格式相當(dāng)熟悉;第二種方法是重新實(shí)現(xiàn)一個文本顯示控件,需要對Android控件的實(shí)現(xiàn)機(jī)理有深入研究[1],對程序員的能力要求比較高;第三種方法相對簡單,只需在現(xiàn)有控件的基礎(chǔ)上,覆蓋文本輸出方法。本文采用第三種方法,即擴(kuò)展TextView控件功能。
1 TextView控件機(jī)理
要擴(kuò)展TextView控件功能,首先要對TextView控件的實(shí)現(xiàn)機(jī)理有一定了解。Android的每一個控件雖然實(shí)現(xiàn)起來相當(dāng)復(fù)雜,但除了具體實(shí)現(xiàn)細(xì)節(jié)外,幾乎所有的可視控件都包含兩個要素:一個是與用戶的交互界面(UI),另一個是與用戶交互的用戶輸入事件。TextView作為Android的一個基礎(chǔ)控件也不例外,用戶界面是通過在畫布上繪制UI,用戶主要是鍵盤輸入以及觸摸屏輸入。因此,分析TextView控件的機(jī)理就是要搞清控件界面的繪制框架及其輸入過程。本文主要關(guān)注TextView控件的界面繪制步驟。控件的UI繪制操作通常分為3步,分別是測量、布局和繪制。
1.1 測量
對于一個可視控件,必須確定其所占空間的大小,所以TextView要重寫父類View的成員函數(shù)onMeasure。該函數(shù)有兩個參數(shù),分別是用來描述寬度測量規(guī)范的widthMeasureSpec和高度測量規(guī)范的heightMeasureSpec。測量規(guī)范使用1個int值來表示,這個int值包含了2個分量。第1個是mode分量,使用最高2位來表示。測量模式有3種,分別是MeasureSpec.UNSPECIFIED(0)、MeasureSpec.EXACTLY(1)和MeasureSpec.AT_MOST(2);第2個是size分量,使用低30位來表示。當(dāng)mode分量等于MeasureSpec.EXACTLY時,size分量的值就是父視圖設(shè)置的寬度或者高度;當(dāng)mode分量等于MeasureSpec.AT_MOST時,size分量的值就是父視圖限定當(dāng)前控件設(shè)置的最大寬度或者高度;當(dāng)mode分量等于MeasureSpec.UNSPECIFIED時,父視圖不限定當(dāng)前控件所設(shè)置的寬度或者高度,這時候當(dāng)前控件就按照實(shí)際需求來設(shè)置寬度和高度。
1.2 布局
通過測量后確定了控件的大小,但是控件的位置還未確定??丶奈恢檬峭ㄟ^布局這個操作來完成的。Android可視控件是按照樹形結(jié)構(gòu)組織在一起的,其中,子控件的位置由父控件來設(shè)置,也就是說,只有容器類控件才執(zhí)行布局操作,通過重寫父類View的成員函數(shù)onLayout來實(shí)現(xiàn)。由于TextView控件不是容器類控件,因此,它可以不重寫父類View的成員函數(shù)onLayout。
1.3 繪制
經(jīng)過測量和布局操作后,就確定了控件TextView的大小和位置,接下來繪制UI??丶榱死L制UI,必須重寫父類View的成員函數(shù)onDraw。該函數(shù)只有一個參數(shù)canvas,canvas描述的是一塊畫布,控件的UI就是繪制在這塊畫布上的。畫布提供了豐富的接口來繪制UI,例如畫線(drawLine)、畫圓(drawCircle)、輸出文字(drawText)和貼圖(drawBitmap)等等。有了這些UI畫圖接口之后,就可以隨心所欲地繪制控件的UI了。
通過分析TextView控件的機(jī)理,不難發(fā)現(xiàn),TextView對文本的顯示是通過在畫布(canvas)上輸出文本實(shí)現(xiàn)的。因此要在TextView的基礎(chǔ)上實(shí)現(xiàn)文本的兩端對齊,關(guān)鍵是要重新安排每一行的字符數(shù),控制字間距,在TextView的畫布(canvas)上精確地輸出每一個字符,從而確保每一行的第一個字符和最后一個字符是對齊的。
2 擴(kuò)展TextView設(shè)計(jì)
2.1 兩端對齊顯示的基本要求
要實(shí)現(xiàn)文本的兩端對齊,表面上是每行的最后一個字符在縱坐標(biāo)上保持一致,其實(shí)還要考慮文本在顯示格式上的要求,特別是每一行的第一個字符(行首)和最后一個字符(行尾)是否符合格式規(guī)范。需要考慮的格式規(guī)范要求有:①行首字符不能是以下字符:句號(。.)、問號(??)、嘆號(!?。⒍禾枺?,,)、冒號(::)和分號(;;)和引號(””)、括號())]})、書名號(》>)的后一半等;②行尾字符不能是以下字符:引號(“”)、括號((([{)、書名號(《<)的前一半。
2.2 ExTextView設(shè)計(jì)[2]
ExTextView繼承了Android的TextView,其繼承關(guān)系如圖1所示,ExTextView的類圖如圖2所示。
ExTextView主要的屬性包括文本高度(m_iTextHeight)、文本寬度(m_iTextWidth)、畫筆(mPaint)、文本(text)、行間距(LineSpace)、左邊距(left_Margin)、右邊距(right_Margin)、上邊距(top_Margin)、下邊距(bottom_Margin)、字體高度(m_iFontHeight)、所有行屬性(strings)。
其中,描述行屬性的內(nèi)部類Tlineattr包含3個成員,分別是該行所包含的字符串(linetext)、該行的字間距(extrawidth)和該行每一個字符的輸出寬度(widths)。
ExTextView的機(jī)理是:確定每一行字符屬性的IniLines()方法和覆蓋父類的OnDraw()方法。IniLines()方法主要是確定第一行所包含的字符,保證行首字符和行尾字符符合格式規(guī)范的要求,其判斷邏輯如圖3所示。OnDraw()方法主要是根據(jù)每一行所包含的字符,確定字間距,保證所有行首字符的水平坐標(biāo)值一致,所有行尾字符的水平坐標(biāo)值一致,操作流程如圖4所示。
3 擴(kuò)展TextView實(shí)現(xiàn)
3.1 主要代碼
ExTextView要能夠?qū)崿F(xiàn)兩端對齊,其核心是先要分配每一行的字符,然后重寫父類的成員函數(shù)onDraw ()。
分配字符的方法IniLines()實(shí)現(xiàn)如下:
private void IniLines() {
strings.clear();
String Text = text;//得到要輸出的文本
intm_LineWidth = m_iTextWidth - left_Margin - right_Margin; //可輸出的畫布寬度
float[] widths = new float[Text.length()]; //保存每個字符所占寬度的數(shù)組
mPaint.getTextWidths(Text, widths); //得到每個字符輸出時的寬度
final String Laststr = "((《“{[<"; //不能是行尾的字符
final String Firststr = "、,。;:)?”,.;:)》?/-]}>"; //不能是行首的字符
float curwidth = 0; //保存當(dāng)前行所含字符的總的寬度
intstartindex = 0; //行開始的字符位置
intendindex = 0; //行結(jié)束的字符位置
String lastch = ""; //上一個字符
Boolean IsCalExtraWidth = false;//是否需要計(jì)算字間距
for (inti = 0; i Boolean lineclosed = false; //當(dāng)前行是否結(jié)束 String ch = Text.substring(i, i + 1); //得到當(dāng)前字符 curwidth = curwidth + widths[i]; //當(dāng)前行已輸出字符的寬度 if (curwidth>m_LineWidth) { //超出了行寬 lineclosed = true;//當(dāng)前行結(jié)束 IsCalExtraWidth = true;//需要計(jì)算字間距 if (Firststr.contains(ch)) { //如果當(dāng)前字符不能為行首,則當(dāng)前字符為該行的行尾 endindex = i; //當(dāng)前行結(jié)束的字字符位置 curwidth = 0; //下一行的行寬 } else { //當(dāng)前字符可以為行首 if (Laststr.contains(lastch)) { //上一個字符不能為行尾,則上一個字符為下一行的行首 endindex = i - 2;//當(dāng)前行的行尾為當(dāng)前字符的前兩個字符 curwidth = widths[i - 1] + widths[i]; //下一行的行寬 } else { //上一個字符可以成為行尾 endindex = i - 1; //當(dāng)前行的行尾為上一個字符 curwidth = widths[i]; //下一行的行寬 } } lastch = ch; } if (!lineclosed) { if (ch.equals("n") || i == Text.length() - 1) { //或當(dāng)前字符為換行符或文本最后一個字符,則當(dāng)前行結(jié)束 lineclosed = true; endindex = i; IsCalExtraWidth = false;//不需要計(jì)算字間距 curwidth = 0; } } if (lineclosed) { //如果當(dāng)前行結(jié)束 intlen = endindex - startindex + 1;//當(dāng)前行字符個數(shù) float[] linewidths = new float[len]; //當(dāng)前行每一個字符的寬度
float linewidth = 0; //當(dāng)前行所有字符輸出寬度之和
for (int j = startindex; j linewidths[j - startindex] = widths[j]; linewidth = linewidth + widths[j]; } float extrawidth = 0;//調(diào)整字間距 if (IsCalExtraWidth) { //需要計(jì)算調(diào)整字間距 extrawidth = (m_LineWidth - linewidth) / len; } stringlist.add(Text.substring(startindex, endindex + 1)); //當(dāng)前行所包含的字符 strings.add(new Tlineattr(Text.substring(startindex, endindex + 1), extrawidth, linewidths)); startindex = endindex + 1; //下一行的起始字符的位置 } }//結(jié)束對字符的歷遍 if (endindex != (Text.length() - 1)) { //若上一行的行尾不是文本的最后一個字符,則還剩最后一行 endindex = Text.length() - 1; intlen = endindex - startindex + 1;//當(dāng)前行字符個數(shù) float[] linewidths = new float[len]; //當(dāng)前行每一個字符的寬度 float linewidth = 0; //當(dāng)前行所有字符輸出寬度之和 for (int j = startindex; j linewidths[j - startindex] = widths[j]; linewidth = linewidth + widths[j]; } stringlist.add(Text.substring(startindex, endindex + 1)); //當(dāng)前行所包含的字符 strings.add(new Tlineattr(Text.substring(startindex, endindex + 1), 0, linewidths)); } } 成員函數(shù)onDraw()的實(shí)現(xiàn)代碼如下。 @Override protected void onDraw(Canvas canvas) { float drawx=0; //繪制字符的橫坐標(biāo) float drawy=m_iFontHeight; //繪制字符的縱坐標(biāo) 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); // 繪制當(dāng)前字符 drawx=drawx+ strings.get(i).extrawidth+strings.get(i).widths[j]; //下一個字符的橫坐標(biāo) }//結(jié)束當(dāng)前行的循環(huán) drawy=drawy+m_iFontHeight; //下一行字符的縱坐標(biāo) }//結(jié)束所有行的循環(huán) } 3.2 應(yīng)用實(shí)例 ExTextView控件的使用與TextView控件的使用是一樣的,這里不作詳細(xì)介紹。對于同一段文本,圖5是Android自帶的TextView顯示效果,圖6是ExTextView的顯示效果。 4 結(jié)語 具有兩端對齊功能的ExTextView控件,還不十分完善。比如,為防止英文單詞被截?cái)?,需要加上中英文混合分詞技術(shù),等等。本文只是提供了擴(kuò)展TextView功能一種思路,讀者可以沿著這種思路不斷擴(kuò)展,實(shí)現(xiàn)所需功能。