kotlin中关于下载查看PDF并更新下载进度问题

本文介绍了一个基于Retrofit的PDF文件下载方法,详细解释了如何使用流式接口进行PDF文件的下载,并将其写入本地。此外,还展示了如何利用腾讯TBS内核自定义WebView来显示本地PDF文件,包括进度条的实现、文件下载进度监听以及日志拦截器的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

定义接口

    @Streaming
    @GET
    fun downLoadPDF(@Url url: String):Call<ResponseBody>

发起请求

 getService(ServerApi::class.java).downLoadPDF(url)
            .enqueue(object : Callback<ResponseBody?> {
                override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
                    logE("call下载PDF异常 ${t.printStackTrace()}")
                }

                override fun onResponse(
                    call: Call<ResponseBody?>,
                    response: retrofit2.Response<ResponseBody?>
                ) {
                  if (response!=null &&response.isSuccessful){
                      logE("Call下载PDF成功 ${response.body()}")
                      var savePdf=writePDF(response.body()!!)
                      if (savePdf){
                          logE("保存成功")
                      }else{
                          logE("保存失败")
                      }
                  }else{
                      logE("call下载PDF失败 ${response.body()}")
                  }
                }
            })

将PDF写入本地并更新进度

 
    fun writePDF(body: ResponseBody): Boolean {
        return try {
            val files = File(PDF_PATH)
            if (!files.exists()) {
                files.mkdirs()
            }
            val futureStudioIconFile = File(PDF_PATH + "direction.pdf")
            var inputStream: InputStream? = null
            var outputStream: OutputStream? = null
            try {
                val fileReader = ByteArray(1024)
                val fileSize: Long = body.contentLength()
                inputStream = body.byteStream()
                outputStream = FileOutputStream(futureStudioIconFile)
                while (true) {
                    val read: Int = inputStream.read(fileReader)
                    if (read == -1) {
                        break
                    }
                    outputStream.write(fileReader, 0, read)
                    length += read
                    var result:Int = (100*length/fileSize).toInt()
                    downLoadPDFProgress.postValue(result)
//                    logE("总大小:${result} 进度:${result}%" )
                }
                outputStream.flush()
                true
            } catch (e: IOException) {
                logE("网络错误${e.printStackTrace()}")
                false
            } finally {
                if (inputStream != null) {
                    inputStream.close()
                }
                if (outputStream != null) {
                    outputStream.close()
                }
            }
        } catch (e: IOException) {
            false
        }
    }

基于腾讯内核自定义webview



import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.widget.AbsoluteLayout;
import android.widget.ProgressBar;

import com.beans.base.R;
import com.tencent.smtt.export.external.interfaces.SslErrorHandler;
import com.tencent.smtt.export.external.interfaces.WebResourceError;
import com.tencent.smtt.export.external.interfaces.WebResourceRequest;
import com.tencent.smtt.export.external.interfaces.WebResourceResponse;
import com.tencent.smtt.sdk.DownloadListener;
import com.tencent.smtt.sdk.WebChromeClient;
import com.tencent.smtt.sdk.WebSettings;
import com.tencent.smtt.sdk.WebView;
import com.tencent.smtt.sdk.WebViewClient;

/**
 * @Author yangtianfu
 * @CreateTime 2020/4/15 19:52
 * @Describe 腾讯的TBS浏览器WebView
 */
public class TBSWebView extends WebView {

    private ProgressBar progressbar;  //进度条
    private int progressHeight = 5;  //进度条的高度,默认10px
    private Context mContext;
    private boolean isShowProgressbar = true;//是否显示进度条

    public TBSWebView(Context context) {
        super(context);
        this.mContext = context;
        initView(context);
    }

    public TBSWebView(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
        this.mContext = context;
        initView(context);
    }

