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 true
reset()
include 'armeabi-v7a' //如果想包含其他cpu平台使用的so,修改这里即可
}
}
}

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

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

6、实现PDF阅读功能

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

6.1、功能点:

1、可加载assets中的pdf文件

2、可加载uri类型的pdf文件(如果是线上的pdf文件,可通过网络库先下载到本地,取其uri,本次Demo就不写网络下载了)

3、pdf的基本展示功能(使用android-pdf-viewer的控件实现:PDFView)

4、可跳转至目录页面(目录数据可通过intent直接传递过去)

5、可跳转至预览页面(pdf文件信息可通过intent直接传递过去)

6、根据目录页面、预览页面带回的页码,跳转至指定的pdf页面

6.2、代码实现

重点内容:

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

PDF阅读页面的代码:PDFActivity

/**
* UI页面:PDF阅读
* <p>
* 主要功能:
* 1、接收传递过来的pdf文件(包括assets中的文件名、文件uri)
* 2、显示PDF文件
* 3、接收目录页面、预览页面返回的PDF页码,跳转到指定的页面
* <p>
* 作者:齐行超
* 日期:2019.08.07
*/
public class PDFActivity extends AppCompatActivity implements
OnPageChangeListener,
OnLoadCompleteListener,
OnPageErrorListener {
//PDF控件
PDFView pdfView;
//按钮控件:返回、目录、缩略图
Button btn_back, btn_catalogue, btn_preview;
//页码
Integer pageNumber = 0;
//PDF目录集合
List<TreeNodeData> catelogues; //pdf文件名(限:assets里的文件)
String assetsFileName;
//pdf文件uri
Uri uri; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UIUtils.initWindowStyle(getWindow(), getSupportActionBar());//设置沉浸式
setContentView(R.layout.activity_pdf); initView();//初始化view
setEvent();//设置事件
loadPdf();//加载PDF文件
} /**
* 初始化view
*/
private void initView() {
pdfView = findViewById(R.id.pdfView);
btn_back = findViewById(R.id.btn_back);
btn_catalogue = findViewById(R.id.btn_catalogue);
btn_preview = findViewById(R.id.btn_preview);
} /**
* 设置事件
*/
private void setEvent() {
//返回
btn_back.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PDFActivity.this.finish();
}
});
//跳转目录页面
btn_catalogue.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(PDFActivity.this, PDFCatelogueActivity.class);
intent.putExtra("catelogues", (Serializable) catelogues);
PDFActivity.this.startActivityForResult(intent, 200);
}
});
//跳转缩略图页面
btn_preview.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(PDFActivity.this, PDFPreviewActivity.class);
intent.putExtra("AssetsPdf", assetsFileName);
intent.setData(uri);
PDFActivity.this.startActivityForResult(intent, 201);
}
});
} /**
* 加载PDF文件
*/
private void loadPdf() {
Intent intent = getIntent();
if (intent != null) {
assetsFileName = intent.getStringExtra("AssetsPdf");
if (assetsFileName != null) {
displayFromAssets(assetsFileName);
} else {
uri = intent.getData();
if (uri != null) {
displayFromUri(uri);
}
}
}
} /**
* 基于assets显示 PDF 文件
*
* @param fileName 文件名称
*/
private void displayFromAssets(String fileName) {
pdfView.fromAsset(fileName)
.defaultPage(pageNumber)
.onPageChange(this)
.enableAnnotationRendering(true)
.onLoad(this)
.scrollHandle(new DefaultScrollHandle(this))
.spacing(10) // 单位 dp
.onPageError(this)
.pageFitPolicy(FitPolicy.BOTH)
.load();
} /**
* 基于uri显示 PDF 文件
*
* @param uri 文件路径
*/
private void displayFromUri(Uri uri) {
pdfView.fromUri(uri)
.defaultPage(pageNumber)
.onPageChange(this)
.enableAnnotationRendering(true)
.onLoad(this)
.scrollHandle(new DefaultScrollHandle(this))
.spacing(10) // 单位 dp
.onPageError(this)
.load();
} /**
* 当成功加载PDF:
* 1、可获取PDF的目录信息
*
* @param nbPages the number of pages in this PDF file
*/
@Override
public void loadComplete(int nbPages) {
//获得文档书签信息
List<PdfDocument.Bookmark> bookmarks = pdfView.getTableOfContents();
if (catelogues != null) {
catelogues.clear();
} else {
catelogues = new ArrayList<>();
}
//将bookmark转为目录数据集合
bookmarkToCatelogues(catelogues, bookmarks, 1);
} /**
* 将bookmark转为目录数据集合(递归)
*
* @param catelogues 目录数据集合
* @param bookmarks 书签数据
* @param level 目录树级别(用于控制树节点位置偏移)
*/
private void bookmarkToCatelogues(List<TreeNodeData> catelogues, List<PdfDocument.Bookmark> bookmarks, int level) {
for (PdfDocument.Bookmark bookmark : bookmarks) {
TreeNodeData nodeData = new TreeNodeData();
nodeData.setName(bookmark.getTitle());
nodeData.setPageNum((int) bookmark.getPageIdx());
nodeData.setTreeLevel(level);
nodeData.setExpanded(false);
catelogues.add(nodeData);
if (bookmark.getChildren() != null && bookmark.getChildren().size() > 0) {
List<TreeNodeData> treeNodeDatas = new ArrayList<>();
nodeData.setSubset(treeNodeDatas);
bookmarkToCatelogues(treeNodeDatas, bookmark.getChildren(), level + 1);
}
}
} @Override
public void onPageChanged(int page, int pageCount) {
pageNumber = page;
} @Override
public void onPageError(int page, Throwable t) {
} /**
* 从缩略图、目录页面带回页码,跳转到指定PDF页面
*
* @param requestCode
* @param resultCode
* @param data
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
int pageNum = data.getIntExtra("pageNum", 0);
if (pageNum > 0) {
pdfView.jumpTo(pageNum);
}
}
} @Override
protected void onDestroy() {
super.onDestroy();
//是否内存
if (pdfView != null) {
pdfView.recycle();
}
}
}

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

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"> <RelativeLayout
android:id="@+id/rl_top"
android:layout_width="match_parent"
android:layout_height="70dp"
android:layout_alignParentTop="true"
android:background="#03a9f5"> <Button
android:id="@+id/btn_back"
android:layout_width="60dp"
android:layout_height="30dp"
android:background="@drawable/shape_button"
android:text="返回"
android:textColor="#ffffff"
android:textSize="18sp"
android:layout_alignParentBottom="true"
android:layout_marginBottom="10dp"
android:layout_marginLeft="10dp"/> <Button
android:id="@+id/btn_catalogue"
android:layout_width="60dp"
android:layout_height="30dp"
android:background="@drawable/shape_button"
android:text="目录"
android:textColor="#ffffff"
android:textSize="18sp"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_marginBottom="10dp"
android:layout_marginRight="10dp"/> <Button
android:id="@+id/btn_preview"
android:layout_width="60dp"
android:layout_height="30dp"
android:background="@drawable/shape_button"
android:text="预览"
android:textColor="#ffffff"
android:textSize="18sp"
android:layout_toLeftOf="@+id/btn_catalogue"
android:layout_alignParentBottom="true"
android:layout_marginBottom="10dp"
android:layout_marginRight="10dp"/>
</RelativeLayout> <com.github.barteksc.pdfviewer.PDFView
android:id="@+id/pdfView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/rl_top"/> </RelativeLayout>

7、PDF目录树的实现

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

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

7.1、PDF目录树效果图

7.2、树形控件如何实现?

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

如上图所示:

列表每一行为一条目录数据,主要包括:名称、页码;

如果有子目录,则出现箭头图片,该项可折叠、展开,箭头方向随之改变;

子目录的名称文本随目录树级别递增向右偏移;

当前Demo实现方式为RecyclerView,应该如何实现上面的效果?

可在adapter中处理页面效果、事件效果:

1、列表项内容展示

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

2、折叠效果

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

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

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

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

7.3、代码实现:

树形控件的数据对象TreeNodeData:

/**
* 树形控件数据类(会用于页面间传输,所以需实现Serializable 或 Parcelable)
* 作者:齐行超
* 日期:2019.08.07
*/
public class TreeNodeData implements Serializable {
//名称
private String name;
//页码
private int pageNum;
//是否已展开(用于控制树形节点图片显示,即箭头朝向图片)
private boolean isExpanded;
//展示级别(1级、2级...,用于控制树形节点缩进位置)
private int treeLevel;
//子集(用于加载子节点,也用于判断是否显示箭头图片,如集合不为空,则显示)
private List<TreeNodeData> subset; public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public int getPageNum() {
return pageNum;
} public void setPageNum(int pageNum) {
this.pageNum = pageNum;
} public boolean isExpanded() {
return isExpanded;
} public void setExpanded(boolean expanded) {
isExpanded = expanded;
} public int getTreeLevel() {
return treeLevel;
} public void setTreeLevel(int treeLevel) {
this.treeLevel = treeLevel;
} public List<TreeNodeData> getSubset() {
return subset;
} public void setSubset(List<TreeNodeData> subset) {
this.subset = subset;
}
}

