1、背景

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

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

2、安卓PDF现状

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

  1. 1google doc 在线阅读,基于webview,国内需翻墙访问(不可行)
  2. 2、跳转设备中默认pdf app打开,前提需要手机安装了pdf 软件(可按需选择)
  3. 3、内置 android-pdfview,基于原生native, apk增加约15~20M(可行,不过安装包有点大)
  4. 4、内置 mupdf,基于原生native, 集成有点麻烦,增加约9M(可行,不过安装包稍有点大)
  5. 5、内置 pdf.js,功能丰富,apk增加5M(基于Webview,性能低,js实现,功能定制复杂)
  6. 6、使用x5内核,需要客户端完全使用x5内核(基于Webview,性能低,不能定制功能)

查阅官方资料,这些方案虽然能实现基本的PDF阅读功能,但是多数方案,集成过程较复杂,且性能低下,容易内存溢出造成App闪退。

3、方案选择

经过对各方案的反复比对,本次实现PDF Demo,决定使用:android-pdfview。
原因:

  1. 1android-pdfview基于PDFium实现(PDFium是谷歌 + 福昕软件的PDF开源项目);
  2. 2android-pdfview Github仍在维护;
  3. 3android-pdfview Github获得的星星较多;
  4. 4、客户端集成较方便;

问题分析:
运行android-pdfview官方demo,问题也很多:

  1. 1、仅实现了pdf滑动阅读、手势伸缩的功能;
  2. 2、缺少pdf目录树、缩略图等功能;
  3. 3、安装包过大;
  4. 4UI不美观;
  5. 5、内存问题;
  6. 6、其他...

不过,不用担心,解决了这些问题不就没有问题了嘛,哈、哈、哈(笑声有点勉强哈)

下面,咱们开始实现Demo吧。

4、Demo设计

4.1、工程结构