    private void initView(Context context) {
        startTime = System.currentTimeMillis();

        //开启js脚本支持
        WebSettings settings = getSettings();
        settings.setJavaScriptEnabled(true);

        //创建进度条
        progressbar = new ProgressBar(context, null,
                android.R.attr.progressBarStyleHorizontal);
        //设置加载进度条的高度
        progressbar.setLayoutParams(new AbsoluteLayout.LayoutParams(LayoutParams.MATCH_PARENT, progressHeight, 0, 0));

        Drawable drawable = context.getResources().getDrawable(R.drawable.base_webview_progress);
        progressbar.setProgressDrawable(drawable);

        //添加进度到WebView
        addView(progressbar);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            settings.setMixedContentMode(android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
        }
        //适配手机大小
        settings.setUseWideViewPort(true);
        settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
        settings.setLoadWithOverviewMode(true);
        settings.setSupportZoom(true);
        settings.setBuiltInZoomControls(true);
        settings.setDisplayZoomControls(false);
        settings.setGeolocationEnabled(true);
        String dir = context.getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath();
        settings.setGeolocationDatabasePath(dir);
        // enable Web Storage: localStorage, sessionStorage
        settings.setDomStorageEnabled(true);
        // 设置 缓存模式
//        s.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
        // 设置 缓存模式
        settings.setAppCacheMaxSize(1024 * 1024 * 15);// 设置缓冲大小,我设的是15M
        String cacheDirPath = context.getFilesDir().getAbsolutePath() + "cache/";
        settings.setAppCachePath(cacheDirPath);
        // 开启 Application Caches 功能
        settings.setAppCacheEnabled(true);
        // 设置 Application Caches 缓存目录
        settings.setAppCachePath(cacheDirPath);
        settings.setAllowFileAccess(true);
        // 触摸焦点起作用
        requestFocus();
        setWebChromeClient(new WVChromeClient());
        setWebViewClient(new WVClient());

        setDownloadListener(new DownloadListener() {
            @Override
            public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype,
                                        long contentLength) {
                Uri uri = Uri.parse(url);
                Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                mContext.startActivity(intent);
            }
        });

    }

    @Override
    public void loadUrl(String s) {
        if (TextUtils.isEmpty(s)) {
            return;
        }
        //设置cookie
//        SystemUtils.synCookies(AppApplication.getInstance(), s);
        super.loadUrl(s);
    }

    /**
     * 是否显示进度条===默认显示
     * @param flagBar
     */
    public void isShowProgressBar(boolean flagBar) {
        this.isShowProgressbar = flagBar;
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            if (canGoBack()) {
                goBack();
                return true;
            }
        }
        return super.onKeyDown(keyCode, event);
    }

    //进度显示
    private class WVChromeClient extends WebChromeClient {
        @Override
        public void onProgressChanged(WebView view, int newProgress) {
            if (isShowProgressbar) {
                if (newProgress == 100) {
                    progressbar.setVisibility(GONE);
                } else {
                    if (progressbar.getVisibility() == GONE)
                        progressbar.setVisibility(VISIBLE);
                    progressbar.setProgress(newProgress);
                }
            } else {
                progressbar.setVisibility(GONE);
            }
            super.onProgressChanged(view, newProgress);
        }

        @Override
        public void onReceivedTitle(WebView webView, String s) {
            super.onReceivedTitle(webView, s);
            if (mListener != null) {
                mListener.onReceivedTitle(s);
            }

        }
    }

    private long startTime;

    private class WVClient extends WebViewClient {

        @Override
        public void onPageStarted(WebView webView, String s, Bitmap bitmap) {
            super.onPageStarted(webView, s, bitmap);
            if (mListener != null) {
                mListener.onStart();
            }
        }

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
//            LogUtil.e("shouldOverrideUrlLoading===" + url);
            //这里需要写成false,true会导致h5中的二级界面可能无法跳转
            return false;
        }

        @Override
        public WebResourceResponse shouldInterceptRequest(WebView webView, WebResourceRequest webResourceRequest) {
//            synCookies(webView.getContext(), webResourceRequest.getUrl().toString());
            return super.shouldInterceptRequest(webView, webResourceRequest);
        }

        @Override
        public void onReceivedSslError(WebView webView, SslErrorHandler sslErrorHandler, com.tencent.smtt.export.external.interfaces.SslError sslError) {
            super.onReceivedSslError(webView, sslErrorHandler, sslError);
            //设置接收所有证书
            sslErrorHandler.proceed();
        }

        @Override
        public void onReceivedError(WebView webView, WebResourceRequest webResourceRequest, WebResourceError webResourceError) {
            super.onReceivedError(webView, webResourceRequest, webResourceError);
            if (mListener != null) {
                mListener.onLoadError();
            }
        }

        @Override
        public void onPageFinished(WebView view, String url) {
//            LogUtil.e("onPageFinished===" + url);
//            ToastUtils.getInstance().showToast("耗时=" + (System.currentTimeMillis() - startTime));
            progressbar.setVisibility(GONE);
            if (mListener != null) {
                mListener.onPageFinish(view);
            }
            super.onPageFinished(view, url);

        }

    }

    private OnWebViewListener mListener;

    public void setOnWebViewListener(OnWebViewListener listener) {
        this.mListener = listener;
    }

    //进度回调接口
    public interface OnWebViewListener {
        void onStart();

        void onLoadError();

        void onPageFinish(WebView view);

        void onReceivedTitle(String title);
    }
}

