当前位置: 移动技术网 > IT编程>移动开发>Android > Android原生PDF功能实现

Android原生PDF功能实现

2019年10月12日  | 移动技术网IT编程  | 我要评论

海贼王米娜2无敌版,杨树鹏多高,淘宝信誉平台

pdf demo 效果

1、背景

近期,公司希望实现安卓原生端的pdf功能,要求:高效、实用。

经过两天的调研、编码,实现了一个简单demo,如上图所示。
关于安卓原生端的pdf功能实现,技术点还是很多的,为了咱们安卓开发的同学少走弯路,通过此文章,简单讲解下demo的实现原理和主要技术点,并附上源码。

2、安卓pdf现状

目前,pdf功能仍然是安卓的一个短板,不像ios,有官方强大的pdf kit可供集成。
不过,安卓也有一些主流的方案,不过各有优缺点:

1、google doc 在线阅读,基于webview,国内需翻墙访问(不可行)
2、跳转设备中默认pdf app打开,前提需要手机安装了pdf 软件(可按需选择)
3、内置 android-pdfview,基于原生native, apk增加约15~20m(可行,不过安装包有点大)
4、内置 mupdf,基于原生native, 集成有点麻烦,增加约9m(可行,不过安装包稍有点大)
5、内置 pdf.js,功能丰富,apk增加5m(基于webview,性能低,js实现,功能定制复杂)
6、使用x5内核,需要客户端完全使用x5内核(基于webview,性能低,不能定制功能)

查阅官方资料,这些方案虽然能实现基本的pdf阅读功能,但是多数方案,集成过程较复杂,且性能低下,容易内存溢出造成app闪退。

3、方案选择

经过对各方案的反复比对,本次实现pdf demo,决定使用:android-pdfview。
原因:

1、android-pdfview基于pdfium实现(pdfium是谷歌 + 福昕软件的pdf开源项目);
2、android-pdfview github仍在维护;
3、android-pdfview github获得的星星较多;
4、客户端集成较方便;

问题分析:
运行android-pdfview官方demo,问题也很多:

1、仅实现了pdf滑动阅读、手势伸缩的功能;
2、缺少pdf目录树、缩略图等功能;
3、安装包过大;
4、ui不美观;
5、内存问题;
6、其他...

不过,不用担心,解决了这些问题不就没有问题了嘛,哈、哈、哈(笑声有点勉强哈)

下面,咱们开始实现demo吧。

4、demo设计

4.1、工程结构

在设计之前,应明确demo的实现目标:

1、android-pdfview已实现了pdfview,可用于阅读pdf文件,手势伸缩pdf页面、跳转pdf页面,
   那么,咱们基于android-pdfview扩展功能即可,功能包括:目录树、缩略图等;

2、扩展的功能应逻辑解耦,不能影响android-pdfview代码的可替换性
  (即:如果android-pdfview有新版本,直接替换即可)

3、客户端应很方便集成
  (如:客户端仅需要传递过来pdf文件,所有的加载、操作、内存管理均无需关心)

demo工程如何设计:
下载android-pdfview最新源码,可以看到共包含两个moudle:

android-pdf-viewer(最新源码)
sample (示例app)

如果,我们要接管封装pdf的所有功能,让sample只传递pdf文件即可,且不影响将来替换android-pdf-viewer的源码,那么我们创建一个modle即可,如下图:

sample (依赖pdfui)
pdfui (依赖android-pdf-viewer)
android-pdf-viewer

4.2、pdf功能设计

为了便于用户阅读pdf,应该包含以下功能:
1、pdf阅读(包含:手指滑动pdf页面、手势伸缩页面内容、跳转pdf指定页面)
2、pdf目录导航功能(包含:目录展示、目录节点折叠、展开、点击跳转pdf页面)
3、pdf缩略图导航功能(包含:缩略图展示、手指滑动、图片缓存管理、点击跳转pdf页面)

pdf功能代码结构

5、编码之前,先解决安装包过大的问题

反编译demo的安装包,可以看到,安装包中默认集成了各cpu平台对应的so库文件,安装包过大的原因也就在这儿。其实正常项目开发中,对于各cpu平台对应的so库的保留或舍弃,主要考虑cpu平台兼容性、设备覆盖率。

通常情况下,仅保留armeabi-v7a可以兼容市面上绝大多数安卓设备,那么,如何编译时删除其他的so呢?

可在android gradle中配置,如下:

android{
......
 splits {
        abi {
            enable true
            reset()
            include 'armeabi-v7a' //如果想包含其他cpu平台使用的so,修改这里即可
        }
    }
}

重新编译,生成的安装包,仅剩5m左右了。

注意:如果项目中还有其他so库,要根据项目实际需求,认真思考如何取舍了。

6、实现pdf阅读功能

很简单,因为android-pdf-viewer源码中已经实现了该功能,我们写一份精简版的吧。

6.1、功能点:

1、可加载assets中的pdf文件
2、可加载uri类型的pdf文件(如果是线上的pdf文件,可通过网络库先下载到本地,取其uri,本次demo就不写网络下载了)
3、pdf的基本展示功能(使用android-pdf-viewer的控件实现:pdfview)
4、可跳转至目录页面(目录数据可通过intent直接传递过去)
5、可跳转至预览页面(pdf文件信息可通过intent直接传递过去)
6、根据目录页面、预览页面带回的页码,跳转至指定的pdf页面

pdf阅读功能效果图

6.2、代码实现

重点内容:

1、pdfview控件的使用;(比较简单,详见代码)
2、如何从pdf文件中获得目录信息;(如何获得目录信息、什么时机获取,详见代码)

pdf阅读页面的代码:pdfactivity

/**
 * ui页面:pdf阅读
 * <p>
 * 主要功能:
 * 1、接收传递过来的pdf文件(包括assets中的文件名、文件uri)
 * 2、显示pdf文件
 * 3、接收目录页面、预览页面返回的pdf页码,跳转到指定的页面
 * <p>
 * 作者:齐行超
 * 日期:2019.08.07
 */
public class pdfactivity extends appcompatactivity implements
        onpagechangelistener,
        onloadcompletelistener,
        onpageerrorlistener {
    //pdf控件
    pdfview pdfview;
    //按钮控件:返回、目录、缩略图
    button btn_back, btn_catalogue, btn_preview;
    //页码
    integer pagenumber = 0;
    //pdf目录集合
    list<treenodedata> catelogues;

    //pdf文件名(限:assets里的文件)
    string assetsfilename;
    //pdf文件uri
    uri uri;


    @override
    protected void oncreate(bundle savedinstancestate) {
        super.oncreate(savedinstancestate);
        uiutils.initwindowstyle(getwindow(), getsupportactionbar());//设置沉浸式
        setcontentview(r.layout.activity_pdf);

        initview();//初始化view
        setevent();//设置事件
        loadpdf();//加载pdf文件
    }

    /**
     * 初始化view
     */
    private void initview() {
        pdfview = findviewbyid(r.id.pdfview);
        btn_back = findviewbyid(r.id.btn_back);
        btn_catalogue = findviewbyid(r.id.btn_catalogue);
        btn_preview = findviewbyid(r.id.btn_preview);
    }

    /**
     * 设置事件
     */
    private void setevent() {
        //返回
        btn_back.setonclicklistener(new view.onclicklistener() {
            @override
            public void onclick(view v) {
                pdfactivity.this.finish();
            }
        });
        //跳转目录页面
        btn_catalogue.setonclicklistener(new view.onclicklistener() {
            @override
            public void onclick(view v) {
                intent intent = new intent(pdfactivity.this, pdfcatelogueactivity.class);
                intent.putextra("catelogues", (serializable) catelogues);
                pdfactivity.this.startactivityforresult(intent, 200);
            }
        });
        //跳转缩略图页面
        btn_preview.setonclicklistener(new view.onclicklistener() {
            @override
            public void onclick(view v) {
                intent intent = new intent(pdfactivity.this, pdfpreviewactivity.class);
                intent.putextra("assetspdf", assetsfilename);
                intent.setdata(uri);
                pdfactivity.this.startactivityforresult(intent, 201);
            }
        });
    }

    /**
     * 加载pdf文件
     */
    private void loadpdf() {
        intent intent = getintent();
        if (intent != null) {
            assetsfilename = intent.getstringextra("assetspdf");
            if (assetsfilename != null) {
                displayfromassets(assetsfilename);
            } else {
                uri = intent.getdata();
                if (uri != null) {
                    displayfromuri(uri);
                }
            }
        }
    }

    /**
     * 基于assets显示 pdf 文件
     *
     * @param filename 文件名称
     */
    private void displayfromassets(string filename) {
        pdfview.fromasset(filename)
                .defaultpage(pagenumber)
                .onpagechange(this)
                .enableannotationrendering(true)
                .onload(this)
                .scrollhandle(new defaultscrollhandle(this))
                .spacing(10) // 单位 dp
                .onpageerror(this)
                .pagefitpolicy(fitpolicy.both)
                .load();
    }

    /**
     * 基于uri显示 pdf 文件
     *
     * @param uri 文件路径
     */
    private void displayfromuri(uri uri) {
        pdfview.fromuri(uri)
                .defaultpage(pagenumber)
                .onpagechange(this)
                .enableannotationrendering(true)
                .onload(this)
                .scrollhandle(new defaultscrollhandle(this))
                .spacing(10) // 单位 dp
                .onpageerror(this)
                .load();
    }

    /**
     * 当成功加载pdf:
     * 1、可获取pdf的目录信息
     *
     * @param nbpages the number of pages in this pdf file
     */
    @override
    public void loadcomplete(int nbpages) {
        //获得文档书签信息
        list<pdfdocument.bookmark> bookmarks = pdfview.gettableofcontents();
        if (catelogues != null) {
            catelogues.clear();
        } else {
            catelogues = new arraylist<>();
        }
        //将bookmark转为目录数据集合
        bookmarktocatelogues(catelogues, bookmarks, 1);
    }

    /**
     * 将bookmark转为目录数据集合(递归)
     *
     * @param catelogues 目录数据集合
     * @param bookmarks  书签数据
     * @param level      目录树级别(用于控制树节点位置偏移)
     */
    private void bookmarktocatelogues(list<treenodedata> catelogues, list<pdfdocument.bookmark> bookmarks, int level) {
        for (pdfdocument.bookmark bookmark : bookmarks) {
            treenodedata nodedata = new treenodedata();
            nodedata.setname(bookmark.gettitle());
            nodedata.setpagenum((int) bookmark.getpageidx());
            nodedata.settreelevel(level);
            nodedata.setexpanded(false);
            catelogues.add(nodedata);
            if (bookmark.getchildren() != null && bookmark.getchildren().size() > 0) {
                list<treenodedata> treenodedatas = new arraylist<>();
                nodedata.setsubset(treenodedatas);
                bookmarktocatelogues(treenodedatas, bookmark.getchildren(), level + 1);
            }
        }
    }

    @override
    public void onpagechanged(int page, int pagecount) {
        pagenumber = page;
    }

    @override
    public void onpageerror(int page, throwable t) {
    }

    /**
     * 从缩略图、目录页面带回页码,跳转到指定pdf页面
     *
     * @param requestcode
     * @param resultcode
     * @param data
     */
    @override
    protected void onactivityresult(int requestcode, int resultcode, intent data) {
        super.onactivityresult(requestcode, resultcode, data);
        if (resultcode == result_ok) {
            int pagenum = data.getintextra("pagenum", 0);
            if (pagenum > 0) {
                pdfview.jumpto(pagenum);
            }
        }
    }

    @override
    protected void ondestroy() {
        super.ondestroy();
        //是否内存
        if (pdfview != null) {
            pdfview.recycle();
        }
    }
}

