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

Posted 夜尽天明89

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了安卓加载展示PDF文件(腾讯X5内核(TbsReaderView)+PDFView)相关的知识,希望对你有一定的参考价值。

最近项目要求加载、展示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()

以上是关于安卓加载展示PDF文件(腾讯X5内核(TbsReaderView)+PDFView)的主要内容,如果未能解决你的问题,请参考以下文章

集成腾讯TBS x5浏览器内核笔记

安卓x5内核加载失败怎么办 导致失败的原因是啥呢

电视机 正在加载腾讯x5内核

Android 腾讯X5内核WebView加载失败原因

腾讯X5内核集成-解决遇到的问题(ABI平台匹配加载理解)

Android 使用腾讯X5内核WebView和原生做交互