树形控件适配器 : TreeAdapter

/**
* 树形控件适配器
* 作者:齐行超
* 日期:2019.08.07
*/
public class TreeAdapter extends RecyclerView.Adapter<TreeAdapter.TreeNodeViewHolder> {
//上下文
private Context context;
//数据
public List<TreeNodeData> data;
//展示数据(由层级结构改为平面结构)
public List<TreeNodeData> displayData;
//treelevel间隔(dp)
private int maginLeft;
//委托对象
private TreeEvent delegate; /**
* 构造函数
*
* @param context 上下文
* @param data 数据
*/
public TreeAdapter(Context context, List<TreeNodeData> data) {
this.context = context;
this.data = data;
maginLeft = UIUtils.dip2px(context, 20);
displayData = new ArrayList<>(); //数据转为展示数据
dataToDiaplayData(data);
} /**
* 数据转为展示数据
*
* @param data 数据
*/
private void dataToDiaplayData(List<TreeNodeData> data) {
for (TreeNodeData nodeData : data) {
displayData.add(nodeData);
if (nodeData.isExpanded() && nodeData.getSubset() != null) {
dataToDiaplayData(nodeData.getSubset());
}
}
} /**
* 数据集合转为可显示的集合
*/
private void reDataToDiaplayData() {
if (this.data == null || this.data.size() == 0) {
return;
}
if(displayData == null){
displayData = new ArrayList<>();
}else{
displayData.clear();
}
dataToDiaplayData(this.data);
notifyDataSetChanged();
} @Override
public TreeNodeViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.tree_item, null);
return new TreeNodeViewHolder(view);
} @Override
public void onBindViewHolder(TreeNodeViewHolder holder, int position) {
final TreeNodeData data = displayData.get(position);
//设置图片
if (data.getSubset() != null) {
holder.img.setVisibility(View.VISIBLE);
if (data.isExpanded()) {
holder.img.setImageResource(R.drawable.arrow_h);
} else {
holder.img.setImageResource(R.drawable.arrow_v);
}
} else {
holder.img.setVisibility(View.INVISIBLE);
}
//设置图片偏移位置
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.img.getLayoutParams();
int ratio = data.getTreeLevel() <= 0? 0 : data.getTreeLevel()-1;
params.setMargins(maginLeft * ratio, 0, 0, 0);
holder.img.setLayoutParams(params); //显示文本
holder.title.setText(data.getName());
holder.pageNum.setText(String.valueOf(data.getPageNum())); //图片点击事件
holder.img.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//控制树节点展开、折叠
data.setExpanded(!data.isExpanded());
//刷新数据源
reDataToDiaplayData();
}
});
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//回调结果
if(delegate!=null){
delegate.onSelectTreeNode(data);
}
}
});
} @Override
public int getItemCount() {
return displayData.size();
} /**
* 定义RecyclerView的ViewHolder对象
*/
class TreeNodeViewHolder extends RecyclerView.ViewHolder {
ImageView img;
TextView title;
TextView pageNum; public TreeNodeViewHolder(View view) {
super(view);
img = view.findViewById(R.id.iv_arrow);
title = view.findViewById(R.id.tv_title);
pageNum = view.findViewById(R.id.tv_pagenum);
}
} /**
* 接口:Tree事件
*/
public interface TreeEvent{
/**
* 当选择了某tree节点
* @param data tree节点数据
*/
void onSelectTreeNode(TreeNodeData data);
} /**
* 设置Tree的事件
* @param treeEvent Tree的事件对象
*/
public void setTreeEvent(TreeEvent treeEvent){
this.delegate = treeEvent;
}
}

