AndroidPdfHelper——一个基于PdfRenderer的PDF加载库

前言

PDF文件是如今常见的文档格式之一,最近遇到一个加载网络PDF文件的需求,首先想到的是WebView的方式,想着IOS中自带的WebView支持加载PDF,想必Android应该也可以直接解析吧,然而调研一番发现


并没有类似的支持,只能自己动手丰衣足食了...

 

效果预览

目前Android中常见的加载PDF文件的方式有以下几种:

1.通过Google提供的文档服务加载

WebView本身并不能解析PDF文件,但是利用Google Docs提供的服务可以解析在线PDF链接:
只需要在原 url 前面拼上 http://docs.google.com/gview?url=,如下:

webview.loadurl("http://docs.google.com/gview?url=你的PDF链接");

但是使用这种方式的前提是要能连接上Google的服务,在国内的网络环境下有明显的局限性。

2.通过PDF.js结合WebView加载

PDF.js是Mozilla开源的一个PDF的阅读器,详情可参见 Mozilla/PDF.js
这个库所支持的功能也是比较全面的,但集成到项目中会导致包体增大5M,会有体积问题,当然,也可以采用服务器动态下发js文件加载的方式。

3.通过集成第三方库加载

目前已知的第三方库在功能和性能等指标可能存在较大的区别,集成原生第三方库的优点是体验好,但缺点是会显著的增加包大小,例如比较知名的 AndroidPdfViewer ,它是基于 PdfiumAndroid 的基础上进行封装,其so库文件也大幅度增加了包体的大小。

4.调起第三方支持PDF阅读的应用

由于系统本身的WebView不支持,可以采用应用外跳转的引导方式,将链接传递过去,使用其他支持的应用来加载:

File file = new File(getExternalCacheDir(),FILE_NAME);
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri = Uri.fromFile(file);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(uri, "application/pdf");
startActivity(intent);

这种方式只能加载本地PDF文件,且要求设备上有支持阅读PDF文件的应用

5.通过原生PdfRenderer加载

Android本身也提供了加载PDF的 PDFRenderer,可以将PDF每一页转换为位图显示,但是只支持加载本地文件,所以加载在线文件需要先下载到本地再解析,另外,必须API>=21才能使用。
如果对文件的操作要求不高,可以考虑使用该方式,优点是几乎不影响包体的大小。

 
综合以上,决定基于PdfRenderer封装成一个PDF预览组件,项目地址:AndroidPdfHelper,满足基本的页面切换、手势放大缩小、自定义预览样式、快速导航等功能,最终效果如下:

pdf_preview_1.png

特性

1. 基于PdfRenderer实现,不同于其它第三方库,占用包体小
2. 支持PDF文件的上下页切换
3. 支持PDF单页的放大缩小查看
4. 支持设置文件预览清晰度
5. 支持自定义控制栏样式
6. 支持AndroidX

 

如何使用

在项目根目录的build.gradle添加:

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

在项目的build.gradle添加如下依赖:

implementation 'com.github.GitHubZJY:AndroidPdfHelper:v1.0.0'

1)以View的方式引用(适用于需要自定义界面样式的场景)

<com.zjy.pdfview.PdfView
        android:id="@+id/pdf_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

</com.zjy.pdfview.PdfView>

在代码中初始化PdfView

PdfView pdfView = findViewById(R.id.pdf_view);

 
预览在线PDF文件:

pdfView.loadPdf("http://.....xx.pdf");

 
预览asset文件:

pdfView.loadPdf("file:///android_asset/test.pdf");

 

2)以页面方式调起(使用组件默认页面样式)

以页面的形式,自带了默认的顶部标题栏,适配Android 5.0以下,会自动下载并调用浏览器打开
预览在线PDF文件:

PdfPreviewUtils.previewPdf(context, "http://.....xx.pdf");

 
预览asset文件:

PdfPreviewUtils.previewPdf(context, "file:///android_asset/test.pdf");

 

3)设置预览清晰度

<com.zjy.pdfview.PdfView
    android:id="@+id/pdf_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:quality="medium">

</com.zjy.pdfview.PdfView>

 
通过设置 quality 属性即可,目前一共有低、中、高三种清晰度,如下:

高清晰度:high
中等清晰度:medium
低清晰度:low

 

实现思路

PdfRenderer只支持加载本地PDF,如何加载网络文件?

由于PdfRenderer只支持加载本地PDF文件,如果要加载在线文件,可以先在后台开启下载任务,下载完成之后再调用PdfRenderer去解析。
 

PdfRenderer只支持API21以上,Android5.0以下如何适配?

由于PdfRenderer只支持Android5.0以上,所以判断当前版本,如果是5.0以下,则采用浏览器跳转方式。
 

如何解析PDF文件?

//存储位图数据集合
List<Bitmap> pageList = new ArrayList<Bitmap>();
PdfRenderer pdfRenderer = new PdfRenderer(getFileDescriptor());
int pageCount = pdfRenderer.getPageCount();
pageList.clear();
for (int i=0; i<pageCount; i++) {
    PdfRenderer.Page item = pdfRenderer.openPage(i);
    int qualityRatio = getResources().getDisplayMetrics().densityDpi / (quality * 72);
    Bitmap bitmap = Bitmap.createBitmap(qualityRatio * item.getWidth(), qualityRatio * item.getHeight(), Bitmap.Config.ARGB_4444);
    item.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
    item.close();
    pageList.add(bitmap);
}

