IOS CoreText --- 代码封装

前几节中,我转载他人的博客,详细的描述了Core Text的基本概念及使用,但看上去他所提供的demo是面向过程的,代码不容易管理及维护。接下来几节,我将逐步封装Core Text代码,让其看起来不那么凌乱(因为Core Text是纯C的语法)。下面,我们先看一张 “iOS Text Design and Rendering Architecture”
架构图。

上图中,最底层的Core Graphics是核心绘画,我在Quartz 2D章节已经进行了详细的说明,然后上面一层的就是Core Text。 先看看我实现的一个Core Text的demo效果图。

1. 我们先看看原始的代码实现过程,可以看出,代码中,将坐标系的变换,路径的初始化,字符串的处理,frame的创建及最终的绘制 全部放在一起处理了,当遇到复杂的业务需求的话,代码显的臃肿和不利于维护。

CGContextRef context = UIGraphicsGetCurrentContext();

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

    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);

    NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:@"大话西游台词..."];
    // 这里截断字符串,然后对每段进行文字及颜色的设置

    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrString.length), path, NULL);

    CTFrameDraw(frame, context);

    CFRelease(frame);
    CFRelease(path);
    CFRelease(framesetter);

2. 考虑对代码进行封装

对于一个复杂的排版引擎来说,可以将其功能拆分成以下几个类来完成。

1)一个显示用的类,仅负责显示内容,不负责排版。

2)一个模型类,用于承载显示所需要的所有数据。

3)一个排版类,用于实现文字内容的排版。

4)一个配置类,用于实现一些排版时的可配置项。

按照以上的描述,我们可以将上面的代码内容拆分,分成4个类:

1)CTFrameParserConfig类,用于配置绘制的参数,例如文字颜色、大小、行间距等。

2)CTFrameParser类,用于生成最后绘制界面需要的CTFrameRef实例。

3)CoreTextData类,用于保存由CTFrameParser类生成的CTFrameRef实例以及CTFrameRef实际绘制需要的高度。

4)CTDisplayView类,持有CoreTextData类的实例,负责将CTFrameRef绘制到界面上。

下面我就一一介绍上面说描述的类:

1)CTFrameParserConfig类,主要是初始化了文字宽度、大小、行间距、颜色信息。 代码如下:

@interface CTFrameParserConfig : NSObject

@property (nonatomic,assign) CGFloat width;
@property (nonatomic,assign) CGFloat fontSize;
@property (nonatomic,assign) CGFloat lineSpace;
@property (nonatomic,strong) UIColor *textColor;

@end

#define RGB(A,B,C) [UIColor colorWithRed:(A/255.0) green:(B/255.0) blue:(C/255.0) alpha:1.0]

#import "CTFrameParserConfig.h"

@implementation CTFrameParserConfig

- (instancetype)init {
    if (self = [super init]) {
        self.width = 300.0f;
        self.fontSize = 16.0f;
        self.lineSpace = 8.0f;
        self.textColor = RGB(108, 108, 108);
    }
    return  self;
}

@end

2)CoreTextData类,定义了两个属性,用来存储绘制所需要的CTFrameRef及最终绘制的View的高度(因为高度是根据文字内容动态计算出来的)。

@interface CoreTextData : NSObject

@property (nonatomic,assign) CTFrameRef ctFrame;
@property (nonatomic,assign) CGFloat height;

@end

@implementation CoreTextData

- (void)setCtFrame:(CTFrameRef)ctFrame {
    if (_ctFrame != ctFrame) {
        if(_ctFrame != nil) {
            CFRelease(_ctFrame);
        }

        CFRetain(ctFrame);
        _ctFrame = ctFrame;
    }
}

- (void)dealloc {
    if (_ctFrame != nil) {
        CFRelease(_ctFrame);
        _ctFrame = nil;
    }
}

@end

3)CTDisplayView类, 定义了属性data,用于接收外面传递进来的模型数据,然后在drwRect方法中完成绘制工作。

I. 在绘制之前先进行坐标系翻转,因为Core Text的默认坐标系原点在左下角。