pdf阅读页面的布局文件:activity_pdf.xml

<?xml version="1.0" encoding="utf-8"?>
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <relativelayout
        android:id="@+id/rl_top"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:layout_alignparenttop="true"
        android:background="#03a9f5">

        <button
            android:id="@+id/btn_back"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:background="@drawable/shape_button"
            android:text="返回"
            android:textcolor="#ffffff"
            android:textsize="18sp"
            android:layout_alignparentbottom="true"
            android:layout_marginbottom="10dp"
            android:layout_marginleft="10dp"/>

        <button
            android:id="@+id/btn_catalogue"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:background="@drawable/shape_button"
            android:text="目录"
            android:textcolor="#ffffff"
            android:textsize="18sp"
            android:layout_alignparentright="true"
            android:layout_alignparentbottom="true"
            android:layout_marginbottom="10dp"
            android:layout_marginright="10dp"/>

        <button
            android:id="@+id/btn_preview"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:background="@drawable/shape_button"
            android:text="预览"
            android:textcolor="#ffffff"
            android:textsize="18sp"
            android:layout_toleftof="@+id/btn_catalogue"
            android:layout_alignparentbottom="true"
            android:layout_marginbottom="10dp"
            android:layout_marginright="10dp"/>
    </relativelayout>

    <com.github.barteksc.pdfviewer.pdfview
        android:id="@+id/pdfview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/rl_top"/>

</relativelayout>

7、pdf目录树的实现

目录树的数据(目录名称、页码...),已在上个页面获取了,所以此页面只需考虑目录树控件的实现。

注意:之所以没在这个页面单独获取目录树的数据,主要考虑到android-pdfview、pdfium内存占用太大了,不想再次创建pdf的相关对象。

7.1、pdf目录树效果图

7.2、树形控件如何实现?

安卓默认没有树形控件,不过我们可以使用recyclerview或listview实现。
如上图所示:

列表每一行为一条目录数据,主要包括:名称、页码;
如果有子目录,则出现箭头图片,该项可折叠、展开,箭头方向随之改变;
子目录的名称文本随目录树级别递增向右偏移;

当前demo实现方式为recyclerview,应该如何实现上面的效果?
可在adapter中处理页面效果、事件效果:
1、列表项内容展示

1、使用垂直线性布局管理器;
2、每个item包含:箭头图片(如果有子目录,则显示)、命令名称文本、页码文本;

2、折叠效果

1、控制adapter数据集合的内容即可,如果某节点折叠了,就把对应的子目录数据删除即可,
反之,加上,再notifydatasetchanged通知数据源改变;
2、除此之外,还需有一个状态来标记当前节点是展开还是折叠,用于控制箭头图片方向的显示;

3、目录文本向右偏移效果

可通过目录树层级 * 固定左侧间隔(如: 20dp),然后为目录的textview控件设置偏移即可;

目录树层级树如何获取? 可选方案:
1、递归集合自动获取(需要遍历,效率低一点,如果是可编辑的目录结构,建议选择)
2、创建数据的时候,直接写死(因当前demo的pdf目录结构不会被编辑,所以直接选择这个方案吧)

7.3、代码实现:

树形控件的数据对象treenodedata:

/**
 * 树形控件数据类(会用于页面间传输,所以需实现serializable 或 parcelable)
 * 作者:齐行超
 * 日期:2019.08.07
 */