在设计之前,应明确Demo的实现目标:

  1. 1android-pdfview已实现了pdfview,可用于阅读pdf文件,手势伸缩pdf页面、跳转pdf页面,
  2. 那么,咱们基于android-pdfview扩展功能即可,功能包括:目录树、缩略图等;
  3. 2、扩展的功能应逻辑解耦,不能影响android-pdfview代码的可替换性
  4. (即:如果android-pdfview有新版本,直接替换即可)
  5. 3、客户端应很方便集成
  6. (如:客户端仅需要传递过来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中配置,如下:

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

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

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

6、实现PDF阅读功能

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

6.1、功能点:

1、可加载assets中的pdf文件
2、可加载uri类型的pdf文件(如果是线上的pdf文件,可通过网络库先下载到本地,取其uri,本次Demo就不写网络下载了)
3、pdf的基本展示功能(使用android-pdf-viewer的控件实现:PDFView)
4、可跳转至目录页面(目录数据可通过intent直接传递过去)
5、可跳转至预览页面(pdf文件信息可通过intent直接传递过去)
6、根据目录页面、预览页面带回的页码,跳转至指定的pdf页面

6.2、代码实现

重点内容:

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

PDF阅读页面的代码:PDFActivity

  1. /**
  2. * UI页面:PDF阅读
  3. * <p>
  4. * 主要功能:
  5. * 1、接收传递过来的pdf文件(包括assets中的文件名、文件uri)
  6. * 2、显示PDF文件
  7. * 3、接收目录页面、预览页面返回的PDF页码,跳转到指定的页面
  8. * <p>
  9. * 作者:齐行超
  10. * 日期:2019.08.07
  11. */
  12. public class PDFActivity extends AppCompatActivity implements
  13. OnPageChangeListener,
  14. OnLoadCompleteListener,
  15. OnPageErrorListener {
  16. //PDF控件
  17. PDFView pdfView;
  18. //按钮控件:返回、目录、缩略图
  19. Button btn_back, btn_catalogue, btn_preview;
  20. //页码
  21. Integer pageNumber = 0;
  22. //PDF目录集合
  23. List<TreeNodeData> catelogues;
  24. //pdf文件名(限:assets里的文件)
  25. String assetsFileName;
  26. //pdf文件uri
  27. Uri uri;
  28. @Override
  29. protected void onCreate(Bundle savedInstanceState) {
  30. super.onCreate(savedInstanceState);
  31. UIUtils.initWindowStyle(getWindow(), getSupportActionBar());//设置沉浸式
  32. setContentView(R.layout.activity_pdf);
  33. initView();//初始化view
  34. setEvent();//设置事件
  35. loadPdf();//加载PDF文件
  36. }
  37. /**
  38. * 初始化view
  39. */
  40. private void initView() {
  41. pdfView = findViewById(R.id.pdfView);
  42. btn_back = findViewById(R.id.btn_back);
  43. btn_catalogue = findViewById(R.id.btn_catalogue);
  44. btn_preview = findViewById(R.id.btn_preview);
  45. }
  46. /**
  47. * 设置事件
  48. */
  49. private void setEvent() {
  50. //返回
  51. btn_back.setOnClickListener(new View.OnClickListener() {
  52. @Override
  53. public void onClick(View v) {
  54. PDFActivity.this.finish();
  55. }
  56. });
  57. //跳转目录页面
  58. btn_catalogue.setOnClickListener(new View.OnClickListener() {
  59. @Override
  60. public void onClick(View v) {
  61. Intent intent = new Intent(PDFActivity.this, PDFCatelogueActivity.class);
  62. intent.putExtra("catelogues", (Serializable) catelogues);
  63. PDFActivity.this.startActivityForResult(intent, 200);
  64. }
  65. });
  66. //跳转缩略图页面
  67. btn_preview.setOnClickListener(new View.OnClickListener() {
  68. @Override
  69. public void onClick(View v) {
  70. Intent intent = new Intent(PDFActivity.this, PDFPreviewActivity.class);
  71. intent.putExtra("AssetsPdf", assetsFileName);
  72. intent.setData(uri);
  73. PDFActivity.this.startActivityForResult(intent, 201);
  74. }
  75. });
  76. }
  77. /**
  78. * 加载PDF文件
  79. */
  80. private void loadPdf() {
  81. Intent intent = getIntent();
  82. if (intent != null) {
  83. assetsFileName = intent.getStringExtra("AssetsPdf");
  84. if (assetsFileName != null) {
  85. displayFromAssets(assetsFileName);
  86. } else {
  87. uri = intent.getData();
  88. if (uri != null) {
  89. displayFromUri(uri);
  90. }
  91. }
  92. }
  93. }
  94. /**
  95. * 基于assets显示 PDF 文件
  96. *
  97. * @param fileName 文件名称
  98. */
  99. private void displayFromAssets(String fileName) {
  100. pdfView.fromAsset(fileName)
  101. .defaultPage(pageNumber)
  102. .onPageChange(this)
  103. .enableAnnotationRendering(true)
  104. .onLoad(this)
  105. .scrollHandle(new DefaultScrollHandle(this))
  106. .spacing(10) // 单位 dp
  107. .onPageError(this)
  108. .pageFitPolicy(FitPolicy.BOTH)
  109. .load();
  110. }
  111. /**
  112. * 基于uri显示 PDF 文件
  113. *
  114. * @param uri 文件路径
  115. */
  116. private void displayFromUri(Uri uri) {
  117. pdfView.fromUri(uri)
  118. .defaultPage(pageNumber)
  119. .onPageChange(this)
  120. .enableAnnotationRendering(true)
  121. .onLoad(this)
  122. .scrollHandle(new DefaultScrollHandle(this))
  123. .spacing(10) // 单位 dp
  124. .onPageError(this)
  125. .load();
  126. }
  127. /**
  128. * 当成功加载PDF:
  129. * 1、可获取PDF的目录信息
  130. *
  131. * @param nbPages the number of pages in this PDF file
  132. */
  133. @Override
  134. public void loadComplete(int nbPages) {
  135. //获得文档书签信息
  136. List<PdfDocument.Bookmark> bookmarks = pdfView.getTableOfContents();
  137. if (catelogues != null) {
  138. catelogues.clear();
  139. } else {
  140. catelogues = new ArrayList<>();
  141. }
  142. //将bookmark转为目录数据集合
  143. bookmarkToCatelogues(catelogues, bookmarks, 1);
  144. }
  145. /**
  146. * 将bookmark转为目录数据集合(递归)
  147. *
  148. * @param catelogues 目录数据集合
  149. * @param bookmarks 书签数据
  150. * @param level 目录树级别(用于控制树节点位置偏移)
  151. */
  152. private void bookmarkToCatelogues(List<TreeNodeData> catelogues, List<PdfDocument.Bookmark> bookmarks, int level) {
  153. for (PdfDocument.Bookmark bookmark : bookmarks) {
  154. TreeNodeData nodeData = new TreeNodeData();
  155. nodeData.setName(bookmark.getTitle());
  156. nodeData.setPageNum((int) bookmark.getPageIdx());
  157. nodeData.setTreeLevel(level);
  158. nodeData.setExpanded(false);
  159. catelogues.add(nodeData);
  160. if (bookmark.getChildren() != null && bookmark.getChildren().size() > 0) {
  161. List<TreeNodeData> treeNodeDatas = new ArrayList<>();
  162. nodeData.setSubset(treeNodeDatas);
  163. bookmarkToCatelogues(treeNodeDatas, bookmark.getChildren(), level + 1);
  164. }
  165. }
  166. }
  167. @Override
  168. public void onPageChanged(int page, int pageCount) {
  169. pageNumber = page;
  170. }
  171. @Override
  172. public void onPageError(int page, Throwable t) {
  173. }
  174. /**
  175. * 从缩略图、目录页面带回页码,跳转到指定PDF页面
  176. *
  177. * @param requestCode
  178. * @param resultCode
  179. * @param data
  180. */
  181. @Override
  182. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  183. super.onActivityResult(requestCode, resultCode, data);
  184. if (resultCode == RESULT_OK) {
  185. int pageNum = data.getIntExtra("pageNum", 0);
  186. if (pageNum > 0) {
  187. pdfView.jumpTo(pageNum);
  188. }
  189. }
  190. }
  191. @Override
  192. protected void onDestroy() {
  193. super.onDestroy();
  194. //是否内存
  195. if (pdfView != null) {
  196. pdfView.recycle();
  197. }
  198. }
  199. }

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

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent">
  5. <RelativeLayout
  6. android:id="@+id/rl_top"
  7. android:layout_width="match_parent"
  8. android:layout_height="70dp"
  9. android:layout_alignParentTop="true"
  10. android:background="#03a9f5">
  11. <Button
  12. android:id="@+id/btn_back"
  13. android:layout_width="60dp"
  14. android:layout_height="30dp"
  15. android:background="@drawable/shape_button"
  16. android:text="返回"
  17. android:textColor="#ffffff"
  18. android:textSize="18sp"
  19. android:layout_alignParentBottom="true"
  20. android:layout_marginBottom="10dp"
  21. android:layout_marginLeft="10dp"/>
  22. <Button
  23. android:id="@+id/btn_catalogue"
  24. android:layout_width="60dp"
  25. android:layout_height="30dp"
  26. android:background="@drawable/shape_button"
  27. android:text="目录"
  28. android:textColor="#ffffff"
  29. android:textSize="18sp"
  30. android:layout_alignParentRight="true"
  31. android:layout_alignParentBottom="true"
  32. android:layout_marginBottom="10dp"
  33. android:layout_marginRight="10dp"/>
  34. <Button
  35. android:id="@+id/btn_preview"
  36. android:layout_width="60dp"
  37. android:layout_height="30dp"
  38. android:background="@drawable/shape_button"
  39. android:text="预览"
  40. android:textColor="#ffffff"
  41. android:textSize="18sp"
  42. android:layout_toLeftOf="@+id/btn_catalogue"
  43. android:layout_alignParentBottom="true"
  44. android:layout_marginBottom="10dp"
  45. android:layout_marginRight="10dp"/>
  46. </RelativeLayout>
  47. <com.github.barteksc.pdfviewer.PDFView
  48. android:id="@+id/pdfView"
  49. android:layout_width="match_parent"
  50. android:layout_height="match_parent"
  51. android:layout_below="@+id/rl_top"/>
  52. </RelativeLayout>

