zoukankan      html  css  js  c++  java
  • Coretext实现图文混排及Gif图片播放

    CoreText是iOS3.2推出的一套文字排版和渲染框架,可以实现图文混排,富文本显示等效果。

    CoreText中的几个重要的概念: 

    1. CTFont
    2. CTFontCollection
    3. CTFontDescriptor
    4. CTFrame
    5. CTFramesetter
    6. CTGlyphInfo
    7. CTLine
    8. CTParagraphStyle
    9. CTRun
    10. CTTextTab
    11. CTTypesetter

    先来了解一下该框架的整体视窗组合图:

    CTFrame 作为一个整体的画布(Canvas),其中由行(CTLine)组成,而每行可以分为一个或多个小方块(CTRun)。

    注意:你不需要自己创建CTRun,Core Text将根据NSAttributedString的属性来自动创建CTRun。每个CTRun对象对应不同的属性,正因此,你可以自由的控制字体、颜色、字间距等等信息。

    此外还有一点需要注意:一个CTRun是不能跨行的,若是一段文字拥有相同的属性,且跨行,则会被分在多个CTRun当中,每个CTRun拥有相同属性。

    首先来看看使用Coretext的基本步骤:

    第一步:

    要有一个NSMutableAttributedString,用一个字符串来初始化NSMutableAttributedString。

    NSMutableAttributedString  * _mString = [[NSMutableAttributedString alloc] initWithString:_text];

    第二步:对NSMutableAttributedString进行属性设置。

       [_mString beginEditing];

            [_mString addAttributes:textAttribute.attributeDic range:attr.range];

            [_mString addAttribute:@"MTText" value:attr.text range:attr.range];

            [_mString endEditing];

    在这里有两种方式,一个是设置单个属性,一个是直接批量设置属性。属性的key值可以是自己定义的。

    以下是一些常见的属性设置

          1.设置字体属性 

    CTFontRef font = CTFontCreateWithName(CFSTR("Georgia"), 40, NULL);  
     [_mString addAttribute:(id)kCTFontAttributeName value:(id)font range:NSMakeRange(0, 4)]; 

       2.设置斜体字    

    CTFontRef font = CTFontCreateWithName((CFStringRef)[UIFont italicSystemFontOfSize:20].fontName, 14, NULL);  
     [_mString addAttribute:(id)kCTFontAttributeName value:(id)font range:NSMakeRange(0, 4)];  

     

      3.设置连字

    long number = 1;  
    CFNumberRef num = CFNumberCreate(kCFAllocatorDefault,kCFNumberSInt8Type,&number);  
    [mabstring addAttribute:(id)kCTLigatureAttributeName value:(id)num range:NSMakeRange(0, [str length])];  

       

      4.设置下划线

    [_mString addAttribute:(id)kCTUnderlineStyleAttributeName value:(id)[NSNumber numberWithInt:kCTUnderlineStyleDouble] range:NSMakeRange(0, 4)];   

      

      5.设置下划线颜色

    [_mString addAttribute:(id)kCTUnderlineColorAttributeName value:(id)[UIColor redColor].CGColor range:NSMakeRange(0, 4)];

      

      6.设置字体间隔

    long number = 10;  
    CFNumberRef num = CFNumberCreate(kCFAllocatorDefault,kCFNumberSInt8Type,&number);  
    [_mString addAttribute:(id)kCTKernAttributeName value:(id)num range:NSMakeRange(10, 4)];  

     

    最后是画出对应的图像了

      

        CGContextRef context = UIGraphicsGetCurrentContext();

         CGContextTranslateCTM(context , 0 ,self.bounds.size.height);

         CGContextScaleCTM(context, 1.0, -1.0);

      CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString(
                                                        (CFAttributedStringRef) _mString);
        
        CGMutablePathRef path = CGPathCreateMutable();
        CGRect rects = CGRectMake(0 , 0 ,self.bounds.size.width , self.bounds.size.height);
        CGPathAddRect(path,
                      NULL ,
                      rects);
        
        CTFrameRef frame = CTFramesetterCreateFrame(frameSetter,
                                                       CFRangeMake(0, 0),
                                                       path,
                                                       NULL);
        _frameRef = frame;
    
        CTFrameDraw(frame,context);
        
        CGPathRelease(path);
        CFRelease(frameSetter);

    这里有几点比较需要注意:

      1.坐标转换问题。在使用CGcontext进行绘制时坐标轴圆点在屏幕左下方,而UIKit得坐标系圆点在左上方,需要对此进行转换。此外,在后面的操作中也要用到坐标变换。

      2.文字的绘画区域不是整个context,而是在CTFrameRef中,后面我们还会碰到相关的问题。

    CTFrameRef frame = CTFramesetterCreateFrame(frameSetter,
                                                       CFRangeMake(0, 0),
                                                       path,
                                                       NULL);

    现在我们已经能设置文字的基本属性了,但富文本中还有比较重要的一个应用:图文混排

    我们先来说说实现图文混排的基本思路。

    coretext是直接绘制在layer层上的,我们可以在drawRect 方法中直接画一张图片,只要将图片放在适合的位置,就实现了图文混排。

    现在的关键是如何计算出图片所在的位置,此外,当图片添加的时候,文字的排版要如何调整问题。

    为了能插入图片,首先我们要设置一个占位符,正常设置为空格符,因为图片如果没有完全覆盖那个区域,可能显示出占位符。当然你也可以任意设置一个字符并将其

    的颜色设置为clearColor。设置占位符后我们要为它设置属性,

    记住,要为它设置一个单独的属性,不能与相邻的字符属性一致,因为这样系统可能因此将他们合并在一个CTRun当中。

    占位符最好只设置一个,如果占位符是多个的话,有可能占位符处于不同行,会被分成两个CTRun,此时图片就可能超出屏幕边界。

    NSMutableAttributedString *replaceStr = [[NSMutableAttributedString alloc] initWithString:@"1"];

    UIColor *color = [UIColor clearColor];

    NSRange range = NSMakeRange(_mString.length - 1, 1);

    [replaceStr addAttribute:(id)color.CGColor value:(id)kCTForegroundColorAttributeName range:range];

    [_mString appendAttributedString:replaceStr];

    设置完占位符基本属性后,我们需要设置占位符对应得CTRun的回调方法来设置CTRun的大小,以适应图片。 CTRunDelegateCallbacks imageCallBacks;

     imageCallBacks.version = kCTRunDelegateVersion1;
     imageCallBacks.dealloc = RunDelegateDeallocCallback;
     imageCallBacks.getAscent = RunDelegateGetAsent;
     imageCallBacks.getDescent = RunDelegateGetDescent;
     imageCallBacks.getWidth = RunDelegateGetWidthCallBack;
    //传入的参数attr.text可以在回调方法中使用 CTRunDelegateRef runDelegate
    = CTRunDelegateCreate(&imageCallBacks, (__bridge void *)(attr.text)); CTRunDelegateGetRefCon(runDelegate); [_mString addAttribute:(NSString *)kCTRunDelegateAttributeName value:(__bridge id)runDelegate range:attr.range]; [_mString addAttribute:@"imageName" value:attr.text range:attr.range]; CFRelease(runDelegate);



    void
    RunDelegateDeallocCallback{

    }
    
    

    CGFloat RunDelegateGetAsent(void *refCon) {

    
    

        NSString *imageName = (__bridge NSString *)(refCon);

        return [UIImage imageNamed:imageName].size.height;

    }

    CGFloat RunDelegateGetDescent(void *refCon) {   

      return 0;

    }

    CGFloat RunDelegateGetWidth(void *refCon) {

       NSString *imageName = (__bridge NSString *)(refCon);

        return [UIImage imageNamed:imageName].size.width;

    }

    在设置好属性还有回调方法之后,就可以开始绘制图片了。

    基本的思路是获取文本的每一行,再获取每一个CTRun,根据属性来判断是否是绘制图片的点,是的话则获取绘画区域,绘制图片。

        CFArrayRef lines = CTFrameGetLines(_frameRef);
        CGPoint origins[CFArrayGetCount(lines)];
        CTFrameGetLineOrigins(_frameRef, CFRangeMake(0, 0), origins);
        
        NSMutableArray *attrArray = [[NSMutableArray alloc] init];
        for (int i = 0; i < CFArrayGetCount(lines); i ++) {
            
            CTLineRef line = CFArrayGetValueAtIndex(lines, i);
            
            CFArrayRef runs = CTLineGetGlyphRuns(line);
            
            for (int k = 0; k < CFArrayGetCount(runs); k ++) {
                
                CTRunRef run = CFArrayGetValueAtIndex(runs, k);
                NSDictionary *attri = (NSDictionary *)CTRunGetAttributes(run);
                NSString *imageName = [attri objectForKey:@"imageName"];
                
                if (imageName) {
                    CGFloat runAsent;
                    CGFloat runDescent;
                    CGPoint origin = origins[i];
                    CGRect runRect;
                    
                    runRect.size.width = CTRunGetTypographicBounds(run,
                                                                   CFRangeMake(0, 0),
                                                                   &runAsent,
                                                                   &runDescent,
                                                                   NULL);
                    CGFloat offset = CTLineGetOffsetForStringIndex(line,
                                                                   CTRunGetStringRange(run).location,
                                                                   NULL);
                    runRect = CGRectMake(origin.x + offset,
                                         origin.y - runDescent,
                                         runRect.size.width,
                                         runAsent + runDescent);
                    

              UIImage *image = [UIImage imageNamed:imageName];

                      CGContextDrawImage(context, runRect, image.CGImage);

    
                }
            }
        }

    这里有个需要注意的点,我们取到得位置是相对于整个画布,而不是相对于所在的View的位置

    CTFrameRef frame = CTFramesetterCreateFrame(frameSetter,
                                                       CFRangeMake(0, 0),
                                                       path,
                                                       NULL);

    因此,如果CTFrameref如果有不在原点,计算时需要加上这部分的偏移量。

    点击事件和图片绘制差不多,获取点击的点,进行坐标变换

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        
        UITouch *touch = touches.anyObject;
        CGPoint point = [touch locationInView:self];
      //坐标变换 CGPoint location
    = CGPointMake(point.x, self.bounds.size.height - point.y); MTLabelAttribute *attr = [self getAttributeByLocation:location]; _lastAttr = attr; if (attr) { if ([self.delegate respondsToSelector:@selector(clickWithAttibute:andText:)]) { [self.delegate clickWithAttibute:attr andText:attr.text]; } [self setAttributeWithType:@"hightlight" andAttribute:attr]; } //判断point是否在点击的文字范围内 }

    先判断在哪一行,然后判断点击的点在哪一个CTRun上。在这里要注意所要实现点击的字符串可能跨行的情况。

    - (MTLabelAttribute *)getAttributeByLocation:(CGPoint) point{
        
        NSArray *lines = (NSArray *)CTFrameGetLines(_frameRef);
        
        CGPoint origins[lines.count];
        CTFrameGetLineOrigins(_frameRef, CFRangeMake(0, 0), origins);
        CTLineRef ref;
       
        int count = 0;
       
    //判断所在的行
    if (point.y < origins[lines.count - 1].y) { return nil; } for (int i = 1; i < lines.count ; i ++) { CGFloat minY = origins[i].y; CGFloat maxY = origins[i - 1].y; if (point.y >= minY && point.y <= maxY) { count = i ; break; } } ref = (__bridge CTLineRef)lines[count]; CGPoint origin = origins[count]; NSArray *ctRuns = (NSArray *)CTLineGetGlyphRuns(ref); //判断所在的CTRun for (int k = 0; k < ctRuns.count; k ++) { CTRunRef runTest = (__bridge CTRunRef)([ctRuns objectAtIndex:k]); CGFloat offset = CTLineGetOffsetForStringIndex((CTLineRef)lines[count], CTRunGetStringRange(runTest).location, NULL) + 0.0; CGPoint firstPoint = CGPointMake(origin.x + offset , origin.y); CGFloat ascent; CGFloat descent; CGFloat leading; CGFloat width = CTRunGetTypographicBounds(runTest, CFRangeMake(0, 0), &ascent, &descent, &leading); if ( point.x >= firstPoint.x &&point.x <= firstPoint.x + width &&point.y <= origin.y + ascent &&point.y >= origin.y ) { NSDictionary *dic = (NSDictionary *)CTRunGetAttributes(runTest); NSString *string = [dic objectForKey:@"MTText"]; CFRange cfRange = CTRunGetStringRange(runTest); if ([dic objectForKey:@"imageName"]) { return [self getAttributesWithRange:NSMakeRange(cfRange.location, cfRange.length)]; }
           //跨行情况处理 NSString
    *subString = [self.text substringWithRange: NSMakeRange(cfRange.location, cfRange.length)]; NSRange range; NSRange subStringRange = [string rangeOfString:subString]; range = NSMakeRange(cfRange.location - subStringRange.location, string.length); return [self getAttributesWithRange:range]; } } return nil; }

    最后,是Gif的显示

      gif是比较特殊的一种情况,处理起来也比较麻烦。对于gif有两种展示方式,一种是用一个专门的UIView来展示,然后用添加subView的方式使用。用这种方式可以使用

    第三方的框架,使用UIWebView播放等,也可以自己写,下面是使用UIImageView的方式:详情可看http://www.cocoachina.com/bbs/read.php?tid=124430

    UIImageView *gifImageView = [[UIImageView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
        NSArray *gifArray = [NSArray arrayWithObjects:[UIImage imageNamed:@"1"],
                                                      [UIImage imageNamed:@"2"],
                                                      [UIImage imageNamed:@"3"],
                                                      [UIImage imageNamed:@"4"],
                                                      [UIImage imageNamed:@"5"],
                                                      [UIImage imageNamed:@"6"],
                                                      [UIImage imageNamed:@"7"],
                                                      [UIImage imageNamed:@"8"],
                                                      [UIImage imageNamed:@"9"],
                                                      [UIImage imageNamed:@"10"],
                                                      [UIImage imageNamed:@"11"],
                                                      [UIImage imageNamed:@"12"],
                                                      [UIImage imageNamed:@"13"],
                                                      [UIImage imageNamed:@"14"],
                                                      [UIImage imageNamed:@"15"],
                                                      [UIImage imageNamed:@"16"],
                                                      [UIImage imageNamed:@"17"],
                                                      [UIImage imageNamed:@"18"],
                                                      [UIImage imageNamed:@"19"],
                                                      [UIImage imageNamed:@"20"],
                                                      [UIImage imageNamed:@"21"],
                                                      [UIImage imageNamed:@"22"],nil];
        gifImageView.animationImages = gifArray; //动画图片数组
        gifImageView.animationDuration = 5; //执行一次完整动画所需的时长
        gifImageView.animationRepeatCount = 1;  //动画重复次数
        [gifImageView startAnimating];
        [self.view addSubview:gifImageView];
        [gifImageView release]; 

    使用UIImageView的方式是固定的时间间隔,但gif并非每一帧的间隔都一样,因此有些情况可能达不到最好的播放效果。

    在IOS7之后可以使用TextKit中的attachment,直接添加UIWebView播放Gif图片,YYTextKit中也有类似的实现。

    如果我们要手动实现的话,那就只好一帧一帧的往屏幕上画了。我们首先来看看继承UIView的实现方式,记住不能继承自caLayer,虽然最后是在layer层绘画,

    但直接继承calyer,然后用addsublayer方法显示,图像会有重影,大概UIView中有对其进行处理。 

    首先用一个类对Gif图片进行解析

    @interface MTGifAttribute : NSObject
    
    @property (nonatomic, strong, readonly) NSArray *imageFrames;
    @property (nonatomic, strong, readonly) NSArray *properties;
    @property (nonatomic, strong, readonly) NSArray *delayTimes;
    
    @property (nonatomic, assign, readwrite) UIView<MTGifProtocol> * delegate;
    @property (nonatomic, assign, readwrite) CGRect frame;
    //@property (nonatomic, copy) NSString *path;
    @property (nonatomic, assign, readonly) NSInteger index;
    
    - (void)setImageInfoWithFilePath:(NSString *)path;
    - (void)startAnitation;
    
    @end

    解析方法

    @implementation MTGifAttribute
    
    - (instancetype)init{
        
        self = [super init];
        if (self) {
            _index = 0;
        }
        return self;
    }
    
    - (void)setImageInfoWithFilePath:(NSString *)path {
        
        NSMutableArray *imageFrames = [[NSMutableArray alloc] init];
        NSMutableArray *delayTimes = [[NSMutableArray alloc] init];
        
        NSURL *filrUrl = [NSURL fileURLWithPath:path];
        CFURLRef cfUrl = (__bridge CFURLRef)filrUrl;
        
        CGImageSourceRef gifSource = CGImageSourceCreateWithURL(cfUrl, NULL);
        NSInteger count = CGImageSourceGetCount(gifSource);
        
        for (int i = 0; i < count; i++){
            CGImageRef frame = CGImageSourceCreateImageAtIndex(gifSource,
                                                               i,
                                                               NULL);
            
            [imageFrames addObject:(__bridge id)frame];
            CGImageRelease(frame);
            
            NSDictionary *dic = CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(gifSource,
                                                                                     i,
                                                                                     NULL));
            
            NSDictionary *gifDic =[dic valueForKey:(NSString *)kCGImagePropertyGIFDictionary];
            [delayTimes addObject:[gifDic objectForKey:(NSString *)kCGImagePropertyGIFDelayTime]];
            
        }
        
        NSDictionary *dic = CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(gifSource,
                                                                                 0,
                                                                                 NULL));
        
        CGFloat gifWidth = (CGFloat)[[dic valueForKey:(NSString*)kCGImagePropertyPixelWidth]
                                     floatValue];
        
        CGFloat gifHeight = (CGFloat)[[dic valueForKey:(NSString*)kCGImagePropertyPixelHeight]
                                      floatValue];
        
        _frame = CGRectMake(0, 0, gifWidth, gifHeight);
        _imageFrames = imageFrames;
        _delayTimes = delayTimes;
    }
    
    - (void)startAnitation {
        [self changeImage];
    }
    
    
    - (void)changeImage {
          
      //代理方法,调用setNeeddisplay
    if ([self.delegate respondsToSelector:@selector(disPlayInRect:)]) { [self.delegate disPlayInRect:self.frame]; }else{ return; } _index ++; _index = _index % self.imageFrames.count; CGFloat delay = [[self.delayTimes objectAtIndex:_index] floatValue]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self changeImage]; }); }


    图片索引切换方法

    - (void)startAnitation {
        [self changeImage];
    }
    
    
    - (void)changeImage {
        
        if ([self.delegate respondsToSelector:@selector(disPlayInRect:)]) {
            [self.delegate disPlayInRect:self.frame];
        }else{
        
            return;
        }
    
        _index ++;
        _index = _index % self.imageFrames.count;
        CGFloat delay = [[self.delayTimes objectAtIndex:_index] floatValue];
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
                           (int64_t)(delay * NSEC_PER_SEC)),
                           dispatch_get_main_queue(), ^{
        
                               [self changeImage];
        
                           });
    }

    继承UIView,重写drawRect方法。

    - (void)drawRect:(CGRect)rect{
        
        
        CGContextRef ctx = UIGraphicsGetCurrentContext();
        CGContextTranslateCTM(ctx , 0 ,self.frame.size.height);
        CGContextScaleCTM(ctx, 1.0, -1.0); 
        
        UIImage *image = [_gifImage.imageFrames objectAtIndex:_index];
        CGImageRef cgimage = image.CGImage;
        UIGraphicsBeginImageContext(CGSizeMake(self.frame.size.width, self.frame.size.height));
        CGContextDrawImage(ctx, _gifImage.frame, cgimage);
        
    
    }

    至此Gif的显示已经完成,但有些时候我们不想单独用一个view来显示。而是想用coretext图文混排的方式来显示,将gif和文字显示在同一个view。

     此时的方式和用一个单独的view显示一样,只是绘画的对象不同而已。

    比较需要注意的一个点是,在每次重绘的时候,因为gif不断地重绘,如果每次都刷新整个View的话有可能会造成性能问题。因此可以使用

    [self setNeedsDisplayInRect:rect];方法,只刷新gif所在区域。

    当然,在这种情况下也要万分注意左边变换问题,如果刷新的区域,与变换坐标后的绘画区域不对应,图片会消失或者只显示部分。

  • 相关阅读:
    C++ 二元作用域运算符(::)
    C 桶排序
    C 递归的选择排序
    C 归并算法
    C 可变长实参列表
    C条件编译的一些例子
    C实现将中缀算术式转换成后缀表达式
    Activiti6-数据库配置-dbconfig(学习笔记)
    idea在Terminal中使用maven指令
    Spring Boot的web开发
  • 原文地址:https://www.cnblogs.com/bigly/p/5796684.html
Copyright © 2011-2022 走看看