CoreText那些事儿

一、前言

当我们的产品某天站在你身后拍着你的肩膀说:"Hi guys,看看这个效果可以做不,对,这段文字的首字大写,文字主体呢用黑色,而文字中某些词句可以显示为红色...",你可能会沉思一会,考虑是否用WebView+html或者直接制定一套规则然后用CoreGraphics去画,采用WebView+html的方式不失为一个办法,但是如果需求是TableView中每个Cell都要这么显示,并且内容不定,cell高度可变,哈哈,光管理每个Cell中WebView的加载和高度计算就够头疼了,还要考虑内存问题,你懂的,WebView可是个吃内存的大户;那么CoreGraphics+规则呢?拜托,你是要山寨一个Word吗?其实呢,苹果已经为我们提供了一种富文本的解决方案:CoreText!

二、一些概念

本着严谨的治学态度,先来了解一下一些基本概念,这些概念有助于让我们知道屏幕上出现的文字是如何显示,如果觉得只想知道CoreText是怎么用的,那么可以直接跳过这一章节。

我们平时接触最多的排版系统应该就是微软的Word了吧,作为一名程序员,我习惯在使用一些软件的时候思考他们是怎么做的,对于Word这种软件,如果我来开发,简单的方案就是字形+字号+前景色+背景色进行图像绘制(暂时不考虑下划线、删除线和阴影等附加效果),比如一行文字,首先根据设置的字体获得各个字的字形描述,字形描述可以是一个矩阵,矩阵中每个点有两个值,代表显示前景色或背景色,然后根据设置的字号按一定的算法缩放矩阵,最后根据前景色和背景色绘制出这个字的图像,当这一行所有的字的图像生成好后,那么这一行的高度应该是这一行中高度最大的字图像的高度,然后加上行间距。当然现实情况是,Word比这个复杂的多的多,可以先看一下下面一些概念性的东西,会更好的理解排版系统是怎么工作的(我觉得不管是Word还是CoreText,原理大同小异)。

2.1 文字排版的概念

  • <B>字体(Font):</B>和我们平时说的字体不同,计算机意义上的字体表示的是同一大小,同一样式(Style)字形的集合。从这个意义上来说,当我们为文字设置粗体,斜体时其实是使用了另外一种字体(下划线不算)。而平时我们所说的字体只是具有相同设计属性的字体集合,即Font Family或typeface。

  • <B>字符(Character)和字形(Glyphs):</B>排版过程中一个重要的步骤就是从字符到字形的转换,字符表示信息本身,而字形是它的图形表现形式。字符一般就是指某种编码,如Unicode编码,而字形则是这些编码对应的图片。但是他们之间不是一一对应关系,同个字符的不同字体族,不同字体大小,不同字体样式都对应了不同的字形。而由于连写(Ligatures)的存在,多个字符也会存在对应一个字形的情况。

<div style='text-align:center'>
连字

</div>

  • <B>字形描述集(Glyphs Metris):</B>即字形的各个参数。如下图所示:

<div style='text-align:center'><img src="
http://7jpr6l.com1.z0.glb.clouddn.com/blog_img_coretext_glyphs1.png" alt="连字" /></div>

<div style='text-align:center'>
连字

</div>

  • <B>边框(Bounding Box):</B>一个假想的边框,尽可能地容纳整个字形。

  • <B>基线(Baseline):</B>一条假想的参照线,以此为基础进行字形的渲染。一般来说是一条横线。

  • <B>基础原点(Origin):</B>基线上最左侧的点。

  • <B>行间距(Leading):</B>行与行之间的间距。

  • <B>字间距(Kerning):</B>字与字之间的距离,为了排版的美观,并不是所有的字形之间的距离都是一致的,但是这个基本步影响到我们的文字排版。

  • <B>上行高度(Ascent)和下行高度(Decent):</B>一个字形最高点和最低点到基线的距离,前者为正数,而后者为负数。当同一行内有不同字体的文字时,就取最大值作为相应的值。这样,行高LineHeight = Ascent + |Descent| + Leading

2.2 CoreText

CoreText提供了一些低级的API用于富文本排版,它的数据源是NSAttributedString。它可以根据NSAttributedString的定义的每个range的subNSAttributedString的样式进行对字符串的渲染。可以这样说,这是一个富文本渲染器。

