• Reader开发(二)增加PDF阅读功能


    最近任务很多很忙,所以更新博客的速度很慢。

    大概上周就为Reader加了一个PDF阅读的功能,但是一直没时间写上来。昨晚找一下文件发现扩展了功能的Demo居然在文件目录下看不到任何文件,但是却显示有文件大小,而且删除的时候还显示已锁定,应该不是文件被隐藏了的问题。没有办法之下,今天下午又重新把该功能在原来未修改过的Demo上写了回来,又花了一些时间。文件备份太重要了。


    PDF文件和RTF,TXT这些格式的文件不同,这种文件中显示出的是图像而不是单纯的文字(就我肤浅的看法来看),这样Text Kit这个强大的文字处理引擎似乎就派不上用场了,不过可以使用官方给出的CGPDFDocumentRef和CGPDFPageRef类以及UIView的drawRect:方法来创建PDF文件和呈现PDF视图。

    跟着之前Reader开发的思路,由于PDF的阅读视图是draw出来的,而RTF和TXT的阅读视图是直接使用AttributedString的,两者思路完全不同,如果将其阅读视图塞进一个ViewController中似乎会显得很乱,所以我新建了一个PDFViewController和一个PDFView类来专门管理PDF文件的阅读。

    首先是在BookList表格中如果选中了PDF文件,那么跳转的目的视图控制器不是之前的ReadingViewController,而是新的PDFViewController,代码如下:

    else if (indexPath.section == 2) { // pdf
            name = sPdfArray_[indexPath.row];
            PDFViewController *pdfVC = [[PDFViewController alloc] initWithPDFName:name];
            [self.navigationController pushViewController:pdfVC animated:YES];
            return;
        }

    在这里使用导航控制器push了一个PDFViewController进栈,而不是present视图控制器了。


    首先给出PDFViewController的接口部分,了解一下PDFViewController的成员结构:

    #import <UIKit/UIKit.h>
    #import "PDFView.h"
    
    @interface PDFViewController : UIViewController
    @property (strong, nonatomic) PDFView *curView;  // 当前PDF页面视图
    @property (strong, nonatomic) PDFView *addView;  // 新的PDF页面视图
    @property (strong, nonatomic) PDFView *backView; // 用于制造翻页效果的视图
    @property (strong, nonatomic) UIScrollView *scrollView; // 滚动视图,用于显示完整的PDF页面
    @property (retain, nonatomic) CAGradientLayer *shadow;  // 用于制造阴影效果的Layer
    @property (retain, nonatomic) CAGradientLayer *margin;  // 用于制造页边效果的Layer
    -(id)initWithPDFName:(NSString *)name; // 通过PDF文件名初始化
    @end

    以及匿名接口部分,里面包括一些私有的成员:

    @interface PDFViewController ()
    {
        BOOL next_;     // 是否翻向下一页
        BOOL enlarged_; // pdf视图是否被放大
        NSUInteger currentPage_; // 当前页号
        NSUInteger totalPages_;  // 总页数
        CGFloat startX_;    // 翻页手势起点的x值
        CGFloat curoffset_; // 翻页手势的位移值
        CGFloat minoffset_; // 翻页手势有效的最小位移值
        CGRect pdfRect_; // 完整的PDF页面的框架矩形
        CGRect fitRect_; // 适配后的PDF页面的框架矩形
        CGPDFDocumentRef pdfRef_;  // pdf文件
        CGPDFPageRef     pdfPage_; // pdf页面
    }
    @property (strong, nonatomic) UITapGestureRecognizer *doubleTap_; // 双击手势,用于查看完整的PDF页面
    @property (strong, nonatomic) UIView *viewForPDF; // self.view中用于放置pdf阅读视图的子视图
    @end


    来看看PDFViewController的初始化方法:

    #pragma mark -
    #pragma mark Initialize
    
    /* 通过PDF文件名初始化 */
    -(id)initWithPDFName:(NSString *)name {
        self = [super init];
        if (self) {
            /* 根据pdf文件路径初始化pdf阅读视图 */
            NSString *filePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:name]; // 获取PDF文件名的完整路径
            NSLog(@"filePath = %@", filePath); // 例如:filePath = /Users/one/Library/Application Support/iPhone Simulator/7.0/Applications/5815AD09-13F2-4C77-9CAE-ADD399E85A5E/PDFReader_i7_Demo.app/CGPDFDocument.pdf
            pdfRef_ = [self createPDFFromExistFile:filePath]; // 创建pdf文件对象
            pdfPage_ = CGPDFDocumentGetPage(pdfRef_, 1); // 创建pdf首页页面
            currentPage_ = 1; // 页号,从1开始
        }
        return self;
    }
    
    /* 根据文件路径创建pdf文件 */
    - (CGPDFDocumentRef)createPDFFromExistFile:(NSString *)aFilePath {
        CFStringRef path;
        CFURLRef url;
        CGPDFDocumentRef document;
        
        path = CFStringCreateWithCString(NULL, [aFilePath UTF8String], kCFStringEncodingUTF8);
        url = CFURLCreateWithFileSystemPath(NULL, path, kCFURLPOSIXPathStyle, NO);
        CFRelease(path);
        
        document = CGPDFDocumentCreateWithURL(url);
        CFRelease(url);
        
        totalPages_ = CGPDFDocumentGetNumberOfPages(document); // 设置PDF文件总页数
        NSLog(@"totalPages = %d", totalPages_);
        if (totalPages_ == 0) { // 创建出错处理
            NSLog(@"Create Error");
            return NULL;
        }
        return document;
    }

    其中initWithPDFName:方法通过createPDFFromeExistFile:方法初始化了CGPDFDocumentRef类的对象。PDF文件对象的创建基本完成。


    由于在阅读PDF阅读时要通过手势的移动来实现翻页,所以这里我沿用了之前的Touches in view的思路和框架,在PDFViewController的self.view中动态添加PDF阅读视图来实现阅读功能,那么就涉及到了PDFView类的使用,先看看初始化方法:

    /* 初始化PDFView对象 */
    - (id)initWithPDFRef:(CGPDFDocumentRef)pdfr {
        pdfRef = pdfr;
        pdfPage = CGPDFDocumentGetPage(pdfRef, 1); // 创建pdf首页页面
        self.pageIndex = 1; // 要展示的页面号,从1开始
        CGRect mediaRect = CGPDFPageGetBoxRect(pdfPage, kCGPDFMediaBox);
        self = [super initWithFrame:mediaRect];
        return self;
    }

    PDFView负责呈现PDF文件中的内容,PDFViewController负责控制PDFView的显示和布局。

    注意PDFView是UIView类的子类,所以该类自带了一个drawRect:方法,要描绘出PDF的阅读内容,就必须要实现该方法:

    /* drawRect:方法,每个UIView的自带方法 */
    - (void)drawRect:(CGRect)rect {
        CGContextRef context = UIGraphicsGetCurrentContext(); // 获取当前的绘图上下文
        [[UIColor whiteColor] set];
        CGContextFillRect(context, rect);
        CGContextGetCTM(context);
        CGContextScaleCTM(context, 1, -1);
        CGContextTranslateCTM(context, 0, -rect.size.height);
        pdfPage = CGPDFDocumentGetPage(pdfRef, self.pageIndex);
        CGRect mediaRect = CGPDFPageGetBoxRect(pdfPage, kCGPDFCropBox);
        CGContextScaleCTM(context, rect.size.width / mediaRect.size.width, rect.size.height / mediaRect.size.height);
        CGContextTranslateCTM(context, -mediaRect.origin.x, -mediaRect.origin.y);
        CGContextDrawPDFPage(context, pdfPage); // 绘制当前页面
    }


    另外在翻页时PDFView的内容必须作出更新,可以使用setNeedsDisplay方法来实现,而该方法必须被PDFViewController调用,所以可以将其写成一个接口供其它类使用:

    /* 更新视图,例如翻页的时候需要更新 */
    - (void)reloadView {
        [self setNeedsDisplay];
    }

    接口部分:

    @interface PDFView : UIView
    {
        CGPDFDocumentRef pdfRef; // pdf文件
        CGPDFPageRef pdfPage;    // pdf页面
    }
    @property (assign, nonatomic) NSUInteger pageIndex; // 页面号
    - (id)initWithPDFRef:(CGPDFDocumentRef)pdfr;
    - (void)reloadView;
    @end

    完成PDFView的任务后,我们回到PDFViewController上来,首先当然是viewDidLoad:方法了:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        /* 初始化参数 */
        minoffset_ = self.view.frame.size.width / 5.;
        enlarged_ = NO; // 初始的PDF视图的放大状态为NO
        
        
        /* 初始化视图 */
        self.navigationItem.title = [NSString stringWithFormat:@"%d / %d", currentPage_, totalPages_];
        curView  = [[PDFView alloc] initWithPDFRef:pdfRef_];
        addView  = [[PDFView alloc] initWithPDFRef:pdfRef_];
        backView = [[PDFView alloc] initWithPDFRef:pdfRef_];
        backView.pageIndex = 0;
        scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
        viewForPDF = [[UIView alloc] initWithFrame:CGRectMake(0., 60., self.view.frame.size.width, self.view.frame.size.height - 60.0)];
        
        
        /* 设置PDF阅读视图的页面布局 */
        CGFloat w = curView.frame.size.width;
        CGFloat h = curView.frame.size.height;
        pdfRect_  = curView.frame;
        CGFloat scale = h / w; // PDF原视图高度和宽度的比例
        NSLog(@"w = %f", w);
        NSLog(@"h = %f", h);
        CGFloat href = self.view.frame.size.width * scale; // 经过页面适配后的高度
        CGFloat yref = (self.view.frame.size.height - 60.0 - href) / 2.; // 经过页面适配后的原点y值
        NSLog(@"href = %f", href);
        NSLog(@"yref = %f", yref);
        curView.frame = CGRectMake(0., yref, self.view.frame.size.width, href); // 设置适配后PDF视图的位置和大小
        fitRect_ = curView.frame; // 保存适配后的框架矩形
        [self.view addSubview:viewForPDF];
        [viewForPDF addSubview:curView]; // 添加页面适配后的PDF视图
        
        
        /* 为视图添加双击手势 */
        doubleTap_ = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(enlargePDFPage:)];
        doubleTap_.numberOfTapsRequired = 2;
        [self.view addGestureRecognizer:doubleTap_];
    }



    在这里说一下设置PDF阅读视图的页面布局这一段吧,由于PDFView的drawRect:方法没有draw出最适合页面的显示,所以看到如下显示:



    也就是在没有UIScrollView来呈现PDFView的情况下,我们只能看到PDF页面的部分视图(放大后的),由于我对CGContextDraw这些方法真的一点都不熟悉,所以只能通过设置PDFView的frame来解决该问题了。

    首先获取初始的PDFView的视图尺寸并将其保存起来:

    CGFloat w = curView.frame.size.width;
    CGFloat h = curView.frame.size.height;
    pdfRect_  = curView.frame;
    2013-09-13 18:02:34.210 Reader_i7_Demo[2257:a0b] w = 612.000000
    2013-09-13 18:02:34.211 Reader_i7_Demo[2257:a0b] h = 792.000000

    然后通过宽高比例进行适配并将其保存起来:

        CGFloat scale = h / w; // PDF原视图高度和宽度的比例
        NSLog(@"w = %f", w);
        NSLog(@"h = %f", h);
        CGFloat href = 320. * scale; // 经过页面适配后的高度
        CGFloat yref = (510. - href) / 2.; // 经过页面适配后的原点y值
        NSLog(@"href = %f", href);
        NSLog(@"yref = %f", yref);
        curView.frame = CGRectMake(0., yref, self.view.frame.size.width, href); // 设置适配后PDF视图的位置和大小
        fitRect_ = curView.frame; // 保存适配后的框架矩形

    来看看适配后的页面视图:


    现在另一个问题来了,文字太小,看不到完整的pdf内容(以iPhone的尺寸来看),这个时候可以在视图中添加一个双击手势来显示完整的pdf内容:

        /* 为视图添加双击手势 */
        doubleTap_ = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(enlargePDFPage:)];
        doubleTap_.numberOfTapsRequired = 2;
        [self.view addGestureRecognizer:doubleTap_];


    看看响应的方法:

    /* 双击手势的响应方法 */
    -(void)enlargePDFPage:(id)sender {
        if (enlarged_ == NO) { // 如果PDF页面未被放大
            [curView removeFromSuperview];     //首先移除当前PDF页面
            [self.view addSubview:scrollView]; // 在self.view中添加scrollView
            [scrollView addSubview:curView];   // 在scrollView上重新添加curView
            curView.frame = pdfRect_; // 设置curView的框架为原始PDF视图的框架
            scrollView.contentSize = pdfRect_.size; // 设置scrollView的内容尺寸
            enlarged_ = YES; // 设置放大状态
            self.navigationController.navigationBarHidden = YES; // 隐藏导航条
        }
        else { // 如果PDF页面已经被放大
            [scrollView removeFromSuperview]; // 移除scrollView和curView
            [viewForPDF addSubview:curView]; // 在viewForPDF子视图重新添加curView
            curView.frame = fitRect_;
            enlarged_ = NO; // 取消放大状态
            self.navigationController.navigationBarHidden = NO; // 显示导航条
        }
    }

    这样一来,在双击视图后,就可以查看全屏状态下的pdf视图了:


    在全屏状态下再次双击视图,又看到原来的PDFView了:



    最后解决一下翻页的问题,这里我沿用了之前的方法:

    #pragma mark -
    #pragma mark Touches in view
    
    -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
        //记录手势起点的x值
        UITouch *touch = [touches anyObject];
        startX_        = [touch locationInView:self.view].x;
    }
    
    -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
        //将视图中已经存在的渐变或页边阴影去掉
        if (shadow) {
            [shadow removeFromSuperlayer];
        }
        if (margin) {
            [margin removeFromSuperlayer];
        }
        
        //获取当前手势触点的x值
        UITouch *touch = [touches anyObject];
        float x        = [touch locationInView:self.view].x;
        if (x - startX_ >= 0) {
            curoffset_ = x - startX_;
        }
        else {
            curoffset_ = startX_ - x;
        }
        
        // 设定翻转页面的矩形范围
        CGRect rect = self.view.bounds;
        if (x >= 160) {
            rect.size.width = (320 / x - 1) * 160;
            rect.origin.x   = x - rect.size.width;
        }
        else {
            rect.size.width = 320 - x;
            rect.origin.x   = x - rect.size.width;
        }
        int tempX           = rect.origin.x; //保存翻转页面起点的x值
        backView.frame      = rect;
        
        //rect用于设定翻页时左边页面的范围
        rect = self.view.bounds;
        rect.size.width = x;
        
        
        // 判断手势并设定页面,制造翻页效果
        if (x - startX_ > 0) { //向右划的手势,上一页
            next_ = NO;
            if (currentPage_ == 1) {
                return; // 如果是第一页则不接受手势
            }
            else {
                addView.frame = rect;
                addView.clipsToBounds = YES;
                addView.pageIndex = currentPage_ - 1;
                [addView reloadView];
                
                [viewForPDF insertSubview:addView aboveSubview:curView];
                
                [viewForPDF insertSubview:backView aboveSubview:addView];
            }
        }
        else { //向左划的手势,下一页
            next_ = YES;
            
            if (currentPage_ == totalPages_) {
                return; // 如果到达最后一页则不接受手势
            }
            else {
                curView.frame = rect;
                addView.pageIndex = currentPage_ + 1;
                addView.frame = fitRect_;
                [addView reloadView];
                
                [viewForPDF insertSubview:addView belowSubview:curView];
                
                [viewForPDF insertSubview:backView aboveSubview:curView];
            }
        }
        
        //设定翻页时backPage视图两边的渐变阴影效果
        shadow            = [[CAGradientLayer alloc] init];
        shadow.colors     = [NSArray arrayWithObjects:
                             (id)[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.1].CGColor,
                             (id)[UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.2].CGColor,
                             (id)[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.1].CGColor,
                             nil];
        rect              = self.view.bounds;
        rect.size.width   = 50;
        rect.origin.x     = x - 25;
        shadow.frame      = rect;
        shadow.startPoint = CGPointMake(0.0, 0.5);
        shadow.endPoint   = CGPointMake(1.0, 0.5);
        [self.view.layer addSublayer:shadow];
        
        margin            = [[CAGradientLayer alloc] init];
        margin.colors     = [NSArray arrayWithObjects:
                             (id)[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.2].CGColor,
                             (id)[UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.3].CGColor,
                             nil];
        margin.frame      = CGRectMake(tempX - 35, 0, 50, self.view.bounds.size.height);
        margin.startPoint = CGPointMake(0.0, 0.5);
        margin.endPoint   = CGPointMake(1.0, 0.5);
        [self.view.layer addSublayer:margin];
    }
    
    -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
        // 如果是第一页并且翻向上一页
        if (currentPage_ == 1) {
            if (next_ == NO) {
                return;
            }
        }
        
        // 如果是最后一页并且翻向下一页
        if (currentPage_ == totalPages_) {
            if (next_ == YES) {
                UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"注意" message:@"已经到达最后一页" delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
                [av show];
                return;
            }
        }
        
        if (curoffset_ < minoffset_) {
            curView.frame = fitRect_;
            curView.pageIndex = currentPage_ ;
            [curView reloadView];
            
            [addView  removeFromSuperview];
            [backView removeFromSuperview];
            
            //移除阴影效果
            [shadow removeFromSuperlayer];
            [margin removeFromSuperlayer];
            
            return;
        }
        
        if (next_ == YES) { // 下一页
            currentPage_++;
            NSLog(@"%d / %d", currentPage_, totalPages_);
            curView.frame = fitRect_;
            curView.pageIndex = currentPage_;
            [curView reloadView];
            
            self.navigationItem.title = [NSString stringWithFormat:@"%d / %d", currentPage_, totalPages_];
        }
        else { // 上一页
            currentPage_--;
            NSLog(@"%d / %d", currentPage_, totalPages_);
            curView.frame = fitRect_;
            curView.pageIndex = currentPage_;
            [curView reloadView];
            
            self.navigationItem.title = [NSString stringWithFormat:@"%d / %d", currentPage_, totalPages_];
        }
        
        [addView  removeFromSuperview];
        [backView removeFromSuperview];
        
        //移除阴影效果
        [shadow removeFromSuperlayer];
        [margin removeFromSuperlayer];
    }


    原理和Reader开发(一)中的翻页效果原理是一样的,最后还是上张程序运行的图:




    至此,实现PDF阅读的基本功能已经实现,不需要考虑分页的问题,对于获取PDF上面的内容可能要用到Core Text,这样可能要用另一种思维来写,暂时到此为止吧,如果对于这方面有什么新想法我会继续改进并且更新博客的。

    以上关于PDF文件阅读的代码参考了网上的一些文章,大家也可以参考一下:

    http://blog.csdn.net/yiyaaixuexi/article/details/7645725

    http://2015.iteye.com/blog/1333272

    http://www.cnblogs.com/mainPage/archive/2010/10/22/1858666.html

  • 相关阅读:
    Win7系统重启网卡批处理
    第一个应用程序HelloWorld
    JS异步流程控制(序列模式、并发模式、有限并发模式)
    bootstrap+MVC3在Moon.Orm中的应用(含有代码下载)
    google guava使用例子/示范
    证明Hadoop工作的正确性和可靠性只需4步图文并茂的过程
    python 图 自身遍历 及弱引用使用
    界面布局决定系统设计的成败
    .NET:栈的生长与消亡.
    IIS 6 & Server.MapPath
  • 原文地址:https://www.cnblogs.com/riskyer/p/3320020.html
Copyright © 2020-2023  润新知