PDF目录树页面:PDFCatelogueActivity

/**
* UI页面:PDF目录
* <p>
* 1、用于显示Pdf目录信息
* 2、点击tree item,带回Pdf页码到前一个页面
* <p>
* 作者:齐行超
* 日期:2019.08.07
*/
public class PDFCatelogueActivity extends AppCompatActivity implements TreeAdapter.TreeEvent { RecyclerView recyclerView;
Button btn_back; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UIUtils.initWindowStyle(getWindow(), getSupportActionBar());
setContentView(R.layout.activity_catelogue); initView();//初始化控件
setEvent();//设置事件
loadData();//加载数据
} /**
* 初始化控件
*/
private void initView() {
btn_back = findViewById(R.id.btn_back);
recyclerView = findViewById(R.id.rv_tree);
} /**
* 设置事件
*/
private void setEvent() {
btn_back.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PDFCatelogueActivity.this.finish();
}
});
} /**
* 加载数据
*/
private void loadData() {
//从intent中获得传递的数据
Intent intent = getIntent();
List<TreeNodeData> catelogues = (List<TreeNodeData>) intent.getSerializableExtra("catelogues"); //使用RecyclerView加载数据
LinearLayoutManager llm = new LinearLayoutManager(this);
llm.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(llm);
TreeAdapter adapter = new TreeAdapter(this, catelogues);
adapter.setTreeEvent(this);
recyclerView.setAdapter(adapter);
} /**
* 点击tree item,带回Pdf页码到前一个页面
*
* @param data tree节点数据
*/
@Override
public void onSelectTreeNode(TreeNodeData data) {
Intent intent = new Intent();
intent.putExtra("pageNum", data.getPageNum());
setResult(Activity.RESULT_OK, intent);
finish();
}
}

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

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"> <RelativeLayout
android:id="@+id/rl_top"
android:layout_width="match_parent"
android:layout_height="70dp"
android:layout_alignParentTop="true"
android:background="#03a9f5"> <Button
android:id="@+id/btn_back"
android:layout_width="60dp"
android:layout_height="30dp"
android:layout_alignParentBottom="true"
android:layout_marginLeft="10dp"
android:layout_marginBottom="10dp"
android:background="@drawable/shape_button"
android:text="返回"
android:textColor="#ffffff"
android:textSize="18sp" /> <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="15dp"
android:text="目录列表"
android:textColor="#ffffff"
android:textSize="18sp" />
</RelativeLayout> <android.support.v7.widget.RecyclerView
android:id="@+id/rv_tree"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/rl_top" /> </RelativeLayout>

