前言:之前做了公司閱讀類的App,最近有時間來寫一下閱讀部分的實現過程,供梳理邏輯,計劃會寫一個系列希望能涉及到儘量多的方面與細節,歡迎大家交流、吐槽、拍磚,共同進步。 閱讀的排版用的是coretext,這篇介紹用coretext實現基本的排版功能。 關於coretext的實現原理,可以查看文檔或其 ...
前言:之前做了公司閱讀類的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,這會在後面介紹。