7、PDF目录树的实现

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

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

7.1、PDF目录树效果图

7.2、树形控件如何实现?

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

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

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

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

2、折叠效果

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

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

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

7.3、代码实现:

树形控件的数据对象TreeNodeData:

  1. /**
  2. * 树形控件数据类(会用于页面间传输,所以需实现Serializable 或 Parcelable)
  3. * 作者:齐行超
  4. * 日期:2019.08.07
  5. */
  6. public class TreeNodeData implements Serializable {
  7. //名称
  8. private String name;
  9. //页码
  10. private int pageNum;
  11. //是否已展开(用于控制树形节点图片显示,即箭头朝向图片)
  12. private boolean isExpanded;
  13. //展示级别(1级、2级...,用于控制树形节点缩进位置)
  14. private int treeLevel;
  15. //子集(用于加载子节点,也用于判断是否显示箭头图片,如集合不为空,则显示)
  16. private List<TreeNodeData> subset;
  17. public String getName() {
  18. return name;
  19. }
  20. public void setName(String name) {
  21. this.name = name;
  22. }
  23. public int getPageNum() {
  24. return pageNum;
  25. }
  26. public void setPageNum(int pageNum) {
  27. this.pageNum = pageNum;
  28. }
  29. public boolean isExpanded() {
  30. return isExpanded;
  31. }
  32. public void setExpanded(boolean expanded) {
  33. isExpanded = expanded;
  34. }
  35. public int getTreeLevel() {
  36. return treeLevel;
  37. }
  38. public void setTreeLevel(int treeLevel) {
  39. this.treeLevel = treeLevel;
  40. }
  41. public List<TreeNodeData> getSubset() {
  42. return subset;
  43. }
  44. public void setSubset(List<TreeNodeData> subset) {
  45. this.subset = subset;
  46. }
  47. }