II.直接调用CTFrameDraw方法,完成绘制工作。

@interface CTDisplayView : UIView
@property (nonatomic,strong) CoreTextData *data;
@end

- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);

    if (self.data) {
        CTFrameDraw(self.data.ctFrame, context);
    }
}

4)CTFrameParser类,这个类是核心类,主要完成用于生成最后绘制界面需要的CTFrameRef实例。先看看CTFrameParser.h文件中的代码:

@interface CTFrameParser : NSObject

/* 对整段文字进行排版 */
+ (CoreTextData *)parseContent:(NSString *)content config:(CTFrameParserConfig *)config;

/* 自定义自己的排版 */
+ (CoreTextData *)parseTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config;

@end

I. 对整段文字进行排版,直接操作传递过来的content字符串,最终显示出来的效果是:所有字体的大小,颜色,行间距均一致。比如:小说应用。

先来看看parseContent方法:

+ (CoreTextData *)parseContent:(NSString *)content config:(CTFrameParserConfig *)config {

    NSDictionary *attributes = [self attributesWithConfig:config];
    NSAttributedString *contentString = [[NSAttributedString alloc] initWithString:content attributes:attributes];

    return [self parseAttributedContent:contentString config:config];
}

代码中第一句attrbutesWithConfig方法就是初始化从CTFrameParserConfig中传递过来的默认信息值。

+ (NSDictionary *)attributesWithConfig:(CTFrameParserConfig *)config {
    CGFloat fontSize = config.fontSize;
    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
    CGFloat lineSpacing = config.lineSpace;

    const CFIndex kNumberOfSettings = 3;
    CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
        {kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpacing},
        {kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(CGFloat),&lineSpacing},
        {kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(CGFloat),&lineSpacing}
    };
    CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);

    UIColor *textColor = config.textColor;

    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    dict[(id)kCTForegroundColorAttributeName] = (id)textColor.CGColor;
    dict[(id)kCTFontAttributeName] = (__bridge id)fontRef;
    dict[(id)kCTParagraphStyleAttributeName] = (__bridge id)theParagraphRef;

    CFRelease(theParagraphRef);
    CFRelease(fontRef);

    return dict;
}

代码中的parseAttributeContent方法,就是要返回最终的CoreTextData模型数据,最终给CTDisplayView绘制使用。

+ (CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config {
    // 创建CTFramesetterRef实例
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
    // 获得要绘制区域的高度
    CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
    CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), nil, restrictSize, nil);
    CGFloat textHeight = coreTextSize.height;

    // 生成CTFrameRef实例
    CTFrameRef frame = [self createFrameWithFramesetter:framesetter config:config height:textHeight];

    // 将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,并返回
    CoreTextData *data = [[CoreTextData alloc] init];
    data.ctFrame = frame;
    data.height = textHeight;

    // 内存释放
    CFRelease(frame);
    CFRelease(framesetter);

    return data;
}

II.自定义排版,正如我文章开头贴出来的效果。

/* 自定义自己的排版 */
+ (CoreTextData *)parseTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config {
    NSAttributedString *content = [self loadTemplateFile:path config:config];
    return [self parseAttributedContent:content config:config];
}

先来看看loadTempllateFile方法,就是用来加载数据源,在“对整段文字进行排版”,数据源就是content字符串;而现在这种情况,数据源是自定义的了,它是plist,json等数据形式。为了演示方便,我这里采取的是plist形式。准备的plist结构大致如下:

loadTemplateFile方法具体实现如下:

+ (NSAttributedString *)loadTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config {
    NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
    // JSON方式获取数据
    //        NSArray *array = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
    NSArray *array = [NSArray arrayWithContentsOfFile:path];
    if (array) {
        if ([array isKindOfClass:[NSArray class]]) {
            for (NSDictionary *dict in array) {
                NSAttributedString *as = [self parseAttributedContentFromNSDictionary:dict config:config];
                [result appendAttributedString:as];
            }
        }
    }
    return result;
}

