Cocos2dx-- 聊天系统之富文本(RichText)与字体对齐

前言

游戏开发中我们一般都会有聊天系统,我们可以同时或单独发文字,图片,表情,超链接等信息的文本即称为富文本。如下图所示:
Cocos2dx-- 聊天系统之富文本(RichText)与字体对齐_第1张图片

我使用的是cocos-3.4引擎版本里的RichText富文本控件实现这些操作的,但cocos自带封装的RichText还有一些问题,1:如上图看到的一样,当中英文混输时右边字体没对齐(看需求,QQ都没处理这问题)。2:聊天信息有时会需求像上面一样还要一个背景底框装着,底框大小随发送内容变化,这时我们就需要知道富文本的宽度和高度而cocos的RichText没有封装这个接口需要我们自己扩展。本章就讲讲怎么来解决这两个问题。

解决步骤

1,在UIRichText.h中新增获取富文本大小接口

public:
Size xxgetRealSize();
Size xxrealSize;

2,默认右没不齐是因为它的换行是根据字的平均长度来计算的,即单个字长度=总长度/总字数,而不同字的宽度是很可能不相同的。现在我的改法是:循环计算出每个字的大小求和换行,当然这样的效率是比默认的低,所以这个对齐还是看需求,有的QQ消息也没处理这个对齐问题。下面是UIRichText.cpp修改后的完整代码,有注释,修改处都用@cxx标记了。

/**************************************************************************** Copyright (c) 2013 cocos2d-x.org http://www.cocos2d-x.org Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ****************************************************************************/

#include "UIRichText.h"
#include "platform/CCFileUtils.h"
#include "2d/CCLabel.h"
#include "2d/CCSprite.h"
#include "base/ccUTF8.h"
#include "ui/UIHelper.h"

#include "platform/CCDevice.h" //@cxx
#include "base/ccMacros.h" //@cxx
#include "base/CCDirector.h" //@cxx

NS_CC_BEGIN

namespace ui {


bool RichElement::init(int tag, const Color3B &color, GLubyte opacity)
{
    _tag = tag;
    _color = color;
    _opacity = opacity;
    return true;
}


RichElementText* RichElementText::create(int tag, const Color3B &color, GLubyte opacity, const std::string& text, const std::string& fontName, float fontSize)
{
    RichElementText* element = new (std::nothrow) RichElementText();
    if (element && element->init(tag, color, opacity, text, fontName, fontSize))
    {
        element->autorelease();
        return element;
    }
    CC_SAFE_DELETE(element);
    return nullptr;
}

bool RichElementText::init(int tag, const Color3B &color, GLubyte opacity, const std::string& text, const std::string& fontName, float fontSize)
{
    if (RichElement::init(tag, color, opacity))
    {
        _text = text;
        _fontName = fontName;
        _fontSize = fontSize;
        return true;
    }
    return false;
}

RichElementImage* RichElementImage::create(int tag, const Color3B &color, GLubyte opacity, const std::string& filePath)
{
    RichElementImage* element = new (std::nothrow) RichElementImage();
    if (element && element->init(tag, color, opacity, filePath))
    {
        element->autorelease();
        return element;
    }
    CC_SAFE_DELETE(element);
    return nullptr;
}

bool RichElementImage::init(int tag, const Color3B &color, GLubyte opacity, const std::string& filePath)
{
    if (RichElement::init(tag, color, opacity))
    {
        _filePath = filePath;
        return true;
    }
    return false;
}

RichElementCustomNode* RichElementCustomNode::create(int tag, const Color3B &color, GLubyte opacity, cocos2d::Node *customNode)
{
    RichElementCustomNode* element = new (std::nothrow) RichElementCustomNode();
    if (element && element->init(tag, color, opacity, customNode))
    {
        element->autorelease();
        return element;
    }
    CC_SAFE_DELETE(element);
    return nullptr;
}

bool RichElementCustomNode::init(int tag, const Color3B &color, GLubyte opacity, cocos2d::Node *customNode)
{
    if (RichElement::init(tag, color, opacity))
    {
        _customNode = customNode;
        _customNode->retain();
        return true;
    }
    return false;
}

RichText::RichText():
_formatTextDirty(true),
_leftSpaceWidth(0.0f),
_verticalSpace(0.0f),
_elementRenderersContainer(nullptr)
{

}

RichText::~RichText()
{
    _richElements.clear();
}

RichText* RichText::create()
{
    RichText* widget = new (std::nothrow) RichText();
    if (widget && widget->init())
    {
        widget->autorelease();
        return widget;
    }
    CC_SAFE_DELETE(widget);
    return nullptr;
}

bool RichText::init()
{
    if (Widget::init())
    {
        return true;
    }
    return false;
}

void RichText::initRenderer()
{
    _elementRenderersContainer = Node::create();
    _elementRenderersContainer->setAnchorPoint(Vec2(0.5f, 0.5f));
    addProtectedChild(_elementRenderersContainer, 0, -1);
}

void RichText::insertElement(RichElement *element, int index)
{
    _richElements.insert(index, element);
    _formatTextDirty = true;
}

void RichText::pushBackElement(RichElement *element)
{
    _richElements.pushBack(element);
    _formatTextDirty = true;
}

void RichText::removeElement(int index)
{
    _richElements.erase(index);
    _formatTextDirty = true;
}

void RichText::removeElement(RichElement *element)
{
    _richElements.eraseObject(element);
    _formatTextDirty = true;
}

