Weixin Official Accounts Platform

前外交部副部长傅莹:一旦中美闹翻,有没有国家会站在中国一边

终于找到了高清版《人间中毒》,各种姿势的图,都能看

去泰国看了一场“成人秀”,画面尴尬到让人窒息.....

2017年受难周每日默想经文(值得收藏!)

生成图片,分享到微信朋友圈

自由微信安卓APP发布,立即下载! | 提交文章网址
查看原文

Android原生PDF功能实现

徐公 2022-04-23

作者:齐行超 

链接:https://www.cnblogs.com/qixingchao/p/11658226.html


PDF Demo 效果

1、背景

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

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

2、安卓PDF现状

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

1、google doc 在线阅读,基于webview,国内需FQ访问(不可行)
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即可,如下图:

img

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中配置,如下:

 1android{
2......
3 splits {
4        abi {
5            enable true
6            reset()
7            include 'armeabi-v7a' //如果想包含其他cpu平台使用的so,修改这里即可
8        }
9    }
10}

重新编译,生成的安装包,仅剩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、代码实现

重点内容:

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

PDF阅读页面的代码:PDFActivity

  1/**
2 * UI页面:PDF阅读
3 * <p>
4 * 主要功能:
5 * 1、接收传递过来的pdf文件(包括assets中的文件名、文件uri)
6 * 2、显示PDF文件
7 * 3、接收目录页面、预览页面返回的PDF页码,跳转到指定的页面
8 * <p>
9 * 作者:齐行超
10 * 日期:2019.08.07
11 */