iOS/OSX中用于描述富文本的类是NSAttributedString,顾名思义,它比NSString多了Attribute的概念。它可以包含很多属性,粗体,斜体,下划线,颜色,背景色等等,每个属性都有其对应的字符区域。在OSX上我们只需解析完毕相应的数据,准备好NSAttributedString即可,底层的绘制完全可以交给相应的控件完成。但是在iOS6.0之前就没有这么方便,想要绘制Attributed String就需要用到CoreText了,iOS6.0以及以后的版本,UILabel和UITextView提供了一个attributedText属性,供我们设置AttributeString,然后UILabel和UITextView会调用CoreText进行排版。

2.2.1 CoreText工作流

<div style='text-align:center'>
连字

</div>

使用CoreText进行NSAttributedString的绘制,最重要的两个概念就是CTFrameSetter和CTFrame。

其中CTFramesetter是由CFAttributedString(NSAttributedString)初始化而来,可以认为它是CTFrame的一个Factory,通过传入CGPath生成相应的CTFrame并使用它进行渲染:直接以CTFrame为参数使用CTFrameDraw绘制或者从CTFrame中获取CTLine进行微调后使用CTLineDraw进行绘制。

一个CTFrame是由一行一行的CLine组成,每个CTLine又会包含若干个CTRun(既字形绘制的最小单元),通过相应的方法可以获取到不同位置的CTRun和CTLine,以实现对不同位置touch事件的响应。CTFrame可以认为是一个整体的画布(Canvas),

2.2.2 Attribute String

使用Attribute String主要有两种途径,一个是使用Foundation框架中的NSAttributeString或NSMutableAttributeString类,另一个则是直接使用较为底层的API创建CFAttributedStringRef变量。我们可以猜测NSAttributeString和NSMutableAttributeString是对底层CFAttributedStringRef的封装。

使用NSAttributeString或NSMutableAttributeString相对简单,如下:

NSString *string = @"这是一张小图";
NSDictionary *attr = @{
                           NSFontAttributeName:[UIFont systemFontOfSize:14],
                           NSForegroundColorAttributeName:[UIColor redColor]
                      };
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];

我们可以看到初始化方法中有一个attributes参数,类型为NSDictionary,也就是说在设置一段文字的排版时,Attribute主要是通过该参数以key-value的形式进行设置。Foundation框架在iOS6.0之后提供NSString类型的key供我们使用,而且6.0之前,则需要使用CFStringRef类型的key,我们需要对其进行bridge转换转为NSString进行使用。本文写作时最高iOS版本为9.3,而且当前绝大多数App已经不支持iOS6以下甚至iOS7以下的设备,所以我们着重看一下Foundation提供的NSString类型的Attribute Key。

NSString类型的Attribute Key有如下可选:

UIKIT_EXTERN NSString * const NSFontAttributeName NS_AVAILABLE(10_0, 6_0);                // 字体,UIFont类型, 默认为 Helvetica(Neue) 12

UIKIT_EXTERN NSString * const NSParagraphStyleAttributeName NS_AVAILABLE(10_0, 6_0);      // 段落样式,值为NSParagraphStyle类型, 默认为defaultParagraphStyle

UIKIT_EXTERN NSString * const NSForegroundColorAttributeName NS_AVAILABLE(10_0, 6_0);     // 前景色(文字颜色), UIColor类型, 默认为黑色

UIKIT_EXTERN NSString * const NSBackgroundColorAttributeName NS_AVAILABLE(10_0, 6_0);     // 背景色, UIColor类型,默认为nil

UIKIT_EXTERN NSString * const NSLigatureAttributeName NS_AVAILABLE(10_0, 6_0);    // 连体属性,取值为NSNumber 对象(整数),0 表示没有连体字符,1 表示使用默认的连体字符

UIKIT_EXTERN NSString * const NSKernAttributeName NS_AVAILABLE(10_0, 6_0);                // 字间距,float的NSNumber装箱

UIKIT_EXTERN NSString * const NSStrikethroughStyleAttributeName NS_AVAILABLE(10_0, 6_0);  // 设置删除线,取值为 NSNumber 对象(整数),枚举常量

UIKIT_EXTERN NSString * const NSUnderlineStyleAttributeName NS_AVAILABLE(10_0, 6_0);      // 设置下划线,取值为 NSNumber 对象(整数),0为没有下划线

