前言
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,满足基本的页面切换、手势放大缩小、自定义预览样式、快速导航等功能,最终效果如下:
特性
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自定义带侧滑菜单的二维表格控件
GitHub:GitHubZJY
简 书:Android小Y
在GitHub上建了一个炫酷自定义View的集合ZJYWidget,主要是平时实现的一些实用的自定义View源码及demo,会长期维护,如有不足之处或建议还望指正,相互学习,相互进步~