    //渲染富文本
void RichText::formatText()
{
    if (_formatTextDirty)
    {
        _elementRenderersContainer->removeAllChildren();
        _elementRenders.clear();
        if (_ignoreSize) //是否忽略换行
        {
            addNewLine();
            for (ssize_t i=0; i<_richElements.size(); i++)
            {
                RichElement* element = _richElements.at(i);
                Node* elementRenderer = nullptr;
                switch (element->_type)
                {
                    case RichElement::Type::TEXT:
                    {
                        RichElementText* elmtText = static_cast<RichElementText*>(element);
                        if (FileUtils::getInstance()->isFileExist(elmtText->_fontName))
                        {
                            elementRenderer = Label::createWithTTF(elmtText->_text.c_str(), elmtText->_fontName, elmtText->_fontSize);
                        }
                        else
                        {
                            elementRenderer = Label::createWithSystemFont(elmtText->_text.c_str(), elmtText->_fontName, elmtText->_fontSize);
                        }
                        break;
                    }
                    case RichElement::Type::IMAGE:
                    {
                        RichElementImage* elmtImage = static_cast<RichElementImage*>(element);
                        elementRenderer = Sprite::create(elmtImage->_filePath.c_str());
                        break;
                    }
                    case RichElement::Type::CUSTOM:
                    {
                        RichElementCustomNode* elmtCustom = static_cast<RichElementCustomNode*>(element);
                        elementRenderer = elmtCustom->_customNode;
                        break;
                    }
                    default:
                        break;
                }
                elementRenderer->setColor(element->_color);
                elementRenderer->setOpacity(element->_opacity);
                pushToContainer(elementRenderer);
            }
        }
        else
        {
            addNewLine();
            for (ssize_t i=0; i<_richElements.size(); i++)
            {

                RichElement* element = static_cast<RichElement*>(_richElements.at(i));
                switch (element->_type)
                {
                    case RichElement::Type::TEXT:
                    {
                        RichElementText* elmtText = static_cast<RichElementText*>(element);
                        handleTextRenderer(elmtText->_text.c_str(), elmtText->_fontName.c_str(), elmtText->_fontSize, elmtText->_color, elmtText->_opacity);
                        break;
                    }
                    case RichElement::Type::IMAGE:
                    {
                        RichElementImage* elmtImage = static_cast<RichElementImage*>(element);
                        handleImageRenderer(elmtImage->_filePath.c_str(), elmtImage->_color, elmtImage->_opacity);
                        break;
                    }
                    case RichElement::Type::CUSTOM:
                    {
                        RichElementCustomNode* elmtCustom = static_cast<RichElementCustomNode*>(element);
                        handleCustomRenderer(elmtCustom->_customNode);
                        break;
                    }
                    default:
                        break;
                }
            }
        }
        formarRenderers();
        _formatTextDirty = false;
    }
}

//begin @cxx 得到一行文字的个数
bool s_getLineTextNumber(const std::string& text, const std::string& fontName, float fontSize , float rowWidth , int &textNumber)
{
    bool ret =  true ;
    float maxW = rowWidth ;
    int count = 0 ;

    while(true)
    {
        std::string tempChar = Helper::getSubStringOfUTF8String(text,count,1);  //截取字符串
        float tempW,tempH ;
        if(tempChar.length() == 0 )
        {
            break ;
        }
        bool bSuccess = Device::CalculateTextSize(tempChar.c_str(),fontName.c_str(),fontSize, tempW, tempH);
        if( !bSuccess )
        {
            ret = false ;
            break ;
        }
        //tempW = tempW / CC_CONTENT_SCALE_FACTOR();

        maxW -= tempW ;
        if(maxW< -tempW / 2)
        {
            break ;
        }
        count++ ;
    }

    textNumber = count ;

    return ret ;
}
//end @cxx

void RichText::handleTextRenderer(const std::string& text, const std::string& fontName, float fontSize, const Color3B &color, GLubyte opacity)
{
    auto fileExist = FileUtils::getInstance()->isFileExist(fontName);
    Label* textRenderer = nullptr;
    if (fileExist)
    {
        textRenderer = Label::createWithTTF(text, fontName, fontSize);
    } 
    else
    {
        textRenderer = Label::createWithSystemFont(text, fontName, fontSize);
    }
    float textRendererWidth = textRenderer->getContentSize().width;
    float saveLeftSpaceWidth = _leftSpaceWidth ; //@cxx
    _leftSpaceWidth -= textRendererWidth;
    if (_leftSpaceWidth < 0.0f)
    {
        float overstepPercent = (-_leftSpaceWidth) / textRendererWidth;
        std::string curText = text;
        size_t stringLength = StringUtils::getCharacterCountInUTF8String(text);

        //原来代码(原来是求出整个文字长度和文字个数,用平均长度作为每个字长度,所以混输对不齐)
        //int leftLength = stringLength * (1.0f - overstepPercent);
        //std::string leftWords = Helper::getSubStringOfUTF8String(curText,0,leftLength);
        //std::string cutWords = Helper::getSubStringOfUTF8String(curText, leftLength, stringLength - leftLength);

        //begin @cxx 现在改为循环计算每个字的长度
        int leftLength = 0 ;
        std::string leftWords = "" ;
        std::string cutWords = "" ;

        float rowWidth = saveLeftSpaceWidth ; //_customSize.width ;
        int textNumber = 0  ;
        if( s_getLineTextNumber( curText, fontName, fontSize , rowWidth , textNumber) )//计算一行多少字
        {
            leftLength = textNumber ;
            leftWords = Helper::getSubStringOfUTF8String(curText,0,leftLength);
            cutWords = Helper::getSubStringOfUTF8String(curText, leftLength, stringLength - leftLength);
        }
        else
        {
            leftLength = stringLength * (1.0f - overstepPercent);
            leftWords = Helper::getSubStringOfUTF8String(curText,0,leftLength);
            cutWords = Helper::getSubStringOfUTF8String(curText, leftLength, stringLength - leftLength);
        }
        //end of @cxx

        if (leftLength > 0)
        {
            Label* leftRenderer = nullptr;
            if (fileExist)
            {
                leftRenderer = Label::createWithTTF(Helper::getSubStringOfUTF8String(leftWords, 0, leftLength), fontName, fontSize);
            }
            else
            {
                leftRenderer = Label::createWithSystemFont(Helper::getSubStringOfUTF8String(leftWords, 0, leftLength), fontName, fontSize);
            }
            if (leftRenderer)
            {
                leftRenderer->setColor(color);
                leftRenderer->setOpacity(opacity);
                pushToContainer(leftRenderer);
            }
        }

        addNewLine();
        handleTextRenderer(cutWords.c_str(), fontName, fontSize, color, opacity);
    }
    else
    {
        textRenderer->setColor(color);
        textRenderer->setOpacity(opacity);
        pushToContainer(textRenderer);
    }
}

void RichText::handleImageRenderer(const std::string& fileParh, const Color3B &color, GLubyte opacity)
{
    Sprite* imageRenderer = Sprite::create(fileParh);
    handleCustomRenderer(imageRenderer);
}

void RichText::handleCustomRenderer(cocos2d::Node *renderer)
{
    Size imgSize = renderer->getContentSize();
    _leftSpaceWidth -= imgSize.width;
    if (_leftSpaceWidth < 0.0f)
    {
        addNewLine();
        pushToContainer(renderer);
        _leftSpaceWidth -= imgSize.width;
    }
    else
    {
        pushToContainer(renderer);
    }
}

void RichText::addNewLine()
{
    _leftSpaceWidth = _customSize.width;
    _elementRenders.push_back(new Vector<Node*>());
}

void RichText::formarRenderers()
{
    if (_ignoreSize)
    {
        float newContentSizeWidth = 0.0f;
        float newContentSizeHeight = 0.0f;

        Vector<Node*>* row = (_elementRenders[0]);
        float nextPosX = 0.0f;
        for (ssize_t j=0; j<row->size(); j++)
        {
            Node* l = row->at(j);
            l->setAnchorPoint(Vec2::ZERO);
            l->setPosition(nextPosX, 0.0f);
            _elementRenderersContainer->addChild(l, 1);
            Size iSize = l->getContentSize();
            newContentSizeWidth += iSize.width;
            newContentSizeHeight = MAX(newContentSizeHeight, iSize.height);
            nextPosX += iSize.width;
        }
        _elementRenderersContainer->setContentSize(Size(newContentSizeWidth, newContentSizeHeight));
    }
    else
    {
        float newContentSizeHeight = 0.0f;
        float *maxHeights = new float[_elementRenders.size()];

        for (size_t i=0; i<_elementRenders.size(); i++)
        {
            Vector<Node*>* row = (_elementRenders[i]);
            float maxHeight = 0.0f;
            for (ssize_t j=0; j<row->size(); j++)
            {
                Node* l = row->at(j);
                maxHeight = MAX(l->getContentSize().height, maxHeight);
            }
            maxHeights[i] = maxHeight;
            newContentSizeHeight += maxHeights[i];
        }


        float nextPosY = _customSize.height;
        float realWidth = 0 ;  //@cxx
        for (size_t i=0; i<_elementRenders.size(); i++)
        {
            Vector<Node*>* row = (_elementRenders[i]);
            float nextPosX = 0.0f;
            nextPosY -= (maxHeights[i] + _verticalSpace);

            for (ssize_t j=0; j<row->size(); j++)
            {
                Node* l = row->at(j);
                l->setAnchorPoint(Vec2::ZERO);
                l->setPosition(nextPosX, nextPosY);
                _elementRenderersContainer->addChild(l, 1);
                nextPosX += l->getContentSize().width;
            }

            if( realWidth < nextPosX ) //@cxx
            {
                realWidth = nextPosX ;//@cxx
            }
        }

        float realHeight = _customSize.height - nextPosY;   //@cxx

        //富文本真实宽高
        this->xxrealSize.height = realHeight;               //@cxx
        this->xxrealSize.width = realWidth ; //_contentSize.width; //@cxx

        _elementRenderersContainer->setContentSize(_contentSize);
        delete [] maxHeights;
    }

    size_t length = _elementRenders.size();
    for (size_t i = 0; i<length; i++)
    {
        Vector<Node*>* l = _elementRenders[i];
        l->clear();
        delete l;
    }    
    _elementRenders.clear();

    if (_ignoreSize)
    {
        Size s = getVirtualRendererSize();
        this->setContentSize(s);
    }
    else
    {
        this->setContentSize(_customSize);
    }
    updateContentSizeWithTextureSize(_contentSize);
    _elementRenderersContainer->setPosition(_contentSize.width / 2.0f, _contentSize.height / 2.0f);
}

//获得富文本大小
Size RichText::xxgetRealSize()//@cxx
{
    //this->xxrealSize = this->getContentSize();
    this->formatText();   //需要手动调用
    return this->xxrealSize;
}

//下帧drawScene时调用(基类调过来),这里是富文本渲染入口
void RichText::adaptRenderers()
{
    this->formatText();
}

void RichText::pushToContainer(cocos2d::Node *renderer)
{
    if (_elementRenders.size() <= 0)
    {
        return;
    }
    _elementRenders[_elementRenders.size()-1]->pushBack(renderer);
}

void RichText::setVerticalSpace(float space)
{
    _verticalSpace = space;
}

void RichText::setAnchorPoint(const Vec2 &pt)
{
    Widget::setAnchorPoint(pt);
    _elementRenderersContainer->setAnchorPoint(pt);
}

Size RichText::getVirtualRendererSize() const
{
    return _elementRenderersContainer->getContentSize();
}

void RichText::ignoreContentAdaptWithSize(bool ignore)
{
    if (_ignoreSize != ignore)
    {
        _formatTextDirty = true;
        Widget::ignoreContentAdaptWithSize(ignore);
    }
}

std::string RichText::getDescription() const
{
    return "RichText";
}

}