UIKIT_EXTERN NSString * const NSStrokeColorAttributeName NS_AVAILABLE(10_0, 6_0);         // 填充部分颜色,不是字体颜色,取值为 UIColor 对象

UIKIT_EXTERN NSString * const NSStrokeWidthAttributeName NS_AVAILABLE(10_0, 6_0);         // 设置笔画宽度,取值为 NSNumber 对象(整数),负值填充效果,正值中空效果

UIKIT_EXTERN NSString * const NSShadowAttributeName NS_AVAILABLE(10_0, 6_0);              // 设置阴影属性,取值为 NSShadow 对象

UIKIT_EXTERN NSString *const NSTextEffectAttributeName NS_AVAILABLE(10_10, 7_0);          // 设置文本特殊效果,取值为 NSString 对象,(图版印刷效果)

UIKIT_EXTERN NSString * const NSAttachmentAttributeName NS_AVAILABLE(10_0, 7_0);          // 文本附件,取值为NSTextAttachment对象,常用于文字图片混排

UIKIT_EXTERN NSString * const NSLinkAttributeName NS_AVAILABLE(10_0, 7_0);                // 设置链接属性,点击后调用浏览器打开指定URL地址

UIKIT_EXTERN NSString * const NSBaselineOffsetAttributeName NS_AVAILABLE(10_0, 7_0);      // 设置基线偏移值,取值为 NSNumber (float),正值上偏,负值下偏

UIKIT_EXTERN NSString * const NSUnderlineColorAttributeName NS_AVAILABLE(10_0, 7_0);      // 下划线颜色

UIKIT_EXTERN NSString * const NSStrikethroughColorAttributeName NS_AVAILABLE(10_0, 7_0);  // 删除线颜色

UIKIT_EXTERN NSString * const NSObliquenessAttributeName NS_AVAILABLE(10_0, 7_0);         // 字形倾斜度,取值为 NSNumber (float),正值右倾,负值左倾

UIKIT_EXTERN NSString * const NSExpansionAttributeName NS_AVAILABLE(10_0, 7_0);           // 文本横向拉伸属性,取值为 NSNumber (float),正值横向拉伸文本,负值横向压缩文本

UIKIT_EXTERN NSString * const NSWritingDirectionAttributeName NS_AVAILABLE(10_6, 7_0);    // 文字书写方向

UIKIT_EXTERN NSString * const NSVerticalGlyphFormAttributeName NS_AVAILABLE(10_7, 6_0);   // 文字排版方向,取值为 NSNumber 对象(整数),0 表示横排文本,1 表示竖排文本(目前iOS总是横排文本)

三、CoreText的使用

对于CoreText使用,对于我们程序员来说,应该更喜欢简单粗暴地直接上代码,如果把代码分开按片段来讲解,反而会使思维有断层。

创建项目以及新建导航结构不是本文重点,先略过了,针对CoreText的使用,我简单的从三个方面来演示:

  1. 简单文本的文字排版,使用CoreText和CoteGraphics API
  2. 图文混编加支持事件响应,使用CoreText和CoteGraphics API
  3. 图文混编加支持事件响应,使用Foundation和UIKit框架提供的高级API

下面开始对上述内容逐一进行演示,<font color="red"><b>注意:</b>前方代码简陋,没有重用,更无设计感,只是尽可能展示用法,当然用法也不可能在一篇文章中穷尽,请举一反三!</font>

3.1 简单文本的文字排版

新建一个类CTBaseView,内容如下(有备注但不多,请自行脑补):

@implementation CTBaseView
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = SHRGB(0xee, 0xee, 0xee);
    }
    return self;
}

- (void)drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSaveGState(ctx);
    
    // 1.垂直翻转画布
    CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
    CGContextTranslateCTM(ctx, 0, self.height);
    CGContextScaleCTM(ctx, 1.0, -1.0);
    
    // 2.创建path
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    
    
    // 3.创建Attribute String
    NSMutableAttributedString *muAttrString = [[NSMutableAttributedString alloc]init];
    
    // 3.1
    NSString *string = @"H";
    NSDictionary *attr = @{
                           NSFontAttributeName:[UIFont systemFontOfSize:20],
                           NSForegroundColorAttributeName:[UIColor redColor]
                           };
    NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.2
    string = @"ello 复仇者联盟!\n";
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:14],
             NSForegroundColorAttributeName:[UIColor blueColor]
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.3
    string = @"我们的主页是:";
    attrString = [[NSAttributedString alloc] initWithString:string];
    [muAttrString appendAttributedString:attrString];
    
    // 3.4
    string = @"http://www.dianping.com\n";
    attr = @{NSBackgroundColorAttributeName:[UIColor yellowColor],
             NSForegroundColorAttributeName:SHRGB(0x72, 0xAC, 0xE3),
             NSUnderlineStyleAttributeName:@(NSUnderlineStyleSingle),
             NSLinkAttributeName:@"http://www.baidu.com"};
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.5
    string = @"《复仇者联盟》(Marvel's The Avengers)是漫威影业出品的一部科幻动作电影,取材自漫威漫画,是漫威电影宇宙的第六部电影,同时也是第一阶段的收官作品。由乔斯·韦登执导,小罗伯特·唐尼、克里斯·埃文斯、克里斯·海姆斯沃斯、马克·鲁法洛、斯嘉丽·约翰逊、杰瑞米·雷纳和汤姆·希德勒斯顿联袂出演。\n影片讲述了神盾局指挥官尼克·弗瑞为了对付《雷神》中被流放的洛基,积极奔走寻找最强者,在神盾局斡旋下将钢铁侠、美国队长、雷神托尔、绿巨人、黑寡妇和鹰眼侠六位超级英雄集结在一起,组成了复仇者联盟,共同携手应对邪神洛基。\n影片于2012年5月5日在中国内地正式上映。";
    NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc]init];
    style.lineSpacing = 5;
    style.paragraphSpacing = 20;
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:14],
             NSForegroundColorAttributeName:[UIColor orangeColor],
             NSParagraphStyleAttributeName:style
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 4.创建Framesetter和Frame
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)muAttrString);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, muAttrString.length), path, NULL);
    
    // 5.打印Frame中line和run信息
    NSArray *lines = (NSArray*)CTFrameGetLines(frame);
    NSInteger lineCount = lines.count;
    CGPoint lineOrigin[lineCount];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigin);
    for (NSInteger i=0; i<lineCount; i++) {
        CTLineRef line = (__bridge CTLineRef)lines[i];
        NSArray *runs = (NSArray*)CTLineGetGlyphRuns(line);
        NSInteger runCount = runs.count;
        for (NSInteger j=0; j<runCount; j++) {
            CTRunRef run = (__bridge CTRunRef)runs[j];
            CFRange range = CTRunGetStringRange(run);
            NSLog(@"==> line:%@ - run:%@ [%@ , %@]", @(i),@(j),@(range.location),@(range.length));
        }
    }
    
    CTFrameDraw(frame, ctx);
    CFRelease(frame);
    CFRelease(framesetter);
    CFRelease(path);
    CGContextRestoreGState(ctx);
}
@end

展示效果如下:

<div style='text-align:center'>
连字

</div>

3.2 图文混编加支持事件响应

新建一个类CTMixView,内容如下(有备注但不多,请自行脑补):

static CGFloat ascentCallback(void *ref)
{
    return [[((__bridge NSDictionary*)ref) objectForKey:@"height"] floatValue];
}

static CGFloat descentCallback(void *ref)
{
    return 0;
}

static CGFloat widthCallback(void *ref)
{
    return [[((__bridge NSDictionary*)ref) objectForKey:@"width"] floatValue];
}

@interface CTMixView ()

@property (nonatomic, strong) NSMutableArray *imageRects;

@end

@implementation CTMixView
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = SHRGB(0xee, 0xee, 0xee);
        self.imageRects = [NSMutableArray new];
        UIGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
        [self addGestureRecognizer:gesture];
        self.userInteractionEnabled = YES;
    }
    return self;
}