可以看出,代码中,遍历plist文件中的每一个字典数据,然后再调用parseAttributedContentFromNSDictionary方法进行文字的具体处理,这样就可以保证,每段文字的风格不一致了,以达到自定义的效果。parseAttributedContentFromNSDictionary方法代码如下:

+ (NSAttributedString *)parseAttributedContentFromNSDictionary:(NSDictionary *)dict config:(CTFrameParserConfig *)config {
    NSMutableDictionary *attributes = (NSMutableDictionary *)[self attributesWithConfig:config];

    UIColor *color = [self colorFromTemplate:dict[@"color"]];
    if (color) {
        attributes[(id)kCTForegroundColorAttributeName] = (id)color.CGColor;
    }

    CGFloat fontSize = [dict[@"size"] floatValue];
    if (fontSize > 0) {
        CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
        attributes[(id)kCTFontAttributeName] = (__bridge id)(fontRef);
        CFRelease(fontRef);
    }

    NSString *content = dict[@"content"];
    return [[NSAttributedString alloc] initWithString:content attributes:attributes];
}

+ (UIColor *)colorFromTemplate:(NSString *)name {
    if ([name isEqualToString:@"blue"]) {
        return [UIColor blueColor];
    } else if ([name isEqualToString:@"green"]) {
        return [UIColor greenColor];
    } else if ([name isEqualToString:@"red"]) {
        return [UIColor redColor];
    } else if ([name isEqualToString:@"purple"]) {
        return [UIColor purpleColor];
    } else {
        return nil;
    }
}

至此,所有类的定义及实现全部完成了,最终我们调用的代码如下:

CTFrameParserConfig *config = [[CTFrameParserConfig alloc] init];
    config.width = self.ctView.width;

    NSString *path = [[NSBundle mainBundle] pathForResource:@"TempData.plist" ofType:nil];
    CoreTextData *data = [CTFrameParser parseTemplateFile:path config:config];
    // 传递数据给CTDisplayView,然后绘制内容
    self.ctView.data = data;
    // 设置CTDisplayView的高度
    self.ctView.height = data.height;

看到上面的代码,是不是瞬间觉得简单易读,并且屏蔽了那些枯燥无味的C语言接口。所以我们想实现自定义模板,只要提供好plist或者json文件的数据形式及内容。

下图是框架的UML示意图:

1. CTFrameParser通过CTFrameParserConfig实例来生成CoreTextData实例。

2. CTDisplayView通过持有CoreTextData实例来获得绘制所需要的所有信息。

3. ViewController类通过配置CTFrameParserConfig实例,进而获得生成的CoreTextData实例,最后将其赋值给它的CTDisplayView成员,达到将指定内容显示在界面上得效果。

时间: 2024-10-31 08:37:50

IOS CoreText --- 代码封装的相关文章

【iOS开发-50】利用创建新的类实现代码封装,从而不知不觉实践一个简单的MVC实验