树形控件适配器 : TreeAdapter

  1. /**
  2. * 树形控件适配器
  3. * 作者:齐行超
  4. * 日期:2019.08.07
  5. */
  6. public class TreeAdapter extends RecyclerView.Adapter<TreeAdapter.TreeNodeViewHolder> {
  7. //上下文
  8. private Context context;
  9. //数据
  10. public List<TreeNodeData> data;
  11. //展示数据(由层级结构改为平面结构)
  12. public List<TreeNodeData> displayData;
  13. //treelevel间隔(dp)
  14. private int maginLeft;
  15. //委托对象
  16. private TreeEvent delegate;
  17. /**
  18. * 构造函数
  19. *
  20. * @param context 上下文
  21. * @param data 数据
  22. */
  23. public TreeAdapter(Context context, List<TreeNodeData> data) {
  24. this.context = context;
  25. this.data = data;
  26. maginLeft = UIUtils.dip2px(context, 20);
  27. displayData = new ArrayList<>();
  28. //数据转为展示数据
  29. dataToDiaplayData(data);
  30. }
  31. /**
  32. * 数据转为展示数据
  33. *
  34. * @param data 数据
  35. */
  36. private void dataToDiaplayData(List<TreeNodeData> data) {
  37. for (TreeNodeData nodeData : data) {
  38. displayData.add(nodeData);
  39. if (nodeData.isExpanded() && nodeData.getSubset() != null) {
  40. dataToDiaplayData(nodeData.getSubset());
  41. }
  42. }
  43. }
  44. /**
  45. * 数据集合转为可显示的集合
  46. */
  47. private void reDataToDiaplayData() {
  48. if (this.data == null || this.data.size() == 0) {
  49. return;
  50. }
  51. if(displayData == null){
  52. displayData = new ArrayList<>();
  53. }else{
  54. displayData.clear();
  55. }
  56. dataToDiaplayData(this.data);
  57. notifyDataSetChanged();
  58. }
  59. @Override
  60. public TreeNodeViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  61. View view = LayoutInflater.from(context).inflate(R.layout.tree_item, null);
  62. return new TreeNodeViewHolder(view);
  63. }
  64. @Override
  65. public void onBindViewHolder(TreeNodeViewHolder holder, int position) {
  66. final TreeNodeData data = displayData.get(position);
  67. //设置图片
  68. if (data.getSubset() != null) {
  69. holder.img.setVisibility(View.VISIBLE);
  70. if (data.isExpanded()) {
  71. holder.img.setImageResource(R.drawable.arrow_h);
  72. } else {
  73. holder.img.setImageResource(R.drawable.arrow_v);
  74. }
  75. } else {
  76. holder.img.setVisibility(View.INVISIBLE);
  77. }
  78. //设置图片偏移位置
  79. RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.img.getLayoutParams();
  80. int ratio = data.getTreeLevel() <= 0? 0 : data.getTreeLevel()-1;
  81. params.setMargins(maginLeft * ratio, 0, 0, 0);
  82. holder.img.setLayoutParams(params);
  83. //显示文本
  84. holder.title.setText(data.getName());
  85. holder.pageNum.setText(String.valueOf(data.getPageNum()));
  86. //图片点击事件
  87. holder.img.setOnClickListener(new View.OnClickListener() {
  88. @Override
  89. public void onClick(View v) {
  90. //控制树节点展开、折叠
  91. data.setExpanded(!data.isExpanded());
  92. //刷新数据源
  93. reDataToDiaplayData();
  94. }
  95. });
  96. holder.itemView.setOnClickListener(new View.OnClickListener() {
  97. @Override
  98. public void onClick(View v) {
  99. //回调结果
  100. if(delegate!=null){
  101. delegate.onSelectTreeNode(data);
  102. }
  103. }
  104. });
  105. }
  106. @Override
  107. public int getItemCount() {
  108. return displayData.size();
  109. }
  110. /**
  111. * 定义RecyclerView的ViewHolder对象
  112. */
  113. class TreeNodeViewHolder extends RecyclerView.ViewHolder {
  114. ImageView img;
  115. TextView title;
  116. TextView pageNum;
  117. public TreeNodeViewHolder(View view) {
  118. super(view);
  119. img = view.findViewById(R.id.iv_arrow);
  120. title = view.findViewById(R.id.tv_title);
  121. pageNum = view.findViewById(R.id.tv_pagenum);
  122. }
  123. }
  124. /**
  125. * 接口:Tree事件
  126. */
  127. public interface TreeEvent{
  128. /**
  129. * 当选择了某tree节点
  130. * @param data tree节点数据
  131. */
  132. void onSelectTreeNode(TreeNodeData data);
  133. }
  134. /**
  135. * 设置Tree的事件
  136. * @param treeEvent Tree的事件对象
  137. */
  138. public void setTreeEvent(TreeEvent treeEvent){
  139. this.delegate = treeEvent;
  140. }
  141. }