进度条

  • base_webview_progress
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/progress">
        <!--需要这样设置颜色,直接通过drawable设置颜色无效果-->
        <clip>
            <shape>
                <solid android:color="#FD674F"/>
            </shape>

        </clip>
    </item>
    <item android:id="@android:id/background">
        <clip>
            <shape>
                <solid android:color="#F6F6F6"/>
            </shape>

        </clip>
    </item>
</layer-list>

weiview打开本地PDF文件

  • 权限定名
// PDF 文件保存路径
val PDF_PATH: String = android.os.Environment.getExternalStorageDirectory().getPath().toString() + "/wey/"
val PDF_PATH_NAME: String = "${PDF_PATH}direction.pdf"
<!--   webView打开PDF     -->
        <RelativeLayout
            android:id="@+id/rl_pdf"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <com.beans.base.widget.TBSWebView
                android:id="@+id/webView"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
            </com.beans.base.widget.TBSWebView>
        </RelativeLayout>

  var tbsReaderView:TbsReaderView? = null
  
   var  readerCallback:TbsReaderView.ReaderCallback = object : TbsReaderView.ReaderCallback {
        override fun onCallBackAction(p0: Int?, p1: Any?, p2: Any?) {

        }
    }
    
  tbsReaderView = TbsReaderView(this,readerCallback)
        mBinding.rlPdf.addView(tbsReaderView,RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT))
   /**
     * 打开本地PDF文件
     */
    private fun openPDF() {
        mBinding.llInfo.visibility = View.GONE
        mBinding.rlPdf.visibility = View.VISIBLE
        var bundle = Bundle()
        bundle.putString("filePath", PDF_PATH_NAME)
        bundle.putString("tempPath", Environment.getExternalStorageDirectory().path)
        var result = tbsReaderView!!.preOpen(parseFormat(parseName()),false)
        if (result){
            tbsReaderView!!.openFile(bundle)
        }else{
            logE("打开PDF失败")
        }
    }
    private fun parseFormat( fileName:String):String{
       return fileName.substring(fileName.lastIndexOf(".") + 1);
    }
    private fun parseName():String{
      var fileName:String
        fileName = PDF_PATH_NAME.substring(PDF_PATH_NAME.lastIndexOf("/")+1)
        return  fileName
    }

添加拦截器获取真实下载进度

上述方法其实只是获取文件写入本地的进度,此时其实文件已经下载完成,在拦截器中将responsebody发送出去的方式来监听进度下载

       if (response.request.url.toString().contains("upload/weyArticle/pdf")){
 //        下载PDF链接  url = http://wuyoudemo.gwmsystem.com:1212/upload/weyArticle/pdf/1538039027155.pdf
 // 将responseBody发送出去用于监听下载进度
            EventBus.getDefault().post(responseBody)
        }

完整监听进度和日志拦截器


import com.aquila.lib.KLog
import okhttp3.*
import okio.Buffer
import org.greenrobot.eventbus.EventBus
import java.io.EOFException
import java.io.IOException
import java.nio.charset.Charset
import java.nio.charset.UnsupportedCharsetException
import java.util.*
import java.util.concurrent.TimeUnit

/***
 * @date 创建时间 2019-05-07 16:00
 * @author 作者: W.YuLong
 * @description  请求日志的打印
 */
