AndroidPdfHelper——一个基于PdfRendere

作者: Android小Y | 来源:发表于2021-05-10 20:26 被阅读0次

    前言

    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,会长期维护,如有不足之处或建议还望指正,相互学习,相互进步~

    相关文章

      网友评论

        本文标题:AndroidPdfHelper——一个基于PdfRendere

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