接上次案例谈代码封装.上次案例见:[iOS开发-48]九宫格布局案例:自动布局.字典转模型运用.id和instancetype区别.xib重复视图运用及与nib关系 代码封装的原则是:要保证视图控制器尽量少的接触到其他对象的属性,也就是说,尽量把数据或者属性封装到一个类里面,然后利用类或者对象的方法来调用或者设置数据.而是赤裸裸地把属性都写在视图控制器中.核心作用在于:减少视图控制器的代码量,把数据和属性的处理封装起来,这样也便于其他视图控制器的使用. 要做到的结果就是如下(我们要根据数组里面的

iOS 开发代码规范有哪些

对于刚刚入门ios的同学来说,iOS 开发代码规范是很重要的知识的,这里就给大家简单总结了一下. 一.工程规范 1.功能分类 根据所做功能的不同,分为不同的功能模块,比如登录模块,首页模块,个人模块等,根据不同的功能,代码必须要放在不同功能的文件夹下. 2.代码文件分类 不管是MVC模式,MVVM模式,或是其他设计模式,在不同的功能模块下,视图控制器(Controllers),视图(Views),模型类(Models),也必须要分别存放. 3.第三方库分类 工程中会经常使用第三方库,在引入第三方

iOS coretext图文混排

需要引入CoreText框架 然后引入头文件 至于用这个框架排出来的版就是自定义cell决定的了,在这里我们可以引用一个第三方的自定义的cell 给从接口里申请下来的数据创建一个Model接收 初始化在字典里 在需要图文混排的页面 定义一个数组的属性,在viewDodLoad里创建一个个字典并把字典都加到数组里去, 在给字典格式的时候可以把字体的大小和颜色设置好,如果一次要给好几个显示的内容那么就像下面这么写 注意把他们存在字典里的key都是text 现在就是需要把数组里的数据显示在cell上面

【iOS开源代码】(5):MKNetworkKit

ASIHTTPRequest (作者:BenCopsey) 是一个使用简单,可用于各种从简单到复杂的 HTTP 请求,或者可用于处理 Amazon S3.Rackspace 等 REST 服务的强大框架. 不幸的是,Ben 早在2011 年 9 月 21 日就已经声明停止开发和支持该框架(见http://allseeing-i.com/%5Brequest_release%5D; ). Ben 推荐了许多可替代的框架(比如AFNetworking, RestKit 或 LRResty).但最有潜

android 仿ios 对话框已封装成工具类

对话框 在android中是一种很常见的交互提示用户的方式,但是很多产品狗都叫我们这些做android的仿ios,搞的我们android程序员很苦逼,凭什么效果老是仿ios,有没有一点情怀,不过ios在界面封装确实比android好很多,吐槽完毕,比如一种很常见的场景就是在没网的情况下 提示用户,看效果图: 在很多界面都要有提示,那么就自然而然想到了封装,而不至于在每个页面都重写一篇,话不多说直接上代码 CommonDialog.java public class CommonDialog ex

MVP+Dagger2+Rxjava+Retrofit+GreenDao 开发的小应用,包含新闻、图片、视频3个大模块,代码封装良好

练习MVP架构开发的App,算是对自己学过的知识做一个总结,做了有一段时间,界面还算挺多的,代码量还是有的,里面做了大量封装,整体代码整理得很干净,这个我已经尽力整理了.不管是文件(java.xml.资源文件)命名,还是布局设计尽量简单简洁,我对自己写代码的规范还是有信心的- -.代码不会写的很复杂,整个代码结构有很高的统一度,结构也比较简单清晰,方便理解.里面做了大量的封装,包括基类的构建和工具类的封装,再配合Dagger2的使用可以极大地减轻V层(Activity和Fragment)的代码,

[51单片机] EEPROM 24c02 [I2C代码封装-保存实现流水灯]

这里把EEPROM 24c02封装起来,今后可以直接调用,其连线方式为:SDA-P2.1;SCL-P2.0;WP-VCC >_<:i2c.c 1 /*----------------------------------------------- 2 名称:IIC协议 3 内容:函数是采用软件延时的方法产生SCL脉冲,固对高晶振频率要作 一定的修改....(本例是1us机器 4 周期,即晶振频率要小于12MHZ) 5 ---------------------------------------

命名空间:不只是代码封装

命名空间 命名空间并不是新事物,在很多面向对象的编程语言中,都得到了很好的支持,它有效的解决了同一个脚本中的成员命名冲突问题.所以说,命名空间是一种代码封装技术,代码中的每个成员,都是自己的活动空间,彼此互不干扰.在php中,命名空间主要针对三类成员:函数,常量和类,因为他们三个家伙的作用域都是全局的.所以在同一个脚本中,是不允许重复定义函数,常量和类的.下面我们用实例来演示: <?php const SITE_NAME = 'PHP中文网'; //声明常量SITE_NAME function

java代码封装与编译

代码封装: 在这个java程序内调用另一个类 在arrayTool中把这两个函数封装起来. 编译顺序: 先编译ArrayTool再编译ArrayOperatorDemo 因为编译ArrayOperatorDemo文件时会进行语法检查,检查到第九行,会在classPath下面去找是否存在ArrayTool.class文件,如果没有配置classPath会在当前文件目录下找 该类是否存在.如果两个都没有,虚拟机还会去这两个路径下去寻找是否还有同名的原文件.即.java文件