從0到1實(shí)現(xiàn)小說閱讀器(三、分析小說閱讀器的實(shí)現(xiàn))

上篇我們實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的排版引擎,總結(jié)起來很簡(jiǎn)單,在一個(gè)自定義視圖的drawRect:()方法中繪制利用CoreTextCTFrameDraw()方法繪制CTFrameRef,即:

- (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);
    }
}

那么我們?nèi)绾卧诖嘶A(chǔ)上將小說功能實(shí)現(xiàn)呢?首先來分析一下小說的功能

  • 單頁的排版顯示
  • 多頁的排版邏輯,可以左右翻頁
  • 設(shè)置功能(字體大小、切換背景/主題、翻頁模式等)
  • 目錄
  • 書簽
  • 注釋(選中段落劃線、添加注釋)

今天我們主要來分析如何實(shí)現(xiàn)前兩個(gè),因?yàn)檫@兩個(gè)是核心功能。目前我們的排版引擎可以做到單頁排版的顯示,多頁的排版邏輯涉及視圖數(shù)據(jù)的處理邏輯。視圖方面我們可以通過翻頁控制器UIPageViewController來實(shí)現(xiàn),該對(duì)象還自帶了翻頁的動(dòng)畫效果UIPageViewControllerTransitionStylePageCurl;簡(jiǎn)單介紹一下UIPageViewController的使用,它和UITableView一樣,有自己的dataSourcedelegate

1. UIPageViewController 的使用

// 創(chuàng)建翻頁視圖控制器對(duì)象
- (instancetype)initWithTransitionStyle:(UIPageViewControllerTransitionStyle)style navigationOrientation:(UIPageViewControllerNavigationOrientation)navigationOrientation options:(nullable NSDictionary<NSString *, id> *)options;

上面的方法用來初始化UIPageViewController對(duì)象,其中UIPageViewControllerTransitionStyle用來設(shè)置翻頁效果:

typedef NS_ENUM(NSInteger, UIPageViewControllerTransitionStyle) {
    UIPageViewControllerTransitionStylePageCurl = 0, //類似于書本翻頁效果
    UIPageViewControllerTransitionStyleScroll = 1 // 類似于ScrollView的滑動(dòng)效果
};

其中UIPageViewControllerNavigationOrientation用來設(shè)置翻頁方向:

typedef NS_ENUM(NSInteger, UIPageViewControllerNavigationOrientation) {
    UIPageViewControllerNavigationOrientationHorizontal = 0,//水平翻頁
    UIPageViewControllerNavigationOrientationVertical = 1//豎直翻頁
};

下面是UIPageViewController常用的屬性和方法:

//設(shè)置數(shù)據(jù)源
@property (nullable, nonatomic, weak) id <UIPageViewControllerDelegate> delegate;
//設(shè)置代理
@property (nullable, nonatomic, weak) id <UIPageViewControllerDataSource> dataSource;
//獲取翻頁風(fēng)格
@property (nonatomic, readonly) UIPageViewControllerTransitionStyle transitionStyle;
//獲取翻頁方向
@property (nonatomic, readonly) UIPageViewControllerNavigationOrientation navigationOrientation;
//獲取書軸類型
@property (nonatomic, readonly) UIPageViewControllerSpineLocation spineLocation;
//設(shè)置是否雙面顯示
@property (nonatomic, getter=isDoubleSided) BOOL doubleSided;
//設(shè)置要顯示的視圖控制器
- (void)setViewControllers:(nullable NSArray<UIViewController *> *)viewControllers direction:(UIPageViewControllerNavigationDirection)direction animated:(BOOL)animated completion:(void (^ __nullable)(BOOL finished))completion;

上面的spineLocation屬性有些難以理解,其枚舉值如下:

typedef NS_ENUM(NSInteger, UIPageViewControllerSpineLocation) {
    //對(duì)于SCrollView類型的滑動(dòng)效果 沒有書軸 會(huì)返回下面這個(gè)枚舉值
    UIPageViewControllerSpineLocationNone = 0, 
    //以左邊或者上邊為軸進(jìn)行翻轉(zhuǎn) 界面同一時(shí)間只顯示一個(gè)View
    UIPageViewControllerSpineLocationMin = 1,  
    //以中間為軸進(jìn)行翻轉(zhuǎn) 界面同時(shí)可以顯示兩個(gè)View
    UIPageViewControllerSpineLocationMid = 2, 
    //以下邊或者右邊為軸進(jìn)行翻轉(zhuǎn) 界面同一時(shí)間只顯示一個(gè)View
    UIPageViewControllerSpineLocationMax = 3   
};

UIPageViewControllerDataSource中的方法:

//向前翻頁展示的ViewController(上一頁)
- (nullable UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController;
//向后翻頁展示的ViewController(下一頁)
- (nullable UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController;
//設(shè)置分頁控制器的分頁點(diǎn)數(shù)
- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController NS_AVAILABLE_IOS(6_0);
//設(shè)置當(dāng)前分頁控制器所高亮的點(diǎn)
- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController NS_AVAILABLE_IOS(6_0);

UIPageViewControllerDelegate中的方法:

//翻頁視圖控制器將要翻頁時(shí)執(zhí)行的方法
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray<UIViewController *> *)pendingViewControllers NS_AVAILABLE_IOS(6_0);
//翻頁動(dòng)畫執(zhí)行完成后回調(diào)的方法
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray<UIViewController *> *)previousViewControllers transitionCompleted:(BOOL)completed;
//屏幕防線改變時(shí)回到的方法,可以通過返回值重設(shè)書軸類型枚舉
- (UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation;

2.數(shù)據(jù)處理

現(xiàn)在我們已經(jīng)有了翻頁神器UIPageViewControlelr,那么數(shù)據(jù)應(yīng)該如何組織呢?在實(shí)際項(xiàng)目中,小說數(shù)據(jù)都是以加密的方式存儲(chǔ)在服務(wù)器中,我們通過接口請(qǐng)求拿到數(shù)據(jù)后進(jìn)行解密再使用。但是出于性能和部分章節(jié)需要付費(fèi)考慮,都是按章節(jié)請(qǐng)求數(shù)據(jù)。我們這里簡(jiǎn)化一下,一部完整的加密的小說已經(jīng)存在本地,我們只需要解密后就可以直接使用了,不需要考慮子線程請(qǐng)求章節(jié)的邏輯。上文提到我們的排版引擎可以實(shí)現(xiàn)單個(gè)頁面的顯示,然后我們只要把小說先分成章節(jié),再分成頁,然后通過UIPageViewController來呈現(xiàn)出來就基本實(shí)現(xiàn)了小說閱讀器的核心功能。

首先,將小說分成章節(jié),這里使用了正則表達(dá)式:

+ (NSMutableArray *)separateChapterWithContent:(NSString *)content {
    // 創(chuàng)建章節(jié)對(duì)象數(shù)組
    NSMutableArray *chapters = @[].mutableCopy;
    // 正則表達(dá)式條件
    NSString *parten = @"第[0-9一二三四五六七八九十百千]*[章回].*";
    NSError *error = NULL;
    NSRegularExpression *reg = [NSRegularExpression regularExpressionWithPattern:parten options:NSRegularExpressionCaseInsensitive error:&error];
    // 得到分割后的對(duì)象數(shù)組,對(duì)其遍歷創(chuàng)建章節(jié)對(duì)象
    NSArray *match = [reg matchesInString:content options:NSMatchingReportCompletion range:NSMakeRange(0, [content length])];
    if (match.count != 0) {
        __block NSRange lastRange = NSMakeRange(0, 0);
        [match enumerateObjectsUsingBlock:^(NSTextCheckingResult *  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            NSRange range = [obj range];
            NSInteger local = range.location;
            if (idx == 0) { // 第一章標(biāo)題
                HYChapterModel *model = [[HYChapterModel alloc] init];
                model.title = @"開始";
                NSUInteger len = local;
                model.content = [content substringWithRange:NSMakeRange(0, len)];
                [chapters addObject:model];
            }
            if (idx > 0) {
                HYChapterModel *model = [[HYChapterModel alloc] init];
                model.title = [content substringWithRange:lastRange];
                NSUInteger len = local -lastRange.location;
                model.content = [content substringWithRange:NSMakeRange(lastRange.location, len)];
                [chapters addObject:model];
            }
            if (idx == match.count-1) { // 最后一章
                HYChapterModel *model = [[HYChapterModel alloc] init];
                model.title = [content substringWithRange:range];
                model.content = [content substringWithRange:NSMakeRange(local, content.length - local)];
                [chapters addObject:model];
            }
            lastRange = range;
        }];
    } else {
        HYChapterModel *model = [[HYChapterModel alloc] init];
        model.content = content;
        [chapters addObject:model];
    }
    return chapters;
}

其次,將章節(jié)分成頁,這里就用到了CoreText的方法。我們拿到設(shè)置了attribute的富文本字符串后,根據(jù)顯示區(qū)域rect可以得到CTFrameRef,再通過CTFrameGetVisibleStringRange方法可以得到當(dāng)前可見字符串區(qū)域,遍歷后可以得到每一頁的區(qū)域range,如此邊完成了分頁邏輯。

- (NSArray *)pagingContentWithAttributeStr:(NSAttributedString *)attributeStr pageSize:(CGSize)pageSize {
    NSMutableArray<NSValue *> *resultRange = [NSMutableArray array]; // 返回結(jié)果數(shù)組
    CGRect rect = CGRectMake(0, 0, pageSize.width, pageSize.height); // 每頁的顯示區(qū)域大小
    NSUInteger curIndex = 0; // 分頁起點(diǎn),初始為第0個(gè)字符
    while (curIndex < attributeStr.length) { // 沒有超過最后的字符串,表明至少剩余一個(gè)字符
        NSUInteger maxLength = MIN(1000, attributeStr.length - curIndex); // 1000為最小字體的每頁最大數(shù)量,減少計(jì)算量
        NSAttributedString * subString = [attributeStr attributedSubstringFromRange:NSMakeRange(curIndex, maxLength)]; // 截取字符串
        CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef) subString); // 根據(jù)富文本創(chuàng)建排版類CTFramesetterRef
        UIBezierPath * bezierPath = [UIBezierPath bezierPathWithRect:rect];
        CTFrameRef frameRef = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), bezierPath.CGPath, NULL); // 創(chuàng)建排版數(shù)據(jù),第個(gè)參數(shù)的range.length=0表示放字符直到區(qū)域填滿
        CFRange visiableRange = CTFrameGetVisibleStringRange(frameRef); // 獲取當(dāng)前可見的字符串區(qū)域
        NSRange realRange = {curIndex, visiableRange.length}; // 當(dāng)頁在原始字符串中的區(qū)域
        [resultRange addObject:[NSValue valueWithRange:realRange]]; // 記錄當(dāng)頁結(jié)果
        curIndex += realRange.length; //增加索引
        CFRelease(frameRef);
        CFRelease(frameSetter);
    };
    return resultRange;
}

以上就是一個(gè)簡(jiǎn)易的小說閱讀器的核心邏輯。代碼實(shí)現(xiàn)分為4層:

  • 交互層:處理小說的左右翻頁邏輯和其他操作響應(yīng)
  • 邏輯層:數(shù)據(jù)請(qǐng)求、數(shù)據(jù)存儲(chǔ)、數(shù)據(jù)轉(zhuǎn)換和排版邏輯
  • 數(shù)據(jù)層:小說模型、章節(jié)數(shù)據(jù)、排版設(shè)置數(shù)據(jù)
  • 顯示層:對(duì)排版結(jié)果進(jìn)行渲染

類圖如下:


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容