Android 预览 PDF 技术方案总结

作者: Little丶Jerry | 来源:发表于2018-12-27 21:54 被阅读160次

    一、借助第三方应用

    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 主页查看使用方法:

    https://github.com/barteksc/AndroidPdfViewer

    • 优点:无需联网,使用方便,稳定,速度快,可监听文件打开的各项事件。

    • 缺点:APK 体积增大 16M,且只能打开本地 pdf 文件。因此如果是对 APK 大小有要求的用户不建议使用,如果是专业的阅读功能的 App 则可以使用。

    四、PDF.js

    PDF.js 官网

    PDF.js GitHub

    如果说 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

    PdfRenderer API

    这个类可以用来渲染 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 的视图复用问题。

    相关文章

      网友评论

        本文标题:Android 预览 PDF 技术方案总结

        本文链接:https://www.haomeiwen.com/subject/dxxdlqtx.html