美文网首页
从0到1实现小说阅读器(二、实现一个简单的排版引擎)

从0到1实现小说阅读器(二、实现一个简单的排版引擎)

作者: UncleFool | 来源:发表于2022-06-30 16:34 被阅读0次

    上篇展示了CoreText排版的基础能力,但是制作一个排版引擎需要进一步封装,根据设计模式的“单一功能”原则应该把不同功能分给不同类中处理。如下:

    1.一个显示用的类,仅负责内容显示,不负责排版;
    2.一个模型类,用于承载显示做需要的所有数据;
    3.一个排版类,用于实现文字内容的排版;
    4.一个配置类,用于实现排版是的可配置项。

    根据以上原则,我们拆分出了4个类:
    1.CTFrameParserConfig,用于配置绘制参数,比如:字体大小、字体颜色、行间距等等;
    2.CTFrameParser,用于生成最后需要绘制的CTFrameRef实例;
    3.CoreTextData,用于CTFrameParser生成的CTFrameRef实例,以及CTFrameRef实际绘制需要的高度。
    4.CTDisplayView,持有CoreTextData类的实例,负责讲CTFrameRef绘制到界面上。具体代码实现如下:

    CTFrameParserConfig 类:

    #import <Foundation/Foundation.h>
    
    @interface CTFrameParserConfig : NSObject
    
    @property (nonatomic, assign) CGFloat width;
    @property (nonatomic, assign) CGFloat fontSize;
    @property (nonatomic, assign) CGFloat lineSpace;
    @property (nonatomic, strong) UIColor *textColor;
    
    @end
    
    #import "CTFrameParserConfig.h"
    
    @implementation CTFrameParserConfig
    
    - (instancetype)init {
        self = [super init];
        if (self) {
            _width = 200.0f;
            _fontSize = 16.0f;
            _lineSpace = 8.0f;
            _textColor = RGB(108, 108, 108);
        }
        return self;
    }
    
    @end
    

    CTFrameParser 类:

    #import <Foundation/Foundation.h>
    #import "CoreTextData.h"
    #import "CTFrameParserConfig.h"
    
    @interface CTFrameParser : NSObject
    
    + (CoreTextData *)parserContent:(NSString *)content config:(CTFrameParserConfig *)config;
    
    @end
    
    + (CoreTextData *)parserContent:(NSString *)content config:(CTFrameParserConfig *)config {
        NSDictionary *attributes = [self attributesWithConfig:config];
        NSAttributedString *contentString = [[NSAttributedString alloc] initWithString:content attributes:attributes];
        
        // 创建CTFramesetterRef实例
        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)contentString);
        
        // 获得绘制区域高度
        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实例
        CoreTextData *data = [[CoreTextData alloc] init];
        data.ctFrame = frame;
        data.height = textHeight;
        
        // 释放内存
        CFRelease(frame);
        CFRelease(framesetter);
        return data;
    }
    
    + (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;
    }
    
    + (CTFrameRef)createFrameWithFramesetter:(CTFramesetterRef)framesetter config:(CTFrameParserConfig *)config height:(CGFloat)height {
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, CGRectMake(0, 0, config.width, height));
        
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
        CFRelease(path);
        return frame;
    }
    
    @end
    

    CoreTextData 类:

    #import <Foundation/Foundation.h>
    
    @interface CoreTextData : NSObject
    
    @property (nonatomic, assign) CTFrameRef ctFrame;
    @property (nonatomic, assign) CGFloat height;
    
    @end
    
    #import "CoreTextData.h"
    
    @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
    

    CTDisplayView 类:

    #import <UIKit/UIKit.h>
    #import "CoreTextData.h"
    
    @interface CTDisplayView : UIView
    
    @property (nonatomic, strong) CoreTextData *data;
    
    @end
    
    #import "CTDisplayView.h"
    
    @implementation CTDisplayView
    
    - (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);
        }
    }
    

    最终在 ViewController 类中的实现:

    #import "ViewController.h"
    #import "CTDisplayView.h"
    #import "CTFrameParser.h"
    
    @interface ViewController ()
    
    @property (weak, nonatomic) IBOutlet CTDisplayView *displayView;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        CTFrameParserConfig *config = [[CTFrameParserConfig alloc] init];
        config.textColor = [UIColor redColor];
        config.width = self.displayView.width;
        
        CoreTextData *data = [CTFrameParser parserContent:@"小说阅读器,开始...." config:config];
        self.displayView.data = data;
        self.displayView.height = data.height;
        self.displayView.backgroundColor = [UIColor yellowColor];
    }
    
    @end
    

    我们通过UML来看一下各个类之间的关系:


    类图

    总结:(关键类:CTFrameParser)

    如此可以看出来CTFrameParser通过CTFrameParserConfigCoreTextData来完成排版工作,使得排版的工作从CTDisplayView类中完全解放出来,让其只负责最后的绘制和显示。

    相关文章

      网友评论

          本文标题:从0到1实现小说阅读器(二、实现一个简单的排版引擎)

          本文链接:https://www.haomeiwen.com/subject/riwfbrtx.html