class HttpLogInterceptor(val tag: String = "") : Interceptor {


    @Throws(IOException::class)
    @Synchronized
    override fun intercept(chain: Interceptor.Chain): Response {
        val logList = ArrayList<String>()

        val request = chain.request()
        val requestBody = request.body
        logList.add("【${request.method}】 【${request.url}】")

        initHeaders(logList, request.headers)

        if (requestBody != null) {
            val sb = StringBuilder()
            if (requestBody is MultipartBody) {
                for (part in requestBody.parts) {

                    part.headers?.let {
                        for (i in 0 until it.size) {
                            sb.append("【%s = %s】\n", it.name(i), it.value(i))
                        }
                    }

                    val partBody = part.body.contentType()
                    if (partBody != null
                        && when (partBody.type.toLowerCase()) {
                            "video", "image", "file" -> true
                            else -> false
                        }
                    ) {
                        sb.append("*******************************************************************\n")
                        sb.append("***************这里是文件的数据,省略输出**************************\n")
                        sb.append("*******************************************************************\n\n")
                    } else {
                        sb.append(
                            String.format(
                                "【MultipartBody = %s】",
                                formatBodyToString(part.body)
                            )
                        )
                    }
                    sb.append("\n\n")
                }
            } else {
                sb.append(formatBodyToString(requestBody))
            }

            logList.add("【Request Body】: " + sb.toString())
        }
        logList.add(
            String.format(
                "***************** END 【%s】Request ************************************************\n\n",
                request.method
            )
        )

        val startNs = System.nanoTime()
        val response: Response
        try {
            response = chain.proceed(request)
        } catch (e: Exception) {
            logList.add("<-- HTTP FAILED: $e")
            KLog.httpLog(logList, tag)
            e.printStackTrace()
            throw e
        }

        val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)

        val responseBody = response.body
        val contentLength = responseBody!!.contentLength()

        logList.add("【code = ${response.code}】,【url = ${response.request.url}】,【${tookMs}ms】")
        if (response.request.url.toString().contains("upload/weyArticle/pdf")){
 //        下载PDF链接  url = http://wuyoudemo.gwmsystem.com:1212/upload/weyArticle/pdf/1538039027155.pdf
            EventBus.getDefault().post(responseBody)
        }
        initHeaders(logList, response.headers)

        val source = responseBody.source()
        source.request(Long.MAX_VALUE) // Buffer the entire body.
        val buffer = source.buffer

        var charset: Charset? = UTF8
        val contentType = responseBody.contentType()
        if (contentType != null) {
            try {
                charset = contentType.charset(UTF8)
            } catch (e: UnsupportedCharsetException) {
                logList.add("Couldn't decode the response body; charset is likely malformed.")
                logList.add("<-- END HTTP")
                return response
            }

        }

        if (!isPlaintext(buffer)) {
            logList.add("<-- END HTTP (binary " + buffer.size + "-byte body omitted)")
            return response
        }
        if (contentLength != 0L) {
            logList.add("↓↓↓↓↓↓↓↓↓↓↓↓【Response Data】↓↓↓↓↓↓↓↓↓↓↓$contentLength")
            logList.add(buffer.clone().readString(charset!!))
        }
        logList.add("<-- END HTTP (" + buffer.size + " -byte body)")
        KLog.httpLog(logList, tag)
        return response
    }

    private fun formatBodyToString(requestBody: RequestBody): String {
        var result = ""
        try {
            val buffer = Buffer()
            requestBody.writeTo(buffer)
            var charset: Charset? = UTF8
            val contentType = requestBody.contentType()
            if (contentType != null) {
                charset = contentType.charset(UTF8)
            }

            if (isPlaintext(buffer)) {
                result = buffer.readString(charset!!)
            }
        } catch (e: IOException) {
            e.printStackTrace()
            result = "解析错误"
        }

        return result
    }

    private fun initHeaders(logList: ArrayList<String>, headers: Headers) {
        for (i in 0 until headers.size) {
            val name = headers.name(i)
            logList.add(String.format("【%s = %s】", name, headers.value(i)))
        }
    }

    private fun bodyEncoded(headers: Headers): Boolean {
        val contentEncoding = headers.get("Content-Encoding")
        return contentEncoding != null && !contentEncoding.equals("identity", true)
    }

    companion object {
        val UTF8 = Charset.forName("UTF-8")

        /**
         * Returns true if the body in question probably contains human readable text. Uses a small sample
         * of code points to detect unicode control characters commonly used in binary file signatures.
         */
        internal fun isPlaintext(buffer: Buffer): Boolean {
            try {
                val prefix = Buffer()
                val byteCount = if (buffer.size < 64) buffer.size else 64
                buffer.copyTo(prefix, 0, byteCount)
                for (i in 0..15) {
                    if (prefix.exhausted()) {
                        break
                    }
                    val codePoint = prefix.readUtf8CodePoint()
                    if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
                        return false
                    }
                }
                return true
            } catch (e: EOFException) {
                return false // Truncated UTF-8 sequence.
            }

        }
    }


}