12public class PDFActivity extends AppCompatActivity implements
13        OnPageChangeListener,
14        OnLoadCompleteListener,
15        OnPageErrorListener 
{
16    //PDF控件
17    PDFView pdfView;
18    //按钮控件:返回、目录、缩略图
19    Button btn_back, btn_catalogue, btn_preview;
20    //页码
21    Integer pageNumber = 0;
22    //PDF目录集合
23    List<TreeNodeData> catelogues;
24
25    //pdf文件名(限:assets里的文件)
26    String assetsFileName;
27    //pdf文件uri
28    Uri uri;
29
30
31    @Override
32    protected void onCreate(Bundle savedInstanceState) {
33        super.onCreate(savedInstanceState);
34        UIUtils.initWindowStyle(getWindow(), getSupportActionBar());//设置沉浸式
35        setContentView(R.layout.activity_pdf);
36
37        initView();//初始化view
38        setEvent();//设置事件
39        loadPdf();//加载PDF文件
40    }
41
42    /**
43     * 初始化view
44     */

45    private void initView() {
46        pdfView = findViewById(R.id.pdfView);
47        btn_back = findViewById(R.id.btn_back);
48        btn_catalogue = findViewById(R.id.btn_catalogue);
49        btn_preview = findViewById(R.id.btn_preview);
50    }
51
52    /**
53     * 设置事件
54     */

55    private void setEvent() {
56        //返回
57        btn_back.setOnClickListener(new View.OnClickListener() {
58            @Override
59            public void onClick(View v) {
60                PDFActivity.this.finish();
61            }
62        });
63        //跳转目录页面
64        btn_catalogue.setOnClickListener(new View.OnClickListener() {
65            @Override
66            public void onClick(View v) {
67                Intent intent = new Intent(PDFActivity.this, PDFCatelogueActivity.class);
68                intent.putExtra("catelogues", (Serializable) catelogues);
69                PDFActivity.this.startActivityForResult(intent, 200);
70            }
71        });
72        //跳转缩略图页面
73        btn_preview.setOnClickListener(new View.OnClickListener() {
74            @Override
75            public void onClick(View v) {
76                Intent intent = new Intent(PDFActivity.this, PDFPreviewActivity.class);
77                intent.putExtra("AssetsPdf", assetsFileName);
78                intent.setData(uri);
79                PDFActivity.this.startActivityForResult(intent, 201);
80            }
81        });
82    }
83
84    /**
85     * 加载PDF文件
86     */

87    private void loadPdf() {
88        Intent intent = getIntent();
89        if (intent != null) {
90            assetsFileName = intent.getStringExtra("AssetsPdf");
91            if (assetsFileName != null) {
92                displayFromAssets(assetsFileName);
93            } else {
94                uri = intent.getData();
95                if (uri != null) {
96                    displayFromUri(uri);
97                }
98            }
99        }
100    }
101
102    /**
103     * 基于assets显示 PDF 文件
104     *
105     * @param fileName 文件名称
106     */

107    private void displayFromAssets(String fileName) {
108        pdfView.fromAsset(fileName)
109                .defaultPage(pageNumber)
110                .onPageChange(this)
111                .enableAnnotationRendering(true)
112                .onLoad(this)
113                .scrollHandle(new DefaultScrollHandle(this))
114                .spacing(10// 单位 dp
115                .onPageError(this)
116                .pageFitPolicy(FitPolicy.BOTH)
117                .load();
118    }
119
120    /**
121     * 基于uri显示 PDF 文件
122     *
123     * @param uri 文件路径
124     */

125    private void displayFromUri(Uri uri) {
126        pdfView.fromUri(uri)
127                .defaultPage(pageNumber)
128                .onPageChange(this)
129                .enableAnnotationRendering(true)
130                .onLoad(this)
131                .scrollHandle(new DefaultScrollHandle(this))
132                .spacing(10// 单位 dp
133                .onPageError(this)
134                .load();
135    }
136
137    /**
138     * 当成功加载PDF:
139     * 1、可获取PDF的目录信息
140     *
141     * @param nbPages the number of pages in this PDF file
142     */

143    @Override
144    public void loadComplete(int nbPages) {
145        //获得文档书签信息
146        List<PdfDocument.Bookmark> bookmarks = pdfView.getTableOfContents();
147        if (catelogues != null) {
148            catelogues.clear();
149        } else {
150            catelogues = new ArrayList<>();
151        }
152        //将bookmark转为目录数据集合
153        bookmarkToCatelogues(catelogues, bookmarks, 1);
154    }
155
156    /**
157     * 将bookmark转为目录数据集合(递归)
158     *
159     * @param catelogues 目录数据集合
160     * @param bookmarks  书签数据
161     * @param level      目录树级别(用于控制树节点位置偏移)
162     */

163    private void bookmarkToCatelogues(List<TreeNodeData> catelogues, List<PdfDocument.Bookmark> bookmarks, int level) {
164        for (PdfDocument.Bookmark bookmark : bookmarks) {
165            TreeNodeData nodeData = new TreeNodeData();
166            nodeData.setName(bookmark.getTitle());
167            nodeData.setPageNum((int) bookmark.getPageIdx());
168            nodeData.setTreeLevel(level);
169            nodeData.setExpanded(false);
170            catelogues.add(nodeData);
171            if (bookmark.getChildren() != null && bookmark.getChildren().size() > 0) {
172                List<TreeNodeData> treeNodeDatas = new ArrayList<>();
173                nodeData.setSubset(treeNodeDatas);
174                bookmarkToCatelogues(treeNodeDatas, bookmark.getChildren(), level + 1);
175            }
176        }
177    }
178
179    @Override
180    public void onPageChanged(int page, int pageCount) {
181        pageNumber = page;
182    }
183
184    @Override
185    public void onPageError(int page, Throwable t) {
186    }
187
188    /**
189     * 从缩略图、目录页面带回页码,跳转到指定PDF页面
190     *
191     * @param requestCode
192     * @param resultCode
193     * @param data
194     */

195    @Override
196    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
197        super.onActivityResult(requestCode, resultCode, data);
198        if (resultCode == RESULT_OK) {
199            int pageNum = data.getIntExtra("pageNum"0);
200            if (pageNum > 0) {
201                pdfView.jumpTo(pageNum);
202            }
203        }
204    }
205
206    @Override
207    protected void onDestroy() {
208        super.onDestroy();
209        //是否内存
210        if (pdfView != null) {
211            pdfView.recycle();
212        }
213    }
214}

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

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

5
6    <RelativeLayout
7        android:id="@+id/rl_top"
8        android:layout_width="match_parent"
9        android:layout_height="70dp"
10        android:layout_alignParentTop="true"
11        android:background="#03a9f5">

12
13        <Button
14            android:id="@+id/btn_back"
15            android:layout_width="60dp"
16            android:layout_height="30dp"
17            android:background="@drawable/shape_button"
18            android:text="返回"
19            android:textColor="#ffffff"
20            android:textSize="18sp"
21            android:layout_alignParentBottom="true"
22            android:layout_marginBottom="10dp"
23            android:layout_marginLeft="10dp"/>

24
25        <Button
26            android:id="@+id/btn_catalogue"
27            android:layout_width="60dp"
28            android:layout_height="30dp"
29            android:background="@drawable/shape_button"
30            android:text="目录"
31            android:textColor="#ffffff"
32            android:textSize="18sp"
33            android:layout_alignParentRight="true"
34            android:layout_alignParentBottom="true"
35            android:layout_marginBottom="10dp"
36            android:layout_marginRight="10dp"/>

37
38        <Button
39            android:id="@+id/btn_preview"
40            android:layout_width="60dp"
41            android:layout_height="30dp"
42            android:background="@drawable/shape_button"
43            android:text="预览"
44            android:textColor="#ffffff"
45            android:textSize="18sp"
46            android:layout_toLeftOf="@+id/btn_catalogue"
47            android:layout_alignParentBottom="true"
48            android:layout_marginBottom="10dp"
49            android:layout_marginRight="10dp"/>

50    </RelativeLayout>
51
52    <com.github.barteksc.pdfviewer.PDFView
53        android:id="@+id/pdfView"
54        android:layout_width="match_parent"
55        android:layout_height="match_parent"
56        android:layout_below="@+id/rl_top"/>

57
58</RelativeLayout>

7、PDF目录树的实现

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

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

7.1、PDF目录树效果图

img

7.2、树形控件如何实现?

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

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

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

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

2、折叠效果

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

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

1可通过目录树层级 * 固定左侧间隔(如: 20dp),然后为目录的textview控件设置偏移即可;
2
3目录树层级树如何获取? 可选方案:
41、递归集合自动获取(需要遍历,效率低一点,如果是可编辑的目录结构,建议选择)
52、创建数据的时候,直接写死(因当前demo的PDF目录结构不会被编辑,所以直接选择这个方案吧)

7.3、代码实现:

树形控件的数据对象TreeNodeData:

 1/**
2 * 树形控件数据类(会用于页面间传输,所以需实现Serializable 或 Parcelable)
3 * 作者:齐行超
4 * 日期:2019.08.07
5 */

6public class TreeNodeData implements Serializable {
7    //名称
8    private String name;
9    //页码
10    private int pageNum;
11    //是否已展开(用于控制树形节点图片显示,即箭头朝向图片)
12    private boolean isExpanded;
13    //展示级别(1级、2级...,用于控制树形节点缩进位置)
14    private int treeLevel;
15    //子集(用于加载子节点,也用于判断是否显示箭头图片,如集合不为空,则显示)
16    private List<TreeNodeData> subset;
17
18    public String getName() {
19        return name;
20    }
21
22    public void setName(String name) {
23        this.name = name;
24    }
25
26    public int getPageNum() {
27        return pageNum;
28    }
29
30    public void setPageNum(int pageNum) {
31        this.pageNum = pageNum;
32    }
33
34    public boolean isExpanded() {
35        return isExpanded;
36    }
37
38    public void setExpanded(boolean expanded) {
39        isExpanded = expanded;
40    }
41
42    public int getTreeLevel() {
43        return treeLevel;
44    }
45
46    public void setTreeLevel(int treeLevel) {
47        this.treeLevel = treeLevel;
48    }
49
50    public List<TreeNodeData> getSubset() {
51        return subset;
52    }
53
54    public void setSubset(List<TreeNodeData> subset) {
55        this.subset = subset;
56    }
57}

树形控件适配器 :TreeAdapter

  1/**
2 * 树形控件适配器
3 * 作者:齐行超
4 * 日期:2019.08.07
5 */

6public class TreeAdapter extends RecyclerView.Adapter<TreeAdapter.TreeNodeViewHolder> {
7    //上下文
8    private Context context;
9    //数据
10    public List<TreeNodeData> data;
11    //展示数据(由层级结构改为平面结构)
12    public List<TreeNodeData> displayData;
13    //treelevel间隔(dp)
14    private int maginLeft;
15    //委托对象
16    private TreeEvent delegate;
17
18    /**
19     * 构造函数
20     *
21     * @param context 上下文
22     * @param data    数据
23     */

24    public TreeAdapter(Context context, List<TreeNodeData> data) {
25        this.context = context;
26        this.data = data;
27        maginLeft = UIUtils.dip2px(context, 20);
28        displayData = new ArrayList<>();
29
30        //数据转为展示数据
31        dataToDiaplayData(data);
32    }
33
34    /**
35     * 数据转为展示数据
36     *
37     * @param data 数据
38     */

39    private void dataToDiaplayData(List<TreeNodeData> data) {
40        for (TreeNodeData nodeData : data) {
41            displayData.add(nodeData);
42            if (nodeData.isExpanded() && nodeData.getSubset() != null) {
43                dataToDiaplayData(nodeData.getSubset());
44            }
45        }
46    }
47
48    /**
49     * 数据集合转为可显示的集合
50     */

51    private void reDataToDiaplayData() {
52        if (this.data == null || this.data.size() == 0) {
53            return;
54        }
55        if(displayData == null){
56            displayData = new ArrayList<>();
57        }else{
58            displayData.clear();
59        }
60        dataToDiaplayData(this.data);
61        notifyDataSetChanged();
62    }
63
64    @Override
65    public TreeNodeViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
66        View view = LayoutInflater.from(context).inflate(R.layout.tree_item, null);
67        return new TreeNodeViewHolder(view);
68    }
69
70    @Override
71    public void onBindViewHolder(TreeNodeViewHolder holder, int position) {
72        final TreeNodeData data = displayData.get(position);
73        //设置图片
74        if (data.getSubset() != null) {
75            holder.img.setVisibility(View.VISIBLE);
76            if (data.isExpanded()) {
77                holder.img.setImageResource(R.drawable.arrow_h);
78            } else {
79                holder.img.setImageResource(R.drawable.arrow_v);
80            }
81        } else {
82            holder.img.setVisibility(View.INVISIBLE);
83        }
84        //设置图片偏移位置
85        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.img.getLayoutParams();
86        int ratio = data.getTreeLevel() <= 00 : data.getTreeLevel()-1;
87        params.setMargins(maginLeft * ratio, 000);
88        holder.img.setLayoutParams(params);
89
90        //显示文本
91        holder.title.setText(data.getName());
92        holder.pageNum.setText(String.valueOf(data.getPageNum()));
93
94        //图片点击事件
95        holder.img.setOnClickListener(new View.OnClickListener() {
96            @Override
97            public void onClick(View v) {
98                //控制树节点展开、折叠
99                data.setExpanded(!data.isExpanded());
100                //刷新数据源
101                reDataToDiaplayData();
102            }
103        });
104        holder.itemView.setOnClickListener(new View.OnClickListener() {
105            @Override
106            public void onClick(View v) {
107                //回调结果
108                if(delegate!=null){
109                    delegate.onSelectTreeNode(data);
110                }
111            }
112        });
113    }
114
115    @Override
116    public int getItemCount() {
117        return displayData.size();
118    }
119
120    /**
121     * 定义RecyclerView的ViewHolder对象
122     */

123    class TreeNodeViewHolder extends RecyclerView.ViewHolder {
124        ImageView img;
125        TextView title;
126        TextView pageNum;
127
128        public TreeNodeViewHolder(View view) {
129            super(view);
130            img = view.findViewById(R.id.iv_arrow);
131            title = view.findViewById(R.id.tv_title);
132            pageNum = view.findViewById(R.id.tv_pagenum);
133        }
134    }
135
136    /**
137     * 接口:Tree事件
138     */

139    public interface TreeEvent{
140        /**
141         * 当选择了某tree节点
142         * @param data tree节点数据
143         */

144        void onSelectTreeNode(TreeNodeData data);
145    }
146
147    /**
148     * 设置Tree的事件
149     * @param treeEvent Tree的事件对象
150     */

151    public void setTreeEvent(TreeEvent treeEvent){
152        this.delegate = treeEvent;
153    }
154}

