美文网首页
RichText源码解析

RichText源码解析

作者: 许彦峰 | 来源:发表于2022-06-29 20:46 被阅读0次

    为了说明代码逻辑,我选择typescript进行的代码注解

    xml解析

    export default class RichText {
      initWithXML(xml: string) {
        let visitor: MyXMLVisitor = new MyXMLVisitor();
        visitor._richText = this;
        let parser: SAXParser = new SAXParser();
        parser._delegator = visitor;
        parser.parseIntrusive(xml);
      }
    }
    

    以上就是解析xml的入口,这块的解析逻辑是和SAX的设计密切关联,大概思路就是SAXParser会进行RapidXmlSaxHander的绑定注册,当解析tag事件触发后,由RapidXmlSaxHander进行转发到SAXParser,之后SAXParser通过delegator会再次转发到MyXMLVisitor,所以最终的事件逻辑是在MyXMLVisitor里面,代码逻辑稍微有点绕,具体的逻辑梳理就不再展开。下图可以作为参考,整个实现代码结构还是有点臃肿的。


    接下来的重点就是MyXMLVisitor的callback逻辑

    解析tag、attrs

    class Attribute {
      face: string;
      color: string;
      // ... more
    }
    let _tagTables: Map<
      string,
      {
        isFontElement: boolean;
        handlerVisitEnter: Function;
      }
    >;
    export default class MyXMLVisitor extends SAXDelegator {
      startElement(tag: string, attr: string) {
        let tagAttrMap = {};
        let { handlerVisitEnter } = _tagTables[tag];
        // 这里的handler来自setTagDescription,里面会解析tag的attr,并转换为RichText:KEY_XXX
        handlerVisitEnter(tagAttrMap);
    
        let attribute: Attribute = new Attribute();
        // 将handler的RichText:KEY_XXX属性转换为Attribute结构体
        // ... 省略这部分的代码
        
        // 将当前tag标签的属性结果保存起来
        this._fontElements.push(attribute);
      }
      static setTagDescription(
        tag: string,
        isFontElement: boolean,
        handler: Function
      ) {
        _tagTables[tag] = {
          isFontElement,
          handlerVisitEnter: handler,
        };
      }
    }
    

    startElement将每1个tag标签及其对应的属性,解析为Attribute结构体,并保存起来。

    实例化tag的text

    当tag,attr解析完毕后,接下来就会回调到标签的内容了

    export default class MyXMLVisitor extends SAXDelegator {
      textHandler(tag: string, attr: string) {
        // 生成对应的富文本元素,并推送给RichText
        let color = this.getColor();
        let richElement: RichElementText =
          new RichElementText(/*这里省略了很多参数*/);
        this._richText._richElements.push(richElement);
      }
      getColor() {
        // 倒着查找最邻近标签的颜色
        for (let i = this._fontElements.length - 1; i >= 0; i++) {
          let item = this._fontElements[i];
          if (item.hasColor) {
            return item.color;
          }
          return "white";
        }
      }
    }
    

    为啥getColor的逻辑那么奇怪,直接从所有的标签中倒着查找?
    假如有以下的xml,故意写了好多font,但是我们看到RichText其实只是渲染了一个RichElementText?

    因为只有发生了TextHandler调用时,才会搜集组装属性

    <font face="Verdana" size="12" color="#ffffff"> <!-- startElement -->
        <font size="30">                            <!-- startElement -->
            <font color="#00ff00">                  <!-- startElement -->
                text1                               <!-- textHandler -->
            </font>                                 <!-- endElement -->
        </font>                                     <!-- endElement -->
         text2                                      <!-- textHandler -->
    </font>                                         <!-- endElement -->
    
    • 当构建text1属性的时候,此时:
    let attributes = [
      {face:"Verdana", size:12, color="#fffff"},
      {size:30},
      {color="#00ff00"}
    ]
    

    按照倒叙查找的规则,color="00ff00", size=30, face="Verdana"

    • 当构建text2的时候,此时
    let attributes = [
      {face:"Verdana", size:12, color="#fffff"},
    ]
    

    按照倒叙查找的规则,color="ffffff", size=12, face="Verdana"

    这样就实现了属性的继承,即子标签没有的属性,会从父标签获取。参考css样式继承。

    一定要对这个事件顺序非常清晰,这也变相解释了为啥getColor的逻辑那么奇怪,可以认为上边的三个font进行了attr合并,并且以最靠近text的attr为基准。

    扩展

    了解了以上的规则后,思考:

    • 例子1:
    <font color='#ff0000' size='20'>
        <b>
          <i>
              <del>
                加粗倾斜下划线
              </del>
          </i>
          加粗
        </b>
        正常
    </font>
    

    渲染结果:


    可以看到其实b、i、del标签,都可以理解为attr,虽然它们是标签。

    • 例子2:
    <font color='#ff0000' size='20'>
        <b color="#00ff00">
          加粗
        </b>
    </font>
    

    渲染结果:



    b标签的color属性并未生效,因为代码中是没有解析b标签的color属性的

    • 例子3:
    <font color='#ff0000' size='20'>
        <font color="#00ff00"><!--这里的color覆盖了之前的color-->
          绿色
        </font-error><!--没有影响渲染,是因为endElement没有校验配对关系-->
    </font>
    
    

    渲染结果:


    希望以上的例子能让你更加深刻的理解富文本渲染的原理。

    RichElement渲染

    经过前几步的处理,已经将xml转换为了RichElementText,并且保存在_richElements

    class Node {
      visit() {} // 每帧都会访问
    }
    class Widget extends Node {
      adaptRenderers() {}
      visit(): void {
        this.adaptRenderers();// 继承widget的渲染适配接口
      }
    }
    export class RichElement {}
    export class RichElementText extends RichElement {}
    export class RichElementImage extends RichElement {}
    export class RichElementCustomNode extends RichElement {}
    export class RichElementNewLine extends RichElement {}
    export default class RichText extends Widget {
      _richElements: Array<RichElement> = [];
      adaptRenderers() {
        this.formatText(); // RichText的渲染逻辑入口
      }
      formatText() {
        for (let element in this._richElements) {
            // 将收集到的元素实例化为label,并且计算出每一行的具体label信息
        }
      }
    }
    

    元素布局

    formarRenderers之前,已经将每一行元素的个数都已经计算好了
    _elementRenders是一个二维数组,存放着每一行的元素
    接下来就是要进行排版的工作,排版大致流程

    • 先计算出来每一行的高度,在这一步,之前设置的行间距会参与计算
            float newContentSizeHeight = 0.0f;
            float newContentSizeWidth = 0.0f;
            std::vector<float> maxHeights(_elementRenders.size());
    
            for (size_t i = 0, size = _elementRenders.size(); i < size; i++)
            {
                Vector<Node*>& row = _elementRenders[i];
                float maxHeight = 0.0f;
                float maxWidth = 0.0f;
                for (auto& iter : row)
                {
                    maxHeight = MAX(iter->getContentSize().height, maxHeight);
                    maxWidth += iter->getContentSize().width;
                }
                if (i > 0) {
                    // 行间距对最大高度的影响
                    maxHeight += _defaults.at(KEY_VERTICAL_SPACE).asFloat();
                }
                maxHeights[i] = maxHeight;
                newContentSizeHeight += maxHeights[i];
                newContentSizeWidth = MAX(maxWidth, newContentSizeWidth);
            }
    
    • 将每一行元素锚点设置为左下角(0,0),x方向从起点依次排列,y方向从底部往顶部排列,为啥?因为富文本的(0,0)点在左下角
         float nextPosY = _customSize.height;
            for (size_t i = 0, size = _elementRenders.size(); i < size; i++)
            {
                Vector<Node*>& row = _elementRenders[i];
                float nextPosX = 0.0f;
                nextPosY -= maxHeights[i];
    
                for (auto& iter : row)
                {
                    iter->setAnchorPoint(Vec2::ZERO);
                    iter->setPosition(nextPosX, nextPosY);
                    if (iter->getComponent(ListenerComponent::COMPONENT_NAME)) {
                        tag++;
                        this->addChild(iter, 1, tag);
                    }
                    else {
                        tag++;
                        this->addProtectedChild(iter, 1, tag);
                    }
                    nextPosX += iter->getContentSize().width;
                }
    
                doHorizontalAlignment(row, nextPosX);
            }
          // 这里的renderSize方便垂直布局计算使用
         this->setRenderSize(Size(newContentSizeWidth, newContentSizeHeight)); 
    

    每排列好一行之后,都会进行对齐计算doHorizontalAlignment

    void RichText::doHorizontalAlignment(const Vector<cocos2d::Node*>& row, float rowWidth) {
        const auto alignment = static_cast<HorizontalAlignment>(_defaults.at(KEY_HORIZONTAL_ALIGNMENT).asInt());
        if (alignment != HorizontalAlignment::LEFT) {
            // 将空白字符串截取掉,计算相差的宽度
            const auto diff = stripTrailingWhitespace(row); 
            // rowWidth+diff为截取空白字符串的宽度
            // leftOver就是剩余空间的尺寸
            const auto leftOver = getContentSize().width - (rowWidth + diff);
            // 根据不同的对齐方式,对leftOver进行再计算
            const float leftPadding = getPaddingAmount(alignment, leftOver);
            const Vec2 offset(leftPadding, 0.f);
            for (auto& node : row) {
                // 将所有元素X进行平移指定的距离,就实现了水平对齐的效果
                node->setPosition(node->getPosition() + offset);
            }
        }
    }
    float getPaddingAmount(const RichText::HorizontalAlignment alignment, const float leftOver) {
        switch (alignment) {
        case RichText::HorizontalAlignment::CENTER:
            return leftOver / 2.f;
        case RichText::HorizontalAlignment::RIGHT:
            return leftOver;
        default:
            CCASSERT(false, "invalid horizontal alignment!");
            return 0.f;
        }
    }
    
    • 最后处理垂直对齐
    void RichText::doVerticalAlignment() {
        const auto alignment = static_cast<VerticalAlignment>(_defaults.at(KEY_VERTICAL_ALIGNMENT).asInt());
        if (alignment != VerticalAlignment::TOP) {
            float off = 0;
            // renderSize-contentSize
            if (alignment == VerticalAlignment::CENTER) {
                off = (getRenderSize().height - getContentSize().height) * 0.5;
            }
            else if (alignment == VerticalAlignment::BOTTOM) {
                off = getRenderSize().height - getContentSize().height;
            }
    
            for (auto& element : _elementRenders)
            {
                for (auto& iter : element)
                {
                    iter->setPositionY(iter->getPositionY() + off);
                }
            }
        }
    }
    
    

    这里的处理思路比较类似,不同的是offset=renderSize-contentSize

    疑问

    为什么最外层要再加一层<font></font>

    当输入xml数据为<font size="30">text</font>时,最终得到的结果是:

    <font face="Verdana" size="12" color="#ffffff">
        <font size="30">
            text
        </font>
    </font>
    

    也就是在最外层重新包了一层<font></font>,为什么要这样子呢?

    假如我们输入的xml数据为test,其实这并不符合xml格式,发现RichText也能正确渲染,就是因为外层追加的这个<font></font>使最终xml数据格式正确。

    其他收获

    tag 原理
    <small></small> getFontSize() * 0.8f;
    <big></big> getFontSize() * 1.25f;

    相关文章

      网友评论

          本文标题:RichText源码解析

          本文链接:https://www.haomeiwen.com/subject/gedfbrtx.html