CoreText实现气泡图文混排

来源:互联网 发布:mac哑光橘红色 编辑:程序博客网 时间:2024/05/19 03:22
CoreText实现图文混排的原理大概是:通过attributedString拼接,设置string的各段(不是段落,是每个被拼接的子串)的属性,其中图片部分可以用“ ”来拼接,并设置代理回调,这个代理回调就会咨询图片的宽高,然后CT就会预留对应空间,最后,你只需要把对应图片绘制在对应的位置即可。
    对应的位置,这说来就是另外一个难搞的问题了,接下来我们就来细说一下:

   CoreText的组成其实可以理解为一个frame(注意这里不是UIView的frame,而是另一个概念,可以理解为帧),这个frame是怎么来的呢?其实就是根据你给定的一个attributedString和一个绘制区域,CoreText自动将能放下的文字放到这个区域,然后形成了一个帧,我们知道,如果区域大小不同,那么能放下的文字不同,比如,你给一张A4纸张,要写下字体15的字,可能最多只能写2000字,但是给一张更大的纸,可能就可以写下1万字,那多余的字哪去了?很抱歉,CoreText帮你忽略了,那我就这么个区域,能写下多少字呢?这个很幸运的是,CT有给你接口,
CFRange frameRange=CTFrameGetVisibleStringRange(ctFrame);这个返回值就是告诉你,我这里写了哪些范围的字了。那你可能就又问,那我想把所有字写出来,我应该准备多大的纸是吧,很好,这个CT也有接口给我们,一般准备多大张的纸,一定的文字量,如果给的纸张的宽度越大,那么需要的纸张的高度就肯定越小,而且一般我们会限定纸张的宽度(比如手机看的话,手机宽度一般固定,手机高度不够我们可以用滚动视图,来保证足够高)。
CGSize sz = CTFramesetterSuggestFrameSizeWithConstraints(framesetter,CFRangeMake(0,string.length),NULL,_constrainSize,&fitCFRange); 

以上方法就是,在约定的不超出的范围内,需要用到得区域大小。


说到这里,大家应该就明白了,一个聊天气泡应该需要多大了。但是依然没有说到,聊天表情应该绘制在哪个位置。

上面我们说了frame,frame其实就是由N行字组成的,那每行字又是由文字片段组成,这里文字片段是啥概念呢?并不是一个文字一个片,可能是属性很相似的的几个字符组成的字符串,字数不定。(更具体可参考另一篇日志:
http://user.qzone.qq.com/511107989/blog/1369214175

CT给我们提供了一个接口,告诉了我们每一行的起始坐标位置,然后有给了一个接口,告诉我们该行每个文字片段距离起始位置的偏移。而我们是可以通过这个文字片,获取到它所在的文字区域的设为A,这时候好了,我们本来就应该知道表情的区域的(设为B),因此,只要A和B相等,那么这个表情的具体坐标我们就可以知道了,而表情的大小,我们也是早就在回调里设置了得。到这里,我们就可以完整的知道表情应该绘制的位置和大小(frame)。我们就可以在绘制完了帧后,在对应的位置绘制image,就完成了图文混排的效果,同样,如果你要显示的事gif效果图,你可以在对应位置加上UIImageView即可。

上面说到的,我们本该知道的表情区域,是怎么知道的?其实用QQ聊天举例,当我们点击表情后,输入框就会追加一个\表情代号,比如“你太天真了\天真,努力学习吧\奋斗”,其实对这么一句话,我们可以用正则表达式,获取到每个表情的区域的。

  NSArray* chunks = [regex matchesInString:text options:0

                                       range:NSMakeRange(0, [text length])];

    int lastP=0;

    NSMutableArray *pieces=[NSMutableArray array];

    for (NSTextCheckingResult* chunk in chunks)

    {

        NSRange range=chunk.range;
    } 




最后绘制:

    CTFrameDraw((CTFrameRef)ctFrame, context);

    NSPredicate *pre=[NSPredicate predicateWithFormat:@"SELF.type=%d",ChatPieceTypeImage];

    NSArray *iamges=[_chatPieces filteredArrayUsingPredicate:pre];

    for(ChatPiece *imagePice in iamges){ 

        UIImage *img=[UIImage imageNamed:imagePice.text];

        CGContextDrawImage(context, imagePice.frame, img.CGImage);

        NSLog(@"drawing image at :%@",NSStringFromCGRect(imagePice.frame));

    }




那上面的frame是怎么来的?上面提到了,就是通过字符串,生成来的。

 CTFramesetterRef  framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)[selfattributeString]);

 CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(00), path, NULL);

上面参数里的path,也就是所谓的纸张区域了。



那上面提到的,图片用“ ”代替,然后设置回调,又是怎么实现的呢?


-(NSAttributedString *)string

{

    NSString *st=_text;

    NSDictionary *attrDictionaryDelegate;

    if (_type==ChatPieceTypeImage) {

        

        UIImage *image=[UIImage imageNamed:_text];

        st=@" ";

        NSNumber *width=[NSNumber numberWithFloat:image.size.width];

        NSNumber *height=[NSNumber numberWithFloat:image.size.height];

        

        NSDictionary* imgAttr = [NSDictionary dictionaryWithObjectsAndKeys//2

                                  width, @"width",

                                  height, @"height",

                                  nil];

        CTRunDelegateCallbacks callbacks;

        callbacks.version = kCTRunDelegateVersion1;

        callbacks.getAscent = ascentCallback;

        callbacks.getDescent = descentCallback;

        callbacks.getWidth = widthCallback;

        callbacks.dealloc = deallocCallback;

        

        CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)(imgAttr)); //3

        attrDictionaryDelegate = [NSDictionary dictionaryWithObjectsAndKeys:

                                                //set the delegate

                                                (__bridge id)delegate, (NSString*)kCTRunDelegateAttributeName,

                                                nil];

        

    }

    NSAttributedString *attString=[[NSAttributedString allocinitWithString:st attributes:attrDictionaryDelegate];

    

    return attString;

}


static void deallocCallback( void* ref ){

    //[(__bridge id)ref release];

}

static CGFloat ascentCallback( void *ref ){

    return [(NSString*)[(__bridge NSDictionary*)ref objectForKey:@"height"floatValue]*3/4.;

}

static CGFloat descentCallback( void *ref ){

    return [(NSString*)[(__bridge NSDictionary*)ref objectForKey:@"height"floatValue]*1/4.;//[(NSString*)[(__bridge NSDictionary*)ref objectForKey:@"descent"] floatValue];

}

static CGFloat widthCallback( void* ref ){

    float w=[(NSString*)[(__bridge NSDictionary*)ref objectForKey:@"width"floatValue];


    return w;

}





其实写到这里,我发现不知道为什么原因,如果表情是最后一个文字片,好像会出点小问题,我的做法是在后面追加了一个结束符。

-(void)setChatPieces:(NSArray *)chatPieces

{

    ChatPiece *p=[chatPieces lastObject];

    if (p.type==ChatPieceTypeImage) {

        ChatPiece *piece=[[ChatPiece allocinit];

        piece.text=@"\0";

        chatPieces=  [chatPieces arrayByAddingObject:piece];

    }

    _chatPieces=chatPieces;

    //这里还得设置ctframe  

}

1 0