PDF目录树页面:PDFCatelogueActivity

 1/**
2 * UI页面:PDF目录
3 * <p>
4 * 1、用于显示Pdf目录信息
5 * 2、点击tree item,带回Pdf页码到前一个页面
6 * <p>
7 * 作者:齐行超
8 * 日期:2019.08.07
9 */

10public class PDFCatelogueActivity extends AppCompatActivity implements TreeAdapter.TreeEvent {
11
12    RecyclerView recyclerView;
13    Button btn_back;
14
15    @Override
16    protected void onCreate(Bundle savedInstanceState) {
17        super.onCreate(savedInstanceState);
18        UIUtils.initWindowStyle(getWindow(), getSupportActionBar());
19        setContentView(R.layout.activity_catelogue);
20
21        initView();//初始化控件
22        setEvent();//设置事件
23        loadData();//加载数据
24    }
25
26    /**
27     * 初始化控件
28     */

29    private void initView() {
30        btn_back = findViewById(R.id.btn_back);
31        recyclerView = findViewById(R.id.rv_tree);
32    }
33
34    /**
35     * 设置事件
36     */

37    private void setEvent() {
38        btn_back.setOnClickListener(new View.OnClickListener() {
39            @Override
40            public void onClick(View v) {
41                PDFCatelogueActivity.this.finish();
42            }
43        });
44    }
45
46    /**
47     * 加载数据
48     */

49    private void loadData() {
50        //从intent中获得传递的数据
51        Intent intent = getIntent();
52        List<TreeNodeData> catelogues = (List<TreeNodeData>) intent.getSerializableExtra("catelogues");
53
54        //使用RecyclerView加载数据
55        LinearLayoutManager llm = new LinearLayoutManager(this);
56        llm.setOrientation(LinearLayoutManager.VERTICAL);
57        recyclerView.setLayoutManager(llm);
58        TreeAdapter adapter = new TreeAdapter(this, catelogues);
59        adapter.setTreeEvent(this);
60        recyclerView.setAdapter(adapter);
61    }
62
63
64    /**
65     * 点击tree item,带回Pdf页码到前一个页面
66     *
67     * @param data tree节点数据
68     */

69    @Override
70    public void onSelectTreeNode(TreeNodeData data) {
71        Intent intent = new Intent();
72        intent.putExtra("pageNum", data.getPageNum());
73        setResult(Activity.RESULT_OK, intent);
74        finish();
75    }
76}

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

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

