本文講的主要內(nèi)容是如何將CoreText繪圖和自定義的View結(jié)合在一起擎值,進(jìn)行無縫的排版,并且可以控制自定義View元素的對其方式(頂部對其鸠儿、底部對其进每、居中對其)
其它文章:
CoreText 入門(一)-文本繪制
CoreText入門(二)-繪制圖片
CoreText進(jìn)階(三)-事件處理
CoreText進(jìn)階(四)-文字行數(shù)限制和顯示更多
CoreText進(jìn)階(五)- 文字排版樣式和效果
CoreText進(jìn)階(六)-內(nèi)容大小計算和自動布局
CoreText進(jìn)階(七)-添加自定義View和對其
效果
Demo:CoreTextDemo
實現(xiàn)代碼如下:
- (void)viewDidLoad {
[super viewDidLoad];
self.edgesForExtendedLayout = UIRectEdgeNone;
self.view.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1];
CGRect frame = CGRectMake(0, 100, self.view.bounds.size.width, 400);
YTDrawView *textDrawView = [[YTDrawView alloc] initWithFrame:frame];
textDrawView.backgroundColor = [UIColor whiteColor];
// 添加普通的文本
[textDrawView addString:@"Hello World " attributes:self.defaultTextAttributes clickActionHandler:^(id obj) {
}];
// 添加鏈接
[textDrawView addLink:@"http://www.baidu.com" clickActionHandler:^(id obj) {
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"鏈接點擊" message:[NSString stringWithFormat:@"點擊對象%@", obj] preferredStyle:(UIAlertControllerStyleAlert)];
[alert addAction:[UIAlertAction actionWithTitle:@"取消" style:(UIAlertActionStyleCancel) handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}];
// 添加圖片
[textDrawView addImage:[UIImage imageNamed:@"tata_img_hottopicdefault"] size:CGSizeMake(30, 30) clickActionHandler:^(id obj) {
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"圖片點擊" message:[NSString stringWithFormat:@"點擊對象%@", obj] preferredStyle:(UIAlertControllerStyleAlert)];
[alert addAction:[UIAlertAction actionWithTitle:@"取消" style:(UIAlertActionStyleCancel) handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}];
// 添加鏈接
[textDrawView addLink:@"http://www.baidu.com" clickActionHandler:^(id obj) {
}];
// 添加普通的文本
[textDrawView addString:@"這是一個最好的時代品追,也是一個最壞的時代;" attributes:self.defaultTextAttributes clickActionHandler:^(id obj) {
}];
// 添加鏈接
[textDrawView addLink:@" 這是明智的時代遭京,這是愚昧的時代泞莉;這是信任的紀(jì)元,這是懷疑的紀(jì)元斯嚎;這是光明的季節(jié),這是黑暗的季節(jié)糠惫;這是希望的春日钉疫,這是失望的冬日; " clickActionHandler:^(id obj) {
}];
// 添加自定義的View固阁,默認(rèn)是底部對其
UIView* customView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 160, 50)];
customView.backgroundColor = [UIColor colorWithRed:1 green:0.7 blue:1 alpha:0.51];
[customView bk_whenTapped:^{
NSLog(@"customView Tapped");
}];
UILabel *labelInCustomView = [UILabel new];
labelInCustomView.textAlignment = NSTextAlignmentCenter;
labelInCustomView.font = [UIFont systemFontOfSize:12];
labelInCustomView.text = @"可點擊的自定義的View";
[customView addSubview:labelInCustomView];
[labelInCustomView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(customView);
}];
[textDrawView addView:customView size:customView.frame.size clickActionHandler:nil];
// 添加普通的文本
[textDrawView addString:@" Hello " attributes:self.defaultTextAttributes clickActionHandler:nil];
// 添加居中對其的自定義的View
UIView *unClickableCustomView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 160, 50)];
unClickableCustomView.backgroundColor = [UIColor colorWithRed:1 green:0.7 blue:1 alpha:0.51];
UILabel *labelInUnClickableCustomView = [UILabel new];
labelInUnClickableCustomView.textAlignment = NSTextAlignmentCenter;
labelInUnClickableCustomView.font = [UIFont systemFontOfSize:12];
labelInUnClickableCustomView.text = @"居中對其自定義的View";
[unClickableCustomView addSubview:labelInUnClickableCustomView];
[labelInUnClickableCustomView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(unClickableCustomView);
}];
[textDrawView addView:unClickableCustomView size:unClickableCustomView.frame.size align:(YTAttachmentAlignTypeCenter) clickActionHandler:nil];
// 添加普通的文本
[textDrawView addString:@" 我們面前應(yīng)有盡有备燃,我們面前一無所有凌唬; " attributes:self.defaultTextAttributes clickActionHandler:nil];
// 添加自定義的按鈕法瑟,默認(rèn)是底部對其
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectMake(0, 0, 80, 30);
[button setTitle:@"我是按鈕" forState:UIControlStateNormal];
button.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1];
[button bk_addEventHandler:^(id sender) {
NSLog(@"button Clicked");
} forControlEvents:UIControlEventTouchUpInside];
[textDrawView addView:button size:button.frame.size clickActionHandler:nil];
[textDrawView addString:@" " attributes:self.defaultTextAttributes clickActionHandler:nil];
// 添加頂部對其按鈕
button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectMake(0, 0, 90, 30);
[button setTitle:@"頂部對其按鈕" forState:UIControlStateNormal];
button.titleLabel.font = [UIFont systemFontOfSize:14];
button.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1];
[button bk_addEventHandler:^(id sender) {
NSLog(@"button Clicked");
} forControlEvents:UIControlEventTouchUpInside];
[textDrawView addView:button size:button.frame.size align:(YTAttachmentAlignTypeTop) clickActionHandler:nil];
// 添加普通的文本
[textDrawView addString:@" 我們都將直上天堂霎挟,我們都將直下地獄。 " attributes:self.defaultTextAttributes clickActionHandler:nil];
[self.view addSubview:textDrawView];
self.textDrawView = textDrawView;
}
添加View
添加View其實和添加圖片的處理方式很類似赐纱,只不過添加圖片我們是使用CG繪圖的方式把圖片繪制在View上熬北,而添加View是使用UIkit的方法addSubview
把View添加到View的層級上,這里有個稍微有個需要注意的地方就是坐標(biāo)的問題起胰,UI坐標(biāo)系和CG坐標(biāo)系的顛倒的巫延,需要做個額外的處理
首先定義一個添加View的方法,在該方法中主要是進(jìn)行數(shù)據(jù)模型的保存以及生產(chǎn)特殊的占位屬性字符串炉峰,然后添加屬性字符串的RunDelegate
- (void)addView:(UIView *)view size:(CGSize)size align:(YTAttachmentAlignType)align clickActionHandler:(ClickActionHandler)clickActionHandler {
YTAttachmentItem *imageItem = [YTAttachmentItem new];
[self updateAttachment:imageItem withFont:self.font];
imageItem.align = align;
imageItem.attachment = view;
imageItem.type = YTAttachmentTypeView;
imageItem.size = size;
imageItem.clickActionHandler = clickActionHandler;
[self.attachments addObject:imageItem];
NSAttributedString *imageAttributeString = [self attachmentAttributeStringWithAttachmentItem:imageItem size:size];
[self.attributeString appendAttributedString:imageAttributeString];
}
設(shè)置占位屬性字符串的方法和添加圖片時候使用到的是一樣的代碼
- (NSAttributedString *)attachmentAttributeStringWithAttachmentItem:(YTAttachmentItem *)attachmentItem size:(CGSize)size {
// 創(chuàng)建CTRunDelegateCallbacks
CTRunDelegateCallbacks callback;
memset(&callback, 0, sizeof(CTRunDelegateCallbacks));
callback.getAscent = getAscent;
callback.getDescent = getDescent;
callback.getWidth = getWidth;
// 創(chuàng)建CTRunDelegateRef
// NSDictionary *metaData = @{YTRunMetaData: attachmentItem};
CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callback, (__bridge void * _Nullable)(attachmentItem));
// 設(shè)置占位使用的圖片屬性字符串
// 參考:https://en.wikipedia.org/wiki/Specials_(Unicode_block) U+FFFC OBJECT REPLACEMENT CHARACTER, placeholder in the text for another unspecified object, for example in a compound document.
unichar objectReplacementChar = 0xFFFC;
NSMutableAttributedString *imagePlaceHolderAttributeString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithCharacters:&objectReplacementChar length:1] attributes:[self defaultTextAttributes]];
// 設(shè)置RunDelegate代理
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imagePlaceHolderAttributeString, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate);
// 設(shè)置附加數(shù)據(jù)戒劫,設(shè)置點擊效果
NSDictionary *extraData = @{YTExtraDataAttributeTypeKey: attachmentItem.type == YTAttachmentTypeImage ? @(YTDataTypeImage) : @(YTDataTypeView),
YTExtraDataAttributeDataKey: attachmentItem,
};
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imagePlaceHolderAttributeString, CFRangeMake(0, 1), (CFStringRef)YTExtraDataAttributeName, (__bridge CFTypeRef)(extraData));
CFRelease(runDelegate);
return imagePlaceHolderAttributeString;
}
接下來就是需要計算添加的View所在父View中的位置,進(jìn)行相應(yīng)的保存巫橄,這里需要注意的是坐標(biāo)系的問題疯攒,需要做一個額外的轉(zhuǎn)換
- (void)calculateContentPositionWithBounds:(CGRect)bounds {
int imageIndex = 0;
// CTFrameGetLines獲取但CTFrame內(nèi)容的行數(shù)
NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
// CTFrameGetLineOrigins獲取每一行的起始點,保存在lineOrigins數(shù)組中
CGPoint lineOrigins[lines.count];
CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
for (int i = 0; i < lines.count; i++) {
CTLineRef line = (__bridge CTLineRef)lines[i];
NSArray *runs = (NSArray *)CTLineGetGlyphRuns(line);
for (int j = 0; j < runs.count; j++) {
CTRunRef run = (__bridge CTRunRef)(runs[j]);
NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run);
if (!attributes) {
continue;
}
// ..... 部分代碼省略
// 找到代理則開始計算圖片位置信息
CGFloat ascent;
CGFloat desent;
// 可以直接從metaData獲取到圖片的寬度和高度信息
CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &desent, NULL);
CGFloat height = ascent + desent;
// CTLineGetOffsetForStringIndex獲取CTRun的起始位置
CGFloat xOffset = lineOrigins[i].x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
CGFloat yOffset = lineOrigins[i].y;
// 更新ImageItem對象的位置
if (imageIndex < self.attachments.count) {
YTAttachmentItem *imageItem = self.attachments[imageIndex];
// 使用CG繪圖的位置不用矯正贴浙,使用UI繪圖的坐標(biāo)Y軸會上下顛倒,所以需要做調(diào)整
if (imageItem.type == YTAttachmentTypeView) {
yOffset = bounds.size.height - lineOrigins[i].y - ascent;
} else if (imageItem.type == YTAttachmentTypeImage) {
yOffset = yOffset - desent;
}
imageItem.frame = CGRectMake(xOffset, yOffset, width, height);
imageIndex ++;
}
}
}
}
對其實現(xiàn)
處理對其方式之前要了解字形度量的一些概念崎溃,然后在此基礎(chǔ)上進(jìn)行分析不同的對其方式下需要如何正確的設(shè)置排版的參數(shù)袁串,才能渲染繪制出理想中內(nèi)容
字形度量的一些概念
下面的這張圖片來自蘋果官方的參考文檔:Typographical Concepts
字形度量中的幾個概念的說明參考 使用CoreText繪制文本 的是內(nèi)容如下
bounding box(邊界框),這是一個假想的框子赎瑰,它盡可能緊密的裝入字形破镰。
baseline(基線),一條假想的線,一行上的字形都以此線作為上下位置的參考鲜漩,在這條線的左側(cè)存在一個點叫做基線的原點。
ascent(上行高度)踩娘,從原點到字體中最高(這里的高深都是以基線為參照線的)的字形的頂部的距離喉祭,ascent是一個正值。
descent(下行高度)厚脉,從原點到字體中最深的字形底部的距離胶惰,descent是一個負(fù)值(比如一個字體原點到最深的字形的底部的距離為2,那么descent就為-2)中捆。
三種對其方式的分析
以下對其方式的分析都是以下面的這些數(shù)據(jù)為標(biāo)準(zhǔn)的
Font.fontAscent = 33.75.
Font.fontDescent = 27.04.
LineHeight = Font.fontAscent + Font.fontDescent = 60.8.
頂部對其.
頂部對其,需要設(shè)置ascent
值為文字內(nèi)容的ascent
殴蓬,descent
值為attachmen的高度減去ascent
蟋滴,如下圖所示(圖片上的標(biāo)注是2x,并且數(shù)值因為是手動使用工具標(biāo)注津函,會有一些細(xì)微的偏差),內(nèi)容的高度為40涩馆,所以有:
-
ascent
= Font.fontAscent = 33.75. -
descent
= 40 -ascent
= 6.25.
ascent = 33.75.
descent = 6.25.
height = ascent + descent = 40.
baseline = 33.75.
底部對其
底部對其魂那,需要設(shè)置descent
值為文字內(nèi)容的descent
稠项,ascent
值為attachmen的高度減去ascent
,如下圖所示(圖片上的標(biāo)注是2x皿渗,并且數(shù)值因為是手動使用工具標(biāo)注,會有一些細(xì)微的偏差)划乖,內(nèi)容的高度為40挤土,所以有:
-
descent
= Font.fontDescent = 27.04. -
ascent
= 40 -descent
= 12.95.
ascent = 12.95.
descent = 27.04.
height = ascent + descent = 40.
居中對其.
居中對其,descent
值和ascent
值需要經(jīng)過一些簡單的計算迷殿,先計算ascent
值咖杂,ascent
值為文字內(nèi)容的ascent
減去頂部的那一段差值,(如下圖標(biāo)準(zhǔn)中的值為21處的高度)懦尝,然后descent
值為attachmen的高度減去ascent
,如下圖所示(圖片上的標(biāo)注是2x陵霉,并且數(shù)值因為是手動使用工具標(biāo)注踊挠,會有一些細(xì)微的偏差),內(nèi)容的高度為40效床,所以有:
-
ascent
= Font.fontAscent - (LineHeight - 40)/2 = 23.35. -
descent
= 40 -ascent
= 16.64.
ascent = 23.35.
descent = 16.64.
height = ascent + descent = 40.
代碼實現(xiàn)
首先需要在Attachment模型中添加如下幾個屬性,這些屬性在計算attachment內(nèi)容的descent
忍疾、ascent
是必須要用到的
@property (nonatomic, assign) YTAttachmentAlignType align;///<對其方式
@property (nonatomic, assign) CGFloat ascent;///<文本內(nèi)容的ascent谨朝,用于計算attachment內(nèi)容的ascent
@property (nonatomic, assign) CGFloat descent;///<文本內(nèi)容的descent甥绿,用于計算attachment內(nèi)容的descent
@property (nonatomic, assign) CGSize size;///<attachment內(nèi)容的大小
然后根據(jù)以上分析,我們可以很容易的寫出如下的幾個RunDelegate回調(diào)方法的代碼:
// MARK: - CTRunDelegateCallbacks 回調(diào)方法
static CGFloat getAscent(void *ref) {
YTAttachmentItem *attachmentItem = (__bridge YTAttachmentItem *)ref;
if (attachmentItem.align == YTAttachmentAlignTypeTop) {
return attachmentItem.ascent;
} else if (attachmentItem.align == YTAttachmentAlignTypeBottom) {
return attachmentItem.size.height - attachmentItem.descent;
} else if (attachmentItem.align == YTAttachmentAlignTypeCenter) {
return attachmentItem.ascent - ((attachmentItem.descent + attachmentItem.ascent) - attachmentItem.size.height) / 2;
}
return attachmentItem.size.height;
}
static CGFloat getDescent(void *ref) {
YTAttachmentItem *attachmentItem = (__bridge YTAttachmentItem *)ref;
if (attachmentItem.align == YTAttachmentAlignTypeTop) {
return attachmentItem.size.height - attachmentItem.ascent;
} else if (attachmentItem.align == YTAttachmentAlignTypeBottom) {
return attachmentItem.descent;
} else if (attachmentItem.align == YTAttachmentAlignTypeCenter) {
return attachmentItem.size.height - attachmentItem.ascent + ((attachmentItem.descent + attachmentItem.ascent) - attachmentItem.size.height) / 2;
}
return 0;
}
static CGFloat getWidth(void *ref) {
YTAttachmentItem *attachmentItem = (__bridge YTAttachmentItem *)ref;
return attachmentItem.size.width;
}
另外,在更新全局字體的時候需要同步的更新YTAttachmentItem
中的descent
图谷、ascent
屬性
- (void)setFont:(UIFont *)font {
_font = font;
[self.attributeString yt_setFont:_font];
[self updateAttachments];
}
- (void)updateAttachments {
for (YTAttachmentItem *attachment in self.attachments) {
[self updateAttachment:attachment withFont:self.font];
}
}
總結(jié)
以上就是使用Core Text添加自定義的View以及設(shè)置對其方式的一點小總結(jié),如有不妥之處菠镇,還請不吝賜教承璃。