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

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