使用 CGPDFDocument、CATiledLayer 和 UIPageViewController 做簡(jiǎn)單的 PDF 閱讀器(二)

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 的功能主要有如下幾條:

  1. 讀取 PDF 文件
  2. 從 PDF 文件中讀取一個(gè)頁(yè)面
  3. 添加滑動(dòng)手勢(shì),控制翻頁(yè).添加雙擊手勢(shì)進(jìn)行放大
  4. 實(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 客戶(hù)端開(kāi)發(fā),為了實(shí)現(xiàn)相同的效果,花了好幾天時(shí)間用 openGL 畫(huà)了一個(gè)。哎,確實(shí)比我牛好多。
使用 UIPageViewController 需要在它的代理方法(UIPageViewControllerDataSource UIPageViewControllerDelegate)中提供一系列 UIViewController 作為 childViewController 供顯示內(nèi)容用。

  1. 直接翻頁(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)用一下啦。

  1. 更新 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è)效果。
那么,

  1. 生成新的 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è)的效果。

  1. 處理一下放棄翻頁(yè)的情況
    有時(shí)候,用戶(hù)翻頁(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ù)就是表示用戶(hù)是否完成了翻頁(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

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 0 最近項(xiàng)目中需要給客戶(hù)看一些比較私密的文件,為了防止盜取和篡改,需要用 PDF 格式,為 app 添加了一個(gè)簡(jiǎn)單...
    Tatinic閱讀 2,271評(píng)論 1 2
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,035評(píng)論 4 61
  • 拜讀了林莘老師的《在改變和反思中前行》,破冰之旅,艱難并幸福著。 周四那天,參加女兒的家長(zhǎng)開(kāi)放日活動(dòng)!聽(tīng)的是堂數(shù)學(xué)...
    肺話連篇閱讀 519評(píng)論 0 4
  • 一簾煙雨,一剪月色,一徑花香,素淡綿長(zhǎng)的光陰,從心間緩緩流過(guò)。不留戀車(chē)馬喧囂,也不艷羨俗世繁華,卻安于細(xì)水流年,清...
    雜話漫談閱讀 563評(píng)論 0 0
  • 就比如,今夜,昨夜,很多個(gè)夜晚,你不困。你想找到的遠(yuǎn)方,不再是原來(lái),不能忍受。你好久沒(méi)說(shuō)話。然后,你終于再開(kāi)口。遠(yuǎn)...
    浮于浮生閱讀 272評(píng)論 0 0

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