iOS中CoreText框架探究

闲来无事想着自己搞个富文本的工具库,不至于每次遇见这些东西就用别人的第三方。自己研究研究也有助于自己对这方面的理解。通过查找了相关的调研发现CoreText是一个好的框架,我们系统的UILabel等控件就是基于此框架封装的。由此我也打算搞搞看

一、CoreText框架基础

image.png

从此架构图可以看出,CoreText是我们平时使用的UILabel、UITextField更底层的框架。它是基于Core Graphics的,所以性能上更加的快速。

UIWebView也是我们处理复杂的文字排本的方案,那CoreText和基于UIWebView相比有哪些异同呢?

优势:
  • CoreText占用的内存更少,渲染的速度更快,UIWebView占用的内存多,渲染速度更慢。
  • CoreText 在渲染界面前就可以精确地获得显示内容的高度(只要有了CTFrame就可以),而UIWebView只有渲染出内容后,才能获得内容的高度(通过js代码获取)
  • CoreText的CTFrame可以在后台线程渲染,UIWebView的内容只能在主线程(UI线程)渲染。
  • 基于CoreText可以做更好的原生交互效果,交互效果可以更细腻。而UIWebView的交互效果都是用JS来实现的,在交互效果上会有一些卡顿情况存在。例如在UIWebView下,一个简单的按钮按下操作,都无法做出原生按钮的即时和细腻效果。
劣势:
  • CoreText渲染出来的内容不能像UIWebView那样方便的支持内容的复制。
  • 基于CoreText来排版需要自己处理很多复杂逻辑,例如需要自己处理图片与文字混排相关的逻辑,也需要自己实现链接点击操作的支持。

业界很多应用都采用了基于CoreText技术的排版方案,例如:新浪微博客户端、多看阅读客户端。

常用类、属性
  • CTFrameRef
  • CTFramesetterRef
  • CTLineRef
  • CTRunRef
  • CTTypesetterRef
  • CTGlyhInfoRef (NSGlyphInfo)
  • CTParagraphStyleRef (NSParapraphStyle)
  • CTFontRef (UIFont)
  • CFArrayRef (NSArray)
字体结构:
image.png
CTFrame、CTRun、CTLine
image.png
  • CTFrame可以想象成一个画布,画布的大小范围由CGPath决定
  • CTFrame由很多CTLine组成,CTLine表示为一行CTLine由多个CTRun组成,CTRun相当于一行中的多个块(格式为一致的字为一个块)
    但是CTRun不需要你自己创建,由NSAttributedString的属性决定,系统自动生成。每个CTRun对应不同属性。
  • CTFramesetter是一个工厂,创建CTFrame,一个界面上可以有多个CTFrame
  • CTFrame就是一个基本画布,然后一行一行绘制。CoreText会自动根据传入的NSAttributedString属性创建CTRun,包括字体样式,颜色,间距等
流程
image.png
    1. 创建AttributedString,定义样式
  • 2、通过CFAttributedStringRef生成CTFramesetter
    1. 通过CTFramesetter得到CTFrame
  • 4.绘制(CTFrameDraw)
  • 5.如果有图片存在,先在AttributedString对应位置添加占位字符(空字符串),因为CoreText是不支持图片的
    1. 通过回调函数确定图片的宽高(CTRunDelegateCallbacks)
    1. 遍历到对应CTRun上、获取对应CGRect、绘制图片(CGContextDrawImage)
  • 8.如果想做点击对应的图片的回调可以记录图片的位置,同时确定点击的位置是不是在图片的位置上做处理
    1. 如果想做链接点击处理,则需要确定此链接上的所有CTRun的位置,然后判断点击的点是不是在此位置上做处理

二、基本的文本样式的具体代码

CoreText是需要我们自己处理绘制,不像UILabel等最上层的控件,我们必须在drawRect中绘制,为了更好的使用,我们稍微封装一哈,自定义一个UIView

我们在使用最上层的控件时,坐标系的原点在左上角,而底层的CoreGraphics的坐标原点则在左下角,(垮平台图形绘制框架OpenGL的坐标系就是如此的)

- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    
    //step 1:获取当前画布的上下文,用于后续将内容绘制在画布上
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    //step 2: 创建绘制区域,CoreText本身支持各种文字排版的区域,我们这里使用整个UIView作为排版区域
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    

    //step 3:  创建绘制的文字,为NSAttributedString类型
    NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"xXHhofiyYI这是一段中文,前面是大小写"];


    
    //step 4:  通过NSAttributedString转换为CTFramesetterRef,然后通过CTFramesetterRef创建CTFrameRef
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [attributedString length]), path, NULL);
    
    //step 5:  开始绘制文字
    CTFrameDraw(frame,context);
    
    //step 6:  释放对象
    CFRelease(frame);
    CFRelease(path);
    CFRelease(framesetter);
    //使用Create函数建立的对象引用,必须要使用CFRelease掉。
}

此时得到的效果如下:是翻转的


image.png

结果分析:发现文案是反的。原因就是因为coreText的坐标系和UIKit的坐标系不一样的。

image.png

因此我们需要将坐标系进行翻转

CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context,  0,  self.bounds.size.height); 
CGContextScaleCTM(context, 1.0, -1.0);

此时就的得到了一个正常的文字显示。
以上的绘制方式都是基于CTFrame绘制的,还可以按照CTLine和CTRun绘制:

按CTLine绘制
    // 通过CTLine
    // 1.获得CTLine数组
    CFArrayRef lines = CTFrameGetLines(frame);
    // 2.获得行数
    CFIndex indexCount = CFArrayGetCount(lines);
    // 3.获得每一行的origin, CoreText的origin是在字形的baseLine(基准线)处
    CGPoint origins[indexCount];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
    
    // 4.遍历每一行进行绘制
  for (int i = 0; i < indexCount; i++) {


        CTLineRef line = CFArrayGetValueAtIndex(lines, i);

        CTLineDraw(line, context);
    }
   // 绘制的文字内容是:Worl按季度交发十大减肥;阿技术点发觉啊;啥的积分;阿斯加德发;安静是的;发jakdfads;fjas;lsd f安静的首付款撒;时间点发;安静都是;发觉啊;是的发;啊打发; 

结果如下:


image.png

从UIView的底部开始绘制的,且没有绘制安全,还是从基准线分割了文字

按CTRun绘制

用下面函数替换CTLineDraw(line, context)这一句就可以了,

for (int i = 0; i < indexCount; i++) {


        CTLineRef line = CFArrayGetValueAtIndex(lines, i);
        
        CFArrayRef runs = CTLineGetGlyphRuns(line);
        CFIndex runCount = CFArrayGetCount(runs);
        for (int j = 0; j < runCount; j++) {
            
            CTRunRef run = CFArrayGetValueAtIndex(runs, j);
            
            CTRunDraw(run, context, CFRangeMake(0, 0));
        }
    }

结果如下:


image.png

文字叠加了

三、图文混排

CoreText本身是不提供UIImage的绘制的,所以UIImage肯定只能通过Core Graphics绘制,但是绘制时必须要知道绘制单元的长宽,庆幸的是CoreText绘制的最小单元CTRun提供了CTRunDelegate,也就是当设置了kCTRunDelegateAttributedName之后,CTRun的绘制时所需的参考(长宽等)将可以从委托中获取,我们即可通过此方法实现图片的绘。在需要绘制图片的位置,提前预留空白位。

CTRun有几个委托用以实现CTRun的几个参数的获取
以下是CTRunDelegateCallbacks的几个委托代理

typedef struct
{
    CFIndex                         version;
    CTRunDelegateDeallocateCallback dealloc;
    CTRunDelegateGetAscentCallback  getAscent;
    CTRunDelegateGetDescentCallback getDescent;
    CTRunDelegateGetWidthCallback   getWidth;
} CTRunDelegateCallbacks;

以下是基本绘制

