一、借助第三方应用
Intent viewIntent = new Intent();
viewIntent.setAction(Intent.ACTION_VIEW);
viewIntent.setDataAndType(pdfUri, "application/pdf");
viewIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(viewIntent);
-
优点:借助第三方应用可以让开发者无需做额外的工作,而且让用户选择惯用的软件来打开也是一种传统且合理的方案。
-
缺点:如果用户没有安装任何可供预览文档的软件,则用户将无法打开该文档,严重影响用户体验。
二、Google 文档服务
webView.loadUrl("http://docs.google.com/gviewembedded=true&url=" + pdfUrl);
-
优点:使用 WebView 即可。
-
缺点:这种方式在国内网络环境下是无法访问的,需要翻墙。
三、Android PdfViewer 框架
该框架是目前最流行,最稳定,速度最快的框架,使用起来非常简单,可以访问 GitHub 主页查看使用方法:
-
优点:无需联网,使用方便,稳定,速度快,可监听文件打开的各项事件。
-
缺点:APK 体积增大 16M,且只能打开本地 pdf 文件。因此如果是对 APK 大小有要求的用户不建议使用,如果是专业的阅读功能的 App 则可以使用。
四、PDF.js
如果说 Google Docs 有高墙限制,那么 PDF.js 则是墙内人的福音。
方式一:使用 mozilla 部署在 github pages 上的 Viewer
pdfWebView.loadUrl("http://mozilla.github.io/pdf.js/web/viewer.html?file=" + pdfUri);
-
优点:无需翻墙,不增加 APK 体积,界面美观。
-
缺点:需要联网,不稳定,有时访问速度过慢,有跨域问题。移动端自带的页面有很多功能无法使用,影响用户体验。
方式二:下载 PDF.js 放到 assets 目录下
如果 pdf 文件不能跨域访问的话可以使用这种方式,先把文件下载到本地然后传入本地文件路径:
pdfWebView.loadUrl("file:///android_asset/pdfjs/web/viewer.html?file=" + pdfUri);
-
优点:无跨域问题。
-
缺点:导入 PDF.js 库的话 APK 会增大 5MB 左右。可以考虑把 PDF.js 部署到服务端或者使用 CDN 的方式。
方式三:自定义预览界面,PDF.js 使用 CDN 的方式导入
1. 新建一个预览的 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no"/>
<title>Document</title>
<style type="text/css">
canvas {
width: 100%;
height: 100%;
border: 1px solid black;
}
</style>
<script src="https://unpkg.com/pdfjs-dist@1.9.426/build/pdf.min.js"></script>
<script type="text/javascript" src="index.js"></script>
</head>
<body>
</body>
</html>
2. 实现预览 index.js
var url = location.search.substring(1);
PDFJS.cMapUrl = 'https://unpkg.com/pdfjs-dist@1.9.426/cmaps/';
PDFJS.cMapPacked = true;
var pdfDoc = null;
function createPage() {
var div = document.createElement("canvas");
document.body.appendChild(div);
return div;
}
function renderPage(num) {
pdfDoc.getPage(num).then(function (page) {
var viewport = page.getViewport(2.0);
var canvas = createPage();
var ctx = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
page.render({
canvasContext: ctx,
viewport: viewport
});
});
}
PDFJS.getDocument(url).then(function (pdf) {
pdfDoc = pdf;
for (var i = 1; i <= pdfDoc.numPages; i++) {
renderPage(i)
}
});
3. 示例代码
WebSettings webSettings = pdfWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setAllowFileAccess(true);
webSettings.setAllowFileAccessFromFileURLs(true);
webSettings.setAllowUniversalAccessFromFileURLs(true);
webSettings.setBuiltInZoomControls(true);
webSettings.setSupportZoom(true);
webSettings.setDisplayZoomControls(true);
pdfWebView.loadUrl("file:///android_asset/index.html?" + pdfUri);
-
优点:最终放到 assets 目录下的就只有 index.html 和 index.js 两个文件,可以避免全部导入带来的 APK 体积增大问题。另外如果对预览 UI 和交互有要求的话也可以很方便地通过修改 html 和 js 来实现。
-
缺点:加载较大的 PDF 文件速度较慢,甚至会直接崩溃(比如 40 MB)。
4.1 遇到的问题
- 在预览的时候遇到显示模糊
可以在 index.js 文件中设置 scale 系数来解决。
var viewport = page.getViewport(2.0);//设置为2.0
- pdf 内容显示不完整
可以在 index.js 文件中设置 cMapUrl 和 cMapPacked 来解决。
PDFJS.cMapUrl = 'https://unpkg.com/pdfjs-dist@1.9.426/cmaps/';
PDFJS.cMapPacked = true;
4.2 PDF.js 方案总结
-
优点:简单稳定,适用于预览小型的 PDF 文件,绝大多数场景可以使用。
-
缺点:加载速度方面有所欠缺,且加载较大的 PDF 文件会直接导致应用崩溃(比如 40 MB)。
2018-12-27 17:42:53.771 12498-12498/com.example.jerry.mypdfapplication A/chromium:
[FATAL:aw_browser_terminator.cc(79)] Render process (12623)'s crash
wasn't handled by all associated webviews, triggering application crash.
4.3 效果图
mozilla viewer 自定义 Viewer五、PDFRenderer
这个类可以用来渲染 PDF 文档,它是将 PDF 里的每一页渲染成一张 Bitmap,以此显示出来。该类是线程不安全的。PdfRenderer 中核心代码是用的是 native 方法,所以很难将 PdfRenderer 从 SDK 中抽取出来用。
5.1 简单示例代码
// 首先创建一个 PdfRenderer
PdfRenderer renderer = new PdfRenderer(getSeekableFileDescriptor());
// 渲染全部页面
final int pageCount = renderer.getPageCount();
for (int i = 0; i < pageCount; i++) {
Page page = renderer.openPage(i);
// 将一页的 PDF 渲染到一个 Bitmap 上,Bitmap 必须是 ARGB,不可以是 RGB
Bitmap mBitmap = Bitmap.createBitmap(currentPage.getWidth(), currentPage.getHeight(),
Bitmap.Config.ARGB_8888);
page.render(mBitmap, null, null, Page.RENDER_MODE_FOR_DISPLAY);
// 显示该 Bitmap
imageView.setImageBitmap(mBitmap);
// 注意:每次渲染完成之后都要关闭 page
page.close();
}
// 注意:渲染完成之后要关闭 renderer
renderer.close();
5.2 Demo 代码
public class PdfRendererActivity extends AppCompatActivity {
private static final String TAG = "MyPDF";
private static final int REQUEST_PDF_OPEN = 1;
private Button getButton;
private Button openButton;
private Button previousButton;
private Button nextButton;
private ImageView imageView;
private String pdfUri;
private PdfRenderer.Page currentPage;
private PdfRenderer pdfRenderer;
private ParcelFileDescriptor parcelFileDescriptor;
private int pageCount;
private int currentIndex;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pdfrenderer);
initView();
}
private void initView() {
getButton = findViewById(R.id.button_get);
openButton = findViewById(R.id.button_open);
imageView = findViewById(R.id.imageview_pdf_page);
previousButton = findViewById(R.id.button_previous);
nextButton = findViewById(R.id.button_next);
getButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
selectPdf();
}
});
openButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
openPdf();
}
});
previousButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (currentIndex == 0) {
return;
}
currentIndex--;
currentPage = pdfRenderer.openPage(currentIndex);
Bitmap bitmap = Bitmap.createBitmap(currentPage.getWidth(), currentPage.getHeight(),
Bitmap.Config.ARGB_8888);
// say we render for showing on the screen
currentPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
// do stuff with the bitmap
imageView.setImageBitmap(bitmap);
// close the page
currentPage.close();
}
});
nextButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (currentIndex == pageCount - 1) {
return;
}
currentIndex++;
currentPage = pdfRenderer.openPage(currentIndex);
Bitmap bitmap = Bitmap.createBitmap(currentPage.getWidth(), currentPage.getHeight(),
Bitmap.Config.ARGB_8888);
// say we render for showing on the screen
currentPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
// do stuff with the bitmap
imageView.setImageBitmap(bitmap);
// close the page
currentPage.close();
}
});
}
private void selectPdf() {
Intent getIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
getIntent.setType("*/*");
getIntent.addCategory(Intent.CATEGORY_OPENABLE);
PdfRendererActivity.this.startActivityForResult(getIntent, REQUEST_PDF_OPEN);
}
private void openPdf() {
try {
currentIndex = 0;
// create a new renderer
pdfRenderer = new PdfRenderer(getSeekableFileDescriptor());
// let us just render all pages
pageCount = pdfRenderer.getPageCount();
currentPage = pdfRenderer.openPage(currentIndex);
Bitmap bitmap = Bitmap.createBitmap(currentPage.getWidth(), currentPage.getHeight(),
Bitmap.Config.ARGB_8888);
// say we render for showing on the screen
currentPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
// do stuff with the bitmap
imageView.setImageBitmap(bitmap);
// close the page
currentPage.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private ParcelFileDescriptor getSeekableFileDescriptor() {
try {
parcelFileDescriptor = getApplicationContext().getContentResolver().openFileDescriptor(Uri.parse(pdfUri), "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return parcelFileDescriptor;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == REQUEST_PDF_OPEN && resultCode == RESULT_OK) {
pdfUri = data.getDataString();
Log.d(TAG, "onActivityResult: " + Uri.decode(pdfUri));
}
}
@Override
protected void onDestroy() {
super.onDestroy();
// close the renderer
if (null != pdfRenderer) {
pdfRenderer.close();
}
if (null != parcelFileDescriptor) {
try {
parcelFileDescriptor.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
5.3 效果图
第一页 第二页-
优点:原生 API,无需联网,不增加 APK 体积,不借用第三方因此无安全性的风险。权衡利弊,个人认为这是目前最好的技术方案。
-
缺点:仅 Android 5.0 以上可以使用,但对于 Android 版本已经更新到 9.0 的时代,这个限制是可以接受的。因为它每次只能渲染一个页面,所以想做出上下滚动浏览的效果的话需要自己手动实现,可以使用
RecyclerView + 预加载 + 软引用
的方式。要注意RecyclerView
的视图复用问题。
网友评论