PDF目录树页面:PDFCatelogueActivity

  1. /**
  2. * UI页面:PDF目录
  3. * <p>
  4. * 1、用于显示Pdf目录信息
  5. * 2、点击tree item,带回Pdf页码到前一个页面
  6. * <p>
  7. * 作者:齐行超
  8. * 日期:2019.08.07
  9. */
  10. public class PDFCatelogueActivity extends AppCompatActivity implements TreeAdapter.TreeEvent {
  11. RecyclerView recyclerView;
  12. Button btn_back;
  13. @Override
  14. protected void onCreate(Bundle savedInstanceState) {
  15. super.onCreate(savedInstanceState);
  16. UIUtils.initWindowStyle(getWindow(), getSupportActionBar());
  17. setContentView(R.layout.activity_catelogue);
  18. initView();//初始化控件
  19. setEvent();//设置事件
  20. loadData();//加载数据
  21. }
  22. /**
  23. * 初始化控件
  24. */
  25. private void initView() {
  26. btn_back = findViewById(R.id.btn_back);
  27. recyclerView = findViewById(R.id.rv_tree);
  28. }
  29. /**
  30. * 设置事件
  31. */
  32. private void setEvent() {
  33. btn_back.setOnClickListener(new View.OnClickListener() {
  34. @Override
  35. public void onClick(View v) {
  36. PDFCatelogueActivity.this.finish();
  37. }
  38. });
  39. }
  40. /**
  41. * 加载数据
  42. */
  43. private void loadData() {
  44. //从intent中获得传递的数据
  45. Intent intent = getIntent();
  46. List<TreeNodeData> catelogues = (List<TreeNodeData>) intent.getSerializableExtra("catelogues");
  47. //使用RecyclerView加载数据
  48. LinearLayoutManager llm = new LinearLayoutManager(this);
  49. llm.setOrientation(LinearLayoutManager.VERTICAL);
  50. recyclerView.setLayoutManager(llm);
  51. TreeAdapter adapter = new TreeAdapter(this, catelogues);
  52. adapter.setTreeEvent(this);
  53. recyclerView.setAdapter(adapter);
  54. }
  55. /**
  56. * 点击tree item,带回Pdf页码到前一个页面
  57. *
  58. * @param data tree节点数据
  59. */
  60. @Override
  61. public void onSelectTreeNode(TreeNodeData data) {
  62. Intent intent = new Intent();
  63. intent.putExtra("pageNum", data.getPageNum());
  64. setResult(Activity.RESULT_OK, intent);
  65. finish();
  66. }
  67. }

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

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent">
  5. <RelativeLayout
  6. android:id="@+id/rl_top"
  7. android:layout_width="match_parent"
  8. android:layout_height="70dp"
  9. android:layout_alignParentTop="true"
  10. android:background="#03a9f5">
  11. <Button
  12. android:id="@+id/btn_back"
  13. android:layout_width="60dp"
  14. android:layout_height="30dp"
  15. android:layout_alignParentBottom="true"
  16. android:layout_marginLeft="10dp"
  17. android:layout_marginBottom="10dp"
  18. android:background="@drawable/shape_button"
  19. android:text="返回"
  20. android:textColor="#ffffff"
  21. android:textSize="18sp" />
  22. <TextView
  23. android:layout_width="wrap_content"
  24. android:layout_height="wrap_content"
  25. android:layout_alignParentBottom="true"
  26. android:layout_centerHorizontal="true"
  27. android:layout_marginBottom="15dp"
  28. android:text="目录列表"
  29. android:textColor="#ffffff"
  30. android:textSize="18sp" />
  31. </RelativeLayout>
  32. <android.support.v7.widget.RecyclerView
  33. android:id="@+id/rv_tree"
  34. android:layout_width="match_parent"
  35. android:layout_height="match_parent"
  36. android:layout_below="@+id/rl_top" />
  37. </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. /**
  2. * 预览缩略图工具类
  3. *
  4. * 1、pdf页面转为缩略图
  5. * 2、图片缓存管理(仅保存到内存,可使用LruCache,注意空间大小控制)
  6. * 3、多线程管理(线程并发、阻塞、Future任务取消)
  7. *
  8. * 作者:齐行超
  9. * 日期:2019.08.08
  10. */
  11. public class PreviewUtils {
  12. //图片缓存管理
  13. private ImageCache imageCache;
  14. //单例
  15. private static PreviewUtils instance;
  16. //线程池
  17. ExecutorService executorService;
  18. //线程任务集合(可用于取消任务)
  19. HashMap<String, Future> tasks;
  20. /**
  21. * 单例(仅主线程调用,无需做成线程安全的)
  22. *
  23. * @return PreviewUtils实例对象
  24. */
  25. public static PreviewUtils getInstance() {
  26. if (instance == null) {
  27. instance = new PreviewUtils();
  28. }
  29. return instance;
  30. }
  31. /**
  32. * 默认构造函数
  33. */
  34. private PreviewUtils() {
  35. //初始化图片缓存管理对象
  36. imageCache = new ImageCache();
  37. //创建并发线程池(建议最大并发数大于1屏grid item的数量)
  38. executorService = Executors.newFixedThreadPool(20);
  39. //创建线程任务集合,用于取消线程执行
  40. tasks = new HashMap<>();
  41. }
  42. /**
  43. * 从pdf文件中加载图片
  44. *
  45. * @param context 上下文
  46. * @param imageView 图片控件
  47. * @param pdfiumCore pdf核心对象
  48. * @param pdfDocument pdf文档对象
  49. * @param pdfName pdf文件名称
  50. * @param pageNum pdf页码
  51. */
  52. public void loadBitmapFromPdf(final Context context,
  53. final ImageView imageView,
  54. final PdfiumCore pdfiumCore,
  55. final PdfDocument pdfDocument,
  56. final String pdfName,
  57. final int pageNum) {
  58. //判断参数合法性
  59. if (imageView == null || pdfiumCore == null || pdfDocument == null || pageNum < 0) {
  60. return;
  61. }
  62. try {
  63. //缓存key
  64. final String keyPage = pdfName + pageNum;
  65. //为图片控件设置标记
  66. imageView.setTag(keyPage);
  67. Log.i("PreViewUtils", "加载pdf缩略图:" + keyPage);
  68. //获得imageview的尺寸(注意:如果使用正常控件尺寸,太占内存了)
  69. /*int w = imageView.getMeasuredWidth();
  70. int h = imageView.getMeasuredHeight();
  71. final int reqWidth = w == 0 ? UIUtils.dip2px(context,100) : w;
  72. final int reqHeight = h == 0 ? UIUtils.dip2px(context,150) : h;*/
  73. //内存大小= 图片宽度 * 图片高度 * 一个像素占的字节数(RGB_565 所占字节:2)
  74. //注意:如果使用正常控件尺寸,太占内存了,所以此处指定四缩略图看着会模糊一点
  75. final int reqWidth = 100;
  76. final int reqHeight = 150;
  77. //从缓存中取图片
  78. Bitmap bitmap = imageCache.getBitmapFromLruCache(keyPage);
  79. if (bitmap != null) {
  80. imageView.setImageBitmap(bitmap);
  81. return;
  82. }
  83. //使用线程池管理子线程
  84. Future future = executorService.submit(new Runnable() {
  85. @Override
  86. public void run() {
  87. //打开页面(调用renderPageBitmap方法之前,必须确保页面已open,重要)
  88. pdfiumCore.openPage(pdfDocument, pageNum);
  89. //调用native方法,将Pdf页面渲染成图片
  90. final Bitmap bm = Bitmap.createBitmap(reqWidth, reqHeight, Bitmap.Config.RGB_565);
  91. pdfiumCore.renderPageBitmap(pdfDocument, bm, pageNum, 0, 0, reqWidth, reqHeight);
  92. //切回主线程,设置图片
  93. if (bm != null) {
  94. //将图片加入缓存
  95. imageCache.addBitmapToLruCache(keyPage, bm);
  96. //切回主线程加载图片
  97. new Handler(Looper.getMainLooper()).post(new Runnable() {
  98. @Override
  99. public void run() {
  100. if (imageView.getTag().toString().equals(keyPage)) {
  101. imageView.setImageBitmap(bm);
  102. Log.i("PreViewUtils", "加载pdf缩略图:" + keyPage + "......已设置!!");
  103. }
  104. }
  105. });
  106. }
  107. }
  108. });
  109. //将任务添加到集合
  110. tasks.put(keyPage, future);
  111. } catch (Exception ex) {
  112. ex.printStackTrace();
  113. }
  114. }
  115. /**
  116. * 取消从pdf文件中加载图片的任务
  117. *
  118. * @param keyPage 页码
  119. */
  120. public void cancelLoadBitmapFromPdf(String keyPage) {
  121. if (keyPage == null || !tasks.containsKey(keyPage)) {
  122. return;
  123. }
  124. try {
  125. Log.i("PreViewUtils", "取消加载pdf缩略图:" + keyPage);
  126. Future future = tasks.get(keyPage);
  127. if (future != null) {
  128. future.cancel(true);
  129. Log.i("PreViewUtils", "取消加载pdf缩略图:" + keyPage + "......已取消!!");
  130. }
  131. } catch (Exception ex) {
  132. ex.printStackTrace();
  133. }
  134. }
  135. /**
  136. * 获得图片缓存对象
  137. * @return 图片缓存
  138. */
  139. public ImageCache getImageCache(){
  140. return imageCache;
  141. }
  142. /**
  143. * 图片缓存管理
  144. */
  145. public class ImageCache {
  146. //图片缓存
  147. private LruCache<String, Bitmap> lruCache;
  148. //构造函数
  149. public ImageCache() {
  150. //初始化 lruCache
  151. //int maxMemory = (int) Runtime.getRuntime().maxMemory();
  152. //int cacheSize = maxMemory/8;
  153. int cacheSize = 1024 * 1024 * 30;//暂时设定30M
  154. lruCache = new LruCache<String, Bitmap>(cacheSize) {
  155. @Override
  156. protected int sizeOf(String key, Bitmap value) {
  157. return value.getRowBytes() * value.getHeight();
  158. }
  159. };
  160. }
  161. /**
  162. * 从缓存中取图片
  163. * @param key 键
  164. * @return 图片
  165. */
  166. public synchronized Bitmap getBitmapFromLruCache(String key) {
  167. if(lruCache!= null) {
  168. return lruCache.get(key);
  169. }
  170. return null;
  171. }
  172. /**
  173. * 向缓存中加图片
  174. * @param key 键
  175. * @param bitmap 图片
  176. */
  177. public synchronized void addBitmapToLruCache(String key, Bitmap bitmap) {
  178. if (getBitmapFromLruCache(key) == null) {
  179. if (lruCache!= null && bitmap != null)
  180. lruCache.put(key, bitmap);
  181. }
  182. }
  183. /**
  184. * 清空缓存
  185. */
  186. public void clearCache(){
  187. if(lruCache!= null){
  188. lruCache.evictAll();
  189. }
  190. }
  191. }
  192. }

