欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

iOS-电子书开发 笔记

程序员文章站 2022-04-11 21:11:31
前言 刚接手电子书项目时,和安卓开发者pt Cai老师【aipiti Cai,一个我很敬佩很资深的开发工程师,设计领域:c++、Java、安卓、QT等】共同商议了一下,因为项目要做要同步,移动端【手机端】和PC【电脑端】的同步问题,让我们无法决定该用那种方式去呈现电子书,因为PC要展示的电子书有网络 ......

前言

刚接手电子书项目时,和安卓开发者pt Cai老师【aipiti Cai,一个我很敬佩很资深的开发工程师,设计领域:c++、Java、安卓、QT等】共同商议了一下,因为项目要做要同步,移动端【手机端】和PC【电脑端】的同步问题,让我们无法决定该用那种方式去呈现电子书,因为PC要展示的电子书有网络图片,有HTML标签,主要功能是能做标记(涂色、划线、书签等),而且后台数据源返回的只有这一种格式:HTML;所以我们第一时间想到了可以用加载网页的Webview来做;pt Cai老师做了一些基于JS的分页及手势操作,然后对图片进行了适配,但是当我在测试Webview时,效果并不尽人意:

 

  • Webview渲染比较慢,加载需要一定的等待时间,体验不是很好;
  • Webview内存泄漏比较严重;
  • Webview的与本地的交互,交互是有一定的延时,而且对于不断地传递参数不好控制操作;

 

引入Coretext

通过上面的测试,我决定放弃了Webview,用Coretext来尝试做这些排版和操作;我在网上查了很多资料,从对Coretext的基本开始了解,然后查看了猿题库开发者的博客,在其中学到了不少东西,然后就开始试着慢慢的用Coretext来尝试;

demo

1.主框架

做电子书阅读,首先要有一个翻滚阅读页的一个框架,我并没有选择用苹果自带的 UIPageViewController 因为控制效果不是很好,我再Git上找了一个不错的 DZMCoverAnimation,因为是做demo测试,就先选择一个翻滚阅读页做效果,这个覆盖翻页的效果如下:

iOS-电子书开发 笔记

 

2.解析数据源

首先看一下数据源demo,我要求json数据最外层必须是P标签,P标签不能嵌套P标签,但可以包含Img和Br标签,Img标签内必须含有宽高属性,以便做排版时适配,最终的数据源为

iOS-电子书开发 笔记

 

然后我在项目中用CocoaPods引入解析HTML文件的 hpple 三方库,在解析工具类CoreTextSource中添加解析数据模型和方法,假如上面的这个数据源是一章的内容,我把这一章内容最外层的每个P标签当做一个段落,遍历每个段落,然后在遍历每个段落里面的内容和其他标签;

 

CoreTextSource.h

iOS-电子书开发 笔记
#import <Foundation/Foundation.h>
#import <hpple/TFHpple.h>

#import <UIKit/UIKit.h>
typedef NS_ENUM(NSInteger,CoreTextSourceType){
    ///文本
    CoreTextSourceTypeTxt = 1,
    ///图片
    CoreTextSourceTypeImage
};

/**
 文本
 */
@interface CoreTextTxtSource : NSObject
@property (nonatomic,strong) NSString *content;
@end

/**
 图片
 */
@interface CoreTextImgSource : NSObject
@property (nonatomic,strong) NSString *name;
@property (nonatomic,assign) CGFloat width;
@property (nonatomic,assign) CGFloat height;
@property (nonatomic,strong) NSString *url;
// 此坐标是 CoreText 的坐标系,而不是UIKit的坐标系
@property (nonatomic,assign) NSInteger position;
@property (nonatomic,assign) CGRect imagePosition;
@end

/**
 段落内容
 */
@interface CoreTextParagraphSource : NSObject
@property (nonatomic,assign) CoreTextSourceType type;
@property (nonatomic,strong) CoreTextImgSource *imgData;
@property (nonatomic,strong) CoreTextTxtSource *txtData;
@end
///电子书数据源
@interface CoreTextSource : NSObject
///解析HTML格式
+ (NSArray *)arrayReaolveChapterHtmlDataWithFilePath:(NSString *)filePath;
@end
View Code

CoreTextSource.m

iOS-电子书开发 笔记
#import "CoreTextSource.h"

@implementation CoreTextImgSource

@end
@implementation CoreTextParagraphSource

@end
@implementation CoreTextTxtSource

@end

@implementation CoreTextSource

