iOS閱讀器實踐系列(一)coretext純文字排版基礎,ioscoretext
前言:之前做了公司閱讀類的App,最近有時間來寫一下閱讀部分的實現過程,供梳理邏輯,計劃會寫一個系列希望能涉及到盡量多的方面與細節,歡迎大家交流、吐槽、拍磚,共同進步。
閱讀的排版用的是coretext,這篇介紹用coretext實現基本的排版功能。
關於coretext的實現原理,可以查看文檔或其他資料,這裡就不介紹了,只介紹如何應用coretext來實現一個簡單的文本排版功能。
因為coretext是離屏排版的,即在將內容渲染到螢幕之前,內容的排版工作的已經完成了。
排版過程大致過程分為 步:
一、由原始文本資料和需要的相關配置來得到屬性字串。
二、由屬性字串得到CTFramesetter
三、由CTFramesetter和繪製地區得到CTFrame
四、最後將CTFrame渲染到視圖的上下文中
1、由原始文本資料和需要的相關配置來得到屬性字串
這一部最關鍵的是得到相關配置,這些配置可能包括文本對齊、段收尾縮排、行高等,下面是一些相關配置屬性:
@interface CTFrameParserConfigure : NSObject@property (nonatomic, assign) CGFloat frameWidth;@property (nonatomic, assign) CGFloat frameHeight;//字型屬性@property (nonatomic, assign) CGFloat wordSpace;@property (nonatomic, strong) UIColor *textColor;@property (nonatomic, strong) NSString *fontName;@property (nonatomic, assign) CGFloat fontSize;//段落屬性@property (nonatomic, assign) CGFloat lineSpace;@property (nonatomic, assign) CTTextAlignment textAlignment; //文本對齊模式@property (nonatomic, assign) CGFloat firstlineHeadIndent; //段首行縮排@property (nonatomic, assign) CGFloat headIndent; //段左側整體縮排@property (nonatomic, assign) CGFloat tailIndent; //段尾縮排@property (nonatomic, assign) CTLineBreakMode lineBreakMode; //換行模式@property (nonatomic, assign) CGFloat lineHeightMutiple; //行高倍數器(它的值表示原行高的倍數)@property (nonatomic, assign) CGFloat maxLineHeight; //最大行高限制(0表示無限制,是非負的,行高不能超過此值)@property (nonatomic, assign) CGFloat minLineHeight; //最小行高限制@property (nonatomic,assign) CGFloat paragraphBeforeSpace; //段前間距(相對上一段加上的間距)@property (nonatomic, assign) CGFloat paragraphAfterSpace; //段尾間距(相對下一段加上的間距)@property (nonatomic, assign) CTWritingDirection writeDirection; //書寫方向@property (nonatomic, assign) CGFloat lineSpacingAdjustment; //The space in points added between lines within the paragraph (commonly known as leading).@end
接下來我們要利用這些屬性,產生我們需要的配置,在我們根據我們的需要給這些屬性賦值以後,利用下面的方法來得到我們需要的配置:
//返迴文本所有屬性的集合(以字典形式),包括字型、段落等- (NSDictionary *)attributesWithConfig:(CTFrameParserConfigure *)config{ //段落屬性 CGFloat lineSpacing = config.lineSpace; CGFloat firstLineIndent = config.firstlineHeadIndent; CGFloat lineIndent = config.headIndent; CGFloat tailIndent = config.tailIndent; CTLineBreakMode lineBreakMode = config.lineBreakMode; CGFloat lineHeightMutiple = config.lineHeightMutiple; CGFloat paragraphBeforeSpace = config.paragraphBeforeSpace; CGFloat paragraphAfterSpace = config.paragraphAfterSpace; CTWritingDirection writeDirect = config.writeDirection; CTTextAlignment textAlignment = config.textAlignment; const CFIndex kNumberOfSettings = 13; CTParagraphStyleSetting paragraphSettings[kNumberOfSettings] = { { kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &lineSpacing }, { kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(CGFloat), &lineSpacing }, { kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(CGFloat), &lineSpacing }, { kCTParagraphStyleSpecifierAlignment, sizeof(textAlignment), &textAlignment }, { kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(firstLineIndent), &firstLineIndent }, { kCTParagraphStyleSpecifierHeadIndent, sizeof(lineIndent), &lineIndent }, { kCTParagraphStyleSpecifierTailIndent, sizeof(tailIndent), &tailIndent }, { kCTParagraphStyleSpecifierLineBreakMode, sizeof(lineBreakMode), &lineBreakMode }, { kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(lineHeightMutiple), &lineHeightMutiple }, { kCTParagraphStyleSpecifierLineSpacing, sizeof(lineHeightMutiple), &lineHeightMutiple }, { kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(paragraphBeforeSpace), ¶graphBeforeSpace }, { kCTParagraphStyleSpecifierParagraphSpacing, sizeof(paragraphAfterSpace), ¶graphAfterSpace }, { kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(writeDirect), &writeDirect }, }; CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(paragraphSettings, kNumberOfSettings); /** * 字型屬性 */ CGFloat fontSize = config.fontSize; //use the postName after iOS10// CTFontRef fontRef = CTFontCreateWithName((CFStringRef)config.fontName, fontSize, NULL); CTFontRef fontRef = CTFontCreateWithName(NULL, fontSize, NULL);// CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"TimesNewRomanPSMT", fontSize, NULL); UIColor * textColor = config.textColor; //設定字型間距 long number = config.wordSpace; CFNumberRef num = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt8Type, &number); NSMutableDictionary * dict = [NSMutableDictionary dictionary]; dict[(id)kCTForegroundColorAttributeName] = (id)textColor.CGColor; dict[(id)kCTFontAttributeName] = (__bridge id)fontRef; dict[(id)kCTKernAttributeName] = (__bridge id)num; dict[(id)kCTParagraphStyleAttributeName] = (__bridge id)theParagraphRef; CFRelease(num); CFRelease(theParagraphRef); CFRelease(fontRef); return dict;}
上述過程為先根據上面提供的段落屬性值產生段落屬性,然後產生字型、字型間距及字型顏色等屬性,然後依次將他們存入字典中。
需要注意的地方是 CTParagraphStyleSetting 為C語言的數組,需在建立時指定數組元素個數。
建立的CoreFoundation庫中的對象需要手動釋放(大部分到create方法產生的對象)
另外在系統升級到iOS10以後,在調節字型大小重新排版時,變得很慢,用Instrument查了一下,發現
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)config.fontName, fontSize, NULL);
這句代碼執行時間很長,尋找資料發現是字型造成的,iOS10需要用相應的POST NAME。
2、由屬性字串得到CTFramesetter
// 建立 CTFramesetterRef 執行個體 CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)mabStr);
3、由CTFramesetter和繪製地區得到CTFrame
這一步的關鍵是要得到繪製的地區:
// 獲得要繪製的地區的高度 CGSize restrictSize = CGSizeMake(viewWidth, CGFLOAT_MAX); CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,0), nil, restrictSize, nil); CGFloat textHeight = coreTextSize.height;
然後產生CTFrame:
//產生繪製的地區+ (CTFrameRef)createFrameWithFramesetter:(CTFramesetterRef)framesetter frameWidth:(CGFloat)frameWidth stringRange:(CFRange)stringRange orginY:(CGFloat)originY height:(CGFloat)frameHeight{ CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, CGRectMake(0, originY, frameWidth, frameHeight)); //此時path的位置值都是coretext座標系下的值 CTFrameRef frame = CTFramesetterCreateFrame(framesetter, stringRange, path, NULL); CFRelease(frame); CFRelease(path); return frame;}
這裡需要注意的地方就是代碼中注釋的地方,在排版過程中使用的座標都是在coretext座標系下的,即原點在螢幕左下角。
4、將CTFrame渲染到視圖的上下文中
這一步是要在視圖類的drawRect方法中將上步得到的CTFrame繪製出來:
- (void)drawRect:(CGRect)rect{ [super drawRect:rect]; //將座標系轉換為coretext下的座標系 CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM(context, 1.0, -1.0); if (ctFrame != nil) { CTFrameDraw(ctFrame, context); }}
這一步的關鍵是座標系的轉換,因為ctFrame中包含的繪製地區是在coretext座標系下,所以在繪製時應先將座標系轉換為coretext座標系再繪製,才能保證繪製位置正確。
如果渲染時需要精確到行或字型可用CTLine與CTRun,這會在後面介紹。