8、PDF预览缩略图

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

如何将PDF某页面的内容转成图片?(默认是无法从pdfview中获得页面图片的)

如何减少图片内存的占用?(用户可能快速滑动列表,实时读取、显示多张图片)

如何优化PDF预览缩略图列表的滑动体验?(图片的获取需要一定时间)

如何合理的及时释放内存占用?

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

8.2、功能分析

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

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



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

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

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

内存主要包括:

1、pdfium sdk加载pdf文件产生的内存(我们无法优化)

2、android-pdfview产生的内存(如果有需要,可改其源码)

3、我们将pdf页面转为缩略图,而产生的内存(必须优化,否则,容易oom)

3.1、当PdfiumCore、PdfDocument不再使用时,应及时关闭;

3.2、当缩略图不再使用时,应及时释放;

3.3、可使用LruCache临时缓存缩略图,防止重复调用renderPageBitmap获取图片;

3.4、LruCache应合理管控,当预览页面关闭时,必须清空缓存,以释放内存;

3.5、创建图片时,应使用RGB_565,能节约内存开销(一个像素点,占2字节)

3.6、创建图片时,应尽可能小的指定图片的宽高,能看清就行(图片占用的内存 = 宽 * 高 * 一个像素点占的字节数)

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

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

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

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

1、考虑CPU的占用,应使用线程池控制子线程并发、阻塞;

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

8.3、功能实现

预览缩略图工具类:PreviewUtils