/// 固定图文混排
- (void) drawTextAndImg {
    
    // CoreText为了排版,需要将显示的文本内容,位置,字体,字形传递给Quartz
    
    // 步骤1 获取当前画布的上下文,用于后续将内容绘制在画布上
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    /*
     步骤2
     将坐标系上下翻转。对于底层的绘制引擎来说,屏幕的左下角是(0,0)坐标。
     而对于上层的UIKit来说,左上角是(0,0)坐标。所以我们为了之后的坐标系描述按UIKit来做,先在这里做一个坐标系的上下翻转操作。
     翻转之后,底层和上层的(0,0)坐标就是重合了
     
     */
    CGContextSetTextMatrix(context, CGAffineTransformIdentity); // 设置矩阵(纹理)
    CGContextTranslateCTM(context, 0, self.bounds.size.height); // 内容翻转
    CGContextScaleCTM(context, 1.0, -1.0); //
    
    /*
     步骤3
     
     创建绘制的区域,CoreText本身支持各种文字排版的区域,我们这里简单的将UIView的整个界面作为排版的区域。
     
     为了加深理解,我们可以替换区域为下面的椭圆区域
     */
    CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, self.bounds);
//    CGPathAddEllipseInRect(path, NULL, self.bounds); // 椭圆区域
    
    
    /**
     含有图片的  步骤1
     */
    CTRunDelegateCallbacks imageCallBacks;
    imageCallBacks.version = kCTRunDelegateCurrentVersion;
    imageCallBacks.dealloc = ImgRunDelegateDeallocCallback;
    imageCallBacks.getAscent = ImgRunDelegateGetAscentCallback;
    imageCallBacks.getDescent = ImgRunDelegateGetDescentCallback;
    imageCallBacks.getWidth = ImgRunDelegateGetWidthCallback;
    
    NSString *imgName = @"coretext-image-1.jpg";
    CTRunDelegateRef imgRunDelegate = CTRunDelegateCreate(&imageCallBacks, (__bridge void * _Nullable)(imgName)); // 我们也可以传入其他参数
    NSMutableAttributedString *imgAttributedStr = [[NSMutableAttributedString alloc] initWithString:@" "];
    [imgAttributedStr addAttribute:(NSString *)kCTRunDelegateAttributeName value:(__bridge  id)imgRunDelegate range:NSMakeRange(0, 1)];
    
    
    


    
    // 步骤4
    NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:@" Worl按季度交发十大减肥;阿技术点发觉啊;啥的积分;阿斯加德发;安静是的;发jakdfads;fjas;lsd f安静的首付款撒;时间点发;安静都是;发觉啊;是的发;啊打发; "];
    [attString addAttribute:(NSString *)kCTBackgroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 10)];
    [attString addAttribute:(NSString *)kCTFontAttributeName value:[UIFont systemFontOfSize:18] range:NSMakeRange(0, 10)];
    
    
    /**
     含有图片的  步骤2
     */
#define kImgName @"imgName"
    // 图片占位符添加
    [imgAttributedStr addAttribute:kImgName value:imgName range:NSMakeRange(0, 1)];
    [attString insertAttributedString:imgAttributedStr atIndex:30];
    
    
    
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [attString length]), path, NULL);
    
    
    
    

    
    
    // 步骤5  开始绘制
    CTFrameDraw(frame, context);
    
    /**
     含有图片的  步骤3 绘制图片
     */
    // 通过CTLine
    // 1.获得CTLine数组
    CFArrayRef lines = CTFrameGetLines(frame);
    // 2.获得行数
    CFIndex indexCount = CFArrayGetCount(lines);
    // 3.获得每一行的origin, CoreText的origin是在字形的baseLine(基准线)处
    CGPoint lineOrigins[indexCount];
    
    // 获得第几行的起始点
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);

    
    for (int i = 0; i < indexCount; i++) {


        CTLineRef line = CFArrayGetValueAtIndex(lines, i);
        CGFloat lineAscent; // 上缘线
        CGFloat lineDescent; // 下缘线
        CGFloat lineLeading; // 行间距
        // 获取此行的字形参数
        CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, &lineLeading);
        
        
        // 获取此行中每个CTRun
        CFArrayRef runs = CTLineGetGlyphRuns(line);
        CFIndex runCount = CFArrayGetCount(runs);
        for (int j = 0; j < runCount; j++) {
            
            CGFloat runAscent; // 此CTRun上缘线
            CGFloat runDescent; // 此CTRun下缘线
            CGFloat runLeading; // CTRun间距
            CGPoint lineOrigin = lineOrigins[i]; // 此行起点
            
        
            
            // 获取此CTRun
            CTRunRef run = CFArrayGetValueAtIndex(runs, j);
            
            // 获取该run上的属性特征
            NSDictionary *runAttributeds = (NSDictionary *)CTRunGetAttributes(run);
            
            CGRect runRect;
            // 获取此CTRun的上缘线、下缘线,并由此获取CTRun和宽
            runRect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &runAscent, &runDescent, &runLeading);
            
            // CTRun的X坐标
            CGFloat runOrgX = lineOrigin.x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
