安卓加载展示PDF文件(腾讯X5内核(TbsReaderView)+PDFView)

it2025-08-07  16

最近项目要求加载、展示PDF文件,因为之前项目中已经有X5浏览器了,用X5内核加载PDF文件也简单,就很快写完。但是没想到,测试出来几个坑。这里总结整理下。

读写权限,必须要有

写在前面:引入PDFView,会让包的体积大几M;用其他的,我查了下,有各种问题:如水印加载不全、放大缩小不顺、超过3M的PDF文件就会OOM等。最后选择了PDFView

PDFView 的GitHub 地址 https://github.com/barteksc/AndroidPdfViewer

X5内核(TbsReaderView)+PDFView 实现PDF的加载,已经成功,并应用到项目中了

我的测试用PDF文件,找了3个:几百K、几兆、18M

全部源代码我会放在后面(关于项目包名的,我都去掉了,复制的时候,用自己需要的代码就好),我这里先说我踩的坑或因项目需求而产生的额外操作

项目要求: 1、在线加载,本地不保留PDF文件; 2、不做缓存。如:一个PDF文件有100页,当前打开看到50页,退出去,再次进来,加载完后,从第一页开始; 3、加载PDF文件正常(这点我单独写出来,是因为X5内核启动有问题,和这个“要求”冲突,后面会说)

实现项目要求办法: 1、TbsReaderView加载PDF,是把PDF先下载了,然后加载,但是项目要求本地不保留文件,我想到个折中方法:先下载展示,等退出界面的时候,删掉文件。 这里会有个问题:用户不退出呢?如果用户加载完界面,不杀死APP,直接切换到手机文件夹,就能找到PDF文件了。会有这个问题

2、我看了不少技术博客,没有提及清除PDF文件的,但是我在实现功能的过程中,看到了这个

//存放临时文件的目录。运行后,会在 //Environment.getExternalStorageDirectory().getPath() 的目录下生成.tbs 的文件 bundle.putString( "tempPath", Environment.getExternalStorageDirectory().getPath() )

然后我就找啊找,在文件夹的根目录下(因为后面没有加自定义文件夹的名字),找到了这个文件夹

.TbsReaderTemp包名 如:项目的包名是 com.chen.demo 这个文件夹的名字就是 .TbsReaderTempcom.chen.demo

在 .TbsReaderTemp包名 中,真有 .tbs 文件。进过测试(1、打开PDF文件,定位到某一页;2、退出界面;3、重新打开PDF文件,查看文件定位到的页数;4、重复1、2;5、切换到这个文件夹下;6、删除 .tbs 文件;7、回到APP,重新打开PDF文件,查看文件定位到的页数;8、和步骤3中的情况做对比),删除 .tbs 后,真的可以从第一页开始展示 注意: (1)删掉这个临时文件,重新打开PDF文件时,会慢一点。这个需要自己恒量了。 (2).TbsReaderTemp包名 这个文件夹,在一些手机上,是不可见的。如:我的 华为mate20(安卓10、EMUI 10.1.0 )手机,就找不到,但是通过文件是否存在,可以判断出来

val s: String = "${Environment.getExternalStorageDirectory().getPath()}/.TbsReaderTemp包名/" Log.e("s:",s) val f: File = File(s) if (f.exists()) { ...... }

3、为了解决PDF的正确加载,我遇到了一个巨坑:X5内核,首次安装启动的时候,不一定会加载成功,如果加载失败 result 会变成false,即:tbsReaderView 无法加载PDF