/**
* 预览缩略图工具类
*
* 1、pdf页面转为缩略图
* 2、图片缓存管理(仅保存到内存,可使用LruCache,注意空间大小控制)
* 3、多线程管理(线程并发、阻塞、Future任务取消)
*
* 作者:齐行超
* 日期:2019.08.08
*/
public class PreviewUtils {
//图片缓存管理
private ImageCache imageCache;
//单例
private static PreviewUtils instance;
//线程池
ExecutorService executorService;
//线程任务集合(可用于取消任务)
HashMap<String, Future> tasks; /**
* 单例(仅主线程调用,无需做成线程安全的)
*
* @return PreviewUtils实例对象
*/
public static PreviewUtils getInstance() {
if (instance == null) {
instance = new PreviewUtils();
}
return instance;
} /**
* 默认构造函数
*/
private PreviewUtils() {
//初始化图片缓存管理对象
imageCache = new ImageCache();
//创建并发线程池(建议最大并发数大于1屏grid item的数量)
executorService = Executors.newFixedThreadPool(20);
//创建线程任务集合,用于取消线程执行
tasks = new HashMap<>();
} /**
* 从pdf文件中加载图片
*
* @param context 上下文
* @param imageView 图片控件
* @param pdfiumCore pdf核心对象
* @param pdfDocument pdf文档对象
* @param pdfName pdf文件名称
* @param pageNum pdf页码
*/
public void loadBitmapFromPdf(final Context context,
final ImageView imageView,
final PdfiumCore pdfiumCore,
final PdfDocument pdfDocument,
final String pdfName,
final int pageNum) {
//判断参数合法性
if (imageView == null || pdfiumCore == null || pdfDocument == null || pageNum < 0) {
return;
} try {
//缓存key
final String keyPage = pdfName + pageNum; //为图片控件设置标记
imageView.setTag(keyPage); Log.i("PreViewUtils", "加载pdf缩略图:" + keyPage); //获得imageview的尺寸(注意:如果使用正常控件尺寸,太占内存了)
/*int w = imageView.getMeasuredWidth();
int h = imageView.getMeasuredHeight();
final int reqWidth = w == 0 ? UIUtils.dip2px(context,100) : w;
final int reqHeight = h == 0 ? UIUtils.dip2px(context,150) : h;*/ //内存大小= 图片宽度 * 图片高度 * 一个像素占的字节数(RGB_565 所占字节:2)
//注意:如果使用正常控件尺寸,太占内存了,所以此处指定四缩略图看着会模糊一点
final int reqWidth = 100;
final int reqHeight = 150; //从缓存中取图片
Bitmap bitmap = imageCache.getBitmapFromLruCache(keyPage);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
} //使用线程池管理子线程
Future future = executorService.submit(new Runnable() {
@Override
public void run() {
//打开页面(调用renderPageBitmap方法之前,必须确保页面已open,重要)
pdfiumCore.openPage(pdfDocument, pageNum); //调用native方法,将Pdf页面渲染成图片
final Bitmap bm = Bitmap.createBitmap(reqWidth, reqHeight, Bitmap.Config.RGB_565);
pdfiumCore.renderPageBitmap(pdfDocument, bm, pageNum, 0, 0, reqWidth, reqHeight); //切回主线程,设置图片
if (bm != null) {
//将图片加入缓存
imageCache.addBitmapToLruCache(keyPage, bm); //切回主线程加载图片
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
if (imageView.getTag().toString().equals(keyPage)) {
imageView.setImageBitmap(bm);
Log.i("PreViewUtils", "加载pdf缩略图:" + keyPage + "......已设置!!");
}
}
});
}
}
}); //将任务添加到集合
tasks.put(keyPage, future);
} catch (Exception ex) {
ex.printStackTrace();
}
} /**
* 取消从pdf文件中加载图片的任务
*
* @param keyPage 页码
*/
public void cancelLoadBitmapFromPdf(String keyPage) {
if (keyPage == null || !tasks.containsKey(keyPage)) {
return;
}
try {
Log.i("PreViewUtils", "取消加载pdf缩略图:" + keyPage);
Future future = tasks.get(keyPage);
if (future != null) {
future.cancel(true);
Log.i("PreViewUtils", "取消加载pdf缩略图:" + keyPage + "......已取消!!");
}
} catch (Exception ex) {
ex.printStackTrace();
}
} /**
* 获得图片缓存对象
* @return 图片缓存
*/
public ImageCache getImageCache(){
return imageCache;
} /**
* 图片缓存管理
*/
public class ImageCache {
//图片缓存
private LruCache<String, Bitmap> lruCache; //构造函数
public ImageCache() {
//初始化 lruCache
//int maxMemory = (int) Runtime.getRuntime().maxMemory();
//int cacheSize = maxMemory/8;
int cacheSize = 1024 * 1024 * 30;//暂时设定30M
lruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
};
} /**
* 从缓存中取图片
* @param key 键
* @return 图片
*/
public synchronized Bitmap getBitmapFromLruCache(String key) {
if(lruCache!= null) {
return lruCache.get(key);
}
return null;
} /**
* 向缓存中加图片
* @param key 键
* @param bitmap 图片
*/
public synchronized void addBitmapToLruCache(String key, Bitmap bitmap) {
if (getBitmapFromLruCache(key) == null) {
if (lruCache!= null && bitmap != null)
lruCache.put(key, bitmap);
}
} /**
* 清空缓存
*/
public void clearCache(){
if(lruCache!= null){
lruCache.evictAll();
}
}
}
}