public class treenodedata implements serializable {
    //名称
    private string name;
    //页码
    private int pagenum;
    //是否已展开(用于控制树形节点图片显示,即箭头朝向图片)
    private boolean isexpanded;
    //展示级别(1级、2级...,用于控制树形节点缩进位置)
    private int treelevel;
    //子集(用于加载子节点,也用于判断是否显示箭头图片,如集合不为空,则显示)
    private list<treenodedata> subset;

    public string getname() {
        return name;
    }

    public void setname(string name) {
        this.name = name;
    }

    public int getpagenum() {
        return pagenum;
    }

    public void setpagenum(int pagenum) {
        this.pagenum = pagenum;
    }

    public boolean isexpanded() {
        return isexpanded;
    }

    public void setexpanded(boolean expanded) {
        isexpanded = expanded;
    }

    public int gettreelevel() {
        return treelevel;
    }

    public void settreelevel(int treelevel) {
        this.treelevel = treelevel;
    }

    public list<treenodedata> getsubset() {
        return subset;
    }

    public void setsubset(list<treenodedata> subset) {
        this.subset = subset;
    }
}

树形控件适配器 : treeadapter

/**
 * 树形控件适配器
 * 作者:齐行超
 * 日期:2019.08.07
 */
public class treeadapter extends recyclerview.adapter<treeadapter.treenodeviewholder> {
    //上下文
    private context context;
    //数据
    public list<treenodedata> data;
    //展示数据(由层级结构改为平面结构)
    public list<treenodedata> displaydata;
    //treelevel间隔(dp)
    private int maginleft;
    //委托对象
    private treeevent delegate;

    /**
     * 构造函数
     *
     * @param context 上下文
     * @param data    数据
     */
    public treeadapter(context context, list<treenodedata> data) {
        this.context = context;
        this.data = data;
        maginleft = uiutils.dip2px(context, 20);
        displaydata = new arraylist<>();

        //数据转为展示数据
        datatodiaplaydata(data);
    }

    /**
     * 数据转为展示数据
     *
     * @param data 数据
     */
    private void datatodiaplaydata(list<treenodedata> data) {
        for (treenodedata nodedata : data) {
            displaydata.add(nodedata);
            if (nodedata.isexpanded() && nodedata.getsubset() != null) {
                datatodiaplaydata(nodedata.getsubset());
            }
        }
    }

    /**
     * 数据集合转为可显示的集合
     */
    private void redatatodiaplaydata() {
        if (this.data == null || this.data.size() == 0) {
            return;
        }
        if(displaydata == null){
            displaydata = new arraylist<>();
        }else{
            displaydata.clear();
        }
        datatodiaplaydata(this.data);
        notifydatasetchanged();
    }

    @override
    public treenodeviewholder oncreateviewholder(viewgroup parent, int viewtype) {
        view view = layoutinflater.from(context).inflate(r.layout.tree_item, null);
        return new treenodeviewholder(view);
    }

    @override
    public void onbindviewholder(treenodeviewholder holder, int position) {
        final treenodedata data = displaydata.get(position);
        //设置图片
        if (data.getsubset() != null) {
            holder.img.setvisibility(view.visible);
            if (data.isexpanded()) {
                holder.img.setimageresource(r.drawable.arrow_h);
            } else {
                holder.img.setimageresource(r.drawable.arrow_v);
            }
        } else {
            holder.img.setvisibility(view.invisible);
        }
        //设置图片偏移位置
        relativelayout.layoutparams params = (relativelayout.layoutparams) holder.img.getlayoutparams();
        int ratio = data.gettreelevel() <= 0? 0 : data.gettreelevel()-1;
        params.setmargins(maginleft * ratio, 0, 0, 0);
        holder.img.setlayoutparams(params);

        //显示文本
        holder.title.settext(data.getname());
        holder.pagenum.settext(string.valueof(data.getpagenum()));

        //图片点击事件
        holder.img.setonclicklistener(new view.onclicklistener() {
            @override
            public void onclick(view v) {
                //控制树节点展开、折叠
                data.setexpanded(!data.isexpanded());
                //刷新数据源
                redatatodiaplaydata();
            }
        });
        holder.itemview.setonclicklistener(new view.onclicklistener() {
            @override
            public void onclick(view v) {
                //回调结果
                if(delegate!=null){
                    delegate.onselecttreenode(data);
                }
            }
        });
    }

    @override
    public int getitemcount() {
        return displaydata.size();
    }

    /**
     * 定义recyclerview的viewholder对象
     */
    class treenodeviewholder extends recyclerview.viewholder {
        imageview img;
        textview title;
        textview pagenum;

        public treenodeviewholder(view view) {
            super(view);
            img = view.findviewbyid(r.id.iv_arrow);
            title = view.findviewbyid(r.id.tv_title);
            pagenum = view.findviewbyid(r.id.tv_pagenum);
        }
    }

    /**
     * 接口:tree事件
     */
    public interface treeevent{
        /**
         * 当选择了某tree节点
         * @param data tree节点数据
         */
        void onselecttreenode(treenodedata data);
    }

    /**
     * 设置tree的事件
     * @param treeevent tree的事件对象
     */
    public void settreeevent(treeevent treeevent){
        this.delegate = treeevent;
    }
}

pdf目录树页面:pdfcatelogueactivity

