1. 程式人生 > >iOS-電子書開發 筆記

iOS-電子書開發 筆記

.com 引用 開發 坐標系 imp void ridge () clas

前言

剛接手電子書項目時,和安卓開發者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測試,就先選擇一個翻滾閱讀頁做效果,這個覆蓋翻頁的效果如下:

技術分享圖片

2.解析數據源

首先看一下數據源demo,我要求json數據最外層必須是P標簽,P標簽不能嵌套P標簽,但可以包含Img和Br標簽,Img標簽內必須含有寬高屬性,以便做排版時適配,最終的數據源為

技術分享圖片

然後我在項目中用CocoaPods引入解析HTML文件的 hpple 三方庫,在解析工具類CoreTextSource中添加解析數據模型和方法,假如上面的這個數據源是一章的內容,我把這一章內容最外層的每個P標簽當做一個段落,遍歷每個段落,然後在遍歷每個段落裏面的內容和其他標簽;

CoreTextSource.h

技術分享圖片
#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

技術分享圖片
#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

技術分享圖片
#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

技術分享圖片
#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

技術分享圖片
///圖片下載
+ (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

技術分享圖片
#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-電子書開發 筆記