grid列表适配器: GridAdapter

/**
* grid列表适配器
* 作者:齐行超
* 日期:2019.08.08
*/
public class GridAdapter extends RecyclerView.Adapter<GridAdapter.GridViewHolder> { Context context;
PdfiumCore pdfiumCore;
PdfDocument pdfDocument;
String pdfName;
int totalPageNum; public GridAdapter(Context context, PdfiumCore pdfiumCore, PdfDocument pdfDocument, String pdfName, int totalPageNum) {
this.context = context;
this.pdfiumCore = pdfiumCore;
this.pdfDocument = pdfDocument;
this.pdfName = pdfName;
this.totalPageNum = totalPageNum;
} @Override
public GridViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.grid_item, null);
return new GridViewHolder(view);
} @Override
public void onBindViewHolder(GridViewHolder holder, int position) {
//设置PDF图片
final int pageNum = position;
PreviewUtils.getInstance().loadBitmapFromPdf(context, holder.iv_page, pdfiumCore, pdfDocument, pdfName, pageNum);
//设置PDF页码
holder.tv_pagenum.setText(String.valueOf(position));
//设置Grid事件
holder.iv_page.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(delegate!=null){
delegate.onGridItemClick(pageNum);
}
}
});
return;
} @Override
public void onViewDetachedFromWindow(GridViewHolder holder) {
super.onViewDetachedFromWindow(holder);
try {
//item不可见时,取消任务
if(holder.iv_page!=null){
PreviewUtils.getInstance().cancelLoadBitmapFromPdf(holder.iv_page.getTag().toString());
} //item不可见时,释放bitmap (注意:本Demo使用了LruCache缓存来管理图片,此处可注释掉)
/*Drawable drawable = holder.iv_page.getDrawable();
if (drawable != null) {
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null;
Log.i("PreViewUtils","销毁pdf缩略图:"+holder.iv_page.getTag().toString());
}
}*/
}catch (Exception ex){
ex.printStackTrace();
}
} @Override
public int getItemCount() {
return totalPageNum;
} class GridViewHolder extends RecyclerView.ViewHolder {
ImageView iv_page;
TextView tv_pagenum; public GridViewHolder(View itemView) {
super(itemView);
iv_page = itemView.findViewById(R.id.iv_page);
tv_pagenum = itemView.findViewById(R.id.tv_pagenum);
}
} /**
* 接口:Grid事件
*/
public interface GridEvent{
/**
* 当选择了某Grid项
* @param position tree节点数据
*/
void onGridItemClick(int position);
} /**
* 设置Grid事件
* @param event Grid事件对象
*/
public void setGridEvent(GridEvent event){
this.delegate = event;
} //Grid事件委托
private GridEvent delegate;
}

PDF预览缩略图页面:PDFPreviewActivity

