平台无关的RICHTEXT实现
【简述】
本文讲述了一个简单的平台无关的RICHTEXT的实现方法。
这个RICHTEXT特性如下:
- 使用UTF-16作为字符编码
- 使用行来排版,文字从左到右显示
- 支持可独立设置字体颜色的文字和链接
- 支持自定义元素用来实现图像和动画
【平台无关】
平台无关实际上是使用统一的接口来封装不同平台的实现方法来做到的。在RICHTEXT中使用到的平台相关的有两个:
- 文字大小获取。
- 文字的绘制。
我们把它封装到一个字体的纯虚接口类中去:
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;
};
【实现】
- 模块划分
RICHTEXT在这里划分为两个模块:一个称为RichTextDoc,用来存储内容的,称为文档;一个称为RichTextView,用来存储表现的,称为视图。
- 模块实现:RichTextDoc
RichTextDoc主要实现了内容管理。
RichTextDoc内部存储两项内容
- 字符
- 元素(不同的元素类型,或者同种元素类型但属性不同)
字符存储了文字和链接的原始字符,而元素存储了同属性的一组字符、链接或者一个自定义元素。他们使用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;
};
- 模块实现:RichTextView
RichTextView 主要实现了排版和绘制。
A 排版功能
它的基本排版单位是LINE(行),也就是显示行。在LINE的内部存储了数个RUN。每个RUN仅对应一个DOC中的元素,但是一个DOC中的元素可以对应多个RUN(被拆分成多行的情况)。
RichTextView中排版是通过拆分DOC中的每个元素实现的。因为有IFont接口以及IRichTextCustomElement接口,就可以获取到文字和自定义元素的大小,依次累加到元素结束或者LINE宽度溢出,就可以结束一个RUN,开始下一个RUN。
在这个模块的实现中,需要注意下面几个问题:
- 如何确定一个LINE的高度:在实现里,是根据每个RUN对应的元素的高度取MAX来实现的。
- 根据段落来适时的换行。
- LINK根据需求来决定是否可以拆分成多个LINE中的多个RUN。(实际需求里是禁止拆分LINK)
- 行间距与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时的快速取用。