Coretext实现图文混排及Gif图片播放
CoreText是iOS3.2推出的一套文字排版和渲染框架,可以实现图文混排,富文本显示等效果。
CoreText中的几个重要的概念:
- CTFont
- CTFontCollection
- CTFontDescriptor
- CTFrame
- CTFramesetter
- CTGlyphInfo
- CTLine
- CTParagraphStyle
- CTRun
- CTTextTab
- 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图片播放的更多相关文章
- 【iOS】使用CoreText实现图文混排
iOS没有现成的支持图文混排的控件,而要用多个基础控件组合拼成图文混排这样复杂的排版,是件很苦逼的事情.对此的解决方案有使用CoreText进行绘制,或者使用TextKit.本文主要讲解对于CoreT ...
- CoreText实现图文混排之点击事件-b
CoreText实现图文混排之点击事件 主要思路 我们知道,CoreText是基于UIView去绘制的,那么既然有UIView,就有 -(void)touchesBegan:(NSSet<UIT ...
- CoreText 实现图文混排
CoreText 实现图文混排 相关博文推荐 IOS CoreText.framework - 基本用法 IOS CoreText.framework - 段落样子CTParagraphStyle h ...
- CoreText实现图文混排之点击事件
今天呢,我们继续把CoreText图文混排的点击事件补充上,这样我们的图文混排也算是圆满了. 哦,上一篇的链接在这里 http://www.jianshu.com/p/6db3289fb05d Cor ...
- CoreText实现图文混排
CoreText的介绍 Core Text 是基于 iOS 3.2+ 和 OSX 10.5+ 的一种能够对文本格式和文本布局进行精细控制的文本引擎.它良好的结合了 UIKit 和 Core Graph ...
- CoreText实现图文混排之文字环绕及点击算法
系列文章: CoreText实现图文混排:http://www.jianshu.com/p/6db3289fb05d CoreText实现图文混排之点击事件:http://www.jianshu.co ...
- Android中Textview显示Html,图文混排,支持图片点击放大
本文首发于网易云社区 对于呈现Html文本来说,Android提供的Webview控件可以得到很好的效果,但使用Webview控件的弊端是效率相对比较低,对于呈现简单的html文本的话,杀鸡不必使用牛 ...
- DIV+CSS 图文混排的图片居中办法
不少人为了让 Div 图文混排的图片可以居中,给 IMG 套各式各样的 SPAN.DIV.LI 等等,以便于使用 text-align来进行居中. <div>图文混排 <br> ...
- 简单的Coretext 图文混排
在很多新闻类或有文字展示的应用中现在都会出现图文混排的界面例如网易新闻等,乍一看去相似一个网页,其实这样效果并非由UIWebView 加载网页实现.现在分享一种比较简单的实现方式 iOS sdk中为我 ...
随机推荐
- [NOIP2011] 聪明的质检员(二分答案)
题目描述 小T 是一名质量监督员,最近负责检验一批矿产的质量.这批矿产共有 n 个矿石,从 1到n 逐一编号,每个矿石都有自己的重量 wi 以及价值vi .检验矿产的流程是: 1 .给定m 个区间[L ...
- 配置JDK环境变量,与各步骤的意义
配置JDK环境变量 1,新建变量名:JAVA_HOME,变量值:C:\Program Files\Java\jdk1.7.0 (变量值为jdk安装路径) 2,打开P ...
- 如何分析解决Android ANR
来自: http://blog.csdn.net/tjy1985/article/details/6777346 http://blog.csdn.net/tjy1985/article/detail ...
- jQuery实例——jQuery实现联动下拉列表查询框--转http://www.cnblogs.com/picaso/archive/2012/04/08/2437442.html#undefined
jQuery实例--jQuery实现联动下拉列表查询框 在查询与列表显示的时候经常用到联动列表显示,比如一级选项是国家,二级选项是省,三级是市,这样的联动是联系的实时导出的,比如你不可能选择了四川 ...
- Servlet、JSP中页面跳转的方式
一.Servlet:当然,在servlet中,一般跳转都发生在doGet, doPost等方法里面.1) redirect 方式response.sendRedirect("success ...
- 在 anyproxy 上做 mock 和 fuzz 测试
引言 写这个工具,主要有几个原因: 最近老大在尝试不同视角的测试----健壮性测试,任务下来,所以挽起袖子就开撸了 app很可能因为后端api做了变更,返回了一个异常的值而出现难以预知的问题,健壮性受 ...
- [java] jsoup 解析网页获取省市区域信息
到国家统计局抓取数据, 到该class下解析数据 /** * jsoup解析网页 * @author xwolf * @date 2016-12-13 18:11 * @since V1.0.0 */ ...
- [Spring MVC] - 500/404错误处理
Spring MVC中404 找不到页面错误可以直接使用web.xml中配置: 在<web-app/>节点内加入: <error-page> <error-code> ...
- winform里操作打开在panel里的form窗体,子窗体操作同级子窗体或者父窗体的方法
最近开始了一个winform项目,原先一直都是web项目.遇到个问题,就是在框架内,左侧和中间的main都是用panel来实现的form,就是把form窗体打开到panel里,实现左侧是导航,中间是操 ...
- HoloLens模拟器仿真器与文档现已向开发者们开放
HoloLens仿真器与文档现已向开发者们开放 直接上链接吧:http://mt.sohu.com/20160301/n438961462.shtml