/**
 * ui页面:pdf目录
 * <p>
 * 1、用于显示pdf目录信息
 * 2、点击tree item,带回pdf页码到前一个页面
 * <p>
 * 作者:齐行超
 * 日期:2019.08.07
 */
public class pdfcatelogueactivity extends appcompatactivity implements treeadapter.treeevent {

    recyclerview recyclerview;
    button btn_back;

    @override
    protected void oncreate(bundle savedinstancestate) {
        super.oncreate(savedinstancestate);
        uiutils.initwindowstyle(getwindow(), getsupportactionbar());
        setcontentview(r.layout.activity_catelogue);

        initview();//初始化控件
        setevent();//设置事件
        loaddata();//加载数据
    }

    /**
     * 初始化控件
     */
    private void initview() {
        btn_back = findviewbyid(r.id.btn_back);
        recyclerview = findviewbyid(r.id.rv_tree);
    }

    /**
     * 设置事件
     */
    private void setevent() {
        btn_back.setonclicklistener(new view.onclicklistener() {
            @override
            public void onclick(view v) {
                pdfcatelogueactivity.this.finish();
            }
        });
    }

    /**
     * 加载数据
     */
    private void loaddata() {
        //从intent中获得传递的数据
        intent intent = getintent();
        list<treenodedata> catelogues = (list<treenodedata>) intent.getserializableextra("catelogues");

        //使用recyclerview加载数据
        linearlayoutmanager llm = new linearlayoutmanager(this);
        llm.setorientation(linearlayoutmanager.vertical);
        recyclerview.setlayoutmanager(llm);
        treeadapter adapter = new treeadapter(this, catelogues);
        adapter.settreeevent(this);
        recyclerview.setadapter(adapter);
    }


    /**
     * 点击tree item,带回pdf页码到前一个页面
     *
     * @param data tree节点数据
     */
    @override
    public void onselecttreenode(treenodedata data) {
        intent intent = new intent();
        intent.putextra("pagenum", data.getpagenum());
        setresult(activity.result_ok, intent);
        finish();
    }
}

pdf目录树的布局文件:activity_catelogue.xml

<?xml version="1.0" encoding="utf-8"?>
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <relativelayout
        android:id="@+id/rl_top"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:layout_alignparenttop="true"
        android:background="#03a9f5">

        <button
            android:id="@+id/btn_back"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:layout_alignparentbottom="true"
            android:layout_marginleft="10dp"
            android:layout_marginbottom="10dp"
            android:background="@drawable/shape_button"
            android:text="返回"
            android:textcolor="#ffffff"
            android:textsize="18sp" />

        <textview
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignparentbottom="true"
            android:layout_centerhorizontal="true"
            android:layout_marginbottom="15dp"
            android:text="目录列表"
            android:textcolor="#ffffff"
            android:textsize="18sp" />
    </relativelayout>

    <android.support.v7.widget.recyclerview
        android:id="@+id/rv_tree"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/rl_top" />

</relativelayout>

8、pdf预览缩略图

这个功能算是本demo中最为复杂的一个了:

如何将pdf某页面的内容转成图片?(默认是无法从pdfview中获得页面图片的)
如何减少图片内存的占用?(用户可能快速滑动列表,实时读取、显示多张图片)
如何优化pdf预览缩略图列表的滑动体验?(图片的获取需要一定时间)
如何合理的及时释放内存占用?

8.1、pdf预览缩略图列表的效果图

8.2、功能分析

1、如何将pdf某页面的内容转成图片?

查看android-pdfview的源码,无法通过pdfview控件获得某页面的图片,所以只能分析pdfium sdk的api了,如下图:

pdfium的renderpagebitmap方法可以将页面渲染成图片,不过需要传递一系列参数,而且要小心outofmemoryerror。

那么,我们需要在代码中获取或者创建pdfiumcore对象,调用该方法,传递pdfdocument等参数,当bitmap使用完后,应及时释放掉。

2、如何减少内存的占用?

内存主要包括:
1、pdfium sdk加载pdf文件产生的内存(我们无法优化)
2、android-pdfview产生的内存(如果有需要,可改其源码)
3、我们将pdf页面转为缩略图,而产生的内存(必须优化,否则,容易oom)

3.1、当pdfiumcore、pdfdocument不再使用时,应及时关闭;
3.2、当缩略图不再使用时,应及时释放;
3.3、可使用lrucache临时缓存缩略图,防止重复调用renderpagebitmap获取图片;
3.4、lrucache应合理管控,当预览页面关闭时,必须清空缓存,以释放内存;
3.5、创建图片时,应使用rgb_565,能节约内存开销(一个像素点,占2字节)
3.6、创建图片时,应尽可能小的指定图片的宽高,能看清就行(图片占用的内存 = 宽 * 高 * 一个像素点占的字节数)

3、如何优化pdf预览缩略图列表的滑动体验?

查看pdfium源码,调用renderpagebitmap方法之前,还必须确保对应的页面已被打开,即调用了openpage方法。然而,这两个方法都需要一定时间才能执行完成的。