+ (NSArray *)arrayReaolveChapterHtmlDataWithFilePath:(NSString *)filePath{
    NSData  * data   = [NSData dataWithContentsOfFile:filePath];
    
    TFHpple * dataSource = [[TFHpple alloc] initWithHTMLData:data];
    NSArray * elements = [dataSource searchWithXPathQuery:@"//p"];
    
    NSMutableArray *arrayData = [NSMutableArray array];
    
    for (TFHppleElement *element in elements) {
        NSArray *arrrayChild = [element children];
        for (TFHppleElement *elementChild in arrrayChild) {
            CoreTextParagraphSource *paragraphSource = [[CoreTextParagraphSource alloc]init];
            NSString *type = [elementChild tagName];
            if ([type isEqualToString:@"text"]) {
                CoreTextTxtSource *text = [[CoreTextTxtSource alloc]init];
                text.content = elementChild.content;
                paragraphSource.txtData = text;
                paragraphSource.type = CoreTextSourceTypeTxt;
            }
            else if ([type isEqualToString:@"img"]){
                CoreTextImgSource *image = [[CoreTextImgSource alloc]init];
                NSDictionary *dicAttributes = [elementChild attributes];
                image.name = [dicAttributes[@"src"] lastPathComponent];
                image.url = dicAttributes[@"src"];
                image.width = [dicAttributes[@"width"] floatValue];
                image.height = [dicAttributes[@"height"] floatValue];
                paragraphSource.imgData = image;
                paragraphSource.type = CoreTextSourceTypeImage;
                
                if (image.width >= (Scr_Width - 30)) {
                    CGFloat ratioHW = image.height/image.width;
                    image.width = Scr_Width - 30;
                    image.height = image.width * ratioHW;
                }
            }
            else if ([type isEqualToString:@"br"]){
                CoreTextTxtSource *text = [[CoreTextTxtSource alloc]init];
                text.content = @"\n";
                paragraphSource.txtData = text;
                paragraphSource.type = CoreTextSourceTypeTxt;
            }
            
            [arrayData addObject:paragraphSource];
        }
        
        ///每个个<P>后加换行
        CoreTextParagraphSource *paragraphNewline = [[CoreTextParagraphSource alloc]init];
        CoreTextTxtSource *textNewline = [[CoreTextTxtSource alloc]init];
        textNewline.content = @"\n";
        paragraphNewline.txtData = textNewline;
        paragraphNewline.type = CoreTextSourceTypeTxt;
        [arrayData addObject:paragraphNewline];
    }
    
    return arrayData;
}
@end
View Code

3.图片处理和分页

添加好CoreTextSource类之后,就可以通过 arrayReaolveChapterHtmlDataWithFilePath 方法获取这一章的所有段落内容;但是还有一个问题,既然用Coretext来渲染,那图片要在渲染之前下载好,从本地获取下载好的图片进行渲染,具体什么时候下载,视项目而定;我在CoreTextDataTools类中添加了图片下载方法,该类主要用于分页;在分页之前,添加每个阅读页的model -> CoreTextDataModel,具体图片的渲染,先详看CoreTextDataTools分页类中 wkj_coreTextPaging 方法和其中引用到的方法;

CoreTextDataModel.h

iOS-电子书开发 笔记
#import <Foundation/Foundation.h>

///标记显示模型
@interface CoreTextMarkModel : NSObject
@property (nonatomic,assign) BookMarkType type;
@property (nonatomic,assign) NSRange range;
@property (nonatomic,strong) NSString *content;
@property (nonatomic,strong) UIColor *color;
@end

@interface CoreTextDataModel : NSObject
///
@property (nonatomic,assign) CTFrameRef ctFrame;
@property (nonatomic,strong) NSAttributedString *content;
@property (nonatomic,assign) NSRange range;
///图片数据模型数组 CoreTextImgSource
@property (nonatomic,strong) NSArray *arrayImage;
///标记数组
@property (nonatomic,copy) NSArray *arrayMark;
@end
View Code 

CoreTextDataModel.m

iOS-电子书开发 笔记
#import "CoreTextDataModel.h"
@implementation CoreTextMarkModel

@end

@implementation CoreTextDataModel
- (void)setCtFrame:(CTFrameRef)ctFrame{
    if (_ctFrame != ctFrame) {
        if (_ctFrame != nil) {
            CFRelease(_ctFrame);
        }
        CFRetain(ctFrame);
        _ctFrame = ctFrame;
    }
}
@end
View Code

 

CoreTextDataTools.h

