Android PDF原生实现 PDF阅读、PDF手势伸缩、PDF目录、PDF预览缩略图 PDF方案选择 google doc android-pdfview mupdf pdf.js x5
##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相关推荐
- 前端实现 PDF 预览的常见方案
前端实现 PDF 预览的常见方案 由于在搭建个人博客时,想实现在线预览 pdf 格式的个人简历,经过查阅大致有三大类实现方案:本文共涉及以下 5 种实现方案,如下所示: 使用 HTML 标签 ifra ...
- java 预览word文档_Java实现office文档与pdf文档的在线预览功能
最近项目有个需求要java实现office文档与pdf文档的在线预览功能,刚刚接到的时候就觉得有点难,以自己的水平难以在三四天做完.压力略大.后面查找百度资料.以及在同事与网友的帮助下,四天多把它做完 ...
- 文档在线预览(二)word、pdf、excel文件转html以实现文档在线预览
文章目录 一.前言 1.aspose 2 .poi + pdfbox 3 spire 二.将文件转换成html字符串 1.将word文件转成html字符串 1.1 使用aspose 1.2 使用poi ...
- vue-pdf 组件实现 pdf预览效果+点击打印按钮直接弹窗打印指定pdf文件
预览效果图: 打印指定文件效果图: 点击按钮 直接弹出该打印页面: 依赖: 实现预览+打印功能: npm install --save vue-pdf 代码: 打印部分:<a-button @c ...
- 微信公众号 - 实现 H5 网页在微信内置浏览器中下载文件,可预览和下载 office 文件(doc / xls / ppt / pdf 等)适用于任何前端技术栈网站,兼容安卓和苹果系统!
前言 网上的教程都是让你写页面 "引导" 右上角三个点里,让用户自己去浏览器打开,其实这样用户体验并不好. 本文实现了 最新微信公众号 H5 网页(微信内置浏览器中),预览下载 o ...
- Android Camera API 2使用OpenGL ES 2.0和GLSurfaceView对预览进行实时二次处理(黑白滤镜)
这段时间有点忙,一直没时间写第三篇教程,其实代码很早之前就写好了.本系列教程会有三篇文章讲解Android平台滤镜的实现方式,希望在阅读本文之前先阅读前面两篇文档. 第一篇 Android Camer ...
- 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绘 ...
- word文档转pdf并在任意浏览器预览打印一体化方案
近日,遇到一个需求,要将 word 文档转化为 pdf 文档,并且能在 IE 浏览器.火狐浏览器.谷歌浏览器等主流浏览器上展示 pdf 内容. 分析:目前在线预览 word 文档用的是卓正的 page ...
- vue3 - 【完整源码】超详细实现网站 / H5 在线预览 pdf 文件功能,支持缩放、旋转、全屏预览、打印、下载、内容检索、主题色定制、侧边缩略图、页码跳转等等(最好用的pdf预览器,注释详细!)
效果图 在 Vue3.js 项目中,实现了快速高效的 pdf 预览器工具组件,附带详细的使用教程与详细的注释,保证一键复制轻松搞定! 详细的注释很容易二次修改,很多实用功能,你也可以自定义界面上的样式 ...
最新文章
- Elgg网站迁移指南
- 【每日一算法】二叉树的最大深度
- script中用php
- itextsharp php,C#_C#使用iTextSharp设置PDF所有页面背景图功能实例,本文实例讲述了C#使用iTextSharp - phpStudy...
- 外部开发:部件属性 外部exe启动UG NX
- Axure企业官网通用模板web端+公司官网通用模板web端高保真原型+服务企业门户官网+加入我们+在线招聘+企业宣传+新闻动态+企业理念+产品与服务+公司通用版官方电脑端门户网站
- asp.net ajax 源码,asp.net+jquery+ajax简单留言板 v1.2
- java8接口写静态方法_Java 8接口更改–静态方法,默认方法
- c# 线程 WPF 进度百分比(菜鸟)
- 网络机顶盒固件增删预装APP步骤
- JavaWeb项目练习(一)——客户信息管理系统
- python — 二手房
- iOS清除缓存功能开发
- python 转盘 圆形,用python实现一个转盘
- day10:声明式事务控制
- Mac菜鸟必备小工具- Mounty 原生支持 NTFS 读写驱动应用
- oracle 监听 lsnrctl 命令
- 怎样做出完美的高达模型
- Python3.6笔记之腌制泡菜(pickle模块的用法)
- 受害者被锤 法官遭殃 背后的它公关赢了?
热门文章
- 用计算机计算找到的规律奇妙的数字教学设计,四年级上册数学《探索与发现(一)有趣的算式》教案及教学反思...
- 玩一回没有“蒋氏”的溪口
- Windows 8.1中添加美式键盘
- 2015阿里巴巴数据分析师实习生招聘面试经验
- ​如何用大数据软件确定宠物用品店铺选址​
- 小明学会画几何图形了,他能根据要求,画出多行的星星,组成平行四边形。
- Edge浏览器爱上WebVR
- linux命令cp建立硬链接,Linux命令ln、cp、硬链接和软链接
- 1 3 倍频程谱 c语言,频谱、能谱、功率谱、倍频程谱、1/3 倍频程谱
- Android 10.0 行为变更(一)针对所有 API 级别的应用