那么,如果我们直接在主线程中让每个recylervew的item分别调用renderpagebitmap方法,滑动列表时,会感觉特别卡,所以该方法只能放在子线程中调用了。

那么问题又来了,那么多子线程应该如何管控?

1、考虑cpu的占用,应使用线程池控制子线程并发、阻塞;
2、考虑到用户滑动速度,有可能某线程正执行或者阻塞着呢,页面已经滑过去了,那么,即使该线程加载出来了图片,也无法显示到列表中。所以对于recyclerview已不可见的item项对应的线程,应及时取消,防止做无用功,也节省了内存和cpu开销。

8.3、功能实现

预览缩略图工具类:previewutils

/**
 * 预览缩略图工具类
 *
 * 1、pdf页面转为缩略图
 * 2、图片缓存管理(仅保存到内存,可使用lrucache,注意空间大小控制)
 * 3、多线程管理(线程并发、阻塞、future任务取消)
 *
 * 作者:齐行超
 * 日期:2019.08.08
 */
public class previewutils {
    //图片缓存管理
    private imagecache imagecache;
    //单例
    private static previewutils instance;
    //线程池
    executorservice executorservice;
    //线程任务集合(可用于取消任务)
    hashmap<string, future> tasks;

    /**
     * 单例(仅主线程调用,无需做成线程安全的)
     *
     * @return previewutils实例对象
     */
    public static previewutils getinstance() {
        if (instance == null) {
            instance = new previewutils();
        }
        return instance;
    }

    /**
     * 默认构造函数
     */
    private previewutils() {
        //初始化图片缓存管理对象
        imagecache = new imagecache();
        //创建并发线程池(建议最大并发数大于1屏grid item的数量)
        executorservice = executors.newfixedthreadpool(20);
        //创建线程任务集合,用于取消线程执行
        tasks = new hashmap<>();
    }

    /**
     * 从pdf文件中加载图片
     *
     * @param context     上下文
     * @param imageview   图片控件
     * @param pdfiumcore  pdf核心对象
     * @param pdfdocument pdf文档对象
     * @param pdfname     pdf文件名称
     * @param pagenum     pdf页码
     */
    public void loadbitmapfrompdf(final context context,
                                  final imageview imageview,
                                  final pdfiumcore pdfiumcore,
                                  final pdfdocument pdfdocument,
                                  final string pdfname,
                                  final int pagenum) {
        //判断参数合法性
        if (imageview == null || pdfiumcore == null || pdfdocument == null || pagenum < 0) {
            return;
        }

        try {
            //缓存key
            final string keypage = pdfname + pagenum;

            //为图片控件设置标记
            imageview.settag(keypage);

            log.i("previewutils", "加载pdf缩略图:" + keypage);

            //获得imageview的尺寸(注意:如果使用正常控件尺寸,太占内存了)
            /*int w = imageview.getmeasuredwidth();
            int h = imageview.getmeasuredheight();
            final int reqwidth = w == 0 ? uiutils.dip2px(context,100) : w;
            final int reqheight = h == 0 ? uiutils.dip2px(context,150) : h;*/

            //内存大小= 图片宽度 * 图片高度 * 一个像素占的字节数(rgb_565 所占字节:2)
            //注意:如果使用正常控件尺寸,太占内存了,所以此处指定四缩略图看着会模糊一点
            final int reqwidth = 100;
            final int reqheight = 150;

            //从缓存中取图片
            bitmap bitmap = imagecache.getbitmapfromlrucache(keypage);
            if (bitmap != null) {
                imageview.setimagebitmap(bitmap);
                return;
            }

            //使用线程池管理子线程
            future future = executorservice.submit(new runnable() {
                @override
                public void run() {
                    //打开页面(调用renderpagebitmap方法之前,必须确保页面已open,重要)
                    pdfiumcore.openpage(pdfdocument, pagenum);

                    //调用native方法,将pdf页面渲染成图片
                    final bitmap bm = bitmap.createbitmap(reqwidth, reqheight, bitmap.config.rgb_565);
                    pdfiumcore.renderpagebitmap(pdfdocument, bm, pagenum, 0, 0, reqwidth, reqheight);

                    //切回主线程,设置图片
                    if (bm != null) {
                        //将图片加入缓存
                        imagecache.addbitmaptolrucache(keypage, bm);

                        //切回主线程加载图片
                        new handler(looper.getmainlooper()).post(new runnable() {
                            @override
                            public void run() {
                                if (imageview.gettag().tostring().equals(keypage)) {
                                    imageview.setimagebitmap(bm);
                                    log.i("previewutils", "加载pdf缩略图:" + keypage + "......已设置!!");
                                }
                            }
                        });
                    }
                }
            });

            //将任务添加到集合
            tasks.put(keypage, future);
        } catch (exception ex) {
            ex.printstacktrace();
        }
    }

    /**
     * 取消从pdf文件中加载图片的任务
     *
     * @param keypage 页码
     */
    public void cancelloadbitmapfrompdf(string keypage) {
        if (keypage == null || !tasks.containskey(keypage)) {
            return;
        }
        try {
            log.i("previewutils", "取消加载pdf缩略图:" + keypage);
            future future = tasks.get(keypage);
            if (future != null) {
                future.cancel(true);
                log.i("previewutils", "取消加载pdf缩略图:" + keypage + "......已取消!!");
            }
        } catch (exception ex) {
            ex.printstacktrace();
        }
    }