grid列表适配器: GridAdapter

  1. /**
  2. * grid列表适配器
  3. * 作者:齐行超
  4. * 日期:2019.08.08
  5. */
  6. public class GridAdapter extends RecyclerView.Adapter<GridAdapter.GridViewHolder> {
  7. Context context;
  8. PdfiumCore pdfiumCore;
  9. PdfDocument pdfDocument;
  10. String pdfName;
  11. int totalPageNum;
  12. public GridAdapter(Context context, PdfiumCore pdfiumCore, PdfDocument pdfDocument, String pdfName, int totalPageNum) {
  13. this.context = context;
  14. this.pdfiumCore = pdfiumCore;
  15. this.pdfDocument = pdfDocument;
  16. this.pdfName = pdfName;
  17. this.totalPageNum = totalPageNum;
  18. }
  19. @Override
  20. public GridViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  21. View view = LayoutInflater.from(context).inflate(R.layout.grid_item, null);
  22. return new GridViewHolder(view);
  23. }
  24. @Override
  25. public void onBindViewHolder(GridViewHolder holder, int position) {
  26. //设置PDF图片
  27. final int pageNum = position;
  28. PreviewUtils.getInstance().loadBitmapFromPdf(context, holder.iv_page, pdfiumCore, pdfDocument, pdfName, pageNum);
  29. //设置PDF页码
  30. holder.tv_pagenum.setText(String.valueOf(position));
  31. //设置Grid事件
  32. holder.iv_page.setOnClickListener(new View.OnClickListener() {
  33. @Override
  34. public void onClick(View v) {
  35. if(delegate!=null){
  36. delegate.onGridItemClick(pageNum);
  37. }
  38. }
  39. });
  40. return;
  41. }
  42. @Override
  43. public void onViewDetachedFromWindow(GridViewHolder holder) {
  44. super.onViewDetachedFromWindow(holder);
  45. try {
  46. //item不可见时,取消任务
  47. if(holder.iv_page!=null){
  48. PreviewUtils.getInstance().cancelLoadBitmapFromPdf(holder.iv_page.getTag().toString());
  49. }
  50. //item不可见时,释放bitmap (注意:本Demo使用了LruCache缓存来管理图片,此处可注释掉)
  51. /*Drawable drawable = holder.iv_page.getDrawable();
  52. if (drawable != null) {
  53. Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
  54. if (bitmap != null && !bitmap.isRecycled()) {
  55. bitmap.recycle();
  56. bitmap = null;
  57. Log.i("PreViewUtils","销毁pdf缩略图:"+holder.iv_page.getTag().toString());
  58. }
  59. }*/
  60. }catch (Exception ex){
  61. ex.printStackTrace();
  62. }
  63. }
  64. @Override
  65. public int getItemCount() {
  66. return totalPageNum;
  67. }
  68. class GridViewHolder extends RecyclerView.ViewHolder {
  69. ImageView iv_page;
  70. TextView tv_pagenum;
  71. public GridViewHolder(View itemView) {
  72. super(itemView);
  73. iv_page = itemView.findViewById(R.id.iv_page);
  74. tv_pagenum = itemView.findViewById(R.id.tv_pagenum);
  75. }
  76. }
  77. /**
  78. * 接口:Grid事件
  79. */
  80. public interface GridEvent{
  81. /**
  82. * 当选择了某Grid项
  83. * @param position tree节点数据
  84. */
  85. void onGridItemClick(int position);
  86. }
  87. /**
  88. * 设置Grid事件
  89. * @param event Grid事件对象
  90. */
  91. public void setGridEvent(GridEvent event){
  92. this.delegate = event;
  93. }
  94. //Grid事件委托
  95. private GridEvent delegate;
  96. }