/**
* UI页面:PDF预览缩略图(注意:此页面,需多关注内存管控)
* <p>
* 1、用于显示Pdf缩略图信息
* 2、点击缩略图,带回Pdf页码到前一个页面
* <p>
* 作者:齐行超
* 日期:2019.08.07
*/
public class PDFPreviewActivity extends AppCompatActivity implements GridAdapter.GridEvent { RecyclerView recyclerView;
Button btn_back;
PdfiumCore pdfiumCore;
PdfDocument pdfDocument;
String assetsFileName; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UIUtils.initWindowStyle(getWindow(), getSupportActionBar());
setContentView(R.layout.activity_preview); initView();//初始化控件
setEvent();
loadData();
} /**
* 初始化控件
*/
private void initView() {
btn_back = findViewById(R.id.btn_back);
recyclerView = findViewById(R.id.rv_grid);
} /**
* 设置事件
*/
private void setEvent() {
btn_back.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//回收内存
recycleMemory(); PDFPreviewActivity.this.finish();
}
}); } /**
* 加载数据
*/
private void loadData() {
//加载pdf文件
loadPdfFile(); //获得pdf总页数
int totalCount = pdfiumCore.getPageCount(pdfDocument); //绑定列表数据
GridAdapter adapter = new GridAdapter(this, pdfiumCore, pdfDocument, assetsFileName, totalCount);
adapter.setGridEvent(this);
recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
recyclerView.setAdapter(adapter);
} /**
* 加载pdf文件
*/
private void loadPdfFile() {
Intent intent = getIntent();
if (intent != null) {
assetsFileName = intent.getStringExtra("AssetsPdf");
if (assetsFileName != null) {
loadAssetsPdfFile(assetsFileName);
} else {
Uri uri = intent.getData();
if (uri != null) {
loadUriPdfFile(uri);
}
}
}
} /**
* 加载assets中的pdf文件
*/
void loadAssetsPdfFile(String assetsFileName) {
try {
File f = FileUtils.fileFromAsset(this, assetsFileName);
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
pdfiumCore = new PdfiumCore(this);
pdfDocument = pdfiumCore.newDocument(pfd);
} catch (Exception ex) {
ex.printStackTrace();
}
} /**
* 基于uri加载pdf文件
*/
void loadUriPdfFile(Uri uri) {
try {
ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r");
pdfiumCore = new PdfiumCore(this);
pdfDocument = pdfiumCore.newDocument(pfd);
}catch (Exception ex){
ex.printStackTrace();
}
} /**
* 点击缩略图,带回Pdf页码到前一个页面
*
* @param position 页码
*/
@Override
public void onGridItemClick(int position) {
//回收内存
recycleMemory(); //返回前一个页码
Intent intent = new Intent();
intent.putExtra("pageNum", position);
setResult(Activity.RESULT_OK, intent);
finish();
} /**
* 回收内存
*/
private void recycleMemory(){
//关闭pdf对象
if (pdfiumCore != null && pdfDocument != null) {
pdfiumCore.closeDocument(pdfDocument);
pdfiumCore = null;
}
//清空图片缓存,释放内存空间
PreviewUtils.getInstance().getImageCache().clearCache();
}
}

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

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"> <RelativeLayout
android:id="@+id/rl_top"
android:layout_width="match_parent"
android:layout_height="70dp"
android:layout_alignParentTop="true"
android:background="#03a9f5"> <Button
android:id="@+id/btn_back"
android:layout_width="60dp"
android:layout_height="30dp"
android:layout_alignParentBottom="true"
android:layout_marginLeft="10dp"
android:layout_marginBottom="10dp"
android:background="@drawable/shape_button"
android:text="返回"
android:textColor="#ffffff"
android:textSize="18sp" /> <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="15dp"
android:text="预览缩略图列表"
android:textColor="#ffffff"
android:textSize="18sp" />
</RelativeLayout> <android.support.v7.widget.RecyclerView
android:id="@+id/rv_grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/rl_top" />
</RelativeLayout>

总结

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

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

Demo下载地址(github + 百度网盘):

https://github.com/qxcwanxss/AndroidPdfViewerDemo

https://pan.baidu.com/s/1_Py36avgQqcJ5C87BaS5Iw