NS_CC_END

3,getSubStringOfUTF8String这个截取字符串的接口要修改,它的返回值不对。

std::string Helper::getSubStringOfUTF8String(const std::string& str, std::string::size_type start, std::string::size_type length)
{
    if (length==0)
    {
        return "";
    }
    std::string::size_type c, i, ix, q, min=std::string::npos, max=std::string::npos;
    for (q=0, i=0, ix=str.length(); i < ix; i++, q++)
    {
        if (q==start)
        {
            min = i;
        }
        if (q <= start+length || length==std::string::npos)   //结束是以start位置开始,所以截取是min->max-min
        {
            max = i;
        }

        c = (unsigned char) str[i];

        if      (c<=127) i+=0;
        else if ((c & 0xE0) == 0xC0) i+=1;
        else if ((c & 0xF0) == 0xE0) i+=2;
        else if ((c & 0xF8) == 0xF0) i+=3;
        else return "";//invalid utf8
    }
    if (q <= start+length || length == std::string::npos)
    {
        max = i;
    }
    if (min==std::string::npos || max==std::string::npos)
    {
        return "";
    }
    return str.substr(min,max-min);  //@cxx 原来是: return str.substr(min,max);
}

4,在CCDevice.h中新增计算字体大小的接口,具体实现和平台相关,这里我只实现的ios平台。