PDF预览缩略图页面:PDFPreviewActivity

  1. /**
  2. * UI页面:PDF预览缩略图(注意:此页面,需多关注内存管控)
  3. * <p>
  4. * 1、用于显示Pdf缩略图信息
  5. * 2、点击缩略图,带回Pdf页码到前一个页面
  6. * <p>
  7. * 作者:齐行超
  8. * 日期:2019.08.07
  9. */
  10. public class PDFPreviewActivity extends AppCompatActivity implements GridAdapter.GridEvent {
  11. RecyclerView recyclerView;
  12. Button btn_back;
  13. PdfiumCore pdfiumCore;
  14. PdfDocument pdfDocument;
  15. String assetsFileName;
  16. @Override
  17. protected void onCreate(Bundle savedInstanceState) {
  18. super.onCreate(savedInstanceState);
  19. UIUtils.initWindowStyle(getWindow(), getSupportActionBar());
  20. setContentView(R.layout.activity_preview);
  21. initView();//初始化控件
  22. setEvent();
  23. loadData();
  24. }
  25. /**
  26. * 初始化控件
  27. */
  28. private void initView() {
  29. btn_back = findViewById(R.id.btn_back);
  30. recyclerView = findViewById(R.id.rv_grid);
  31. }
  32. /**
  33. * 设置事件
  34. */
  35. private void setEvent() {
  36. btn_back.setOnClickListener(new View.OnClickListener() {
  37. @Override
  38. public void onClick(View v) {
  39. //回收内存
  40. recycleMemory();
  41. PDFPreviewActivity.this.finish();
  42. }
  43. });
  44. }
  45. /**
  46. * 加载数据
  47. */
  48. private void loadData() {
  49. //加载pdf文件
  50. loadPdfFile();
  51. //获得pdf总页数
  52. int totalCount = pdfiumCore.getPageCount(pdfDocument);
  53. //绑定列表数据
  54. GridAdapter adapter = new GridAdapter(this, pdfiumCore, pdfDocument, assetsFileName, totalCount);
  55. adapter.setGridEvent(this);
  56. recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
  57. recyclerView.setAdapter(adapter);
  58. }
  59. /**
  60. * 加载pdf文件
  61. */
  62. private void loadPdfFile() {
  63. Intent intent = getIntent();
  64. if (intent != null) {
  65. assetsFileName = intent.getStringExtra("AssetsPdf");
  66. if (assetsFileName != null) {
  67. loadAssetsPdfFile(assetsFileName);
  68. } else {
  69. Uri uri = intent.getData();
  70. if (uri != null) {
  71. loadUriPdfFile(uri);
  72. }
  73. }
  74. }
  75. }
  76. /**
  77. * 加载assets中的pdf文件
  78. */
  79. void loadAssetsPdfFile(String assetsFileName) {
  80. try {
  81. File f = FileUtils.fileFromAsset(this, assetsFileName);
  82. ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
  83. pdfiumCore = new PdfiumCore(this);
  84. pdfDocument = pdfiumCore.newDocument(pfd);
  85. } catch (Exception ex) {
  86. ex.printStackTrace();
  87. }
  88. }
  89. /**
  90. * 基于uri加载pdf文件
  91. */
  92. void loadUriPdfFile(Uri uri) {
  93. try {
  94. ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r");
  95. pdfiumCore = new PdfiumCore(this);
  96. pdfDocument = pdfiumCore.newDocument(pfd);
  97. }catch (Exception ex){
  98. ex.printStackTrace();
  99. }
  100. }
  101. /**
  102. * 点击缩略图,带回Pdf页码到前一个页面
  103. *
  104. * @param position 页码
  105. */
  106. @Override
  107. public void onGridItemClick(int position) {
  108. //回收内存
  109. recycleMemory();
  110. //返回前一个页码
  111. Intent intent = new Intent();
  112. intent.putExtra("pageNum", position);
  113. setResult(Activity.RESULT_OK, intent);
  114. finish();
  115. }
  116. /**
  117. * 回收内存
  118. */
  119. private void recycleMemory(){
  120. //关闭pdf对象
  121. if (pdfiumCore != null && pdfDocument != null) {
  122. pdfiumCore.closeDocument(pdfDocument);
  123. pdfiumCore = null;
  124. }
  125. //清空图片缓存,释放内存空间
  126. PreviewUtils.getInstance().getImageCache().clearCache();
  127. }
  128. }

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

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent">
  5. <RelativeLayout
  6. android:id="@+id/rl_top"
  7. android:layout_width="match_parent"
  8. android:layout_height="70dp"
  9. android:layout_alignParentTop="true"
  10. android:background="#03a9f5">
  11. <Button
  12. android:id="@+id/btn_back"
  13. android:layout_width="60dp"
  14. android:layout_height="30dp"
  15. android:layout_alignParentBottom="true"
  16. android:layout_marginLeft="10dp"
  17. android:layout_marginBottom="10dp"
  18. android:background="@drawable/shape_button"
  19. android:text="返回"
  20. android:textColor="#ffffff"
  21. android:textSize="18sp" />
  22. <TextView
  23. android:layout_width="wrap_content"
  24. android:layout_height="wrap_content"
  25. android:layout_alignParentBottom="true"
  26. android:layout_centerHorizontal="true"
  27. android:layout_marginBottom="15dp"
  28. android:text="预览缩略图列表"
  29. android:textColor="#ffffff"
  30. android:textSize="18sp" />
  31. </RelativeLayout>
  32. <android.support.v7.widget.RecyclerView
  33. android:id="@+id/rv_grid"
  34. android:layout_width="match_parent"
  35. android:layout_height="match_parent"
  36. android:layout_below="@+id/rl_top" />
  37. </RelativeLayout>