iOS-电子书开发 笔记
///图片下载
+ (void)wkj_downloadBookImage:(NSArray *)arrayParagraph;
///分页
+ (NSArray *)wkj_coreTextPaging:(NSAttributedString *)str
                       textArea:(CGRect)textFrame
           arrayParagraphSource:(NSArray *)arrayParagraph;
///根据一个章节的所有段落内容,来生成 AttributedString 包括图片
+ (NSAttributedString *)wkj_loadChapterParagraphArray:(NSArray *)arrayArray;
View Code

CoreTextDataTools.m

iOS-电子书开发 笔记
#import "CoreTextDataTools.h"
#import <SDWebImage/UIImage+MultiFormat.h>

@implementation CoreTextDataTools
+ (void)wkj_downloadBookImage:(NSArray *)arrayParagraph{
    dispatch_group_t group = dispatch_group_create();
    // 有多张图片URL的数组
    for (CoreTextParagraphSource *paragraph in arrayParagraph) {
        if (paragraph.type == CoreTextSourceTypeTxt) {
            continue;
        }
        
        dispatch_group_enter(group);
        // 需要加载图片的控件(UIImageView, UIButton等)
        NSData *data = [NSData dataWithContentsOfURL:[NSURL  URLWithString:paragraph.imgData.url]];
        UIImage *image = [UIImage sd_imageWithData:data];
        // 本地沙盒目录
        NSString *path = wkj_documentPath;
        ///创建文件夹
        NSString *folderName = [path stringByAppendingPathComponent:@"wkjimage"];
        
        if (![[NSFileManager defaultManager]fileExistsAtPath:folderName]) {
            
            [[NSFileManager defaultManager] createDirectoryAtPath:folderName  withIntermediateDirectories:YES  attributes:nil error:nil];
            
        }else{
            NSLog(@"有这个文件了");
        }
        
        // 得到本地沙盒中名为"MyImage"的路径,"MyImage"是保存的图片名
        //        NSString *imageFilePath = [path stringByAppendingPathComponent:@"MyImage"];
        
        // 将取得的图片写入本地的沙盒中,其中0.5表示压缩比例,1表示不压缩,数值越小压缩比例越大
        
        folderName = [folderName stringByAppendingPathComponent:[paragraph.imgData.url lastPathComponent]];
        
        BOOL success = [UIImageJPEGRepresentation(image, 0.1) writeToFile:folderName  atomically:YES];
        if (success){
            NSLog(@"写入本地成功");
        }
        
        dispatch_group_leave(group);
        
    }
    // 下载图片完成后, 回到主线
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 刷新UI
        
    });
}
/**
 CoreText 分页
 str: NSAttributedString属性字符串
 textFrame: 绘制区域
 */
