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);
}
}
}
}
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();
}
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();
}
@Override @Override @Override PDF阅读页面的布局文件:activity_pdf.xml 目录树的数据(目录名称、页码…),已在上个页面获取了,所以此页面只需考虑目录树控件的实现。 注意:之所以没在这个页面单独获取目录树的数据,主要考虑到android-pdfview、pdfium内存占用太大了,不想再次创建Pdf的相关对象。 安卓默认没有树形控件,不过我们可以使用RecyclerView或ListView实现。 列表每一行为一条目录数据,主要包括:名称、页码; 当前Demo实现方式为RecyclerView,应该如何实现上面的效果? 1、使用垂直线性布局管理器; 2、折叠效果 1、控制adapter数据集合的内容即可,如果某节点折叠了,就把对应的子目录数据删除即可, 3、目录文本向右偏移效果 可通过目录树层级 * 固定左侧间隔(如: 20dp),然后为目录的textview控件设置偏移即可; 目录树层级树如何获取? 可选方案: 树形控件的数据对象TreeNodeData: public String getName() { public void setName(String name) { public int getPageNum() { public void setPageNum(int pageNum) { public boolean isExpanded() { public void setExpanded(boolean expanded) { public int getTreeLevel() { public void setTreeLevel(int treeLevel) { public List getSubset() { public void setSubset(List subset) { 树形控件适配器 : TreeAdapter //数据转为展示数据 @Override @Override //显示文本 //图片点击事件 @Override public TreeNodeViewHolder(View view) { PDF目录树页面:PDFCatelogueActivity RecyclerView recyclerView; @Override initView();//初始化控件 //使用RecyclerView加载数据 PDF目录树的布局文件:activity_catelogue.xml android:id="@+id/rv_tree" 这个功能算是本Demo中最为复杂的一个了: 如何将PDF某页面的内容转成图片?(默认是无法从pdfview中获得页面图片的) 查看android-pdfview的源码,无法通过PDFView控件获得某页面的图片,所以只能分析pdfium sdk的A PI了,如下图: pdfium的renderPageBitmap方法可以将页面渲染成图片,不过需要传递一系列参数,而且要小心OutOfMemoryError。 那么,我们需要在代码中获取或者创建PdfiumCore对象,调用该方法,传递Pdfdocument等参数,当bitmap使用完后,应及时释放掉。 内存主要包括: 3.1、当PdfiumCore、Pdfdocument不再使用时,应及时关闭; 查看pdfium源码,调用renderPageBitmap方法之前,还必须确保对应的页面已被打开,即调用了openPage方法。然而,这两个方法都需要一定时间才能执行完成的。 那么,如果我们直接在主线程中让每个RecylerVew的item分别调用renderPageBitmap方法,滑动列表时,会感觉特别卡,所以该方法只能放在子线程中调用了。 那么问题又来了,那么多子线程应该如何管控? 1、考虑CPU的占用,应使用线程池控制子线程并发、阻塞; 预览缩略图工具类:PreviewUtils try { //为图片控件设置标记 Log.i(“PreViewUtils”, “加载pdf缩略图:” + keyPage); //获得imageview的尺寸(注意:如果使用正常控件尺寸,太占内存了) //内存大小= 图片宽度 * 图片高度 * 一个像素占的字节数(RGB_565 所占字节:2) //从缓存中取图片 //使用线程池管理子线程 //调用native方法,将Pdf页面渲染成图片 //切回主线程,设置图片 //切回主线程加载图片 //将任务添加到集合 //构造函数 grid列表适配器: GridAdapter Context context; public GridAdapter(Context context, PdfiumCore pdfiumCore, Pdfdocument pdfdocument, String pdfName, int totalPageNum) { @Override @Override @Override //item不可见时,释放bitmap (注意:本Demo使用了LruCache缓存来管理图片,此处可注释掉) @Override class GridViewHolder extends RecyclerView.ViewHolder { public GridViewHolder(View itemView) { //Grid事件委托 PDF预览缩略图页面:PDFPreviewActivity @Override class GridViewHolder extends RecyclerView.ViewHolder { public GridViewHolder(View itemView) { //Grid事件委托 PDF预览缩略图页面:PDFPreviewActivity /**
@Override
public void loadComplete(int nbPages) {
//获得文档书签信息
List
if (catelogues != null) {
catelogues.clear();
} else {
catelogues = new ArrayList<>();
}
//将bookmark转为目录数据集合
bookmarkToCatelogues(catelogues, bookmarks, 1);
}
private void bookmarkToCatelogues(List catelogues, List
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 treeNodeDatas = new ArrayList<>();
nodeData.setSubset(treeNodeDatas);
bookmarkToCatelogues(treeNodeDatas, bookmark.getChildren(), level + 1);
}
}
}
public void onPageChanged(int page, int pageCount) {
pageNumber = page;
}
public void onPageError(int page, Throwable t) {
}
@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);
}
}
}
protected void onDestroy() {
super.onDestroy();
//是否内存
if (pdfView != null) {
pdfView.recycle();
}
}
}
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:layout_below="@+id/rl_top"/>
如上图所示:
如果有子目录,则出现箭头图片,该项可折叠、展开,箭头方向随之改变;
子目录的名称文本随目录树级别递增向右偏移;
可在adapter中处理页面效果、事件效果:
1、列表项内容展示
2、每个item包含:箭头图片(如果有子目录,则显示)、命令名称文本、页码文本;
反之,加上,再notifyDataSetChanged通知数据源改变;
2、除此之外,还需有一个状态来标记当前节点是展开还是折叠,用于控制箭头图片方向的显示;
1、递归集合自动获取(需要遍历,效率低一点,如果是可编辑的目录结构,建议选择)
2、创建数据的时候,直接写死(因当前demo的PDF目录结构不会被编辑,所以直接选择这个方案吧)
public class TreeNodeData implements Serializable {
//名称
private String name;
//页码
private int pageNum;
//是否已展开(用于控制树形节点图片显示,即箭头朝向图片)
private boolean isExpanded;
//展示级别(1级、2级…,用于控制树形节点缩进位置)
private int treeLevel;
//子集(用于加载子节点,也用于判断是否显示箭头图片,如集合不为空,则显示)
private List subset;
return name;
}
this.name = name;
}
return pageNum;
}
this.pageNum = pageNum;
}
return isExpanded;
}
isExpanded = expanded;
}
return treeLevel;
}
this.treeLevel = treeLevel;
}
return subset;
}
this.subset = subset;
}
}
public class TreeAdapter extends RecyclerView.Adapter
//上下文
private Context context;
//数据
public List data;
//展示数据(由层级结构改为平面结构)
public List displayData;
//treelevel间隔(dp)
private int maginLeft;
//委托对象
private TreeEvent delegate;
public TreeAdapter(Context context, List data) {
this.context = context;
this.data = data;
maginLeft = UIUtils.dip2px(context, 20);
displayData = new ArrayList<>();
dataToDiaplayData(data);
}
private void dataToDiaplayData(List 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();
}
public TreeNodeViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.tree_item, null);
return new TreeNodeViewHolder(view);
}
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);
}
}
});
}
public int getItemCount() {
return displayData.size();
}
class TreeNodeViewHolder extends RecyclerView.ViewHolder {
ImageView img;
TextView title;
TextView pageNum;
super(view);
img = view.findViewById(R.id.iv_arrow);
title = view.findViewById(R.id.tv_title);
pageNum = view.findViewById(R.id.tv_pagenum);
}
}
void onSelectTreeNode(TreeNodeData data);
}
public void setTreeEvent(TreeEvent treeEvent){
this.delegate = treeEvent;
}
}
public class PDFCatelogueActivity extends AppCompatActivity implements TreeAdapter.TreeEvent {
Button btn_back;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UIUtils.initWindowStyle(getWindow(), getSupportActionBar());
setContentView(R.layout.activity_catelogue);
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 catelogues = (List) intent.getSerializableExtra(“catelogues”);
LinearLayoutManager llm = new LinearLayoutManager(this);
llm.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(llm);
TreeAdapter adapter = new TreeAdapter(this, catelogues);
adapter.setTreeEvent(this);
recyclerView.setAdapter(adapter);
}
@Override
public void onSelectTreeNode(TreeNodeData data) {
Intent intent = new Intent();
intent.putExtra(“pageNum”, data.getPageNum());
setResult(Activity.RESULT_OK, intent);
finish();
}
}
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:layout_below="@+id/rl_top" />
8.1、PDF预览缩略图列表的效果图
8.2、功能分析
1、如何将PDF某页面的内容转成图片?
如何减少图片内存的占用?(用户可能快速滑动列表,实时读取、显示多张图片)
如何优化PDF预览缩略图列表的滑动体验?(图片的获取需要一定时间)
如何合理的及时释放内存占用?《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 开源分享
1、pdfium sdk加载pdf文件产生的内存(我们无法优化)
2、android-pdfview产生的内存(如果有需要,可改其源码)
3、我们将pdf页面转为缩略图,而产生的内存(必须优化,否则,容易oom)
3、如何优化PDF预览缩略图列表的滑动体验?
3.2、当缩略图不再使用时,应及时释放;
3.3、可使用LruCache临时缓存缩略图,防止重复调用renderPageBitmap获取图片;
3.4、LruCache应合理管控,当预览页面关闭时,必须清空缓存,以释放内存;
3.5、创建图片时,应使用RGB_565,能节约内存开销(一个像素点,占2字节)
3.6、创建图片时,应尽可能小的指定图片的宽高,能看清就行(图片占用的内存 = 宽 * 高 * 一个像素点占的字节数)
8.3、功能实现
2、考虑到用户滑动速度,有可能某线程正执行或者阻塞着呢,页面已经滑过去了,那么,即使该线程加载出来了图片,也无法显示到列表中。所以对于RecyclerView已不可见的Item项对应的线程,应及时取消,防止做无用功,也节省了内存和cpu开销。
public class PreviewUtils {
//图片缓存管理
private ImageCache imageCache;
//单例
private static PreviewUtils instance;
//线程池
ExecutorService executorService;
//线程任务集合(可用于取消任务)
HashMap
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<>();
}
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;
}
//缓存key
final String keyPage = pdfName + pageNum;
imageView.setTag(keyPage);
/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;/
//注意:如果使用正常控件尺寸,太占内存了,所以此处指定四缩略图看着会模糊一点
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);
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();
}
}
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();
}
}
public ImageCache getImageCache(){
return imageCache;
}
public class ImageCache {
//图片缓存
private LruCache
public ImageCache() {
//初始化 lruCache
//int maxMemory = (int) Runtime.getRuntime().maxMemory();
//int cacheSize = maxMemory/8;
int cacheSize = 1024 * 1024 * 30;//暂时设定30M
lruCache = new LruCache
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
};
}
public synchronized Bitmap getBitmapFromLruCache(String key) {
if(lruCache!= null) {
return lruCache.get(key);
}
return null;
}
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();
}
}
}
}
public class GridAdapter extends RecyclerView.Adapter
PdfiumCore pdfiumCore;
Pdfdocument pdfdocument;
String pdfName;
int totalPageNum;
this.context = context;
this.pdfiumCore = pdfiumCore;
this.pdfdocument = pdfdocument;
this.pdfName = pdfName;
this.totalPageNum = totalPageNum;
}
public GridViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.grid_item, null);
return new GridViewHolder(view);
}
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;
}
public void onViewDetachedFromWindow(GridViewHolder holder) {
super.onViewDetachedFromWindow(holder);
try {
//item不可见时,取消任务
if(holder.iv_page!=null){
PreviewUtils.getInstance().cancelLoadBitmapFromPdf(holder.iv_page.getTag().toString());
}
/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();
}
}
public int getItemCount() {
return totalPageNum;
}
ImageView iv_page;
TextView tv_pagenum;
super(itemView);
iv_page = itemView.findViewById(R.id.iv_page);
tv_pagenum = itemView.findViewById(R.id.tv_pagenum);
}
}
void onGridItemClick(int position);
}
public void setGridEvent(GridEvent event){
this.delegate = event;
}
private GridEvent delegate;
}
}catch (Exception ex){
ex.printStackTrace();
}
}
public int getItemCount() {
return totalPageNum;
}
ImageView iv_page;
TextView tv_pagenum;
super(itemView);
iv_page = itemView.findViewById(R.id.iv_page);
tv_pagenum = itemView.findViewById(R.id.tv_pagenum);
}
}
void onGridItemClick(int position);
}
public void setGridEvent(GridEvent event){
this.delegate = event;
}
private GridEvent delegate;
}