static bool CalculateTextSize(const char * text, const char * fontName, float size, float &width, float &height ) ; //@cxx

5,ios平台CalculateTextSize的实现

//@cxx begin
static CGSize _my_calculateStringSize(NSString *str, id font, CGSize *constrainSize)
{
    CGSize textRect = CGSizeZero;
    textRect.width = constrainSize->width > 0 ? constrainSize->width
    : 0x7fffffff;
    textRect.height = constrainSize->height > 0 ? constrainSize->height
    : 0x7fffffff;


    CGSize dim;
    if(s_isIOS7OrHigher){
        NSDictionary *attibutes = @{NSFontAttributeName:font};
        dim = [str boundingRectWithSize:textRect options:(NSStringDrawingOptions)(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) attributes:attibutes context:nil].size;
    }
    else {
        dim = [str sizeWithFont:font constrainedToSize:textRect];
    }

    dim.width = (dim.width);
    dim.height = (dim.height);

    return dim;
}

// 计算字体大小
bool Device::CalculateTextSize(const char * text, const char * fontName, float size,  float &width, float &height )
{
    //float constrainWidth, float constrainHeight ,
    bool ret = false ;
    CGSize dim , constrainSize ;
    dim.width = 0.0f ;
    dim.height = 0.0f;
    constrainSize.width = 0.0f ; //constrainWidth ;
    constrainSize.height = 0.0f ;// constrainHeight ;
    do
    {
        CC_BREAK_IF(! text );

        NSString * str          = [NSString stringWithUTF8String:text];
        NSString * fntName      = [NSString stringWithUTF8String:fontName];

        fntName = [[fntName lastPathComponent] stringByDeletingPathExtension];

        // create the font
        id font = [UIFont fontWithName:fntName size:size];

        if (font)
        {
            dim = _my_calculateStringSize(str, font, &constrainSize);
        }
        else
        {
            if (!font)
            {
                font = [UIFont systemFontOfSize:size];
            }

            if (font)
            {
                dim = _my_calculateStringSize(str, font, &constrainSize);
            }
        }

        if (font)
        {
            ret = true ;
        }
    }while(false);

    width = dim.width ;
    height = dim.height ;

    return ret;
}
//@cxx end

测试示例

auto _richText = RichText::create();
_richText->ignoreContentAdaptWithSize(false);
_richText->setContentSize(Size(100, 100));

auto str1 = "是减肥了快iii速的减肥了快SD卡路附近ffm国恢复共和国fkdsjfkldsj假了jjiij经济ii的快速减肥了mshfjksdhKSDFHJKDSJFKLlfj。、里的时刻福建路口的.sdfdsjflkjlksdjfl JlkdsjflkdsfjkldsjflkdsjfkljLjkd是福建代理商福建路口的";

RichElementText* re1 = RichElementText::create(1, Color3B::WHITE, 255, str1, "Marker Felt", 20);

_richText->pushBackElement(re1);
_richText->setPosition(Vec2(visibleSize.width / 2, visibleSize.height / 2 + 100));
this->addChild(_richText);

系统默认的对齐效果图:
Cocos2dx-- 聊天系统之富文本(RichText)与字体对齐_第2张图片

修改后的对齐效果图:
Cocos2dx-- 聊天系统之富文本(RichText)与字体对齐_第3张图片

你可能感兴趣的:(富文本,cocos2dx,聊天系统,richtext,富文本字体对齐)