总结

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

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

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

from:https://www.cnblogs.com/qixingchao/p/11658226.html

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

  1. Android原生PDF功能实现

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

  2. Android原生PDF功能实现:PDF阅读、PDF页面跳转、PDF手势伸缩、PDF目录树、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. pandas 之 数据清洗-缺失值

    Abstract During the course fo doing data analysis and modeling, a significant amount of time is spen ...

  2. WinForm背景图片及图片位置

    设置背景图片:BackgroundImage属性选择对应的图片就可以了. 背景图片随窗体的变化而变化:BackgroundImageLayout属性值设置为Stretch. 窗体放置图片:Pictur ...

  3. PHP在无限分类时注意的一些问题(不保证代码完全正确哦)

    转自:PHP在无限分类时注意的一些问题(http://lxiaoke.cn) (注意:代码使用的是原生PHP,旨在提供解决思路)1 无限分类的查找(获取所有节点) 代码: /** * 无限分类查询,默 ...

  4. TCP 协议简介-阮一峰(转载)

      TCP 协议简介 作者: 阮一峰 日期: 2017年6月 8日 TCP 是互联网核心协议之一,本文介绍它的基础知识. 一.TCP 协议的作用 互联网由一整套协议构成.TCP 只是其中的一层,有着自 ...

  5. 机器学习笔记7:矩阵分解Recommender.Matrix.Factorization

    目录 1矩阵分解概述 1.1用在什么地方 1.2推荐的原理 2矩阵分解的原理 2.1目标函数 2.2 损失函数 2.3 通过梯度下降的方法求得结果 3 代码实现 参考地址: 贪心学院:https:// ...

  6. C++使用通配符查找文件(FindFirstFile)

    调用 FindFirstFile 和 FindNextFile 可搜索某个目录下的相应文件. BOOL SearchFilesByWildcard(WCHAR *wildcardPath) { HAN ...

  7. 定制你的“魅力”报告--Allure

    “人世间是一个大囚笼,每个人都在狱中,砥砺前行.九狱台中的刺,是生活中所要面对的砥砺,是锋利的刺,将自己肉身刺得千疮百孔,将自己的道心刺得千疮百孔.” ---<牧神记·九狱锁道心> 一.简 ...

  8. ShareSDK For Unity集成

    Mob ShareSDK Android - V2.7.10 iOS - V3.5.0 Mob下载:https://github.com/MobClub/New-Unity-For-ShareSDK ...

  9. admin端的专业管理模块功能测试

    1.概述 1.1 测试范围 本次所测试的内容是admin端的专业管理模块. 1.2 测试方法 本次测试采用黑盒子方法进行集成测试. 1.3 测试环境 操作系统:Windows 2012 Server ...

  10. driver.implicitly_wait()与time.sleep()的区别

    implicitly_wait(5)属于隐式等待,5秒钟内只要找到了元素就开始执行,5秒钟后未找到,就超时: time.sleep(5)表示必须等待5秒定位: 如何灵活运用这两种方式: 当某个页面元素 ...