5
6    <RelativeLayout
7        android:id="@+id/rl_top"
8        android:layout_width="match_parent"
9        android:layout_height="70dp"
10        android:layout_alignParentTop="true"
11        android:background="#03a9f5">

12
13        <Button
14            android:id="@+id/btn_back"
15            android:layout_width="60dp"
16            android:layout_height="30dp"
17            android:layout_alignParentBottom="true"
18            android:layout_marginLeft="10dp"
19            android:layout_marginBottom="10dp"
20            android:background="@drawable/shape_button"
21            android:text="返回"
22            android:textColor="#ffffff"
23            android:textSize="18sp" />

24
25        <TextView
26            android:layout_width="wrap_content"
27            android:layout_height="wrap_content"
28            android:layout_alignParentBottom="true"
29            android:layout_centerHorizontal="true"
30            android:layout_marginBottom="15dp"
31            android:text="目录列表"
32            android:textColor="#ffffff"
33            android:textSize="18sp" />

34    </RelativeLayout>
35
36    <android.support.v7.widget.RecyclerView
37        android:id="@+id/rv_tree"
38        android:layout_width="match_parent"
39        android:layout_height="match_parent"
40        android:layout_below="@+id/rl_top" />

41
42</RelativeLayout>

8、PDF预览缩略图

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

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

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

