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"), , NULL);
[_mString addAttribute:(id)kCTFontAttributeName value:(id)font range:NSMakeRange(, )];

  2.设置斜体字    

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

 

  3.设置连字

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

  

  4.设置下划线

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

  

  5.设置下划线颜色

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

  

  6.设置字体间隔

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

 

最后是画出对应的图像了

  

    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( , ,self.bounds.size.width , self.bounds.size.height);
CGPathAddRect(path,
NULL ,
rects); CTFrameRef frame = CTFramesetterCreateFrame(frameSetter,
CFRangeMake(, ),
path,
NULL);
_frameRef = frame; CTFrameDraw(frame,context); CGPathRelease(path);
CFRelease(frameSetter);

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

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

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

CTFrameRef frame = CTFramesetterCreateFrame(frameSetter,
CFRangeMake(, ),
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(, ), origins); NSMutableArray *attrArray = [[NSMutableArray alloc] init];
for (int i = ; i < CFArrayGetCount(lines); i ++) { CTLineRef line = CFArrayGetValueAtIndex(lines, i); CFArrayRef runs = CTLineGetGlyphRuns(line); for (int k = ; 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(, ),
&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(, ),
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(, ), origins);
CTLineRef ref; int count = ;

//判断所在的行
if (point.y < origins[lines.count - ].y) {
return nil;
} for (int i = ; i < lines.count ; i ++) { CGFloat minY = origins[i].y;
CGFloat maxY = origins[i - ].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 = ; 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(, ),
&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:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],
[UIImage imageNamed:@""],nil];
gifImageView.animationImages = gifArray; //动画图片数组
gifImageView.animationDuration = ; //执行一次完整动画所需的时长
gifImageView.animationRepeatCount = ; //动画重复次数
[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 = ;
}
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 = ; 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,
,
NULL)); CGFloat gifWidth = (CGFloat)[[dic valueForKey:(NSString*)kCGImagePropertyPixelWidth]
floatValue]; CGFloat gifHeight = (CGFloat)[[dic valueForKey:(NSString*)kCGImagePropertyPixelHeight]
floatValue]; _frame = CGRectMake(, , 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 , ,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所在区域。

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

Coretext实现图文混排及Gif图片播放的更多相关文章

  1. 【iOS】使用CoreText实现图文混排

    iOS没有现成的支持图文混排的控件,而要用多个基础控件组合拼成图文混排这样复杂的排版,是件很苦逼的事情.对此的解决方案有使用CoreText进行绘制,或者使用TextKit.本文主要讲解对于CoreT ...

  2. CoreText实现图文混排之点击事件-b

    CoreText实现图文混排之点击事件 主要思路 我们知道,CoreText是基于UIView去绘制的,那么既然有UIView,就有 -(void)touchesBegan:(NSSet<UIT ...

  3. CoreText 实现图文混排

    CoreText 实现图文混排 相关博文推荐 IOS CoreText.framework - 基本用法 IOS CoreText.framework - 段落样子CTParagraphStyle h ...

  4. CoreText实现图文混排之点击事件

    今天呢,我们继续把CoreText图文混排的点击事件补充上,这样我们的图文混排也算是圆满了. 哦,上一篇的链接在这里 http://www.jianshu.com/p/6db3289fb05d Cor ...

  5. CoreText实现图文混排

    CoreText的介绍 Core Text 是基于 iOS 3.2+ 和 OSX 10.5+ 的一种能够对文本格式和文本布局进行精细控制的文本引擎.它良好的结合了 UIKit 和 Core Graph ...

  6. CoreText实现图文混排之文字环绕及点击算法

    系列文章: CoreText实现图文混排:http://www.jianshu.com/p/6db3289fb05d CoreText实现图文混排之点击事件:http://www.jianshu.co ...

  7. Android中Textview显示Html,图文混排,支持图片点击放大

    本文首发于网易云社区 对于呈现Html文本来说,Android提供的Webview控件可以得到很好的效果,但使用Webview控件的弊端是效率相对比较低,对于呈现简单的html文本的话,杀鸡不必使用牛 ...

  8. DIV+CSS 图文混排的图片居中办法

    不少人为了让 Div 图文混排的图片可以居中,给 IMG 套各式各样的 SPAN.DIV.LI 等等,以便于使用 text-align来进行居中. <div>图文混排 <br> ...

  9. 简单的Coretext 图文混排

    在很多新闻类或有文字展示的应用中现在都会出现图文混排的界面例如网易新闻等,乍一看去相似一个网页,其实这样效果并非由UIWebView 加载网页实现.现在分享一种比较简单的实现方式 iOS sdk中为我 ...

随机推荐

  1. Flex 利用Space控制进行组件的右对齐

    Spacer 控件可帮助您布置父容器中的子项.虽然 Spacer 控件不会绘制任何内容,但它会在父容器中为其本身分配空间. 在以下示例中,使用灵活的 Spacer 控件将 Button 控件推到右侧, ...

  2. Python实例4

    4.输入某年某月某日,判断这一天是这一年的第几天? 正解: 源码:

  3. google 版本号49之后chrome的跨域设置

  4. python ABC

    因为项目需要,总是会有各种各样要重命名文件的场合,manual的方法当然不可取,bat的方法又感觉不够强大,所以就从零开始学python,就为了能够自动批量修改文件名,倒腾了一个周六,总算可以了 :) ...

  5. 论文笔记--Fast RCNN

    很久之前试着写一篇深度学习的基础知识,无奈下笔之后发现这个话题确实太大,今天发一篇最近看的论文Fast RCNN.这篇文章是微软研究院的Ross Girshick大神的一篇作品,主要是对RCNN的一些 ...

  6. unity行为树制作AI简单例子(1)

    用行为树来制作AI是非常方便的,今天就给大家简单介绍一下行为树的强大之处. 所用插件 Behavior Designer v1.421 最开始 我使用过Rain插件,不过用过Behavior Desi ...

  7. javaWeb——图片验证

    publicvoid doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, ...

  8. VS2010 使用 EntityFramework For SQL Server Compact 4.0

    1.安装 SSCERuntime_x64-ENU.exe 或 SSCERuntime_x86-ENU.exe 2.安装 SSCEVSTools-CHS.msi 3.然后就可以通过 SQL Server ...

  9. C#动态编译并执行代码

    先来张运行时截图: using System; using System.Collections.Generic; using System.ComponentModel; using System. ...

  10. ListMultimap 容器

    ListMultimap 最近翻flume源码,在用的容器.之前自己写代码的时候,还苦恼过有时候有这种需求,原来google的工程师都已经解决了这些问题,这很google. 记录下,有时间来总结下相应 ...