- (void)drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSaveGState(ctx);
    
    // 垂直翻转画布
    CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
    CGContextTranslateCTM(ctx, 0, self.height);
    CGContextScaleCTM(ctx, 1.0, -1.0);
    
    // 2.创建path
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    
    // 3.创建Attribute String
    NSMutableAttributedString *muAttrString = [[NSMutableAttributedString alloc]init];
    
    // 3.1
    NSString *string = @"这是一张小图";
    NSDictionary *attr = @{
                           NSFontAttributeName:[UIFont systemFontOfSize:14],
                           NSForegroundColorAttributeName:[UIColor redColor]
                           };
    NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.2 添加一张小图
    [self addImageWithWidth:50 height:50 toAttrString:muAttrString];
    
    // 3.3
    string = @",这是一张大图";
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:14],
             NSForegroundColorAttributeName:[UIColor redColor]
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.4 添加一张大图
    [self addImageWithWidth:200 height:200 toAttrString:muAttrString];
    
    // 3.5
    string = @"后面没图了";
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:20],
             NSForegroundColorAttributeName:[UIColor blueColor]
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 4.创建Framesetter和Frame
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)muAttrString);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, muAttrString.length), path, NULL);
    
    
    // 5.获取添加的图片的Rect
    NSArray *lines = (NSArray*)CTFrameGetLines(frame);
    NSInteger lineCount = lines.count;
    CGPoint lineOrigin[lineCount];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigin);
    for (NSInteger i=0; i<lineCount; i++) {
        CTLineRef line = (__bridge CTLineRef)lines[i];
        NSArray *runs = (NSArray*)CTLineGetGlyphRuns(line);
        NSInteger runCount = runs.count;
        for (NSInteger j=0; j<runCount; j++) {
            CTRunRef run = (__bridge CTRunRef)runs[j];
            NSDictionary *dicAttr = (NSDictionary*)CTRunGetAttributes(run);
            CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[dicAttr objectForKey:(id)kCTRunDelegateAttributeName];
            if (delegate == nil) {
                continue;
            }
            NSDictionary *metaData = CTRunDelegateGetRefCon(delegate);
            if (metaData == nil) {
                continue;
            }
            CGRect rect;
            CGFloat ascent;
            CGFloat descent;
            rect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
            rect.size.height = ascent + descent;
            
            CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
            rect.origin.x = lineOrigin[i].x + xOffset;
            rect.origin.y = lineOrigin[i].y - descent;
            
            CGPathRef tempPath = CTFrameGetPath(frame);
            CGRect colRect = CGPathGetBoundingBox(tempPath);
            rect = CGRectOffset(rect, colRect.origin.x, colRect.origin.y);
            NSValue *value = [NSValue valueWithCGRect:rect];
            [self.imageRects addObject:value];
        }
    }
    
    // 6.绘制文本,释放内存
    CTFrameDraw(frame, ctx);
    CFRelease(frame);
    CFRelease(framesetter);
    CFRelease(path);
    
    // 7.绘制图片
    // 7.1 绘制小图
    UIImage *image = [UIImage imageNamed:@"50x50"];
    NSValue *value = self.imageRects[0];
    CGRect imgRect = [value CGRectValue];
    CGContextDrawImage(ctx, imgRect, image.CGImage);
    
    // 7.2 绘制大图
    image = [UIImage imageNamed:@"200x200"];
    value = self.imageRects[1];
    imgRect = [value CGRectValue];
    CGContextDrawImage(ctx, imgRect, image.CGImage);
    
    // 恢复context
    CGContextRestoreGState(ctx);
}

- (void)addImageWithWidth:(CGFloat)width height:(CGFloat)height toAttrString:(NSMutableAttributedString*)string
{
    CTRunDelegateCallbacks callbacks;
    memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
    callbacks.version = kCTRunDelegateVersion1;
    callbacks.getAscent = ascentCallback;
    callbacks.getWidth = widthCallback;
    callbacks.getDescent = descentCallback;
    CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callbacks, (__bridge void*)@{@"width":@(width),@"height":@(height)});
    
    unichar placeHolder = 0xFFFC;
    NSString *strPlaceHolder = [[NSString alloc] initWithCharacters:&placeHolder length:1];
    NSMutableAttributedString *placeHolerAttr = [[NSMutableAttributedString alloc] initWithString:strPlaceHolder];
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolerAttr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate);
    CFRelease(runDelegate);
    [string appendAttributedString:placeHolerAttr];
}

- (void)onTap:(UIGestureRecognizer*)gesture
{
    CGPoint point = [gesture locationInView:self];
    [self.imageRects enumerateObjectsUsingBlock:^(NSValue *value, NSUInteger idx, BOOL * _Nonnull stop) {
        CGRect rect = value.CGRectValue;
        CGFloat y = self.height - rect.origin.y - rect.size.height;
        CGRect imgRect = CGRectMake(rect.origin.x, y, rect.size.width, rect.size.height);
        if (CGRectContainsPoint(imgRect, point)) {
            NSString *text = idx == 0 ? @"点击了小图" : @"点击了大图";
            UIAlertView *av = [[UIAlertView alloc]initWithTitle:nil message:text delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];
            [av show];
            *stop = YES;
        }
    }];
}
@end