+ (NSArray *)wkj_coreTextPaging:(NSAttributedString *)str
                       textArea:(CGRect)textFrame
           arrayParagraphSource:(NSArray *)arrayParagraph{
    NSMutableArray *arrayCoretext = [NSMutableArray array];
    
    CFAttributedStringRef cfStrRef = (__bridge CFAttributedStringRef)str;
    CTFramesetterRef framesetterRef = CTFramesetterCreateWithAttributedString(cfStrRef);
    CGPathRef path = CGPathCreateWithRect(textFrame, NULL);
    
    int textPos = 0;
    NSUInteger strLength = [str length];
    while (textPos < strLength)  {
        //设置路径
        CTFrameRef frame = CTFramesetterCreateFrame(framesetterRef, CFRangeMake(textPos, 0), path, NULL);
        CFRange frameRange = CTFrameGetVisibleStringRange(frame);
        NSRange range = NSMakeRange(frameRange.location, frameRange.length);
        
        //        [arrayPagingRange addObject:[NSValue valueWithRange:range]];
        //        [arrayPagingStr addObject:[str attributedSubstringFromRange:range]];
    
        
        CoreTextDataModel *model = [[CoreTextDataModel alloc]init];
        model.ctFrame = frame;
        model.range = range;
        model.content = [str attributedSubstringFromRange:range];
        model.arrayImage = [self wkj_arrayCoreTextImgRect:[self wkj_arrayCoreTextImg:arrayParagraph range:range] cfFrame:frame];
        
        [arrayCoretext addObject:model];
        //移动
        textPos += frameRange.length;
        CFRelease(frame);
    }
    CGPathRelease(path);
    CFRelease(framesetterRef);
    //    return arrayPagingStr;
    return arrayCoretext;
}
///获取每页区域内存在的图片
+ (NSArray *)wkj_arrayCoreTextImg:(NSArray *)arrayParagraph
                                  range:(NSRange)range{
    NSMutableArray *array = [NSMutableArray array];
    
    for (CoreTextParagraphSource *paragraph in arrayParagraph) {
        if (paragraph.type == CoreTextSourceTypeTxt) {
            continue;
        }
        
        if (paragraph.imgData.position >= range.location &&
            paragraph.imgData.position < (range.location + range.length)) {
            [array addObject:paragraph.imgData];
        }
    }
    
    return array;
}
///获取每个区域内存在的图片位置
+ (NSArray *)wkj_arrayCoreTextImgRect:(NSArray *)arrayCoreTextImg cfFrame:(CTFrameRef)frameRef{
    NSMutableArray *arrayImgData = [NSMutableArray array];
    
    if (arrayCoreTextImg.count == 0) {
        return arrayCoreTextImg;
    }
    NSArray *lines = (NSArray *)CTFrameGetLines(frameRef);
    NSUInteger lineCount = [lines count];
    CGPoint lineOrigins[lineCount];
    CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), lineOrigins);
    int imgIndex = 0;
    CoreTextImgSource * imageData = arrayCoreTextImg[0];
    for (int i = 0; i < lineCount; ++i) {

        CTLineRef line = (__bridge CTLineRef)lines[i];
        NSArray * runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
        for (id runObj in runObjArray) {
            CTRunRef run = (__bridge CTRunRef)runObj;
            NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
            CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
            if (delegate == nil) {///如果代理为空,则未找到设置的空白字符代理
                continue;
            }
            

            
            CoreTextImgSource * metaImgSource = CTRunDelegateGetRefCon(delegate);
            if (![metaImgSource isKindOfClass:[CoreTextImgSource class]]) {
                continue;
            }
            
            CGRect runBounds;
            CGFloat ascent;
            CGFloat descent;
            runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
            runBounds.size.height = ascent + descent;
            
            CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
            runBounds.origin.x = lineOrigins[i].x + xOffset;
            runBounds.origin.y = lineOrigins[i].y;
            runBounds.origin.y -= descent;
            
            CGPathRef pathRef = CTFrameGetPath(frameRef);
            CGRect colRect = CGPathGetBoundingBox(pathRef);
            
            CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
            
            imageData.imagePosition = delegateBounds;
            CoreTextImgSource *img = imageData;
            [arrayImgData addObject:img];
            imgIndex++;
            if (imgIndex == arrayCoreTextImg.count) {
                imageData = nil;
                break;
            } else {
                imageData = arrayCoreTextImg[imgIndex];
            }
        }
        
        if (imgIndex == arrayCoreTextImg.count) {
            break;
        }
        
    }
    
    return arrayImgData;
    
}




///获取属性字符串字典
+ (NSMutableDictionary *)wkj_attributes{
    CGFloat fontSize = [BookThemeManager sharedManager].fontSize;
    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
    ///行间距
    CGFloat lineSpacing = [BookThemeManager sharedManager].lineSpace;
    ///首行缩进
    CGFloat firstLineHeadIndent = [BookThemeManager sharedManager].firstLineHeadIndent;
    ///段落间距
    CGFloat paragraphSpacing = [BookThemeManager sharedManager].ParagraphSpacing;
    //换行模式
    CTLineBreakMode lineBreak = kCTLineBreakByCharWrapping;
    const CFIndex kNumberOfSettings = 6;
    CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
        ///行间距
        { kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &lineSpacing },
        { kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(CGFloat), &lineSpacing },
        { kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(CGFloat), &lineSpacing },
        ///首行缩进
        { kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(CGFloat), &firstLineHeadIndent },
        ///换行模式
        { kCTParagraphStyleSpecifierLineBreakMode, sizeof(CTLineBreakMode), &lineBreak },
        ///段落间距
        { kCTParagraphStyleSpecifierParagraphSpacing, sizeof(CGFloat), &paragraphSpacing }
    };
    
    CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
    
    UIColor * textColor = [BookThemeManager sharedManager].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;
}






///根据一个章节的所有段落内容,来生成 AttributedString 包括图片
+ (NSAttributedString *)wkj_loadChapterParagraphArray:(NSArray *)arrayArray{
    
    NSMutableAttributedString *resultAtt = [[NSMutableAttributedString alloc] init];
    
    for (CoreTextParagraphSource *paragraph in arrayArray) {
        if (paragraph.type == CoreTextSourceTypeTxt) {///文本
            NSAttributedString *txtAtt = [self wkj_parseContentFromCoreTextParagraph:paragraph];
            [resultAtt appendAttributedString:txtAtt];
        }
        else if (paragraph.type == CoreTextSourceTypeImage){///图片
            paragraph.imgData.position = resultAtt.length;
            NSAttributedString *imageAtt = [self wkj_parseImageFromCoreTextParagraph:paragraph];
            [resultAtt appendAttributedString:imageAtt];
        }
    }
    
    return resultAtt;
}