    /**
     * 获得图片缓存对象
     * @return 图片缓存
     */
    public imagecache getimagecache(){
        return imagecache;
    }

    /**
     * 图片缓存管理
     */
   public class imagecache {
        //图片缓存
        private lrucache<string, bitmap> lrucache;

        //构造函数
        public imagecache() {
            //初始化 lrucache
            //int maxmemory = (int) runtime.getruntime().maxmemory();
            //int cachesize = maxmemory/8;
            int cachesize = 1024 * 1024 * 30;//暂时设定30m
            lrucache = new lrucache<string, bitmap>(cachesize) {
                @override
                protected int sizeof(string key, bitmap value) {
                    return value.getrowbytes() * value.getheight();
                }
            };
        }

        /**
         * 从缓存中取图片
         * @param key 键
         * @return 图片
         */
        public synchronized bitmap getbitmapfromlrucache(string key) {
            if(lrucache!= null) {
                return lrucache.get(key);
            }
            return null;
        }

        /**
         * 向缓存中加图片
         * @param key 键
         * @param bitmap 图片
         */
        public synchronized void addbitmaptolrucache(string key, bitmap bitmap) {
            if (getbitmapfromlrucache(key) == null) {
                if (lrucache!= null && bitmap != null)
                    lrucache.put(key, bitmap);
            }
        }

        /**
         * 清空缓存
         */
        public void clearcache(){
            if(lrucache!= null){
                lrucache.evictall();
            }
        }
    }
}

grid列表适配器: gridadapter

/**
 * grid列表适配器
 * 作者:齐行超
 * 日期:2019.08.08
 */
public class gridadapter extends recyclerview.adapter<gridadapter.gridviewholder> {

    context context;
    pdfiumcore pdfiumcore;
    pdfdocument pdfdocument;
    string pdfname;
    int totalpagenum;


    public gridadapter(context context, pdfiumcore pdfiumcore, pdfdocument pdfdocument, string pdfname, int totalpagenum) {
        this.context = context;
        this.pdfiumcore = pdfiumcore;
        this.pdfdocument = pdfdocument;
        this.pdfname = pdfname;
        this.totalpagenum = totalpagenum;
    }

    @override
    public gridviewholder oncreateviewholder(viewgroup parent, int viewtype) {
        view view = layoutinflater.from(context).inflate(r.layout.grid_item, null);
        return new gridviewholder(view);
    }

    @override
    public void onbindviewholder(gridviewholder holder, int position) {
        //设置pdf图片
        final int pagenum = position;
        previewutils.getinstance().loadbitmapfrompdf(context, holder.iv_page, pdfiumcore, pdfdocument, pdfname, pagenum);
        //设置pdf页码
        holder.tv_pagenum.settext(string.valueof(position));
        //设置grid事件
        holder.iv_page.setonclicklistener(new view.onclicklistener() {
            @override
            public void onclick(view v) {
                if(delegate!=null){
                    delegate.ongriditemclick(pagenum);
                }
            }
        });
        return;
    }

    @override
    public void onviewdetachedfromwindow(gridviewholder holder) {
        super.onviewdetachedfromwindow(holder);
        try {
            //item不可见时,取消任务
            if(holder.iv_page!=null){
                previewutils.getinstance().cancelloadbitmapfrompdf(holder.iv_page.gettag().tostring());
            }

            //item不可见时,释放bitmap  (注意:本demo使用了lrucache缓存来管理图片,此处可注释掉)
            /*drawable drawable = holder.iv_page.getdrawable();
            if (drawable != null) {
                bitmap bitmap = ((bitmapdrawable) drawable).getbitmap();
                if (bitmap != null && !bitmap.isrecycled()) {
                    bitmap.recycle();
                    bitmap = null;
                    log.i("previewutils","销毁pdf缩略图:"+holder.iv_page.gettag().tostring());
                }
            }*/
        }catch (exception ex){
            ex.printstacktrace();
        }
    }

    @override
    public int getitemcount() {
        return totalpagenum;
    }

    class gridviewholder extends recyclerview.viewholder {
        imageview iv_page;
        textview tv_pagenum;

        public gridviewholder(view itemview) {
            super(itemview);
            iv_page = itemview.findviewbyid(r.id.iv_page);
            tv_pagenum = itemview.findviewbyid(r.id.tv_pagenum);
        }
    }

    /**
     * 接口:grid事件
     */
    public interface gridevent{
        /**
         * 当选择了某grid项
         * @param position tree节点数据
         */
        void ongriditemclick(int position);
    }

    /**
     * 设置grid事件
     * @param event grid事件对象
     */
    public void setgridevent(gridevent event){
        this.delegate = event;
    }

    //grid事件委托
    private gridevent delegate;
}

pdf预览缩略图页面:pdfpreviewactivity