子线程写文件到本地并更新UI


    var length = 0L
    //将PDF写入本地
    fun writePDF(body: ResponseBody): Boolean {
        var result = 0
       return  try {
            val files = File(PDF_PATH)
            if (!files.exists()) {
                files.mkdirs()
            }
            val futureStudioIconFile = File(PDF_PATH_NAME)
            var inputStream: InputStream? = null
            var outputStream: OutputStream? = null
            try {
                val fileReader = ByteArray(1024)
                val fileSize: Long = body.contentLength()
                inputStream = body.byteStream()
                outputStream = FileOutputStream(futureStudioIconFile)
                while (true) {
                        val read: Int = inputStream.read(fileReader)
                        if (read == -1) {
                            break
                        }
                        outputStream.write(fileReader, 0, read)
                        length += read
                         result = (100*length/fileSize).toInt()
                        logE("监听PDF写入文件进度 : ${result}%")
                        mBinding.progressBar?.progress = result
                        ownerDashBoardModel.downLoadPDFProgress.postValue(result)
                }
                outputStream.flush()
                true
            } catch (e: IOException) {
                logE("网络错误${e.printStackTrace()}")
                false
            } finally {
                if (inputStream != null) {
                    inputStream.close()
                }
                if (outputStream != null) {
                    outputStream.close()
                }
            }
        } catch (e: IOException) {
            false
        }
    }
    @SuppressLint("AutoDispose")
    @Subscribe(threadMode = ThreadMode.MAIN)
    public fun downLoadPDF(responseBody: ResponseBody){
        logE("收到responseBody.length == ${responseBody.contentLength()}")
        Thread(Runnable {
            writePDF(responseBody)
        }).start()

    }
  • 更新UI
    ownerDashBoardModel.downLoadPDFProgress.observe(this, Observer {
            //监听PDF写入文件进度
            logE("监听PDF写入文件进度*${it}%")
            mBinding.progressBar?.progress = it
            mBinding.tvProgress.text = "下载中 ${it}%"
            if (it == 100){
                mBinding.rlProgress.isClickable = true
                mBinding.tvProgress.text = "查看"
                mBinding.tvProgress.setTextColor(resources.getColor(R.color.color_orange_DAB177))
            }
        })

隐式调用第三方应用打开PDF的适配

 var file = File(PDF_PATH_NAME)
                                    if(!file.exists()) {
                                        ToastUtils.s(this,"文件找不到")
                                        return@OnClickListener
                                    }
                                    val intent = Intent(Intent.ACTION_VIEW)
                                    var uri :Uri
                                    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
                                        uri = Uri.parse(PDF_PATH_NAME)
                                    }else{
                                        uri = Uri.fromFile(file)
                                    }
                                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                                    intent.setDataAndType(uri, "application/pdf")
                                    intent.putExtra("key","pdf")
                                    var componentName:ComponentName = intent.resolveActivity(packageManager)
                                    if(componentName == null){
                                        ToastUtils.s(this,"没有可用的其他程序")
                                        return@OnClickListener
                                    }else{
                                        logE("跳转程序 $componentName")
                                    }
                                    try {
                                        startActivityForResult(intent,PDF_JUMP_CODE)
                                    } catch(e: ActivityNotFoundException) {
                                        Log.e("URLSpan",
                                            "Activity was not found for intent, $intent")
                                    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值