在安卓项目中,加载PDF文件,是一个比较常见的需求。又分为两大类,
1.加载网络PDF
2.加载一个本地静态PDF。
查阅资料,纵观网上在安卓中打开PDF的各种方式,大致可以分为以下几类,
1.直接使用第三方软件打开(包括浏览器打开和第三方软件打开)
如果是在app内部加载PDF文件,虽然安卓原生API对于PDF的支持又不是太好,但是各种第三方专门的阅读或者办公软件支持的是很不错的,可以通过Intent配置data和type实现。
其中,在实际需求中又会分为加载本地PDF和网络PDF的情况。
使用浏览器打开PDF:(APP外部打开,适用于加载网络PDF)
1 2 3 4 5 6 7 8 9 10 |
public static void openPDFInBrowser(Context context, String url) { Uri uri = Uri.parse(url); Intent intent = new Intent(Intent.ACTION_VIEW, uri); intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); try { context.startActivity(intent); } catch (ActivityNotFoundException e) { Log.w("error", "Activity was not found for intent, " + intent.toString()); } } |
使用手机上已安装的可以打开PDF的第三方软件来打开PDF:(APP外部打开,适用于加载本地PDF)
(使用这种方式缺点是:手机上如果一个可以打开PDF的软件,那么就尴尬了~)
1 2 3 4 5 6 7 8 9 10 11 12 |
public static void openPDFInNative(Context context, String FILE_NAME) { File file = new File(context.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"); try { context.startActivity(intent); } catch (ActivityNotFoundException e) { Log.w("URLSpan", "Activity was not found for intent, " + intent.toString()); } } |
2.连接Google服务器解析
安卓的WebView不支持PDF解析,因此通过连接Google的一个服务器转换成功后返回给WebView显示。但是,但是,但是呢,大家都懂的,天朝和Google之间有一道高高的墙。方法还是贴出来,作为国际化APP的一种方案。
1 |
mWebView.loadUrl("http://docs.google.com/gview?embedded=true&url="+ pdfUrl); |
3.用第三方库加载
Github上有一个Java开源项目 https://github.com/barteksc/AndroidPdfViewer ,
这个库的大致原理,是内置了一个PDF解析器,以流的方式将网络PDF从网上Down下来,然后再以流的方式将其还原成PDF展示出来(具体细节没关注)。亲测中,这个库每次进入webview页面都会解析加载一遍PDF,如果PDF过大,费时无缓存不说,最致命的问题乃是,
APK包体积会瞬间增大15M左右,
具体原因不明,估计应该是内置PDF解析器的问题。于是,此方法被我抛弃了。
4.使用Moliza开源的Pdf.js
这个JS类库也是很强大的,配合原生的WebView使用,支持预览,缩放,翻页的功能,实现效果和WKWebView没差。同样也有体积问题,全部放到本地apk的话包大小差不多会增加5M左右。但相比上一种方式还是轻量一些:
http://www.jb51.net/article/136364.htm
5.使用安卓自带的PdfRenderer类加载
如果要求支持的功能不是很多,用安卓提供的PdfRender就可以满足需求了。PdfRender加载多页的话可以配合ViewPager或者RecyclerView使用。需要注意的是使用PdfRender需要先将PDF文件下载到本地,是线程不安全的,并且API>=21才能使用。因为这种方式是将PDF下载到本地,于是就产生了新问题:占内存。如果是静态的PDF文件不大还好,但是如果是频繁加载网络PDF的需求,那就头疼了,这种方式需要做好定时清理删除PDF的工作,否则,GG。
这里提供的示例是阿里巴巴Android开发手册,放到assets目录下.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
@TargetApi(Build.VERSION_CODES.LOLLIPOP) public class MainActivity extends AppCompatActivity { private ViewPager vpPdf; private LayoutInflater mInflater; private ParcelFileDescriptor mDescriptor; private PdfRenderer mRenderer; public static final String FILE_NAME = "alibaba.pdf"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mInflater = LayoutInflater.from(this); vpPdf =findViewById(R.id.vp_pdf); try { openRender(); } catch (IOException e) { e.printStackTrace(); } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void openRender() throws IOException { File file = new File(getExternalCacheDir(), FILE_NAME); if (!file.exists()) { //复制文件到本地存储 InputStream asset = getAssets().open(FILE_NAME); FileOutputStream fos = new FileOutputStream(file); byte[] buffer = new byte[1024]; int size; while ((size = asset.read(buffer)) != -1) { fos.write(buffer, 0, size); } asset.close(); fos.close(); } //初始化PdfRender mDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); if (mDescriptor != null) { mRenderer = new PdfRenderer(mDescriptor); } //初始化ViewPager的适配器并绑定 MyAdapter adapter = new MyAdapter(); vpPdf.setAdapter(adapter); } @Override protected void onDestroy() { //销毁页面的时候释放资源,习惯 try { closeRenderer(); } catch (IOException e) { e.printStackTrace(); } super.onDestroy(); } private void closeRenderer() throws IOException{ mRenderer.close(); mDescriptor.close(); } class MyAdapter extends PagerAdapter{ @Override public int getCount() { return mRenderer.getPageCount(); } @Override public boolean isViewFromObject(View view, Object object) { return view==object; } @Override public Object instantiateItem(ViewGroup container, int position) { View view = mInflater.inflate(R.layout.item_pdf, null); PhotoView pvPdf = view.findViewById(R.id.iv_pdf); pvPdf.enable(); if (getCount() <= position) { return view; } PdfRenderer.Page currentPage = mRenderer.openPage(position); Bitmap bitmap = Bitmap.createBitmap(1080, 1760, Bitmap.Config .ARGB_8888); currentPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); pvPdf.setImageBitmap(bitmap); //关闭当前Page对象 currentPage.close(); container.addView(view); return view; } @Override public void destroyItem(ViewGroup container, int position, Object object) { //销毁需要销毁的视图 container.removeView((View) object); } } } |
PdfRender的大致调用方式是:
- 初始化PdfRender,将PDF文件转换为ParcelFileDescriptor作为入参
- 通过getPageCount()获得Pdf的总页数
- 循环遍历,通过openPage()获得一个封装了当前页Pdf参数的内部类Page
- 创建一个Bitmap对象,,用上步获取的Page加载,相当于把当前Pdf页的内容渲染到了Bitmap上
- 对Bitmap进行你想要的操作,每次循环结束后关闭当前Page。
大部分都是常规操作,但要注意两点:
1. 每次循环创建的Page对象在使用完毕后必须调用close()。否则其内部的openPage()判断会抛异常;
2.Bitmap占用内存较大,如果用ViewPager加载一定要重写Adapter中的destroyItem(),及时的将需要销毁的视图从父容器中移除,否则快速滑动几页后会OOM。