展示效果如下:

<div style='text-align:center'>
连字

</div>

3.3 图文混编加支持事件响应(高级接口)

新建一个类CTMixView,内容如下(有备注但不多,请自行脑补):

@interface CTHightLevelMixView ()

@property (nonatomic, strong) NSMutableArray *positons;

@end

@implementation CTHightLevelMixView
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.positons = [NSMutableArray new];
        self.backgroundColor = SHRGB(0xee, 0xee, 0xee);
        
        [self displayContent];
        UIGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
        [self addGestureRecognizer:gesture];
        self.userInteractionEnabled = YES;
    }
    return self;
}

- (void)displayContent
{
    NSMutableAttributedString *muAttrString = [[NSMutableAttributedString alloc]init];
    
    NSString *string = @"这是一张小图";
    NSDictionary *attr = @{
                           NSFontAttributeName:[UIFont systemFontOfSize:14],
                           NSForegroundColorAttributeName:[UIColor redColor]
                           };
    NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.2 添加一张小图
    [self addImage:@"50x50" width:50 height:50 toAttrString:muAttrString];
    
    // 3.3
    string = @",这是一张大图";
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:14],
             NSForegroundColorAttributeName:[UIColor redColor]
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.4 添加一张大图
    [self addImage:@"200x200" width:200 height:200 toAttrString:muAttrString];
    
    // 3.5
    string = @"后面没图了";
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:20],
             NSForegroundColorAttributeName:[UIColor blueColor]
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    self.attributedText = muAttrString;
}

- (void)addImage:(NSString*)imageName width:(CGFloat)width height:(CGFloat)height toAttrString:(NSMutableAttributedString*)attrString
{
    NSTextAttachment *imageAttachment = [[NSTextAttachment alloc]init];
    imageAttachment.image = [UIImage imageNamed:imageName];
    imageAttachment.bounds = CGRectMake(0, 0, width, height);
    
    NSAttributedString *string = [NSAttributedString attributedStringWithAttachment:imageAttachment];
    NSRange range = NSMakeRange(attrString.length, string.length);
    NSValue *rangeValue = [NSValue valueWithRange:range];
    [self.positons addObject:rangeValue];
    [attrString appendAttributedString:string];
}

- (void)onTap:(UIGestureRecognizer*)gesture
{
    CGPoint point = [gesture locationInView:self];
    [self.positons enumerateObjectsUsingBlock:^(NSValue *value, NSUInteger idx, BOOL * _Nonnull stop) {
        NSRange range = [value rangeValue];
        self.selectedRange = range;
        NSArray *rects = [self selectionRectsForRange:self.selectedTextRange];
        self.selectedRange = NSMakeRange(0, 0);
        for (UITextSelectionRect *textRect in rects) {
            CGRect rect = textRect.rect;
            if (CGRectContainsPoint(rect, point)) {
                NSString *text = idx == 0 ? @"点击了小图" : @"点击了大图";
                UIAlertView *av = [[UIAlertView alloc]initWithTitle:nil message:text delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];
                [av show];
                *stop = YES;
            }
        }
    }];
}
@end

展示效果如下:

<div style='text-align:center'>
连字

</div>

本文Demo下载:Demo源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • CoreText是一个进阶的比较底层的布局文本和处理字体的技术,CoreText API在OS X v10.5 和...
    smalldu阅读 13,364评论 18 129
  • 1.iOS中的round、ceil、floor函数略解 round如果参数是小数,则求本身的四舍五入.ceil如果...
    K_Gopher阅读 1,177评论 1 0
  • 文字排版的基础概念 字体(Font):和我们平时说的字体不同,计算机意义上的字体表示的是同一大小,同一样式(Sty...
    iOS白水阅读 665评论 0 0
  • 健身器材销售产品演示做的好,可以给顾客留下深刻的印象。 销售员:"先生,您平时有没有运动的习惯?" 顾客:"没有时...
    八百诚阅读 488评论 0 0
  • 将军 又喝高了 他重复他的自由 大家们的‘遗留’是他举起的大剑 语言是新鲜伤口上不断冒出的血浆 粘稠 连那种红色也...
    能有谁比我知道阅读 221评论 0 0