///根据段落文本内容获取 AttributedString
+ (NSAttributedString  *)wkj_parseContentFromCoreTextParagraph:(CoreTextParagraphSource *)paragraph{
    NSMutableDictionary *attributes = [self wkj_attributes];
    return [[NSAttributedString alloc] initWithString:paragraph.txtData.content attributes:attributes];
}


/////根据段落图片内容获取 AttributedString 空白占位符
+ (NSAttributedString *)wkj_parseImageFromCoreTextParagraph:(CoreTextParagraphSource *)paragraph{

    CTRunDelegateCallbacks callbacks;
    memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
    callbacks.version = kCTRunDelegateVersion1;
    callbacks.getAscent = ascentCallback;
    callbacks.getDescent = descentCallback;
    callbacks.getWidth = widthCallback;
    CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)(paragraph.imgData));

    // 使用0xFFFC作为空白的占位符
    unichar objectReplacementChar = 0xFFFC;
    NSString * content = [NSString stringWithCharacters:&objectReplacementChar length:1];
    NSMutableDictionary * attributes = [self wkj_attributes];
    //    attributes[(id)kCTBackgroundColorAttributeName] = (id)[UIColor yellowColor].CGColor;
    NSMutableAttributedString * space = [[NSMutableAttributedString alloc] initWithString:content attributes:attributes];
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1),
                                   kCTRunDelegateAttributeName, delegate);
    CFRelease(delegate);
    return space;
}

//+ (NSAttributedString *)wkj_NewlineAttributes{
//    CTRunDelegateCallbacks callbacks;
//    memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
//    callbacks.version = kCTRunDelegateVersion1;
//    callbacks.getAscent = ascentCallback;
//    callbacks.getDescent = descentCallback;
//    callbacks.getWidth = widthCallback;
//    CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)(paragraph));
//
//    // 使用0xFFFC作为空白的占位符
//    unichar objectReplacementChar = 0xFFFC;
//    NSString * content = [NSString stringWithCharacters:&objectReplacementChar length:1];
//    NSMutableDictionary * attributes = [self wkj_attributes];
//    //    attributes[(id)kCTBackgroundColorAttributeName] = (id)[UIColor yellowColor].CGColor;
//    NSMutableAttributedString * space = [[NSMutableAttributedString alloc] initWithString:content attributes:attributes];
//    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1),
//                                   kCTRunDelegateAttributeName, delegate);
//    CFRelease(delegate);
//    return space;
//}

static CGFloat ascentCallback(void *ref){
//    return [(NSNumber*)[(__bridge NSDictionary*)ref objectForKey:@"height"] floatValue];
    CoreTextImgSource *refP = (__bridge CoreTextImgSource *)ref;
    return refP.height;
}

static CGFloat descentCallback(void *ref){
    return 0;
}

static CGFloat widthCallback(void* ref){
//    return [(NSNumber*)[(__bridge NSDictionary*)ref objectForKey:@"width"] floatValue];
    
    CoreTextImgSource *refP = (__bridge CoreTextImgSource *)ref;
    return refP.width;
}

@end
View Code

添加好CoreTextDataTools类之后,就可以通过 wkj_downloadBookImage 方法来下载图片;图片下载完之后,就可以对每页显示的内容区域进行分页;划线和涂色的一些方法在上一篇中已提到;

    ///获取测试数据源文件
    NSString *path = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
    ///获取该章所有段落内容
    NSArray *arrayParagraphSource = [CoreTextSource arrayReaolveChapterHtmlDataWithFilePath:path];
    ///下载该章中的所有图片
    [CoreTextDataTools wkj_downloadBookImage:arrayParagraphSource];
    ///根据一个章节的所有段落内容,来生成 AttributedString 包括图片
    NSAttributedString *att = [CoreTextDataTools wkj_loadChapterParagraphArray:arrayParagraphSource];
    ///给章所有内容分页 返回 CoreTextDataModel 数组
    NSArray *array = [CoreTextDataTools wkj_coreTextPaging:att textArea:CGRectMake(5, 5, self.view.bounds.size.width - 10, self.view.bounds.size.heigh                     t- 120) arrayParagraphSource:arrayParagraphSource];

 

4.效果

iOS-电子书开发 笔记