/**
 * ui页面:pdf预览缩略图(注意:此页面,需多关注内存管控)
 * <p>
 * 1、用于显示pdf缩略图信息
 * 2、点击缩略图,带回pdf页码到前一个页面
 * <p>
 * 作者:齐行超
 * 日期:2019.08.07
 */
public class pdfpreviewactivity extends appcompatactivity implements gridadapter.gridevent {

    recyclerview recyclerview;
    button btn_back;
    pdfiumcore pdfiumcore;
    pdfdocument pdfdocument;
    string assetsfilename;

    @override
    protected void oncreate(bundle savedinstancestate) {
        super.oncreate(savedinstancestate);
        uiutils.initwindowstyle(getwindow(), getsupportactionbar());
        setcontentview(r.layout.activity_preview);

        initview();//初始化控件
        setevent();
        loaddata();
    }

    /**
     * 初始化控件
     */
    private void initview() {
        btn_back = findviewbyid(r.id.btn_back);
        recyclerview = findviewbyid(r.id.rv_grid);
    }

    /**
     * 设置事件
     */
    private void setevent() {
        btn_back.setonclicklistener(new view.onclicklistener() {
            @override
            public void onclick(view v) {
                //回收内存
                recyclememory();

                pdfpreviewactivity.this.finish();
            }
        });

    }

    /**
     * 加载数据
     */
    private void loaddata() {
        //加载pdf文件
        loadpdffile();

        //获得pdf总页数
        int totalcount = pdfiumcore.getpagecount(pdfdocument);

        //绑定列表数据
        gridadapter adapter = new gridadapter(this, pdfiumcore, pdfdocument, assetsfilename, totalcount);
        adapter.setgridevent(this);
        recyclerview.setlayoutmanager(new gridlayoutmanager(this, 3));
        recyclerview.setadapter(adapter);
    }

    /**
     * 加载pdf文件
     */
    private void loadpdffile() {
        intent intent = getintent();
        if (intent != null) {
            assetsfilename = intent.getstringextra("assetspdf");
            if (assetsfilename != null) {
                loadassetspdffile(assetsfilename);
            } else {
                uri uri = intent.getdata();
                if (uri != null) {
                    loaduripdffile(uri);
                }
            }
        }
    }

    /**
     * 加载assets中的pdf文件
     */
    void loadassetspdffile(string assetsfilename) {
        try {
            file f = fileutils.filefromasset(this, assetsfilename);
            parcelfiledescriptor pfd = parcelfiledescriptor.open(f, parcelfiledescriptor.mode_read_only);
            pdfiumcore = new pdfiumcore(this);
            pdfdocument = pdfiumcore.newdocument(pfd);
        } catch (exception ex) {
            ex.printstacktrace();
        }
    }

    /**
     * 基于uri加载pdf文件
     */
    void loaduripdffile(uri uri) {
        try {
            parcelfiledescriptor pfd = getcontentresolver().openfiledescriptor(uri, "r");
            pdfiumcore = new pdfiumcore(this);
            pdfdocument = pdfiumcore.newdocument(pfd);
        }catch (exception ex){
            ex.printstacktrace();
        }
    }

    /**
     * 点击缩略图,带回pdf页码到前一个页面
     *
     * @param position 页码
     */
    @override
    public void ongriditemclick(int position) {
        //回收内存
        recyclememory();

        //返回前一个页码
        intent intent = new intent();
        intent.putextra("pagenum", position);
        setresult(activity.result_ok, intent);
        finish();
    }

    /**
     * 回收内存
     */
    private void recyclememory(){
        //关闭pdf对象
        if (pdfiumcore != null && pdfdocument != null) {
            pdfiumcore.closedocument(pdfdocument);
            pdfiumcore = null;
        }
        //清空图片缓存,释放内存空间
        previewutils.getinstance().getimagecache().clearcache();
    }
}

pdf预览缩略图页面的布局文件:activity_preview.xml

<?xml version="1.0" encoding="utf-8"?>
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <relativelayout
        android:id="@+id/rl_top"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:layout_alignparenttop="true"
        android:background="#03a9f5">

        <button
            android:id="@+id/btn_back"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:layout_alignparentbottom="true"
            android:layout_marginleft="10dp"
            android:layout_marginbottom="10dp"
            android:background="@drawable/shape_button"
            android:text="返回"
            android:textcolor="#ffffff"
            android:textsize="18sp" />

        <textview
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignparentbottom="true"
            android:layout_centerhorizontal="true"
            android:layout_marginbottom="15dp"
            android:text="预览缩略图列表"
            android:textcolor="#ffffff"
            android:textsize="18sp" />
    </relativelayout>

    <android.support.v7.widget.recyclerview
        android:id="@+id/rv_grid"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/rl_top" />
</relativelayout>

总结

文档中涉及的功能点较多,难点也较多,尤其是内存管理、多线程管理,有不明白的建议下载demo,多看下源码。也欢迎留言咨询,就是不一定有时间解答,哈哈。。。。

如果希望把该demo用到项目中,建议多测试一下,因为时间关系,我这边仅做了基本测试。

demo下载地址(github + 百度网盘):
https://github.com/qxcwanxss/androidpdfviewerdemo
https://pan.baidu.com/s/1_py36avgqqcj5c87bas5iw

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网