#warning ---此处的y结果没看懂,高也没看懂---
            // 此处的y结果没看懂  高也没看懂
            runRect = CGRectMake(runOrgX, lineOrigin.y - runDescent, runRect.size.width, runAscent + runDescent);
            
            // 通过run的属性特征获得图片名称的字符串
            NSString *imgName = [runAttributeds objectForKey:kImgName];
            NSLog(@"图片名称===%@",imgName);
            if (imgName != nil) {
                
                UIImage *image = [UIImage imageNamed:imgName];
                
                if (image) {
                    
#warning ---此处的坐标计算也没看懂---
                    CGRect imageRect;
                    imageRect.size = CGSizeMake(40, 20);
                    imageRect.origin.x = runRect.origin.x + lineOrigin.x;
                    imageRect.origin.y = lineOrigin.y;
                    
                    CGContextDrawImage(context, imageRect, image.CGImage);
                }
            }
            
            
        }
    }
    
    
    
    
    
    
    

    // 步骤6  释放内存
    CFRelease(imgRunDelegate);
    CFRelease(frame);
    CFRelease(path);
    CFRelease(framesetter);
}
#pragma mark ---代理函数CTRunDelegateCallbacks---
void ImgRunDelegateDeallocCallback(void *refCon) {
    
}

/// 通过此函数设置图片处上部高
CGFloat ImgRunDelegateGetAscentCallback(void *refCon) {
    
    NSString *imageName = (__bridge  NSString *)refCon;
//    return [UIImage imageNamed:imageName].size.height;
    return 40;
}

/// 通过此函数设置图片处下部高
CGFloat ImgRunDelegateGetDescentCallback(void *refCon) {
    
    return 0;
}

/// 通过此函数设置图片位置宽度
CGFloat ImgRunDelegateGetWidthCallback(void *refCon) {
    
    NSString *imageName = (__bridge  NSString *)refCon;
//    return [UIImage imageNamed:imageName].size.width;
    return 40;
}

结果如下:


image.png

基于以上这个原型,我们可以封装一个比较完整的富文本控件了,比如定义HTML协议或者JSON,然后在内部进行解析,然后根据类型与相应的属性进行绘制。

四、图片点击事件

CoreText就是将内容绘制到画布上,自然没有事件处理,我们要实现图片与链接的点击效果就需要使用触摸事件了。当点击的位置在图片的Rect中,那我们做相应的操作即可,所以基本步骤如下:

  • 记录所有图片所在画布中作为一个CTRun的位置
  • 获取每个图片所在画布中所占的Rect矩形区域
  • 当点击事件发生时,判断点击的点是否在某个需要处理的图片Rect内。

这里为了演示的简单,我们直接在drawRect中记录图片的相应坐标,但是一般我们会在CTDisplayView渲染之前对数据进行相应的处理,比如处理传入的样式数据、记录图片与链接等信息。

用于记录图片信息类

@interface CTImageData : NSObject
@property (nonatomic,strong) NSString *imgHolder;
@property (nonatomic,strong) NSURL *imgPath;
@property (nonatomic) NSInteger idx;
@property (nonatomic) CGRect imageRect;
@end

// 记录图片信息

//以下操作仅仅是演示示例,实战时请在渲染之前处理数据,做到最佳实践。  
if(!_imageDataArray){
    _imageDataArray = [[NSMutableArray alloc]init];
}
BOOL imgExist = NO;
for (CTImageData *ctImageData in _imageDataArray) {
    if (ctImageData.idx == idx) {
        imgExist = YES;
        break;
    }
}
if(!imgExist){
    CTImageData *ctImageData = [[CTImageData alloc]init];
    ctImageData.imgHolder = imgName;
    ctImageData.imageRect = imageRect;
    ctImageData.idx = idx;
    [_imageDataArray addObject:ctImageData];
}
- (void)setupEvents{
    UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(userTapGestureDetected:)];
    
    [self addGestureRecognizer:tapRecognizer];
    
    self.userInteractionEnabled = YES;
}

