本节要点:
1.理解图文混排的概念。
2.研究FTCoreText框架的实现原理。
3.演示使用FTCoreText框架实现图文混排的demo,以及效果图。
图文混排的概念
● 在view上显示的数据既包括文本数据,也包括图片数据。
● 图片的大小可以不同,图片和文字的排列顺序不固定。
● 文本数据的字体、颜色、背景等其他属性可以不同。
● 文本数据中还可以包含超链接。可以点击超链接进入其他页面。
研究FTCoreText框架的实现原理
1>.此框架的功能 和 缺陷
1.1> 功能:此框架能够实现不同风格的文本数据、图片数据的混合显示,还可以设置超链接。
1.2> 缺陷:此框架里面的文本的处理只针对与XML或者HTML格式的数据。(比如这种标签类型的数据:<title>Giraffe</title>)对于JSON格式的数据,可能需要自己修改内部一些方法的实现。
2>.此框架的基本构成
2.1> 此框架有两个类组成,分别是FTCoreTextStyle类、FTCoreTextView类。
2.2> FTCoreTextStyle类主要是用于设置格式的,定义了一些属性和方法。通过这个类就可以定义一种格式。
2.3> FTCoreTextView这个类是此框架的核心类。这个类里面定义了一个私有的类FTCoreTextNode。需要处理的数据中,每一个标签(<title>Giraffe</title>)对应一个节点。节点类里面就封装了一些节点的属性和方法,供FTCoreTextView这个类使用。
2.4> FTCoreTextView这个类主要封装了对数据的处理、绘制图片和文本,其中包含了很多处理细节的方法。
3>.FTCoreText框架内部具体的实现原理
3.1>FTCoreTextStyle类的主要实现
FTCoreTextStyle类封装的一些重要的属性:
<span style="color:#993399;">@property (nonatomic) NSString *name; //格式的名称
@property (nonatomic) NSString *appendedCharacter; //追加的字符串
@property (nonatomic) UIFont *font; //字体
@property (nonatomic) UIColor *color; //颜色
@property (nonatomic, getter=isUnderLined) BOOL underlined; //下划线
@property (nonatomic) FTCoreTextAlignement textAlignment; //文字显示位置
@property (nonatomic) UIEdgeInsets paragraphInset; //段落之间的间隔
@property (nonatomic) CGFloat leading;
@property (nonatomic) CGFloat maxLineHeight; //最大行高
@property (nonatomic) CGFloat minLineHeight; //最小行高
// bullet 格式
@property (nonatomic) NSString *bulletCharacter; //设置bullet字符
@property (nonatomic) UIFont *bulletFont; // 设置bullet字体
@property (nonatomic) UIColor *bulletColor; //设置bullet颜色
//当格式被解析时,回调的block
@property (nonatomic, copy) FTCoreTextCallbackBlock block;
@property (nonatomic, assign) BOOL applyParagraphStyling; //段落格式</span>
FTCoreTextStyle类封装的一些重要的方法:
<span style="color:#006600;">//初始化方法
- (id)init
{
self = [super init];
if (self) {
self.name = @"_default";
self.bulletCharacter = @"•";
self.appendedCharacter = @"";
self.font = [UIFont systemFontOfSize:12];
self.color = [UIColor blackColor];
self.underlined = NO;
self.textAlignment = FTCoreTextAlignementLeft;
self.maxLineHeight = 0;
self.minLineHeight = 0;
self.paragraphInset = UIEdgeInsetsZero;
self.applyParagraphStyling = YES;
self.leading = 0;
self.block=nil;
}
return self;
}</span>
<span style="color:#006600;">//类方法,通过name来定义一种格式
+ (id)styleWithName:(NSString *)name
{
FTCoreTextStyle *style = [[FTCoreTextStyle alloc] init];
[style setName:name];
return style;
}</span>
3.2>FTCoreTextNode的主要实现
FTCoreTextNode封装的一些主要的属性:
<span style="color:#993399;">@property (nonatomic, assign) FTCoreTextNode *supernode; //父节点
@property (nonatomic) NSArray *subnodes; //所有子类节点
@property (nonatomic, copy) FTCoreTextStyle *style; //定义一种格式
@property (nonatomic) NSRange styleRange; //定义格式应用的范围
@property (nonatomic) BOOL isClosed; //是否是关闭
@property (nonatomic) NSInteger startLocation; //开始的位置
@property (nonatomic) BOOL isLink; //是否是链接
@property (nonatomic) BOOL isImage; //是否是图片
@property (nonatomic) BOOL isBullet; //是否是Bullet
@property (nonatomic) NSString *imageName; //图片的名称</span>
FTCoreTextNode封装的一些主要的方法:
<span style="color:#006600;">//在所有子节点的最后插入一个子节点
- (void)addSubnode:(FTCoreTextNode *)node
{
[self insertSubnode:node atIndex:[_subnodes count]];
}
//在指定的位置插入子节点
- (void)insertSubnode:(FTCoreTextNode *)subnode atIndex:(NSUInteger)index
{
//设置父节点
subnode.supernode = self;
NSMutableArray *subnodes = (NSMutableArray *)self.subnodes;
//判断index是否超出数组_subnodes的长度
if (index <= [_subnodes count]) {
[subnodes insertObject:subnode atIndex:index];
}
else {
[subnodes addObject:subnode];
}
}</span>
<span style="color:#006600;">//获取节点的索引
- (NSUInteger)nodeIndex
{
return [_supernode.subnodes indexOfObject:self];
}
//获取指定位置的子节点
- (FTCoreTextNode *)subnodeAtIndex:(NSUInteger)index
{
if (index < [_subnodes count]) {
return [_subnodes objectAtIndex:index];
}
return nil;
}
</span>
3.3> FTCoreTextView 类的主要实现
FTCoreTextView封装的一些主要的属性:
<span style="color:#993399;">@property (nonatomic) NSString *text; //文本内容
@property (nonatomic) NSString *processedString; //处理之后的文本
@property (nonatomic, readonly) NSAttributedString *attributedString; //属性文本
@property (nonatomic, assign) CGPathRef path; //路径
@property (nonatomic) NSMutableDictionary *URLs; //存储所有的URL
@property (nonatomic) NSMutableArray *images; //存储所有图片
@property (nonatomic, assign) id <FTCoreTextViewDelegate> delegate; //设置代理对象
@property (nonatomic) UIColor *shadowColor; //阴影颜色
@property (nonatomic) CGSize shadowOffset; //阴影大小
@property (nonatomic) BOOL verbose;
@property (nonatomic) BOOL highlightTouch; //触摸时高亮状态</span>
<pre name="code" class="objc"><span style="color:#993399;">@property (nonatomic) CTFramesetterRef framesetter;
@property (nonatomic) FTCoreTextNode *rootNode; //节点
@property (nonatomic, readwrite) NSAttributedString *attributedString; //属性文本
@property (nonatomic) NSDictionary *touchedData; //点击的文本数据的属性的key和value的映射
@property (nonatomic) NSArray *selectionsViews; //选中的视图</span>
<span style="font-size:14px;color:#993399;">//以下常量是一些默认的标记(标签)名称
extern NSString * const FTCoreTextTagDefault; //默认标记
extern NSString * const FTCoreTextTagImage; //图片标记
extern NSString * const FTCoreTextTagBullet; //Bullet标记
extern NSString * const FTCoreTextTagPage; //< _page / >分页标记
extern NSString * const FTCoreTextTagLink; //超链接标记
extern NSString * const FTCoreTextTagParagraph; //段落标记</span>
<span style="font-size:14px;color:#993399;">//以下这些常量被使用在字典中的属性,这个字典是作为代理对象的协议方法的参数
extern NSString * const FTCoreTextDataURL; //定义URL
extern NSString * const FTCoreTextDataName; //定义Name
extern NSString * const FTCoreTextDataFrame; //定义Frame
extern NSString * const FTCoreTextDataAttributes; //定义Attributes
@protocol FTCoreTextViewDelegate; //定义协议</span>
FTCoreTextView
封装的一些主要的方法(主要方法的调用流程及实现的功能):
/*
调整视图的高度:
1、创建一个CGSize宽度就为视图当前的宽度,高度无穷大
2、调用将此suggestedSizeConstrainedToSize方法,CGSize作为参数。在这个方法中通过_framesetter和CGSize计算出一个合适的size,并返回size。由此确定当前视图的frame.
*/
- (void)fitToSuggestedHeight
{
CGSize suggestedSize = [self <span style="background-color: rgb(255, 153, 102);">suggestedSizeConstrainedToSize:CGSizeMake(CGRectGetWidth(self.frame), MAXFLOAT)</span>];
CGRect viewFrame = self.frame;
viewFrame.size.height = suggestedSize.height;
self.frame = viewFrame;
}
//计算文本的CGSize
- (CGSize)suggestedSizeConstrainedToSize:(CGSize)size
{
CGSize suggestedSize;
[self <span style="background-color: rgb(255, 153, 0);">updateFramesetterIfNeeded</span>];
if (_framesetter == NULL) {
return CGSizeZero;
}
suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(_framesetter, CFRangeMake(0, 0), NULL, size, NULL);
suggestedSize = CGSizeMake(ceilf(suggestedSize.width), ceilf(suggestedSize.height));
return suggestedSize;
}
//更新Framesetter
- (void)updateFramesetterIfNeeded
{
if (!_coreTextViewFlags.updatedAttrString) {
if (_framesetter != NULL) CFRelease(_framesetter);
_framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)<span style="background-color: rgb(255, 153, 0);">self.attributedString);</span>
_coreTextViewFlags.updatedAttrString = YES;
_coreTextViewFlags.updatedFramesetter = YES;
}
}
<span style="color:#008400;">//取得处理之后的属性文本
- (NSAttributedString *)attributedString
{
if (!_coreTextViewFlags.updatedAttrString) {
_coreTextViewFlags.updatedAttrString = YES;
if (_processedString == nil || _coreTextViewFlags.textChangesMade || !_coreTextViewFlags.updatedFramesetter) {
_coreTextViewFlags.textChangesMade = NO;
[</span><span style="color: rgb(0, 132, 0); background-color: rgb(255, 153, 0);">self processText</span><span style="color:#008400;">];
}
if (_processedString) {
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:_processedString];
for (FTCoreTextNode *node in [_rootNode allSubnodes]) {
[self </span><span style="color:#009900;background-color: rgb(255, 153, 0);">applyStyle:node.style inRange:node.styleRange onString:&string</span><span style="color:#008400;">];
}
_attributedString = string;
}
}
return _attributedString;
}</span>
<span style="color:#006600;">/*
</span><span style="color:#ff0000;">处理文本的方法的实现(比较核心的方法):</span><span style="color:#006600;">
1、定义正则表达式,在要处理的字符串中查找出符合要求的字符串。
NSString *regEx = @"<(/){0,1}.*?( /){0,1}>"; 此正则表达式,可以查找出三类标签,例如:<a 属性=@“123”> </a> <a/>
2、以上三类标签分别对应代码中的3中类型:
1>. <a 属性=@“123”> 对应这种类型 tagType = FTCoreTextTagTypeOpen;
2>. </a> 对应这种类型 tagType = FTCoreTextTagTypeClose;
3>. <a/> 对应这种类型 tagType = FTCoreTextTagTypeSelfClose;
3、获取到标签后,取得标签的名称。在对应的标签类型下进行判断和处理。
4、每个这样的标签(<a>123</a>)对应一个节点。每次取得一个标签的头部(标签类型:FTCoreTextTagTypeOpen),就创建一个节点。通过标签的名称判断是否是超链接、图片等等。
判断完之后,就设置当前节点的属性(节点有很多属性:isLink,isImage等等)。如果是图片,则设置 newNode.isImage = YES; 进行下次循环时,取得标签尾部(标签类型:FTCoreTextTagTypeClose),就直接通过当前节点的属性判断是否是图片标签,或者其他类型。根据判断结果,使用封装好的FTCoreTextStyle类,为不同的标签内容设置不同的格式Style为标签的内容设置不同的格式。
*/
- (void)processText
{
if (!_text || [_text length] == 0) return;
[_URLs removeAllObjects];
[_images removeAllObjects];
FTCoreTextNode *rootNode = [FTCoreTextNode new];
rootNode.style = [_styles objectForKey:[self defaultTagNameForKey:FTCoreTextTagDefault]];
FTCoreTextNode *currentSupernode = rootNode;
NSMutableString *processedString = [NSMutableString stringWithString:_text];
BOOL finished = NO;
NSRange remainingRange = NSMakeRange(0, [processedString length]);
//定义了正则表达式,查找标签,例如:<a 属性=@“123”> </a> <a/>
<span style="background-color: rgb(204, 204, 255);">NSString *regEx = @"<(/){0,1}.*?( /){0,1}>";</span>
while (!finished) {
NSRange tagRange = [processedString rangeOfString:regEx options:NSRegularExpressionSearch range:remainingRange];
if (tagRange.location == NSNotFound) {
if (currentSupernode != rootNode && !currentSupernode.isClosed) {
if (_verbose) NSLog(@"FTCoreTextView :%@ - Couldn't parse text because tag '%@' at position %d is not closed - aborting rendering", self, currentSupernode.style.name, currentSupernode.startLocation);
return;
}
finished = YES;
continue;
}
NSString *fullTag = [processedString substringWithRange:tagRange];
FTCoreTextTagType tagType;
if ([fullTag rangeOfString:@"</"].location == 0) {
//标签</b>
tagType = FTCoreTextTagTypeClose;
}
else if ([fullTag rangeOfString:@"/>"].location == NSNotFound && [fullTag rangeOfString:@" />"].location == NSNotFound) {
//标签<b>或者<b 属性=@"123">
tagType = FTCoreTextTagTypeOpen;
}
else {
//标签<b/>
tagType = FTCoreTextTagTypeSelfClose;
}
//获取整个标签所有组成部分
NSArray *tagsComponents = [fullTag componentsSeparatedByString:@" "];
//获取标签名称(里面可能包括"<")
NSString *tagName = (tagsComponents.count > 0) ? [tagsComponents objectAtIndex:0] : fullTag;
//这里获得的tagName的值例如:_image
tagName = [tagName stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"< />"]];
FTCoreTextStyle *style = [_styles objectForKey:tagName];
if (style == nil) {
style = [_styles objectForKey:[self defaultTagNameForKey:FTCoreTextTagDefault]];
if (_verbose) NSLog(@"FTCoreTextView :%@ - Couldn't find style for tag '%@'", self, tagName);
}
//判断标签的类型
switch (tagType) {
case FTCoreTextTagTypeOpen:
{
if (currentSupernode.isLink || currentSupernode.isImage) {
NSString *predefinedTag = nil;
if (currentSupernode.isLink) predefinedTag = [self defaultTagNameForKey:FTCoreTextTagLink];
else if (currentSupernode.isImage) predefinedTag = [self defaultTagNameForKey:FTCoreTextTagImage];
if (_verbose) NSLog(@"FTCoreTextView :%@ - You can't open a new tag inside a '%@' tag - aborting rendering", self, predefinedTag);
return;
}
FTCoreTextNode *newNode = [FTCoreTextNode new];
newNode.style = style;
newNode.startLocation = tagRange.location;
//判断是否是链接
if ([tagName isEqualToString:[self defaultTagNameForKey:FTCoreTextTagLink]]) {
newNode.isLink = YES;
}
//判断是否是Bullet
else if ([tagName isEqualToString:[self defaultTagNameForKey:FTCoreTextTagBullet]]) {
newNode.isBullet = YES;
NSString *appendedString = [NSString stringWithFormat:@"%@\t", newNode.style.bulletCharacter];
[processedString insertString:appendedString atIndex:tagRange.location + tagRange.length];
//bullet styling
//设置子标签的样式
FTCoreTextStyle *bulletStyle = [FTCoreTextStyle new];
bulletStyle.name = @"_FTBulletStyle";
bulletStyle.font = newNode.style.bulletFont;
bulletStyle.color = newNode.style.bulletColor;
bulletStyle.applyParagraphStyling = NO;
bulletStyle.paragraphInset = UIEdgeInsetsMake(0, 0, 0, newNode.style.paragraphInset.left);
FTCoreTextNode *bulletNode = [FTCoreTextNode new];
bulletNode.style = bulletStyle;
bulletNode.styleRange = NSMakeRange(tagRange.location, [appendedString length]);
[newNode addSubnode:bulletNode];
}
//判断是否是图片
else if ([tagName isEqualToString:[self defaultTagNameForKey:FTCoreTextTagImage]]) {
newNode.isImage = YES;
}
[processedString replaceCharactersInRange:tagRange withString:@""];
[currentSupernode addSubnode:newNode];
currentSupernode = newNode;
remainingRange.location = tagRange.location;
remainingRange.length = [processedString length] - tagRange.location;
}
break;
case FTCoreTextTagTypeClose:
{
if ((![currentSupernode.style.name isEqualToString:[self defaultTagNameForKey:FTCoreTextTagDefault]] && ![currentSupernode.style.name isEqualToString:tagName]) ) {
if (_verbose) NSLog(@"FTCoreTextView :%@ - Closing tag '%@' at range %@ doesn't match open tag '%@' - aborting rendering", self, fullTag, NSStringFromRange(tagRange), currentSupernode.style.name);
return;
}
currentSupernode.isClosed = YES;
if (currentSupernode.isLink) {
NSRange elementContentRange = NSMakeRange(currentSupernode.startLocation, tagRange.location - currentSupernode.startLocation);
NSString *elementContent = [processedString substringWithRange:elementContentRange];
NSRange pipeRange = [elementContent rangeOfString:@"|"];
NSString *urlString = nil;
NSString *urlDescription = nil;
if (pipeRange.location != NSNotFound) {
urlString = [elementContent substringToIndex:pipeRange.location] ;
urlDescription = [elementContent substringFromIndex:pipeRange.location + 1];
}
[processedString replaceCharactersInRange:NSMakeRange(elementContentRange.location, elementContentRange.length + tagRange.length) withString:urlDescription];
if (!([[urlString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] hasPrefix:@"http://"] || [[urlString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] hasPrefix:@"https://"]))
{
urlString = [NSString stringWithFormat:@"http://%@", urlString];
}
NSURL *url = [NSURL URLWithString:[urlString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]];
NSRange urlDescriptionRange = NSMakeRange(elementContentRange.location, [urlDescription length]);
[_URLs setObject:url forKey:NSStringFromRange(urlDescriptionRange)];
currentSupernode.styleRange = urlDescriptionRange;
}
else if (currentSupernode.isImage) {
NSRange elementContentRange = NSMakeRange(currentSupernode.startLocation, tagRange.location - currentSupernode.startLocation);
NSString *elementContent = [processedString substringWithRange:elementContentRange];
UIImage *img = [UIImage imageNamed:elementContent];
if (img) {
NSString *lines = @"\n";
float leading = img.size.height;
currentSupernode.style.leading = leading;
currentSupernode.imageName = elementContent;
[processedString replaceCharactersInRange:NSMakeRange(elementContentRange.location, elementContentRange.length + tagRange.length) withString:lines];
[_images addObject:currentSupernode];
currentSupernode.styleRange = NSMakeRange(elementContentRange.location, [lines length]);
}
else {
if (_verbose) NSLog(@"FTCoreTextView :%@ - Couldn't find image '%@' in main bundle", self, [NSValue valueWithRange:elementContentRange]);
[processedString replaceCharactersInRange:tagRange withString:@""];
}
}
else {
currentSupernode.styleRange = NSMakeRange(currentSupernode.startLocation, tagRange.location - currentSupernode.startLocation);
[processedString replaceCharactersInRange:tagRange withString:@""];
}
if ([currentSupernode.style.appendedCharacter length] > 0) {
[processedString insertString:currentSupernode.style.appendedCharacter atIndex:currentSupernode.styleRange.location + currentSupernode.styleRange.length];
NSRange newStyleRange = currentSupernode.styleRange;
newStyleRange.length += [currentSupernode.style.appendedCharacter length];
currentSupernode.styleRange = newStyleRange;
}
if (style.paragraphInset.top > 0) {
if (![style.name isEqualToString:[self defaultTagNameForKey:FTCoreTextTagBullet]] || [[currentSupernode previousNode].style.name isEqualToString:[self defaultTagNameForKey:FTCoreTextTagBullet]]) {
[processedString insertString:@"\n" atIndex:currentSupernode.startLocation];
NSRange topSpacingStyleRange = NSMakeRange(currentSupernode.startLocation, [@"\n" length]);
FTCoreTextStyle *topSpacingStyle = [[FTCoreTextStyle alloc] init];
topSpacingStyle.name = [NSString stringWithFormat:@"_FTTopSpacingStyle_%@", currentSupernode.style.name];
topSpacingStyle.minLineHeight = currentSupernode.style.paragraphInset.top;
topSpacingStyle.maxLineHeight = currentSupernode.style.paragraphInset.top;
FTCoreTextNode *topSpacingNode = [[FTCoreTextNode alloc] init];
topSpacingNode.style = topSpacingStyle;
topSpacingNode.styleRange = topSpacingStyleRange;
[currentSupernode.supernode insertSubnode:topSpacingNode beforeNode:currentSupernode];
[currentSupernode adjustStylesAndSubstylesRangesByRange:topSpacingStyleRange];
}
}
remainingRange.location = currentSupernode.styleRange.location + currentSupernode.styleRange.length;
remainingRange.length = [processedString length] - remainingRange.location;
currentSupernode = currentSupernode.supernode;
}
break;
case FTCoreTextTagTypeSelfClose:
{
FTCoreTextNode *newNode = [FTCoreTextNode new];
newNode.style = style;
[processedString replaceCharactersInRange:tagRange withString:newNode.style.appendedCharacter];
newNode.styleRange = NSMakeRange(tagRange.location, [newNode.style.appendedCharacter length]);
newNode.startLocation = tagRange.location;
[currentSupernode addSubnode:newNode];
if (style.block)
{
NSDictionary* blockDict = [NSDictionary dictionaryWithObjectsAndKeys:tagsComponents,@"components", [NSValue valueWithRange:NSMakeRange(tagRange.location, newNode.style.appendedCharacter.length)],@"range", nil];
style.block(blockDict);
}
remainingRange.location = tagRange.location;
remainingRange.length = [processedString length] - tagRange.location;
}
break;
}
}
rootNode.styleRange = NSMakeRange(0, [processedString length]);
self.rootNode = rootNode;
self.processedString = processedString;
}</span>
<span style="color:#006600;">//在指定的范围内,应用指定的格式
- (void)applyStyle:(FTCoreTextStyle *)style inRange:(NSRange)styleRange onString:(NSMutableAttributedString **)attributedString
{
[*attributedString addAttribute:(id)FTCoreTextDataName
value:(id)style.name
range:styleRange];
[*attributedString addAttribute:(id)kCTForegroundColorAttributeName
value:(id)style.color.CGColor
range:styleRange];
if (style.isUnderLined) {
NSNumber *underline = [NSNumber numberWithInt:kCTUnderlineStyleSingle];
[*attributedString addAttribute:(id)kCTUnderlineStyleAttributeName
value:(id)underline
range:styleRange];
}
CTFontRef ctFont = CTFontCreateFromUIFont(style.font);
[*attributedString addAttribute:(id)kCTFontAttributeName
value:(__bridge id)ctFont
range:styleRange];
CFRelease(ctFont);
CTTextAlignment alignment = style.textAlignment;
CGFloat maxLineHeight = style.maxLineHeight;
CGFloat minLineHeight = style.minLineHeight;
CGFloat paragraphLeading = style.leading;
CGFloat paragraphSpacingBefore = style.paragraphInset.top;
CGFloat paragraphSpacingAfter = style.paragraphInset.bottom;
CGFloat paragraphFirstLineHeadIntent = style.paragraphInset.left;
CGFloat paragraphHeadIntent = style.paragraphInset.left;
CGFloat paragraphTailIntent = style.paragraphInset.right;
//if (SYSTEM_VERSION_LESS_THAN(@"5.0")) {
paragraphSpacingBefore = 0;
//}
CFIndex numberOfSettings = 9;
CGFloat tabSpacing = 28.f;
BOOL applyParagraphStyling = style.applyParagraphStyling;
if ([style.name isEqualToString:[self defaultTagNameForKey:FTCoreTextTagBullet]]) {
applyParagraphStyling = YES;
}
else if ([style.name isEqualToString:@"_FTBulletStyle"]) {
applyParagraphStyling = YES;
numberOfSettings++;
tabSpacing = style.paragraphInset.right;
paragraphSpacingBefore = 0;
paragraphSpacingAfter = 0;
paragraphFirstLineHeadIntent = 0;
paragraphTailIntent = 0;
}
else if ([style.name hasPrefix:@"_FTTopSpacingStyle"]) {
[*attributedString removeAttribute:(id)kCTParagraphStyleAttributeName range:styleRange];
}
if (applyParagraphStyling) {
CTTextTabRef tabArray[] = { CTTextTabCreate(0, tabSpacing, NULL) };
CFArrayRef tabStops = CFArrayCreate( kCFAllocatorDefault, (const void**) tabArray, 1, &kCFTypeArrayCallBacks );
CFRelease(tabArray[0]);
CTParagraphStyleSetting settings[] = {
{kCTParagraphStyleSpecifierAlignment, sizeof(alignment), &alignment},
{kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(CGFloat), &maxLineHeight},
{kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(CGFloat), &minLineHeight},
{kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(CGFloat), ¶graphSpacingBefore},
{kCTParagraphStyleSpecifierParagraphSpacing, sizeof(CGFloat), ¶graphSpacingAfter},
{kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(CGFloat), ¶graphFirstLineHeadIntent},
{kCTParagraphStyleSpecifierHeadIndent, sizeof(CGFloat), ¶graphHeadIntent},
{kCTParagraphStyleSpecifierTailIndent, sizeof(CGFloat), ¶graphTailIntent},
{kCTParagraphStyleSpecifierLineSpacing, sizeof(CGFloat), ¶graphLeading},
{kCTParagraphStyleSpecifierTabStops, sizeof(CFArrayRef), &tabStops}//always at the end
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, numberOfSettings);
[*attributedString addAttribute:(id)kCTParagraphStyleAttributeName
value:(__bridge id)paragraphStyle
range:styleRange];
CFRelease(tabStops);
CFRelease(paragraphStyle);
}
}</span>
//绘制的方法
<span style="color:#006600;">/*
drawRect方法调用流程:
程序运行先调用drawRect方法,然后再调用drawImages方法,将整个页面绘制出来。
drawRect方法的实现:
1、创建CGPath
2、由CGPath和framesetter创建CTFrameRef
3、绘制:CTFrameDraw(drawFrame, context);
4、如果有图片就调用drawImages,进行图片绘制
drawImages方法的实现:
1、创建CGPath
2、由CGPath和framesetter创建CTFrameRef
3、通过这个方法CTFrameGetLines(ctframe);获取lines
4、最后构造出frame,绘制图片[img drawInRect:CGRectIntegral(frame)];
*/
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
[self.backgroundColor setFill];
CGContextFillRect(context, rect);
//更新Framesetter
[self updateFramesetterIfNeeded];
CGMutablePathRef mainPath = CGPathCreateMutable();
if (!_path) {
CGPathAddRect(mainPath, NULL, CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height));
}
else {
CGPathAddPath(mainPath, NULL, _path);
}
CTFrameRef drawFrame = CTFramesetterCreateFrame(_framesetter, CFRangeMake(0, 0), mainPath, NULL);
if (drawFrame == NULL) {
if (_verbose) NSLog(@"f: %@", self.processedString);
}
else {
//draw images 绘制图片
if ([_images count] > 0) [self<span style="background-color: rgb(255, 153, 0);"> drawImages</span>];
//设置阴影颜色
if (_shadowColor) {
CGContextSetShadowWithColor(context, _shadowOffset, 0.f, _shadowColor.CGColor);
}
//转换坐标系统
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
// draw text
CTFrameDraw(drawFrame, context);
}
// cleanup
if (drawFrame) CFRelease(drawFrame);
CGPathRelease(mainPath);
if ([_delegate respondsToSelector:@selector(coreTextViewfinishedRendering:)]) {
[_delegate coreTextViewfinishedRendering:self];
}
}</span><span style="color:#009900;">
</span>
<span style="color:#006600;">//绘制图片
- (void)drawImages
{
CGMutablePathRef mainPath = CGPathCreateMutable();
if (!_path) {
CGPathAddRect(mainPath, NULL, CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height));
}
else {
CGPathAddPath(mainPath, NULL, _path);
}
CTFrameRef ctframe = CTFramesetterCreateFrame(_framesetter, CFRangeMake(0, 0), mainPath, NULL);
CGPathRelease(mainPath);
NSArray *lines = (__bridge NSArray *)CTFrameGetLines(ctframe);
NSInteger lineCount = [lines count];
CGPoint origins[lineCount];
CTFrameGetLineOrigins(ctframe, CFRangeMake(0, 0), origins);
FTCoreTextNode *imageNode = [_images objectAtIndex:0];
for (int i = 0; i < lineCount; i++) {
CGPoint baselineOrigin = origins[i];
//the view is inverted, the y origin of the baseline is upside down
baselineOrigin.y = CGRectGetHeight(self.frame) - baselineOrigin.y;
CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:i];
CFRange cfrange = CTLineGetStringRange(line);
if (cfrange.location > imageNode.styleRange.location) {
CGFloat ascent, descent;
CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, NULL);
CGRect lineFrame = CGRectMake(baselineOrigin.x, baselineOrigin.y - ascent, lineWidth, ascent + descent);
CTTextAlignment alignment = imageNode.style.textAlignment;
UIImage *img = [UIImage imageNamed:imageNode.imageName];
if (img) {
int x = 0;
if (alignment == kCTRightTextAlignment) x = (self.frame.size.width - img.size.width);
if (alignment == kCTCenterTextAlignment) x = ((self.frame.size.width - img.size.width) / 2);
CGRect frame = CGRectMake(x, (lineFrame.origin.y - img.size.height), img.size.width, img.size.height);
// adjusting frame
UIEdgeInsets insets = imageNode.style.paragraphInset;
if (alignment != kCTCenterTextAlignment) frame.origin.x = (alignment == kCTLeftTextAlignment)? insets.left : (self.frame.size.width - img.size.width - insets.right);
frame.origin.y += insets.top;
frame.size.width = ((insets.left + insets.right + img.size.width ) > self.frame.size.width)? self.frame.size.width : img.size.width;
[img drawInRect:CGRectIntegral(frame)];
}
NSInteger imageNodeIndex = [_images indexOfObject:imageNode];
if (imageNodeIndex < [_images count] - 1) {
imageNode = [_images objectAtIndex:imageNodeIndex + 1];
}
else {
break;
}
}
}
CFRelease(ctframe);
}
</span>
//触摸事件触发的方法
<span style="color:#006600;">/*
当用户点击超链接时,方法调用流程:
首先调用touchesBegan方法。在touchesBegan方法中又会调用dataForPoint:activeRects:这个方法,返回被点击的超链接页面绘制需要的参数字典。最后在touchesEnded的方法中调用协议方法,并将参数字典传过去。在协议方法中取得被点击的超链接对应的URL,显示出URL对应的页面。
*/
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
if (self.delegate && [self.delegate respondsToSelector:@selector(coreTextView:receivedTouchOnData:)]) {
CGPoint point = [(UITouch *)[touches anyObject] locationInView:self];
NSMutableArray *activeRects;
NSDictionary *data = [self <span style="background-color: rgb(255, 153, 0);">dataForPoint:point activeRects:&activeRects</span>];
if (data.count > 0) {
NSMutableArray *selectedViews = [NSMutableArray new];
for (NSString *rectString in activeRects) {
CGRect rect = CGRectFromString(rectString);
UIView *view = [[UIView alloc] initWithFrame:rect];
view.layer.cornerRadius = 3;
view.clipsToBounds = YES;
view.backgroundColor = [UIColor colorWithWhite:0 alpha:0.25];
[self addSubview:view];
[selectedViews addObject:view];
}
self.touchedData = data;
self.selectionsViews = selectedViews;
}
}
}</span>
<span style="color:#006600;">/* 在touchBegan方法中调用此方法
此方法的实现:
1、构造CGPathRef
2、通过CTFramesetterRef和CGPath 构造出一个CTFrameRef
3、在从CTFrameRef获取lines: NSArray *lines = (__bridge NSArray *)CTFrameGetLines(ctframe);
4.从lines里面获取runs: CFArrayRef runs = CTLineGetGlyphRuns(line);
5.遍历所有的run,获取到attributes,并且设置了runBounds;
然后将attributes和runBounds存到一个字典中:
[returnedDict setObject:attributes forKey:FTCoreTextDataAttributes];
[returnedDict setObject:NSStringFromCGRect(runBounds) forKey:FTCoreTextDataFrame];
返回字典returnedDict。
*/
- (NSDictionary *)dataForPoint:(CGPoint)point activeRects:(NSArray **)activeRects
{
NSMutableDictionary *returnedDict = [NSMutableDictionary dictionary];
CGMutablePathRef mainPath = CGPathCreateMutable();
if (!_path) {
CGPathAddRect(mainPath, NULL, CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height));
}
else {
CGPathAddPath(mainPath, NULL, _path);
}
CTFrameRef ctframe = CTFramesetterCreateFrame(_framesetter, CFRangeMake(0, 0), mainPath, NULL);
CGPathRelease(mainPath);
NSArray *lines = (__bridge NSArray *)CTFrameGetLines(ctframe);
NSInteger lineCount = [lines count];
//数组存放点
CGPoint origins[lineCount];
if (lineCount != 0) {
CTFrameGetLineOrigins(ctframe, CFRangeMake(0, 0), origins);
for (int i = 0; i < lineCount; i++) {
CGPoint baselineOrigin = origins[i];
//the view is inverted, the y origin of the baseline is upside down
//y 的起点是反向的,视图是颠倒的
baselineOrigin.y = CGRectGetHeight(self.frame) - baselineOrigin.y;
CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:i];
CGFloat ascent, descent;
CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, NULL);
CGRect lineFrame = CGRectMake(baselineOrigin.x, baselineOrigin.y - ascent, lineWidth, ascent + descent);
//判断点是否在lineFrame的范围内
if (CGRectContainsPoint(lineFrame, point)) {
//we look if the position of the touch is correct on the line
CFIndex index = CTLineGetStringIndexForPosition(line, point);
NSArray *urlsKeys = [_URLs allKeys];
for (NSString *key in urlsKeys) {
NSRange range = NSRangeFromString(key);
if (index >= range.location && index < range.location + range.length) {
NSURL *url = [_URLs objectForKey:key];
if (url) [returnedDict setObject:url forKey:FTCoreTextDataURL];
if (activeRects && _highlightTouch) {
//we looks for the rects enclosing the entire active section
NSInteger startIndex = range.location;
NSInteger endIndex = range.location + range.length;
//we look for the line that contains the start index
//寻找线的起点的索引
NSInteger startLineIndex = i;
for (int iLine = i; iLine >= 0; iLine--) {
CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:iLine];
CFRange range = CTLineGetStringRange(line);
if (range.location <= startIndex && range.location + range.length >= startIndex) {
startLineIndex = iLine;
break;
}
}
//we look for the line that contains the end index
//寻找线的终点索引
NSInteger endLineIndex = startLineIndex;
for (int iLine = i; iLine < lineCount; iLine++) {
CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:iLine];
CFRange range = CTLineGetStringRange(line);
if (range.location <= endIndex && range.location + range.length >= endIndex) {
endLineIndex = iLine;
break;
}
}
//we get enclosing rects
//获取到封闭的矩形
NSMutableArray *rectsStrings = [NSMutableArray new];
for (int iLine = startLineIndex; iLine <= endLineIndex; iLine++) {
CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:iLine];
CGFloat ascent, descent;
CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, NULL);
CGPoint baselineOrigin = origins[iLine];
//the view is inverted, the y origin of the baseline is upside down
//y 的起点是反向的,视图是颠倒的
baselineOrigin.y = CGRectGetHeight(self.frame) - baselineOrigin.y;
CGRect lineFrame = CGRectMake(baselineOrigin.x, baselineOrigin.y - ascent, lineWidth, ascent + descent);
CGRect actualRect;
actualRect.size.height = lineFrame.size.height;
actualRect.origin.y = lineFrame.origin.y;
CFRange range = CTLineGetStringRange(line);
if (range.location >= startIndex) {
//the beginning of the line is included
actualRect.origin.x = lineFrame.origin.x;
} else {
actualRect.origin.x = CTLineGetOffsetForStringIndex(line, startIndex, NULL);
}
NSInteger lineRangEnd = range.length + range.location;
if (lineRangEnd <= endIndex) {
//the end of the line is included
actualRect.size.width = CGRectGetMaxX(lineFrame) - CGRectGetMinX(actualRect);
} else {
CGFloat position = CTLineGetOffsetForStringIndex(line, endIndex, NULL);
actualRect.size.width = position - CGRectGetMinX(actualRect);
}
actualRect = CGRectInset(actualRect, -1, 0);
[rectsStrings addObject:NSStringFromCGRect(actualRect)];
}
*activeRects = rectsStrings;
}
break;
}
}
//frame
CFArrayRef runs = CTLineGetGlyphRuns(line);
for(CFIndex j = 0; j < CFArrayGetCount(runs); j++) {
CTRunRef run = CFArrayGetValueAtIndex(runs, j);
NSDictionary* attributes = (__bridge NSDictionary*)CTRunGetAttributes(run);
NSString *name = [attributes objectForKey:FTCoreTextDataName];
if (![name isEqualToString:@"_link"]) continue;
//设置FTCoreTextDataAttributes
[returnedDict setObject:attributes forKey:FTCoreTextDataAttributes];
CGRect runBounds;
runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL); //8
runBounds.size.height = ascent + descent;
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL); //9
runBounds.origin.x = baselineOrigin.x + self.frame.origin.x + xOffset + 0;
runBounds.origin.y = baselineOrigin.y + lineFrame.size.height - ascent;
//设置FTCoreTextDataFrame
[returnedDict setObject:NSStringFromCGRect(runBounds) forKey:FTCoreTextDataFrame];
}
}
if (returnedDict.count > 0) break;
}
}
CFRelease(ctframe);
return returnedDict;
}
</span>
<span style="color:#006600;">//触摸结束后,调用的方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
if (_touchedData) {
if (self.delegate && [self.delegate respondsToSelector:@selector(coreTextView:receivedTouchOnData:)]) {
if ([self.delegate respondsToSelector:@selector(coreTextView:receivedTouchOnData:)]) {
//触摸结束时,调用协议方法
[self.delegate <span style="background-color: rgb(255, 153, 0);">coreTextView:self receivedTouchOnData:_touchedData</span>];
}
}
_touchedData = nil;
[_selectionsViews makeObjectsPerformSelector:@selector(removeFromSuperview)];
_selectionsViews = nil;
}
}</span>
演示使用FTCoreText框架实现图文混排的demo,以及效果图
demo的.h文件:
<span style="color:#006600;">@interface articleViewController : UIViewController <FTCoreTextViewDelegate>
@property (nonatomic) UIScrollView *scrollView;
@property (nonatomic) FTCoreTextView *coreTextView;</span>
demo的.m文件:
<span style="color:#006600;">#pragma mark - View Controller Methods
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad
{
[super viewDidLoad];
//add coretextview
scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
coreTextView = [[FTCoreTextView alloc] initWithFrame:CGRectMake(20, 20, 280, 0)];
coreTextView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
// set text
[coreTextView setText:[self textForView]];
// set styles
[coreTextView addStyles:[self coreTextStyle]];
// set delegate
[coreTextView setDelegate:self];
//设置coreTextView的高度
[coreTextView fitToSuggestedHeight];
[scrollView addSubview:coreTextView];
[scrollView setContentSize:CGSizeMake(CGRectGetWidth(scrollView.bounds), CGRectGetHeight(coreTextView.frame) + 40)];
[self.view addSubview:scrollView];
}<span style="font-size:14px;">
</span></span>
//取得要处理的文本数据
- (NSString *)textForView
{
NSString *text = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"text" ofType:@"txt"] encoding:NSUTF8StringEncoding error:nil];
return text;
}
//定义一组格式
- (NSArray *)coreTextStyle
{
NSMutableArray *result = [NSMutableArray array];
FTCoreTextStyle *defaultStyle = [FTCoreTextStyle new];
defaultStyle.name = FTCoreTextTagDefault; //thought the default name is already set to FTCoreTextTagDefault
defaultStyle.font = [UIFont fontWithName:@"TimesNewRomanPSMT" size:16.f];
defaultStyle.textAlignment = FTCoreTextAlignementJustified;
[result addObject:defaultStyle];
FTCoreTextStyle *titleStyle = [FTCoreTextStyle styleWithName:@"title"]; // using fast method
titleStyle.font = [UIFont fontWithName:@"TimesNewRomanPSMT" size:40.f];
titleStyle.paragraphInset = UIEdgeInsetsMake(0, 0, 25, 0);
titleStyle.textAlignment = FTCoreTextAlignementCenter;
[result addObject:titleStyle];
FTCoreTextStyle *imageStyle = [FTCoreTextStyle new];
imageStyle.paragraphInset = UIEdgeInsetsMake(0,0,0,0);
imageStyle.name = FTCoreTextTagImage;
imageStyle.textAlignment = FTCoreTextAlignementCenter;
[result addObject:imageStyle];
FTCoreTextStyle *firstLetterStyle = [FTCoreTextStyle new];
firstLetterStyle.name = @"firstLetter";
firstLetterStyle.font = [UIFont fontWithName:@"TimesNewRomanPS-BoldMT" size:30.f];
[result addObject:firstLetterStyle];
FTCoreTextStyle *linkStyle = [defaultStyle copy];
linkStyle.name = FTCoreTextTagLink;
linkStyle.color = [UIColor orangeColor];
[result addObject:linkStyle];
FTCoreTextStyle *subtitleStyle = [FTCoreTextStyle styleWithName:@"subtitle"];
subtitleStyle.font = [UIFont fontWithName:@"TimesNewRomanPS-BoldMT" size:25.f];
subtitleStyle.color = [UIColor brownColor];
subtitleStyle.paragraphInset = UIEdgeInsetsMake(10, 0, 10, 0);
[result addObject:subtitleStyle];
FTCoreTextStyle *bulletStyle = [defaultStyle copy];
bulletStyle.name = FTCoreTextTagBullet;
bulletStyle.bulletFont = [UIFont fontWithName:@"TimesNewRomanPSMT" size:16.f];
bulletStyle.bulletColor = [UIColor orangeColor];
bulletStyle.bulletCharacter = @"❧";
[result addObject:bulletStyle];
FTCoreTextStyle *italicStyle = [defaultStyle copy];
italicStyle.name = @"italic";
italicStyle.underlined = YES;
italicStyle.font = [UIFont fontWithName:@"TimesNewRomanPS-ItalicMT" size:16.f];
[result addObject:italicStyle];
FTCoreTextStyle *boldStyle = [defaultStyle copy];
boldStyle.name = @"bold";
boldStyle.font = [UIFont fontWithName:@"TimesNewRomanPS-BoldMT" size:16.f];
[result addObject:boldStyle];
FTCoreTextStyle *coloredStyle = [defaultStyle copy];
[coloredStyle setName:@"colored"];
[coloredStyle setColor:[UIColor redColor]];
[result addObject:coloredStyle];
return result;
}
#pragma mark - FTCoreTextView 协议方法 点击超链接时,会调用此方法
- (void)coreTextView:(FTCoreTextView *)acoreTextView receivedTouchOnData:(NSDictionary *)data
{
NSURL *url = [data objectForKey:FTCoreTextDataURL];
if (!url) return;
[[UIApplication sharedApplication] openURL:url];
}<span style="font-size: 18px;">
</span>
效果 图1 效果 图2
效果图3
CoreText的框架图(有助于理解其中的一些方法的实现):