img

8.2、功能分析

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

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

img


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/**
2 * 预览缩略图工具类
3 *
4 * 1、pdf页面转为缩略图
5 * 2、图片缓存管理(仅保存到内存,可使用LruCache,注意空间大小控制)
6 * 3、多线程管理(线程并发、阻塞、Future任务取消)
7 *
8 * 作者:齐行超
9 * 日期:2019.08.08
10 */

11public class PreviewUtils {
12    //图片缓存管理
13    private ImageCache imageCache;
14    //单例
15    private static PreviewUtils instance;
16    //线程池
17    ExecutorService executorService;
18    //线程任务集合(可用于取消任务)
19    HashMap<String, Future> tasks;
20
21    /**
22     * 单例(仅主线程调用,无需做成线程安全的)
23     *
24     * @return PreviewUtils实例对象
25     */

26    public static PreviewUtils getInstance() {
27        if (instance == null) {
28            instance = new PreviewUtils();
29        }
30        return instance;
31    }
32
33    /**
34     * 默认构造函数
35     */

36    private PreviewUtils() {
37        //初始化图片缓存管理对象
38        imageCache = new ImageCache();
39        //创建并发线程池(建议最大并发数大于1屏grid item的数量)
40        executorService = Executors.newFixedThreadPool(20);
41        //创建线程任务集合,用于取消线程执行
42        tasks = new HashMap<>();
43    }
44
45    /**
46     * 从pdf文件中加载图片
47     *
48     * @param context     上下文
49     * @param imageView   图片控件
50     * @param pdfiumCore  pdf核心对象
51     * @param pdfDocument pdf文档对象
52     * @param pdfName     pdf文件名称
53     * @param pageNum     pdf页码
54     */

55    public void loadBitmapFromPdf(final Context context,
56                                  final ImageView imageView,
57                                  final PdfiumCore pdfiumCore,
58                                  final PdfDocument pdfDocument,
59                                  final String pdfName,
60                                  final int pageNum) 
{
61        //判断参数合法性
62        if (imageView == null || pdfiumCore == null || pdfDocument == null || pageNum < 0) {
63            return;
64        }
65
66        try {
67            //缓存key
68            final String keyPage = pdfName + pageNum;
69
70            //为图片控件设置标记
71            imageView.setTag(keyPage);
72
73            Log.i("PreViewUtils""加载pdf缩略图:" + keyPage);
74
75            //获得imageview的尺寸(注意:如果使用正常控件尺寸,太占内存了)
76            /*int w = imageView.getMeasuredWidth();
77            int h = imageView.getMeasuredHeight();
78            final int reqWidth = w == 0 ? UIUtils.dip2px(context,100) : w;
79            final int reqHeight = h == 0 ? UIUtils.dip2px(context,150) : h;*/

80
81            //内存大小= 图片宽度 * 图片高度 * 一个像素占的字节数(RGB_565 所占字节:2)
82            //注意:如果使用正常控件尺寸,太占内存了,所以此处指定四缩略图看着会模糊一点
83            final int reqWidth = 100;
84            final int reqHeight = 150;
85
86            //从缓存中取图片
87            Bitmap bitmap = imageCache.getBitmapFromLruCache(keyPage);
88            if (bitmap != null) {
89                imageView.setImageBitmap(bitmap);
90                return;
91            }
92
93            //使用线程池管理子线程
94            Future future = executorService.submit(new Runnable() {
95                @Override
96                public void run() {
97                    //打开页面(调用renderPageBitmap方法之前,必须确保页面已open,重要)
98                    pdfiumCore.openPage(pdfDocument, pageNum);
99
100                    //调用native方法,将Pdf页面渲染成图片
101                    final Bitmap bm = Bitmap.createBitmap(reqWidth, reqHeight, Bitmap.Config.RGB_565);
102                    pdfiumCore.renderPageBitmap(pdfDocument, bm, pageNum, 00, reqWidth, reqHeight);
103
104                    //切回主线程,设置图片
105                    if (bm != null) {
106                        //将图片加入缓存
107                        imageCache.addBitmapToLruCache(keyPage, bm);
108
109                        //切回主线程加载图片
110                        new Handler(Looper.getMainLooper()).post(new Runnable() {
111                            @Override
112                            public void run() {
113                                if (imageView.getTag().toString().equals(keyPage)) {
114                                    imageView.setImageBitmap(bm);
115                                    Log.i("PreViewUtils""加载pdf缩略图:" + keyPage + "......已设置!!");
116                                }
117                            }
118                        });
119                    }
120                }
121            });
122
123            //将任务添加到集合
124            tasks.put(keyPage, future);
125        } catch (Exception ex) {
126            ex.printStackTrace();
127        }
128    }
129
130    /**
131     * 取消从pdf文件中加载图片的任务
132     *
133     * @param keyPage 页码
134     */

135    public void cancelLoadBitmapFromPdf(String keyPage) {
136        if (keyPage == null || !tasks.containsKey(keyPage)) {
137            return;
138        }
139        try {
140            Log.i("PreViewUtils""取消加载pdf缩略图:" + keyPage);
141            Future future = tasks.get(keyPage);
142            if (future != null) {
143                future.cancel(true);
144                Log.i("PreViewUtils""取消加载pdf缩略图:" + keyPage + "......已取消!!");
145            }
146        } catch (Exception ex) {
147            ex.printStackTrace();
148        }
149    }
150
151    /**
152     * 获得图片缓存对象
153     * @return 图片缓存
154     */

155    public ImageCache getImageCache(){
156        return imageCache;
157    }
158
159    /**
160     * 图片缓存管理
161     */

162   public class ImageCache {
163        //图片缓存
164        private LruCache<String, Bitmap> lruCache;
165
166        //构造函数
167        public ImageCache() {
168            //初始化 lruCache
169            //int maxMemory = (int) Runtime.getRuntime().maxMemory();
170            //int cacheSize = maxMemory/8;
171            int cacheSize = 1024 * 1024 * 30;//暂时设定30M
172            lruCache = new LruCache<String, Bitmap>(cacheSize) {
173                @Override
174                protected int sizeOf(String key, Bitmap value) {
175                    return value.getRowBytes() * value.getHeight();
176                }
177            };
178        }
179
180        /**
181         * 从缓存中取图片
182         * @param key 键
183         * @return 图片
184         */

185        public synchronized Bitmap getBitmapFromLruCache(String key) {
186            if(lruCache!= null) {
187                return lruCache.get(key);
188            }
189            return null;
190        }
191
192        /**
193         * 向缓存中加图片
194         * @param key 键
195         * @param bitmap 图片
196         */

197        public synchronized void addBitmapToLruCache(String key, Bitmap bitmap) {
198            if (getBitmapFromLruCache(key) == null) {
199                if (lruCache!= null && bitmap != null)
200                    lruCache.put(key, bitmap);
201            }
202        }
203
204        /**
205         * 清空缓存
206         */

207        public void clearCache(){
208            if(lruCache!= null){
209                lruCache.evictAll();
210            }
211        }
212    }
213}

grid列表适配器: GridAdapter

  1/**
2 * grid列表适配器
3 * 作者:齐行超
4 * 日期:2019.08.08
5 */

6public class GridAdapter extends RecyclerView.Adapter<GridAdapter.GridViewHolder> {
7
8    Context context;
9    PdfiumCore pdfiumCore;
10    PdfDocument pdfDocument;
11    String pdfName;
12    int totalPageNum;
13
14
15    public GridAdapter(Context context, PdfiumCore pdfiumCore, PdfDocument pdfDocument, String pdfName, int totalPageNum) {
16        this.context = context;
17        this.pdfiumCore = pdfiumCore;
18        this.pdfDocument = pdfDocument;
19        this.pdfName = pdfName;
20        this.totalPageNum = totalPageNum;
21    }
22
23    @Override
24    public GridViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
25        View view = LayoutInflater.from(context).inflate(R.layout.grid_item, null);
26        return new GridViewHolder(view);
27    }
28
29    @Override
30    public void onBindViewHolder(GridViewHolder holder, int position) {
31        //设置PDF图片
32        final int pageNum = position;
33        PreviewUtils.getInstance().loadBitmapFromPdf(context, holder.iv_page, pdfiumCore, pdfDocument, pdfName, pageNum);
34        //设置PDF页码
35        holder.tv_pagenum.setText(String.valueOf(position));
36        //设置Grid事件
37        holder.iv_page.setOnClickListener(new View.OnClickListener() {
38            @Override
39            public void onClick(View v) {
40                if(delegate!=null){
41                    delegate.onGridItemClick(pageNum);
42                }
43            }
44        });
45        return;
46    }
47
48    @Override
49    public void onViewDetachedFromWindow(GridViewHolder holder) {
50        super.onViewDetachedFromWindow(holder);
51        try {
52            //item不可见时,取消任务
53            if(holder.iv_page!=null){
54                PreviewUtils.getInstance().cancelLoadBitmapFromPdf(holder.iv_page.getTag().toString());
55            }
56
57            //item不可见时,释放bitmap  (注意:本Demo使用了LruCache缓存来管理图片,此处可注释掉)
58            /*Drawable drawable = holder.iv_page.getDrawable();
59            if (drawable != null) {
60                Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
61                if (bitmap != null && !bitmap.isRecycled()) {
62                    bitmap.recycle();
63                    bitmap = null;
64                    Log.i("PreViewUtils","销毁pdf缩略图:"+holder.iv_page.getTag().toString());
65                }
66            }*/

67        }catch (Exception ex){
68            ex.printStackTrace();
69        }
70    }
71
72    @Override
73    public int getItemCount() {
74        return totalPageNum;
75    }
76
77    class GridViewHolder extends RecyclerView.ViewHolder {
78        ImageView iv_page;
79        TextView tv_pagenum;
80
81        public GridViewHolder(View itemView) {
82            super(itemView);
83            iv_page = itemView.findViewById(R.id.iv_page);
84            tv_pagenum = itemView.findViewById(R.id.tv_pagenum);
85        }
86    }
87
88    /**
89     * 接口:Grid事件
90     */

91    public interface GridEvent{
92        /**
93         * 当选择了某Grid项
94         * @param position tree节点数据
95         */

96        void onGridItemClick(int position);
97    }
98
99    /**
100     * 设置Grid事件
101     * @param event Grid事件对象
102     */

103    public void setGridEvent(GridEvent event){
104        this.delegate = event;
105    }
106
107    //Grid事件委托
108    private GridEvent delegate;
109}

PDF预览缩略图页面:PDFPreviewActivity

  1/**
2 * UI页面:PDF预览缩略图(注意:此页面,需多关注内存管控)
3 * <p>
4 * 1、用于显示Pdf缩略图信息
5 * 2、点击缩略图,带回Pdf页码到前一个页面
6 * <p>
7 * 作者:齐行超
8 * 日期:2019.08.07
9 */

10public class PDFPreviewActivity extends AppCompatActivity implements GridAdapter.GridEvent {
11
12    RecyclerView recyclerView;
13    Button btn_back;
14    PdfiumCore pdfiumCore;
15    PdfDocument pdfDocument;
16    String assetsFileName;
17
18    @Override
19    protected void onCreate(Bundle savedInstanceState) {
20        super.onCreate(savedInstanceState);
21        UIUtils.initWindowStyle(getWindow(), getSupportActionBar());
22        setContentView(R.layout.activity_preview);
23
24        initView();//初始化控件
25        setEvent();
26        loadData();
27    }
28
29    /**
30     * 初始化控件
31     */

32    private void initView() {
33        btn_back = findViewById(R.id.btn_back);
34        recyclerView = findViewById(R.id.rv_grid);
35    }
36
37    /**
38     * 设置事件
39     */

40    private void setEvent() {
41        btn_back.setOnClickListener(new View.OnClickListener() {
42            @Override
43            public void onClick(View v) {
44                //回收内存
45                recycleMemory();
46
47                PDFPreviewActivity.this.finish();
48            }
49        });
50
51    }
52
53    /**
54     * 加载数据
55     */

56    private void loadData() {
57        //加载pdf文件
58        loadPdfFile();
59
60        //获得pdf总页数
61        int totalCount = pdfiumCore.getPageCount(pdfDocument);
62
63        //绑定列表数据
64        GridAdapter adapter = new GridAdapter(this, pdfiumCore, pdfDocument, assetsFileName, totalCount);
65        adapter.setGridEvent(this);
66        recyclerView.setLayoutManager(new GridLayoutManager(this3));
67        recyclerView.setAdapter(adapter);
68    }
69
70    /**
71     * 加载pdf文件
72     */

73    private void loadPdfFile() {
74        Intent intent = getIntent();
75        if (intent != null) {
76            assetsFileName = intent.getStringExtra("AssetsPdf");
77            if (assetsFileName != null) {
78                loadAssetsPdfFile(assetsFileName);
79            } else {
80                Uri uri = intent.getData();
81                if (uri != null) {
82                    loadUriPdfFile(uri);
83                }
84            }
85        }
86    }
87
88    /**
89     * 加载assets中的pdf文件
90     */

91    void loadAssetsPdfFile(String assetsFileName) {
92        try {
93            File f = FileUtils.fileFromAsset(this, assetsFileName);
94            ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
95            pdfiumCore = new PdfiumCore(this);
96            pdfDocument = pdfiumCore.newDocument(pfd);
97        } catch (Exception ex) {
98            ex.printStackTrace();
99        }
100    }
101
102    /**
103     * 基于uri加载pdf文件
104     */

105    void loadUriPdfFile(Uri uri) {
106        try {
107            ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r");
108            pdfiumCore = new PdfiumCore(this);
109            pdfDocument = pdfiumCore.newDocument(pfd);
110        }catch (Exception ex){
111            ex.printStackTrace();
112        }
113    }
114
115    /**
116     * 点击缩略图,带回Pdf页码到前一个页面
117     *
118     * @param position 页码
119     */

120    @Override
121    public void onGridItemClick(int position) {
122        //回收内存
123        recycleMemory();
124
125        //返回前一个页码
126        Intent intent = new Intent();
127        intent.putExtra("pageNum", position);
128        setResult(Activity.RESULT_OK, intent);
129        finish();
130    }
131
132    /**
133     * 回收内存
134     */

135    private void recycleMemory(){
136        //关闭pdf对象
137        if (pdfiumCore != null && pdfDocument != null) {
138            pdfiumCore.closeDocument(pdfDocument);
139            pdfiumCore = null;
140        }
141        //清空图片缓存,释放内存空间
142        PreviewUtils.getInstance().getImageCache().clearCache();
143    }
144}

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

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

5
6    <RelativeLayout
7        android:id="@+id/rl_top"
8        android:layout_width="match_parent"
9        android:layout_height="70dp"
10        android:layout_alignParentTop="true"
11        android:background="#03a9f5">

12
13        <Button
14            android:id="@+id/btn_back"
15            android:layout_width="60dp"
16            android:layout_height="30dp"
17            android:layout_alignParentBottom="true"
18            android:layout_marginLeft="10dp"
19            android:layout_marginBottom="10dp"
20            android:background="@drawable/shape_button"
21            android:text="返回"
22            android:textColor="#ffffff"
23            android:textSize="18sp" />

24
25        <TextView
26            android:layout_width="wrap_content"
27            android:layout_height="wrap_content"
28            android:layout_alignParentBottom="true"
29            android:layout_centerHorizontal="true"
30            android:layout_marginBottom="15dp"
31            android:text="预览缩略图列表"
32            android:textColor="#ffffff"
33            android:textSize="18sp" />

34    </RelativeLayout>
35
36    <android.support.v7.widget.RecyclerView
37        android:id="@+id/rv_grid"
38        android:layout_width="match_parent"
39        android:layout_height="match_parent"
40        android:layout_below="@+id/rl_top" />

41</RelativeLayout>

总结

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

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

Demo下载地址(github + 百度网盘):
https://github.com/qxcwanxss/AndroidPdfViewerDemo
https://pan.baidu.com/s/1_Py36avgQqcJ5C87BaS5Iw

推荐阅读

android ViewPager 仿画廊/图书翻页 与 palette 使用

耗时一周,我解决了微信 Matrix 增量编译的 Bug,已提 PR

【开源项目】Compose仿豆瓣榜单客户端,了解一下~

有点酷,仿京东首页体验的嵌套滑动吸顶效果

如果觉得对你有所帮助的话,可以关注我的微信公众号徐公,5 年中大厂工作经验。
  1. 公众号徐公回复黑马,获取 Android 学习视频
  2. 公众号徐公回复徐公666,获取简历模板,教你如何优化简历,走近大厂
  3. 公众号徐公回复面试,可以获得面试常见算法,剑指 ofer 题解
  4. 公众号徐公回复马士兵,可以获得马士兵学习视频一份



文章有问题?点此查看未经处理的缓存