Android原生PDF功能实现的更多相关文章

  1. Android原生PDF功能实现:PDF阅读、PDF页面跳转、PDF手势伸缩、PDF目录树、PDF预览缩略图

    1.背景 近期,公司希望实现安卓原生端的PDF功能,要求:高效.实用. 经过两天的调研.编码,实现了一个简单Demo,如上图所示. 关于安卓原生端的PDF功能实现,技术点还是很多的,为了咱们安卓开发的 ...

  2. 【转】Android原生PDF功能实现

    1.背景 近期,公司希望实现安卓原生端的PDF功能,要求:高效.实用. 经过两天的调研.编码,实现了一个简单Demo,如上图所示.关于安卓原生端的PDF功能实现,技术点还是很多的,为了咱们安卓开发的同 ...

  3. 拓展 Android 原生 CountDownTimer 倒计时

    拓展 Android 原生 CountDownTimer 倒计时 [TOC] CountDownTimer 在系统的CountDownTimer上进行的修改,主要是拓展了功能,当然也保留了系统默认的模 ...

  4. Android原生游戏开发:使用JustWeEngine开发微信打飞机

    使用JustWeEngine开发微信打飞机: 作者博客: 博客园 引擎地址:JustWeEngine 示例代码:EngineDemo JustWeEngine? JustWeEngine是托管在Git ...

  5. Android 带清除功能的输入框控件ClearEditText,仿IOS的输入框

    转载请注明出处http://blog.csdn.net/xiaanming/article/details/11066685 今天给大家带来一个很实用的小控件ClearEditText,就是在Andr ...

  6. [Android Pro] android 4.4 Android原生权限管理:AppOps

    reference : http://m.blog.csdn.net/blog/langzxz/45308199 reference : http://blog.csdn.net/hyhyl1990/ ...

  7. android 原生camera——设置模块修改

    , 此篇博客是记一次客户需求修改,从上周五到现在正好一周时间,期间的各种酸爽,就不说了,还是来看大家关注的技术问题吧. 首先看下以前效果和修改后的效果: 修改前:修改后: 不知道有没有看明白,我在简单 ...

  8. React Native Android原生模块开发实战|教程|心得|怎样创建React Native Android原生模块

    尊重版权,未经授权不得转载 本文出自:贾鹏辉的技术博客(http://blog.csdn.net/fengyuzhengfan/article/details/54691503) 告诉大家一个好消息. ...

  9. 将React Native集成至Android原生应用

    将React Native集成至Android原生应用 Android Studio 2.1 Preview 4生成的空项目 react-native 环境 0.22.2 初次编译后apk有1.1M, ...

随机推荐

  1. IDEA新建一个多maven模块工程(有图)

    对于一些大型的项目来说,将项目的各个模块理清并进行管理,便于后续项目的维护,使用maven管理是很方便的,它可以很好的构建模块来设计项目的整体结构,对一些小型的项目不建议使用 1.新建父maven模块 ...

  2. 【SQL server基础】手动创建数据库和表格

    use master go if exists(select * from sysdatabases where name='learning') drop database learning go ...

  3. 踩坑踩坑之Flask+ uWSGI + Tensorflow的Web服务部署

    一.简介 作为算法开发人员,在算法模块完成后,拟部署Web服务以对外提供服务,从而将算法模型落地应用.本文针对首次基于Flask + uWSGI + Tensorflow + Nginx部署Web服务 ...

  4. 品Spring:真没想到,三十步才能完成一个bean实例的创建

    在容器启动快完成时,会把所有的单例bean进行实例化,也可以叫做预先实例化. 这样做的好处之一是,可以及早地发现问题,及早的抛出异常,及早地解决掉. 本文就来看下整个的实例化过程.其实还是比较繁琐的. ...

  5. asp.net core 3.0 更新简记

    asp.net core 3.0 更新简记 asp.net core 3.0 更新简记 Intro 最近把活动室预约项目从 asp.net core 2.2 更新到了 asp.net core 3.0 ...

  6. 【SQLServer】 查询一个字段里不同值的最新一条记录

    查询用户编号为1165的所有数据: ,,,,,) ' order by JianCeID desc 查询用户编号为1165且监测参数为(1,2,7,15,19,20)的最新数据: select * f ...

  7. CSDN VIP如何添加引流自定义栏目

    几个月前我也开始在csdn上开了博客,一来给自己加几个少的可怜的流量,再者,让公众号的原创文章获得更多的曝光,让有需要的同学看到. 写过csdn博客的同学都知道,默认只有打赏c币功能:也没有专门广告位 ...

  8. python编程基础之二十八

    装饰器:说白了就是闭包,但是内部函数调用外部变量调用的是函数, 好处:就是在不用修改原函数代码的前提下给函数增加新的功能 装饰器有两种写法 第一种: #被修饰的函数 def say_hello(nam ...

  9. 【DP合集】背包 bound

    N 种物品,第 i 种物品有 s i 个,单个重量为 w i ,单个价值为 v i .现有一个限重为 W 的背包,求能容 纳的物品的最大总价值. Input 输入第一行二个整数 N , W ( N ≤ ...

  10. 在C#中调用Python中遇到的坑(No module named xxx)

    例如Python的代码是这个样子的. # coding=<utf-> # -*- coding: utf- *- import requests import urllib def Cle ...