上篇我們實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的排版引擎,總結(jié)起來很簡(jiǎn)單,在一個(gè)自定義視圖的drawRect:()方法中繪制利用CoreText的CTFrameDraw()方法繪制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一樣,有自己的dataSource和delegate。
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)行渲染

類圖如下:
