##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页面)

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

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

可在android gradle中配置,如下:

android{
......splits {abi {enable truereset()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页面

####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 implementsOnPageChangeListener,OnLoadCompleteListener,OnPageErrorListener {//PDF控件PDFView pdfView;//按钮控件:返回、目录、缩略图Button btn_back, btn_catalogue, btn_preview;//页码Integer pageNumber = 0;//PDF目录集合List<TreeNodeData> catelogues;//pdf文件名(限:assets里的文件)String assetsFileName;//pdf文件uriUri uri;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);UIUtils.initWindowStyle(getWindow(), getSupportActionBar());//设置沉浸式setContentView(R.layout.activity_pdf);initView();//初始化viewsetEvent();//设置事件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() {@Overridepublic void onClick(View v) {PDFActivity.this.finish();}});//跳转目录页面btn_catalogue.setOnClickListener(new View.OnClickListener() {@Overridepublic 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() {@Overridepublic 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*/@Overridepublic 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);}}}@Overridepublic void onPageChanged(int page, int pageCount) {pageNumber = page;}@Overridepublic void onPageError(int page, Throwable t) {}/*** 从缩略图、目录页面带回页码,跳转到指定PDF页面** @param requestCode* @param resultCode* @param data*/@Overrideprotected 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);}}}@Overrideprotected 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"><RelativeLayoutandroid:id="@+id/rl_top"android:layout_width="match_parent"android:layout_height="70dp"android:layout_alignParentTop="true"android:background="#03a9f5"><Buttonandroid: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"/><Buttonandroid: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"/><Buttonandroid: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.PDFViewandroid: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();}@Overridepublic TreeNodeViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {View view = LayoutInflater.from(context).inflate(R.layout.tree_item, null);return new TreeNodeViewHolder(view);}@Overridepublic 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() {@Overridepublic void onClick(View v) {//控制树节点展开、折叠data.setExpanded(!data.isExpanded());//刷新数据源reDataToDiaplayData();}});holder.itemView.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {//回调结果if(delegate!=null){delegate.onSelectTreeNode(data);}}});}@Overridepublic 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;@Overrideprotected 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() {@Overridepublic 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节点数据*/@Overridepublic 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"><RelativeLayoutandroid:id="@+id/rl_top"android:layout_width="match_parent"android:layout_height="70dp"android:layout_alignParentTop="true"android:background="#03a9f5"><Buttonandroid: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" /><TextViewandroid: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.RecyclerViewandroid: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 {//缓存keyfinal 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() {@Overridepublic 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() {@Overridepublic 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;//暂时设定30MlruCache = new LruCache<String, Bitmap>(cacheSize) {@Overrideprotected 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;}@Overridepublic GridViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {View view = LayoutInflater.from(context).inflate(R.layout.grid_item, null);return new GridViewHolder(view);}@Overridepublic 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() {@Overridepublic void onClick(View v) {if(delegate!=null){delegate.onGridItemClick(pageNum);}}});return;}@Overridepublic 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();}}@Overridepublic 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;@Overrideprotected 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() {@Overridepublic 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 页码*/@Overridepublic 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"><RelativeLayoutandroid:id="@+id/rl_top"android:layout_width="match_parent"android:layout_height="70dp"android:layout_alignParentTop="true"android:background="#03a9f5"><Buttonandroid: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" /><TextViewandroid: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.RecyclerViewandroid: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

原文链接:https://www.jianshu.com/p/eec432fa89af

Android PDF原生实现 PDF阅读、PDF手势伸缩、PDF目录、PDF预览缩略图 PDF方案选择 google doc android-pdfview mupdf pdf.js x5相关推荐

  1. 前端实现 PDF 预览的常见方案

    前端实现 PDF 预览的常见方案 由于在搭建个人博客时,想实现在线预览 pdf 格式的个人简历,经过查阅大致有三大类实现方案:本文共涉及以下 5 种实现方案,如下所示: 使用 HTML 标签 ifra ...

  2. java 预览word文档_Java实现office文档与pdf文档的在线预览功能

    最近项目有个需求要java实现office文档与pdf文档的在线预览功能,刚刚接到的时候就觉得有点难,以自己的水平难以在三四天做完.压力略大.后面查找百度资料.以及在同事与网友的帮助下,四天多把它做完 ...

  3. 文档在线预览(二)word、pdf、excel文件转html以实现文档在线预览

    文章目录 一.前言 1.aspose 2 .poi + pdfbox 3 spire 二.将文件转换成html字符串 1.将word文件转成html字符串 1.1 使用aspose 1.2 使用poi ...

  4. vue-pdf 组件实现 pdf预览效果+点击打印按钮直接弹窗打印指定pdf文件

    预览效果图: 打印指定文件效果图: 点击按钮 直接弹出该打印页面: 依赖: 实现预览+打印功能: npm install --save vue-pdf 代码: 打印部分:<a-button @c ...

  5. 微信公众号 - 实现 H5 网页在微信内置浏览器中下载文件,可预览和下载 office 文件(doc / xls / ppt / pdf 等)适用于任何前端技术栈网站,兼容安卓和苹果系统!

    前言 网上的教程都是让你写页面 "引导" 右上角三个点里,让用户自己去浏览器打开,其实这样用户体验并不好. 本文实现了 最新微信公众号 H5 网页(微信内置浏览器中),预览下载 o ...

  6. Android Camera API 2使用OpenGL ES 2.0和GLSurfaceView对预览进行实时二次处理(黑白滤镜)

    这段时间有点忙,一直没时间写第三篇教程,其实代码很早之前就写好了.本系列教程会有三篇文章讲解Android平台滤镜的实现方式,希望在阅读本文之前先阅读前面两篇文档. 第一篇 Android Camer ...

  7. android平台下OpenGL ES 3.0使用GLSurfaceView对相机Camera预览实时处理

    OpenGL ES 3.0学习实践 android平台下OpenGL ES 3.0从零开始 android平台下OpenGL ES 3.0绘制纯色背景 android平台下OpenGL ES 3.0绘 ...

  8. word文档转pdf并在任意浏览器预览打印一体化方案

    近日,遇到一个需求,要将 word 文档转化为 pdf 文档,并且能在 IE 浏览器.火狐浏览器.谷歌浏览器等主流浏览器上展示 pdf 内容. 分析:目前在线预览 word 文档用的是卓正的 page ...

  9. vue3 - 【完整源码】超详细实现网站 / H5 在线预览 pdf 文件功能,支持缩放、旋转、全屏预览、打印、下载、内容检索、主题色定制、侧边缩略图、页码跳转等等(最好用的pdf预览器,注释详细!)

    效果图 在 Vue3.js 项目中,实现了快速高效的 pdf 预览器工具组件,附带详细的使用教程与详细的注释,保证一键复制轻松搞定! 详细的注释很容易二次修改,很多实用功能,你也可以自定义界面上的样式 ...

最新文章

  1. Elgg网站迁移指南
  2. 【每日一算法】二叉树的最大深度
  3. script中用php
  4. itextsharp php,C#_C#使用iTextSharp设置PDF所有页面背景图功能实例,本文实例讲述了C#使用iTextSharp - phpStudy...
  5. 外部开发:部件属性 外部exe启动UG NX
  6. Axure企业官网通用模板web端+公司官网通用模板web端高保真原型+服务企业门户官网+加入我们+在线招聘+企业宣传+新闻动态+企业理念+产品与服务+公司通用版官方电脑端门户网站
  7. asp.net ajax 源码,asp.net+jquery+ajax简单留言板 v1.2
  8. java8接口写静态方法_Java 8接口更改–静态方法,默认方法
  9. c# 线程 WPF 进度百分比(菜鸟)
  10. 网络机顶盒固件增删预装APP步骤
  11. JavaWeb项目练习(一)——客户信息管理系统
  12. python — 二手房
  13. iOS清除缓存功能开发
  14. python 转盘 圆形,用python实现一个转盘
  15. day10:声明式事务控制
  16. Mac菜鸟必备小工具- Mounty 原生支持 NTFS 读写驱动应用
  17. oracle 监听 lsnrctl 命令
  18. 怎样做出完美的高达模型
  19. Python3.6笔记之腌制泡菜(pickle模块的用法)
  20. 受害者被锤 法官遭殃 背后的它公关赢了?

热门文章

  1. 用计算机计算找到的规律奇妙的数字教学设计,四年级上册数学《探索与发现(一)有趣的算式》教案及教学反思...
  2. 玩一回没有“蒋氏”的溪口
  3. Windows 8.1中添加美式键盘
  4. 2015阿里巴巴数据分析师实习生招聘面试经验
  5. ​如何用大数据软件确定宠物用品店铺选址​
  6. 小明学会画几何图形了,他能根据要求,画出多行的星星,组成平行四边形。
  7. Edge浏览器爱上WebVR
  8. linux命令cp建立硬链接,Linux命令ln、cp、硬链接和软链接
  9. 1 3 倍频程谱 c语言,频谱、能谱、功率谱、倍频程谱、1/3 倍频程谱
  10. Android 10.0 行为变更(一)针对所有 API 级别的应用