0
上文已經(jīng)講述了如何使用 CGPDFDocument 和 CATiledLayer 繪制 PDF 文件,現(xiàn)在我們添加手勢(shì)來(lái)縮放 PDF, 然后再使用 UIPageViewController 制作一個(gè)翻頁(yè)效果.
單頁(yè) PDF 的縮放
self.pdfContentView = [[UIView alloc]init];
[self.pdfContentContainerView addSubview:self.pdfContentView];
self.pdfContentLayer = [[PDFContentTiledLayer alloc]init];
self.pdfContentLayer.delegate = self;
[self.pdfContentView.layer addSublayer:self.pdfContentLayer];
我們將單頁(yè) PDF 顯示在 PDFContentTiledLayer 然后將 PDFContentTiledLayer 放在 pdfContentView 內(nèi)顯示出來(lái).
因?yàn)槲覀兊?pdfContentView 是使用 autolayout 進(jìn)行布局的,并且支持橫屏和豎屏顯示,所以在設(shè)備方向發(fā)生變化時(shí), pdfContentView 的會(huì)觸發(fā) layout, 我們要在 layout 之后,重置 PDF 縮放狀態(tài).
- (void)viewDidLayoutSubviews{
[self adjustPdfContentFrame];
[self centerScrollViewContent];
[self drawThumb];
[super viewDidLayoutSubviews];
}
重新計(jì)算 PDF 頁(yè)面的縮放狀態(tài)
//調(diào)整 pdfContentView 將 PDF 縮放到 1x
- (void)adjustPdfContentFrame{
CGSize screenSize = [UIScreen mainScreen].bounds.size;
CGRect maxContentRect = self.pdfContentContainerView.bounds;
if (_pdfPage != nil) {
CGRect pdfPageOriginalPageRect = CGPDFPageGetBoxRect(_pdfPage, PDFContentDefaultPDFBox);
CGSize adjustPageSize = maxContentRect.size;
double contentRate = maxContentRect.size.width/(maxContentRect.size.height > 0?maxContentRect.size.height:screenSize.height);
double pdfOriginalRate = pdfPageOriginalPageRect.size.width/(pdfPageOriginalPageRect.size.height > 0? pdfPageOriginalPageRect.size.height:screenSize.height);
//判斷寬高比
//pdf 原始寬度大于顯示區(qū)域?qū)挾?需要將寬度縮放到顯示區(qū)域?qū)挾?并且相應(yīng)的縮放高度
if (pdfOriginalRate > contentRate) {
double scaleRate = pdfPageOriginalPageRect.size.width/maxContentRect.size.width;
double scaleWidth = maxContentRect.size.width;
double scaleHeight = pdfPageOriginalPageRect.size.height/(scaleRate > 0?scaleRate:1);
adjustPageSize = CGSizeMake(scaleWidth, scaleHeight);
}else{
double scaleRate = pdfPageOriginalPageRect.size.height/maxContentRect.size.height;
double scaleWidth = pdfPageOriginalPageRect.size.width/(scaleRate > 0?scaleRate:1);
double scaleHeight = maxContentRect.size.height;
adjustPageSize = CGSizeMake(scaleWidth, scaleHeight);
}
self.pdfContentView.frame = CGRectMake(0, 0, adjustPageSize.width, adjustPageSize.height);
self.pdfContentContainerView.contentSize = self.pdfContentView.bounds.size;
if (!CGRectEqualToRect(self.pdfContentLayer.bounds, self.pdfContentView.layer.bounds)) {
self.pdfContentLayer.frame = self.pdfContentView.layer.bounds;
//重新計(jì)算之后, CATiledLayer 需要重新繪圖,這里重新生成縮略圖避免閃屏
[self drawThumb];
[self.pdfContentLayer setNeedsDisplay];
}
}else{
self.pdfContentView.frame = CGRectZero;
self.pdfContentLayer.frame = self.pdfContentView.layer.bounds;
self.pdfContentContainerView.contentSize = self.pdfContentView.bounds.size;
}
}
//保證 pdf 在 ScrollView 中間位置
- (void)centerScrollViewContent
{
CGFloat iw = 0.0f; CGFloat ih = 0.0f; // Content width and height insets
CGSize boundsSize = self.pdfContentContainerView.bounds.size;
CGSize contentSize = self.pdfContentContainerView.contentSize; // Sizes
if (contentSize.width < boundsSize.width) iw = ((boundsSize.width - contentSize.width) * 0.5f);
if (contentSize.height < boundsSize.height) ih = ((boundsSize.height - contentSize.height) * 0.5f);
UIEdgeInsets insets = UIEdgeInsetsMake(ih, iw, ih, iw); // Create (possibly updated) content insets
if (UIEdgeInsetsEqualToEdgeInsets(self.pdfContentContainerView.contentInset, insets) == false){
self.pdfContentContainerView.contentInset = insets;
}
}
將 PDF 頁(yè)重置后,就開(kāi)始進(jìn)行縮放了.
為了將左滑右滑翻頁(yè)結(jié)合起來(lái),我們不把手勢(shì)放在 PDFContentViewController 類(lèi)里面,而是把這些手勢(shì)定義在 PDFViewController 里面.
這個(gè) PDFViewController 的功能主要有如下幾條:
- 讀取 PDF 文件
- 從 PDF 文件中讀取一個(gè)頁(yè)面
- 添加滑動(dòng)手勢(shì),控制翻頁(yè).添加雙擊手勢(shì)進(jìn)行放大
- 實(shí)現(xiàn)翻頁(yè)效果.
總之這個(gè) PDFViewController 作為 PDFContentViewController 的容器,完成 PDF 顯示之外的控制工作.
當(dāng)然, PDFContentViewController 本身要有縮放功能,并把這些功能的接口暴露出來(lái)給 PDFViewController 使用.
由于 PDFContentViewController 和是使用 UIScrollView 來(lái)進(jìn)行縮放的,這里和 PDFViewController 的翻頁(yè)功能產(chǎn)生了一定的沖突,這里先不提,我們先把縮放效果做完.
翻頁(yè)很簡(jiǎn)單,只要實(shí)現(xiàn) UIScrollview 的 delegate 就可以了,其實(shí),不過(guò)不需要兼顧翻頁(yè)效果,也不用處理 UIScrollview 的 delegate.
#pragma UIScrollViewDelegate
- (void)scrollViewDidZoom:(UIScrollView *)scrollView{
//NSLog(@"%@",scrollView);
}
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale{
NSLog(@"scrollViewDidEndZooming %@ %@ %lf",scrollView,view,scale);
[self centerScrollViewContent];
}
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{
return self.pdfContentView;
}
這里只有一個(gè)要注意的就是,每次縮放后,把 PDF 頁(yè)面中心放到屏幕中間,否則用起來(lái)會(huì)怪怪的.
[self centerScrollViewContent];
縮放就完成了.
翻頁(yè)效果,以及處理翻頁(yè)動(dòng)畫(huà)與 UIScrollView 縮放之間產(chǎn)生的沖突
翻頁(yè)效果的制作
iOS 上實(shí)現(xiàn)翻頁(yè)效果,可以使用系統(tǒng)帶的 UIPageViewController剿涮,方便快捷奢赂,效果非常好偏竟。我的直接領(lǐng)導(dǎo),公司移動(dòng)端的負(fù)責(zé)人民轴,現(xiàn)在負(fù)責(zé) Android 客戶端開(kāi)發(fā)攻柠,為了實(shí)現(xiàn)相同的效果,花了好幾天時(shí)間用 openGL 畫(huà)了一個(gè)后裸。哎瑰钮,確實(shí)比我牛好多。
使用 UIPageViewController 需要在它的代理方法(UIPageViewControllerDataSource UIPageViewControllerDelegate)中提供一系列 UIViewController 作為 childViewController 供顯示內(nèi)容用轻抱。
- 直接翻頁(yè)
//直接切換頁(yè)面
- (void)switchToPage:(NSInteger)pageNumber animated:(BOOL)animated{
self.pdfCurrentViewController = [self showPage:pageNumber];
if (self.pdfCurrentViewController != nil) {
_currentPageNum = pageNumber;
[self.pdfReaderViewController setViewControllers:@[self.pdfCurrentViewController] direction:UIPageViewControllerNavigationDirectionForward animated:animated completion:nil];
}
[self preparePageBuffer];
}
[self.pdfReaderViewController setViewControllers:@[self.pdfCurrentViewController] direction:UIPageViewControllerNavigationDirectionForward animated:animated completion:nil];
第一個(gè)參數(shù)是 UIViewController 的 Array飞涂,作為 UIPageViewController 數(shù)據(jù)的 buffer,第二個(gè)和第三個(gè)參數(shù)當(dāng)然是供做動(dòng)畫(huà)使用了祈搜。
這里作為 data 的 array 我只傳了一個(gè) ViewController较店,是因?yàn)?UIPageViewController 有 UIPageViewControllerDataSource 這個(gè)代理可以動(dòng)態(tài)的提供 buffer 數(shù)據(jù)。當(dāng)然了容燕,buffer 的具體內(nèi)容還是要自己生成的梁呈,我的代碼中有 [self preparePageBuffer]; 這個(gè)方法根據(jù)成員變量 _currentPageNum 來(lái)生成顯示前一頁(yè)和后一頁(yè)的 UIViewController,這里翻頁(yè)之后蘸秘,當(dāng)然要調(diào)用一下啦官卡。
- 更新 buffer
- (nullable UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController{
if (self.pdfPreviousViewController != nil) {
PDFContentViewController* tmpPage = self.pdfPreviousViewController;
return tmpPage;
}
return nil;
}
- (nullable UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController{
if (self.pdfNextViewController != nil) {
PDFContentViewController* tmpPage = self.pdfNextViewController;
return tmpPage;
}
return nil;
}
這兩個(gè)方法就是 delegate 里面動(dòng)態(tài)提供頁(yè)面 buffer 的方法了。
這里需要注意的是:
返回的頁(yè)面應(yīng)該是當(dāng)前顯示頁(yè)面下一頁(yè)的下一頁(yè)
解釋一下:如果當(dāng)前顯示的頁(yè)面是 “3”醋虏,那么- (nullable UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController 應(yīng)該返回第 “5” 頁(yè)寻咒。
這樣做的原因就是,手勢(shì)翻動(dòng)第三頁(yè)的時(shí)候颈嚼,第四頁(yè)內(nèi)容已經(jīng)露出來(lái)了毛秘,要做動(dòng)畫(huà)效果么,那么 buffer 里面當(dāng)然應(yīng)該放第五頁(yè)嘍,這樣才能實(shí)現(xiàn)連續(xù)的翻頁(yè)效果叫挟。
那么艰匙,
- 生成新的 buffer
這里跟上面是一起的,上面只是用生成好的頁(yè)面刷新 UIPageViewController 的 buffer抹恳,這里才是生成頁(yè)面的代碼员凝。
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray<UIViewController *> *)pendingViewControllers{
for (UIViewController* vc in pageViewController.viewControllers) {
if ([vc isKindOfClass:[PDFContentViewController class]]) {
PDFContentViewController* tmpvc = (PDFContentViewController*)vc;
[tmpvc resumeScale];
}
}
if (pendingViewControllers.count > 0) {
PDFContentViewController* currentPage = (PDFContentViewController*)[pendingViewControllers lastObject];
_currentPageNum = currentPage.pageNumber;
[self preparePageBuffer];
}
}
每次翻頁(yè)的時(shí)候,都會(huì)回調(diào)這個(gè)方法奋献。這里面 pendingViewControllers 參數(shù)傳入的是即將顯示的頁(yè)面健霹。比如說(shuō):當(dāng)前閱讀的是第三頁(yè),然后向后翻頁(yè)秽荞,pendingViewControllers 這個(gè)參數(shù)傳來(lái)的是第四頁(yè)骤公。這個(gè)時(shí)候,需要刷新 buffer 提前將第五頁(yè)生成扬跋。
之所以 pendingViewControllers 這里是 NSArray阶捆,是因?yàn)?UIPageViewController 在上下翻頁(yè)的時(shí)候,可能一次顯示多個(gè)頁(yè)面钦听。(我的項(xiàng)目實(shí)現(xiàn)的是左右翻頁(yè)洒试,這個(gè) NSArray 里面每次只包含一個(gè)頁(yè)面)。
如果一次翻好幾頁(yè)朴上,這個(gè)方法也會(huì)調(diào)用幾次垒棋,只要每次都生成 buffer 提供給 UIPageViewController 就能實(shí)現(xiàn)連續(xù)翻頁(yè)的效果。
- 處理一下放棄翻頁(yè)的情況
有時(shí)候痪宰,用戶翻頁(yè)到一半放棄了叼架,這時(shí)候就需要特殊處理一下。
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray<UIViewController *> *)previousViewControllers transitionCompleted:(BOOL)completed{
if (finished == YES && completed == NO) {
if (previousViewControllers.count > 0) {
PDFContentViewController* currentPage = (PDFContentViewController*)[previousViewControllers lastObject];
_currentPageNum = currentPage.pageNumber;
[currentPage resumeScale];
[self preparePageBuffer];
}
}
}
當(dāng)翻頁(yè)動(dòng)畫(huà)完成后衣撬,會(huì)回調(diào)這個(gè)方法乖订。completed 參數(shù)就是表示用戶是否完成了翻頁(yè)(completed == NO 其實(shí)就是 Gesture 變成了 Cancel 狀態(tài))。
如果沒(méi)有完成翻頁(yè)具练,需要刷新一下 buffer乍构。
解決 UIScrollView 與翻頁(yè)之間的沖突
這里需要在翻頁(yè)之前把 UIScrollview 的 scale 恢復(fù)成1,否則看起來(lái)怪怪的扛点。
有兩種情況哥遮,一個(gè)是 UIScrollview 在放大狀態(tài)點(diǎn)擊翻頁(yè),另一個(gè)是橫屏的時(shí)候陵究,UIScrollview 與橫向充滿屏幕眠饮,這個(gè)時(shí)候也處于放大狀態(tài),但是可以出發(fā)滑動(dòng)翻頁(yè)铜邮。
這個(gè)暫時(shí)的解決方案就是在- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray<UIViewController *> *)pendingViewControllers 里面君仆,把 UIScrollview 的 scale 設(shè)置成1
- (void)resumeScale{
[self.pdfContentContainerView setZoomScale:1 animated:NO];
}
但是在橫屏狀態(tài)下,偶爾出現(xiàn) UIScrollview 的 anchorpoint 錯(cuò)誤的情況,留待以后解決吧返咱。
下載 PDF 文件的頁(yè)面
這里就是另一個(gè)話題了這篇文章就先不講了:-D
關(guān)于 PDF 閱讀器內(nèi)存泄露的問(wèn)題
感謝小伙伴抽出寶貴時(shí)間閱讀我的代碼并指出內(nèi)存泄露的問(wèn)題
嘗試修復(fù)了一下幾個(gè)地方來(lái)解決這個(gè)問(wèn)題。
- (void)dealloc{
//確定了,使用CGPDFDocumentRelease釋放 PDFDocument 時(shí)候,會(huì)將 PDFPage 一起釋放
//好奇怪的設(shè)計(jì) https://stackoverflow.com/questions/46903182/cgcontextdrawpdfpage-memory-leak-app-crash
if (_pdfDocument != nil) {
CGPDFDocumentRelease(_pdfDocument);
_pdfDocument = nil;
}
}
- (PDFContentViewController* _Nullable)showPage:(size_t)pageNum{
if (_totalPageNum > 0 && pageNum <= _totalPageNum && pageNum > 0) {
//讀取不同的 pdf 頁(yè)時(shí)牍鞠,不需要將舊的頁(yè)面釋放咖摹,這個(gè)工作由系統(tǒng)在 CGPDFDocumentGetPage 中完成
// if (_currentPage != nil) {
// CGPDFPageRelease(_currentPage);
// _currentPage = nil;
// }
_currentPage = CGPDFDocumentGetPage(_pdfDocument, pageNum);
PDFContentViewController* pdfContentViewController = [[PDFContentViewController alloc]init];
[pdfContentViewController showPdfPage:_currentPage];
pdfContentViewController.pageNumber = pageNum;
return pdfContentViewController;
}else{
return nil;
}
}
#pragma CALayerDelegate
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context{
//這里是怕異步渲染時(shí),self 突然被釋放掉造成崩潰(這個(gè)方法并不是閉包难述,所以不會(huì)自動(dòng)捕獲引用的變量)
PDFContentViewController* strongSelf = self;
//同理萤晴,防止繪制時(shí),pdfPage 被釋放
CGPDFPageRetain(_pdfPage);
double screenScale = [UIScreen mainScreen].scale;
CGSize pdfPageOriginalPageSize = CGPDFPageGetBoxRect(strongSelf->_pdfPage, PDFContentDefaultPDFBox).size;
CGSize imageSize = CGSizeMake(layer.bounds.size.width, layer.bounds.size.height);
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextFillRect(context,CGContextGetClipBoundingBox(context));
CGContextSaveGState(context);
CGContextTranslateCTM(context, 0, layer.bounds.size.height);
CGContextScaleCTM(context, 1, -1);
CGContextScaleCTM(context, imageSize.width/(pdfPageOriginalPageSize.width > 0?pdfPageOriginalPageSize.width:imageSize.width), imageSize.height/(pdfPageOriginalPageSize.height > 0?pdfPageOriginalPageSize.height:imageSize.height));
// CGContextScaleCTM(context, 1/screenScale, 1/screenScale);
CGContextDrawPDFPage(context, strongSelf->_pdfPage);
CGContextRestoreGState(context);
CGPDFPageRelease(_pdfPage);
}
根據(jù) StackOverFlow 上面的說(shuō)法胁后,CGPDFDocumentGetPage 為了快速讀取 PDF 頁(yè)面店读,是有緩存的,通過(guò)打印內(nèi)存地址也印證了這個(gè)說(shuō)法攀芯。這就是手動(dòng)釋放 PDFPage 對(duì)象總是崩潰的原因屯断。
http://stackoverflow.com/questions/13248282/uipageviewcontroller-transition-unbalanced-calls-to-begin-end-appearance-transi
http://blog.csdn.net/lqq200912408/article/details/50412893