val result = tbsReaderView!!.preOpen("pdf", false) if (result) { //X5内核正常,可以直接展示PDF文件 tbsReaderView!!.openFile(bundle) }

我查了资料,也通过自己大量的卸载安装,得出下面的结论: (1)手机上有腾讯类的产品(如:QQ、微信),可能会在手机上安装X5内核,如果有了内核,其他APP会共用; (2)APP启动时,会调用

fun initX5Core() { QbSdk.setDownloadWithoutWifi(true) QbSdk.initX5Environment(this, object : QbSdk.PreInitCallback { override fun onCoreInitFinished() { Log.d("X5core", "x5加载结束") } override fun onViewInitFinished(p0: Boolean) { Log.d("X5core", "x5加载结束$p0") } }) }

如果是第一次安装启动,有可能 onViewInitFinished 的 p0 值是false,表示X5初始化、加载失败,会导致后面的 tbsReaderView 无法加载PDF文件;安装完APP后,从第二次启动APP开始,每次都是正常的 (3)和网络情况也有关系。如果第一次安装、启动,是在WIFI情况下,onViewInitFinished 小概率会 p0 = false,如果是流量情况下 大概率 p0 = false (4)在 result = false 时,重新调用 initX5Core() 也无法解决第一次安装、启动时 X5 初始化失败的问题

解决办法就是:再引入一个其他的PDF加载控件(如:PDFView),如果X5不行了,就切换用其他的去加载。 为什么不直接用PDFView呢? 经过对比:PDFView在展示页数、界面分页等方面,没有 tbsReaderView 好(PDFView的功能方法其实很多,我还没研究透彻,也可能是我研究不透彻的原因)。tbsReaderView更符合我的要求、使用习惯。



以下是源代码

我这里假设X5相关已经集成成功了(so文件项目已经具备了) 1、

api 'com.tencent.tbs.tbssdk:sdk:43903'

2、Application 的 onCreate中

fun initX5Core() { QbSdk.setDownloadWithoutWifi(true) QbSdk.initX5Environment(this, object : QbSdk.PreInitCallback { override fun onCoreInitFinished() { Log.d("X5core", "x5加载结束") } override fun onViewInitFinished(p0: Boolean) { Log.d("X5core", "x5加载结束$p0") } }) }

3、新建文件 LoadPDFAsyncTask(用于下载PDF文件)

import android.os.AsyncTask; import android.text.TextUtils; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; public class LoadPDFAsyncTask extends AsyncTask<String, Integer, File> { private OnLoadPDFListener onLoadPDFListener; private String filePath = "下载的PDF文件存放的位置"; private String fileName; public LoadPDFAsyncTask(String fileName) { this.fileName = fileName; } public LoadPDFAsyncTask(String fileName, OnLoadPDFListener onLoadPDFListener) { this.fileName = fileName; this.onLoadPDFListener = onLoadPDFListener; } public void setOnLoadPDFListener(OnLoadPDFListener onLoadPDFListener) { this.onLoadPDFListener = onLoadPDFListener; } //这里是开始线程之前执行的,是在UI线程 @Override protected void onPreExecute() { super.onPreExecute(); } //当任务被取消时回调 @Override protected void onCancelled() { super.onCancelled(); if (onLoadPDFListener != null) onLoadPDFListener.onFailureListener(); } //当任务被取消时回调 @Override protected void onCancelled(File file) { super.onCancelled(file); } //子线程中执行 @Override protected File doInBackground(String... strings) { String httpUrl = strings[0]; if (TextUtils.isEmpty(httpUrl)) { if (onLoadPDFListener != null) onLoadPDFListener.onFailureListener(); } File pathFile = new File(filePath); if (!pathFile.exists()) { pathFile.mkdirs(); } File pdfFile = new File(filePath + File.separator + fileName); if (pdfFile.exists()) { return pdfFile; } try { URL url = new URL(httpUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(10000); conn.setReadTimeout(10000); conn.setRequestMethod("GET"); conn.setDoInput(true); conn.connect(); int count = conn.getContentLength(); //文件总大小 字节 int curCount = 0; // 累计下载大小 字节 int curRead = -1;// 每次读取大小 字节 if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { InputStream is = conn.getInputStream(); FileOutputStream fos = new FileOutputStream(pdfFile); byte[] buf = new byte[1024]; while ((curRead = is.read(buf)) != -1) { curCount += curRead; fos.write(buf, 0, curRead); publishProgress(curCount, count); } fos.close(); is.close(); conn.disconnect(); } } catch (Exception e) { e.printStackTrace(); if (onLoadPDFListener != null) onLoadPDFListener.onFailureListener(); return null; } return pdfFile; } //更新进度 @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); if (onLoadPDFListener != null) { onLoadPDFListener.onProgressListener(values[0], values[1]); } } //当任务执行完成是调用,在UI线程 @Override protected void onPostExecute(File file) { super.onPostExecute(file); if (onLoadPDFListener != null) { if (file != null && file.exists()) { onLoadPDFListener.onCompleteListener(file); } else { onLoadPDFListener.onFailureListener(); } } } //下载PDF文件时的回调接口 public interface OnLoadPDFListener { //下载完成 void onCompleteListener(File file); //下载失败 void onFailureListener(); //下载进度 字节 void onProgressListener(Integer curPro, Integer maxPro); } }

到此,TbsReaderView 准备完成,还缺布局和界面中的使用,下面会有。继续看 4、引入 PDFView

//https://github.com/barteksc/AndroidPdfViewer implementation 'com.github.barteksc:android-pdf-viewer:2.8.2'

5、加载PDF界面整体布局 activity_show_online_pdf.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white" android:orientation="vertical"> <!--标题--> <RelativeLayout android:id="@+id/show_online_pdf_title_rl" android:layout_width="match_parent" android:layout_height="@dimen/title_height" android:background="@color/white"> <ImageView android:id="@+id/show_online_pdf_back_iv" android:layout_width="23dp" android:layout_height="24dp" android:layout_centerVertical="true" android:layout_marginStart="15dp" android:paddingStart="5dp" android:paddingTop="5dp" android:paddingEnd="10dp" android:paddingBottom="5dp" android:src="@mipmap/ic_zuo_fanhui" /> <TextView android:id="@+id/show_online_pdf_title_tv" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerInParent="true" android:layout_marginStart="60dp" android:layout_marginEnd="60dp" android:ellipsize="end" android:gravity="center" android:singleLine="true" android:textColor="@color/color_252525" android:textSize="16dp" tools:text="PDF文件名字" /> </RelativeLayout> <!--展示pdf--> <LinearLayout android:id="@+id/show_online_pdf_ll" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> </LinearLayout> <com.github.barteksc.pdfviewer.PDFView android:id="@+id/pdfView" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone" /> </LinearLayout>

准备工作已经完成,开始具体使用 6、ShowOnlinePdfActivity

import android.app.Activity import android.content.Intent import android.os.Bundle import android.os.Environment import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import com.jess.arms.base.BaseActivity import com.jess.arms.di.component.AppComponent import com.jess.arms.utils.ArmsUtils import com.jess.arms.utils.LogUtils import com.jess.arms.utils.showToast import com.tencent.smtt.sdk.TbsReaderView import kotlinx.android.synthetic.main.activity_show_online_pdf.* import java.io.File /** * 展示在线pdf的界面 */ class ShowOnlinePdfActivity : BaseActivity<ShowOnlinePdfPresenter>(), ShowOnlinePdfContract.View, TbsReaderView.ReaderCallback { override fun setupActivityComponent(appComponent: AppComponent) { DaggerShowOnlinePdfComponent //如找不到该类,请编译一下项目 .builder() .appComponent(appComponent) .showOnlinePdfModule(ShowOnlinePdfModule(this)) .build() .inject(this) } companion object { private val ShowOnlinePdf_Url: String = "ShowOnlinePdf_Url" private val ShowOnlinePdf_Name: String = "ShowOnlinePdf_Name" fun enterShowOnlinePdf(activity: Activity, pdfUrl: String, pdfName: String) { var intent: Intent = Intent(activity, ShowOnlinePdfActivity::class.java) intent.putExtra(ShowOnlinePdf_Url, pdfUrl) intent.putExtra(ShowOnlinePdf_Name, pdfName) activity.startActivity(intent) } } override fun initView(savedInstanceState: Bundle?): Int { return R.layout.activity_show_online_pdf //如果你不需要框架帮你设置 setContentView(id) 需要自行设置,请返回 0 } private var pdfUrl: String = "" private var pdfName: String = "" private var tbsReaderView: TbsReaderView? = null override fun initData(savedInstanceState: Bundle?) { initClickListener() show_online_pdf_ll?.visibility = View.VISIBLE pdfView?.visibility = View.GONE pdfUrl = intent.getStringExtra(ShowOnlinePdf_Url).orEmpty() pdfName = intent.getStringExtra(ShowOnlinePdf_Name).orEmpty() show_online_pdf_title_tv?.text = pdfName try { tbsReaderView = TbsReaderView(this, this) tbsReaderView?.apply { show_online_pdf_ll?.addView( this, LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) ) } showLoading() LoadPDFAsyncTask(pdfName, object : LoadPDFAsyncTask.OnLoadPDFListener { override fun onProgressListener(curPro: Int?, maxPro: Int?) { } override fun onFailureListener() { hideLoading() showToast("文件加载失败") } override fun onCompleteListener(file: File?) { hideLoading() try { val bundle = Bundle() bundle.putString("filePath", file!!.path) //存放临时文件的目录。运行后,会在 Environment.getExternalStorageDirectory().getPath() 的目录下生成.tbs...的文件 bundle.putString( "tempPath", Environment.getExternalStorageDirectory().getPath() ) val result = tbsReaderView!!.preOpen("pdf", false) if (result) { //X5内核正常,可以直接展示PDF文件 tbsReaderView!!.openFile(bundle) } else { //这是备选方案,兼容X5内核加载失败,无法展示PDF文件的情况 show_online_pdf_ll?.visibility = View.GONE pdfView?.visibility = View.VISIBLE pdfView.fromFile(file) .defaultPage(0) .enableAnnotationRendering(true) .spacing(0) // in dp .load() } } catch (e: Exception) { e.printStackTrace() hideLoading() showToast("文件加载失败") LogUtils.errorInfo("加载在线PDF e = $e") } } }).execute(pdfUrl) } catch (e: Exception) { e.printStackTrace() hideLoading() } } private fun initClickListener() { show_online_pdf_back_iv?.click { killMyself() } } override fun onCallBackAction(p0: Int?, p1: Any?, p2: Any?) { } override fun onDestroy() { try { //项目要求是不保留PDF文件,所以,在界面销毁的时候删掉。如果项目不做要求,可以不删除。 FileUtil.deleteDirectory(存放PDF文件的文件夹) /** * 以下是删除缓存的。(是否需要删除,根据项目要求来) * * 如:打开一个PDF文件,停到第10页,退出去进来,还是第10页。 * * 加上下面的删除临时文件代码,下次进入,就从0开始 */ val s: String = "${Environment.getExternalStorageDirectory().getPath()}/.TbsReaderTemp包名/" LogUtils.errorInfo("s:${s}") val f: File = File(s) if (f.exists()) { FileUtil.deleteDirectory(s) } } catch (e: Exception) { e.printStackTrace() } super.onDestroy() //很重要 if (tbsReaderView != null) { tbsReaderView?.onStop() tbsReaderView = null } } override fun showLoading() { DialogManager.showLoadingDialog(this) } override fun hideLoading() { DialogManager.dismissAllLoadingDialog() } override fun showMessage(message: String) { ArmsUtils.snackbarText(message) } override fun launchActivity(intent: Intent) { ArmsUtils.startActivity(intent) } override fun killMyself() { finish() } }

7、deleteDirectory 工具

/** * 删除文件夹以及目录下的文件 * @param filePath 被删除目录的文件路径 * @return 目录删除成功返回true,否则返回false */ fun deleteDirectory(filePath: String): Boolean { var filePath = filePath var flag = false //如果filePath不以文件分隔符结尾,自动添加文件分隔符 if (!filePath.endsWith(File.separator)) { filePath += File.separator } val dirFile = File(filePath) if (!dirFile.exists() || !dirFile.isDirectory) { return false } flag = true val files = dirFile.listFiles() //遍历删除文件夹下的所有文件(包括子目录) for (i in files.indices) { if (files[i].isFile) { //删除子文件 flag = deleteFile(files[i].absolutePath) if (!flag) break } else { //删除子目录 flag = deleteDirectory(files[i].absolutePath) if (!flag) break } } return if (!flag) false else dirFile.delete() }
最新回复(0)