【簡(jiǎn)述】
本文講述了一個(gè)簡(jiǎn)單的平臺(tái)無(wú)關(guān)的RICHTEXT的實(shí)現(xiàn)方法。
這個(gè)RICHTEXT特性如下:
- 使用UTF-16作為字符編碼
- 使用行來(lái)排版,文字從左到右顯示
- 支持可獨(dú)立設(shè)置字體顏色的文字和鏈接
- 支持自定義元素用來(lái)實(shí)現(xiàn)圖像和動(dòng)畫(huà)
【平臺(tái)無(wú)關(guān)】
平臺(tái)無(wú)關(guān)實(shí)際上是使用統(tǒng)一的接口來(lái)封裝不同平臺(tái)的實(shí)現(xiàn)方法來(lái)做到的。在RICHTEXT中使用到的平臺(tái)相關(guān)的有兩個(gè):
1- 文字大小獲取。
2- 文字的繪制。
我們把它封裝到一個(gè)字體的純虛接口類中去:
class IFont {
public:
// 獲取字體的高度
virtual float GetHeight() const = 0;
// 獲取文字的橫向步進(jìn)
virtual float GetCharsAdvance( const UTF16_CHAR * pChars, float * pAdvanceArray, size_t uCount ) const = 0;
// 繪制文字
virtual void DrawChars( const UTF16_CHAR * pChars, size_t uCount, float fX, float fY, unsigned long ulColor ) const = 0;
};
對(duì)于自定義的元素,也是一個(gè)純虛的接口類:
class IRichTextCustomElement
{
public:
// 獲取元素寬度
virtual float GetWidth() const = 0;
// 獲取元素的高度
virtual float GetHeight() const = 0;
// 繪制元素
virtual void Draw( float fX, float fY ) const = 0;
};
【實(shí)現(xiàn)】
1- 模塊劃分
RICHTEXT在這里劃分為兩個(gè)模塊:一個(gè)稱為RichTextDoc,用來(lái)存儲(chǔ)內(nèi)容的,稱為文檔;一個(gè)稱為RichTextView,用來(lái)存儲(chǔ)表現(xiàn)的,稱為視圖。
2- 模塊實(shí)現(xiàn):RichTextDoc
RichTextDoc主要實(shí)現(xiàn)了內(nèi)容管理。
RichTextDoc內(nèi)部存儲(chǔ)兩項(xiàng)內(nèi)容
1) 字符
2) 元素(不同的元素類型,或者同種元素類型但屬性不同)
字符存儲(chǔ)了文字和鏈接的原始字符,而元素存儲(chǔ)了同屬性的一組字符、鏈接或者一個(gè)自定義元素。他們使用idx和len關(guān)聯(lián)到字符存儲(chǔ)中的原始字符。對(duì)于一個(gè)圖片,在字符中使用了一個(gè)空格作為占位符。
元素中同時(shí)存儲(chǔ)了是否作為一個(gè)段落ID,這用來(lái)描述一組元素是否在同一個(gè)段落里,這個(gè)ID為一個(gè)不為0的正整數(shù)。
RichTextDoc提供了以下接口來(lái)添加內(nèi)容以及訪問(wèn)元素。
class IRichTextDoc
{
public:
// 添加一段文本
virtual void AddText( const UTF16_CHAR * pText, size_t uTextLen ) = 0;
// 添加一個(gè)鏈接
virtual void AddLink( const UTF16_CHAR * pText, size_t uTextLen, unsigned long ulLinkID ) = 0;
// 添加一個(gè)自定義的元素
virtual void AddCustom( IRichTextCustomElement * pElement ) = 0;
// 添加一個(gè)段落
virtual unsigned long AddParagraph() = 0;
// 設(shè)置文字顏色
virtual void SetTextColor( unsigned long ulColor ) = 0;
// 設(shè)置文字字體
virtual void SetTextFont( IFont * pFont ) = 0;
// 獲取元素的數(shù)量
virtual void GetElementCount() const = 0;
// 獲取元素類型
// result: -1 = 非法索引 0=文字 1=鏈接 2=自定義元素
virtual int GetElementType( size_t uElementIndex ) const = 0;
// 獲取元素的字體和顏色
// result: -1 = 失敗 0=成功
// pFont: 返回字體接口
// ulColor: 返回顏色值
virtual int GetElementFontAndColor( size_t uElementIndex, IFont *& ppFont, unsigned long & ulColor ) const = 0;
// 獲取元素的字符
virtual void GetElementChars( size_t uElementIndex, const UTF16_CHAR * &pChars, size_t & uCount ) const = 0;
// 獲取自定義元素
virtual IRichTextCustomElement * GetCustomElement( size_t uElementIndex ) const = 0;
// 獲取元素的段落ID
virtual unsigned long GetElementParagraphID( size_t uElementIndex ) const = 0;
// 獲取元素的鏈接ID
virtual unsigned long GetElementLinkID( size_t uElementIndex ) const = 0;
};
3- 模塊實(shí)現(xiàn):RichTextView
RichTextView 主要實(shí)現(xiàn)了排版和繪制。
A 排版功能
它的基本排版單位是LINE(行),也就是顯示行。在LINE的內(nèi)部存儲(chǔ)了數(shù)個(gè)RUN。每個(gè)RUN僅對(duì)應(yīng)一個(gè)DOC中的元素,但是一個(gè)DOC中的元素可以對(duì)應(yīng)多個(gè)RUN(被拆分成多行的情況)。
RichTextView中排版是通過(guò)拆分DOC中的每個(gè)元素實(shí)現(xiàn)的。因?yàn)橛?/span>IFont接口以及IRichTextCustomElement接口,就可以獲取到文字和自定義元素的大小,依次累加到元素結(jié)束或者LINE寬度溢出,就可以結(jié)束一個(gè)RUN,開(kāi)始下一個(gè)RUN。
在這個(gè)模塊的實(shí)現(xiàn)中,需要注意下面幾個(gè)問(wèn)題:
1) 如何確定一個(gè)LINE的高度:在實(shí)現(xiàn)里,是根據(jù)每個(gè)RUN對(duì)應(yīng)的元素的高度取MAX來(lái)實(shí)現(xiàn)的。
2) 根據(jù)段落來(lái)適時(shí)的換行。
3) LINK根據(jù)需求來(lái)決定是否可以拆分成多個(gè)LINE中的多個(gè)RUN。(實(shí)際需求里是禁止拆分LINK)
4) 行間距與RUN和LINE的HITTEST。
RUN的結(jié)構(gòu)是這樣的:
struct RUN_S {
size_t uElementIndex; // 元素的索引
size_t uInElementCharIndex; // 在元素的字符中的索引
size_t uInElementCharCount; // 在元素中的字符數(shù)量
float fWidth; // RUN的寬度
float fHeight; // RUN的高度
};
LINE 的結(jié)構(gòu)是這樣的:
struct LINE_S {
vector<RUN_S*> vecRuns; // 行內(nèi)的RUN。
float fPosY; // LINE在整個(gè)VIEW中的Y坐標(biāo)。
float fHeight; // LINE的高度
};
B- 繪制功能
繪制功能和拆分排版差不多,主要就是繪制坐標(biāo)根據(jù)RUN和LINE的寬度和高度的累計(jì)。
然后調(diào)用IFont或IRichTextCustomElement的繪制方法。
C- HITTEST
除了排版和繪制之外,VIEW還提供了HITTEST,用來(lái)檢測(cè)點(diǎn)擊命中了哪個(gè)LINE、RUN、或者對(duì)應(yīng)到DOC中的元素,從而實(shí)現(xiàn)點(diǎn)擊鏈接的檢測(cè)。
RichTextView接口如下:
class IRichTextView
{
public:
// 獲取行數(shù)
virtual size_t GetLineCount() const = 0;
// 獲取行的RUN數(shù)量
virtual size_t GetRunCount( size_t uLineIndex ) const = 0;
// 獲取RUN對(duì)應(yīng)的元素索引
virtual size_t GetRunElementIndex( size_t uLineIndex, size_t uRunIndex ) const = 0;
// 用DOC,行寬和行間距建立排版內(nèi)容。
virtual void Build( IRichTextDoc * pDoc, float fLineWidth, float fLineGap ) = 0;
// 檢測(cè)點(diǎn)擊的行
virtual size_t LineHitTest( float fX, float fY ) const = 0;
// 檢測(cè)點(diǎn)擊的RUN
virtual size_t RunHitTest( size_t uLineIndex, float fX, float fY ) const = 0;
// 檢測(cè)點(diǎn)擊的元素索引
virtual size_t ElementHitTest( float fX, float fY ) const = 0;
// 獲取VIEW的高度。
virtual float GetHeight() const = 0;
// 繪制
virtual void Draw(float fX, float fY, float fWidth, float fHeight) const = 0;
// 從某行開(kāi)始繪制
virtual void Draw(size_t uBeginLineIndex, float fX, float fY, float fWidth, float fHeight) const = 0;
};
【應(yīng)用】
目前應(yīng)用在一個(gè)手機(jī)網(wǎng)游的項(xiàng)目中,來(lái)顯示聊天內(nèi)容。
平臺(tái)目前是IOS和WIN32。IOS下字體使用的是CORETEXT+COREGRAPHICS來(lái)實(shí)現(xiàn)的。WIN32下用的是GetGlyphOutline API。渲染使用的OPENGLES 1.1,內(nèi)部用glTexSubImage2D來(lái)實(shí)現(xiàn)了一個(gè)字形的貼圖緩沖。
在項(xiàng)目中,View被綁定在一個(gè)RichText的UI控件中。

【擴(kuò)展】
目前只能顯示富文本,后面需要擴(kuò)展為RICHEDIT使用。
需要增加光標(biāo)的位置判定和光標(biāo)的顯示位置和大小的獲取。
考慮在DOC上增加存儲(chǔ)文字寬度,以便于VIEW上進(jìn)行CHARHITTEST時(shí)的快速取用。