通过 getPageCount 获取到PDF文件总的页数,然后再遍历调用 openPage 加载出每一页的 PdfRenderer.Page 对象,再通过其 render 方法,将页面转换为Bitmap对象。
注:这里的 qualityRatio 是用来计算最终的一个显示的比例,这个参数决定了生成的bitmap的尺寸,进而影响最终显示的清晰度,以及内存占用的大小。另外此处采用的是ARGB_4444的模式,避免内存占用过大。
 

如何实现分页切换?

上面的步骤已经将PDF文件的每一页转换为Bitmap对象并添加到集合中,可以利用这个Bitmap数据集合结合 RecyclerView 来进行跳转,每一个 RecyclerView 的Item就代表一页,滑动到某一页其实就是定位到RecyclerView的某个position,并且还可以根据position来获取当前所处的页标。

PdfPageAdapter pageAdapter = new PdfPageAdapter(getContext(), pageList);
recyclerView.setAdapter(pageAdapter);

例如滑动到下一页可以通过 LayoutManager 去控制:

currentIndex = pageLayoutManager.getCurrentPosition();
if(currentIndex + 1 < pageLayoutManager.getItemCount()) {
    currentIndex++;
    layoutManager.scrollToPosition(currentIndex);
}

 

如何实现手势放大缩小?

由于每一页PDF都会转换成一个 Bitmap,可以考虑采用 ImageView 承载显示,重写其 onTouch 方法监听触摸事件:

@Override
public boolean onTouch(View v, MotionEvent event) {
    switch (event.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            //设置拖动模式
            mMode = MODE_DRAG;
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            reSetMatrix();
            break;
        case MotionEvent.ACTION_MOVE:
            if (mMode == MODE_ZOOM) {
                setZoomMatrix(event);
            } else if (mMode == MODE_DRAG) {
                setDragMatrix(event);
            }
            break;
        case MotionEvent.ACTION_POINTER_DOWN:
            if (mMode == MODE_UNABLE) return true;
            //设置双击缩放模式
            mMode = MODE_ZOOM;
            break;
        default:
            break;
    }

    return mGestureDetector.onTouchEvent(event);
}

先判断出当前手势是单点拖动,还是双点缩放,再分别结合ImageView的 setImageMatrix 方法进行处理,从而达到对图片放大缩小及拖动的效果:

public void setDragMatrix(MotionEvent event) {
    float dx = event.getX() - startPoint.x; // 得到x轴的移动距离
    float dy = event.getY() - startPoint.y; // 得到y轴的移动距离
    //避免和双击冲突,大于10f才算是拖动
    if (Math.sqrt(dx * dx + dy * dy) > 10f) {
        startPoint.set(event.getX(), event.getY());
        //在当前基础上移动
        mCurrentMatrix.set(getImageMatrix());
        float[] values = new float[9];
        mCurrentMatrix.getValues(values);
        dx = checkDxBound(values, dx);
        dy = checkDyBound(values, dy);
        //平移到新的坐标并设置新的矩阵
        mCurrentMatrix.postTranslate(dx, dy);
        setImageMatrix(mCurrentMatrix);
    }
}
private void setZoomMatrix(MotionEvent event) {
    //只有同时触屏两个点的时候才执行
    if (event.getPointerCount() < 2) return;
    float endDis = distance(event);// 结束距离
    if (endDis > 10f) {
        // 两个手指并拢在一起的时候像素大于10
        // 得到缩放倍数
        float scale = endDis / mStartDis;
        mStartDis = endDis;
        //在当前基础上伸缩
        mCurrentMatrix.set(getImageMatrix());
        float[] values = new float[9];
        mCurrentMatrix.getValues(values);

        scale = checkMaxScale(scale, values);
        setImageMatrix(mCurrentMatrix);
    }
}

 

结语

关于本库更多详细的用法可以查看README.md和源代码,目前支持在线或本地PDF文件预览,另外还支持侧边导航滑块,可快速滑动定位到任意一页。
由于PdfRenderer提供的支持有限,主要还是在于预览在线和本地PDF文件,但优点在于其体积小,后续会继续更新,后续会针对操作体验内存占用进一步优化,提供更多PDF预览方面的功能,欢迎issue和star~

 

欢迎关注 Android小Y 的简书,更多Android精选自定义View

『Android自定义View实战』实现一个小清新的弹出式圆环菜单
『Android自定义View实战』玩转PathMeasure之自定义支付结果动画
『Android自定义View实战』自定义弧形旋转菜单栏——卫星菜单
『Android自定义View实战』Android自定义带侧滑菜单的二维表格控件

GitHubGitHubZJY
简 书Android小Y
在GitHub上建了一个炫酷自定义View的集合ZJYWidget,主要是平时实现的一些实用的自定义View源码及demo,会长期维护,如有不足之处或建议还望指正,相互学习,相互进步~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容