- (void)userTapGestureDetected:(UIGestureRecognizer *)recognizer{
    CGPoint point = [recognizer locationInView:self];
    //先判断是否是点击的图片Rect
    for(CTImageData *imageData in _imageDataArray){
        CGRect imageRect = imageData.imageRect;
        CGFloat imageOriginY = self.bounds.size.height - imageRect.origin.y - imageRect.size.height;
        CGRect rect = CGRectMake(imageRect.origin.x,imageOriginY, imageRect.size.width, imageRect.size.height);
        if(CGRectContainsPoint(rect, point)){
            NSLog(@"tap image handle");
            return;
        }
    }
    
    //再判断链接
}

五、链接点击事件

记录链接信息类

@interface CTLinkData : NSObject
@property (nonatomic ,strong) NSString *text;
@property (nonatomic ,strong) NSString *url;
@property (nonatomic ,assign) NSRange range;
@end

记录链接信息

if(!_linkDataArray){
    _linkDataArray = [[NSMutableArray alloc]init];
}
CTLinkData *ctLinkData = [[CTLinkData alloc]init];
ctLinkData.text = [attributedString.string substringWithRange:linkRange];
ctLinkData.url = @"http://www.baidu.com";
ctLinkData.range = linkRange;
[_linkDataArray addObject:ctLinkData];

处理链接事件

if(!_linkDataArray){
    _linkDataArray = [[NSMutableArray alloc]init];
}
CTLinkData *ctLinkData = [[CTLinkData alloc]init];
ctLinkData.text = [attributedString.string substringWithRange:linkRange];
ctLinkData.url = @"http://www.baidu.com";
ctLinkData.range = linkRange;
[_linkDataArray addObject:ctLinkData];

根据点击点获取字符串偏移

- (CFIndex)touchPointOffset:(CGPoint)point{
    //获取所有行
    CFArrayRef lines = CTFrameGetLines(_ctFrame);
    
    if(lines == nil){
        return -1;
    }
    CFIndex count = CFArrayGetCount(lines);
    
    //获取每行起点
    CGPoint origins[count];
    CTFrameGetLineOrigins(_ctFrame, CFRangeMake(0, 0), origins);
    
    
    //Flip
    CGAffineTransform transform =  CGAffineTransformMakeTranslation(0, self.bounds.size.height);
    transform = CGAffineTransformScale(transform, 1.f, -1.f);
    
    CFIndex idx = -1;
    for (int i = 0; i< count; i++) {
        CGPoint lineOrigin = origins[i];
        CTLineRef line = CFArrayGetValueAtIndex(lines, i);
        
        //获取每一行Rect
        CGFloat ascent = 0.0f;
        CGFloat descent = 0.0f;
        CGFloat leading = 0.0f;
        CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
        CGRect lineRect = CGRectMake(lineOrigin.x, lineOrigin.y - descent, width, ascent + descent);
        
        lineRect = CGRectApplyAffineTransform(lineRect, transform);
        
        if(CGRectContainsPoint(lineRect,point)){
            //将point相对于view的坐标转换为相对于该行的坐标
            CGPoint linePoint = CGPointMake(point.x-lineRect.origin.x, point.y-lineRect.origin.y);
            //根据当前行的坐标获取相对整个CoreText串的偏移
            idx = CTLineGetStringIndexForPosition(line, linePoint);
        }
    }
    return idx;
}

下面是我写的一个demo,封装好的图文混排的,可以看看,实战的话稍微修改修改就好,数据是通过json传入的。## demo链接

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

推荐阅读更多精彩内容

  • CoreText简介 处理文字和字体的底层技术。它直接和Core Graphics打交道,是iOS和OSX底层的告...
    taobingzhi阅读 1,061评论 0 3
  • 支持图文混排的排版引擎 改造模版文件 下面我们来进一步改造,让排版引擎支持对于图片的排版。在上一小节中,我们在设置...
    sunney0阅读 772评论 0 0
  • Core Text is an advanced, low-level technology for laying...
    forping阅读 1,808评论 0 3
  • CoreText 是用于处理文字和字体的底层技术。它直接和 Core Graphics(又被称为 Quartz)打...
    SpursGo阅读 1,724评论 0 2
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,562评论 0 11