目录
2、安卓编辑器中建立 Layout ,添加 WebView :
3、extends Activity 将上面的 WebView 赋值给变量(实例化?),并设置其大堆属性,以扩展其功能:
6、重写 WebView 的下载监听器 DownloadListener
参考资料:
1.Android WebView支持下载blob协议文件_Misdirection_XG的博客-CSDN博客_android blob
2. Android Webview实现文件下载功能 - huidaoli - 博客园
3.base64和Blob互相转换_weixin_30776863的博客-CSDN博客
4.Android:你要的WebView与JS交互方式都在这里了 - 百度文库
应用于:html转pdf文件下载之最合理的方法支持中文_jessezappy的博客-CSDN博客
项目需求:
1.网页动态生成 pdf 文件。
2.手机打开以上网页下载 pdf 文件,并用外部程序打开 pdf 文件。
之前的文章:html转pdf文件下载之最合理的方法支持中文_jessezappy的博客-CSDN博客 ,中已经实现了网页动态生成 pdf 文件,通过好多种方式都可在 PC 端实现自动下载动态生成的 pdf 文件,如:
pdf.save("A4.pdf") ;
但我项目原计划是用手机打开这个网页下载 pdf 的,那些方法到了手机端,用 WebView 打开网页后,均无法下载由网页 js 生成的 pdf 文档。
深度研究 jspdf.umd.js 后,发现,其输出 pdf 数据有好多种参数:
- /**
- * Generates the PDF document.
- *
- * If `type` argument is undefined, output is raw body of resulting PDF returned as a string.
- *
- * @param {string} type A string identifying one of the possible output types.
- * Possible values are:
- * 'arraybuffer' -> (ArrayBuffer)
- * 'blob' -> (Blob)
- * 'bloburi'/'bloburl' -> (string)
- * 'datauristring'/'dataurlstring' -> (string)
- * 'datauri'/'dataurl' -> (undefined) -> change location to generated datauristring/dataurlstring
- * 'dataurlnewwindow' -> (window | null | undefined) throws error if global isn't a window object(node)
- * 'pdfobjectnewwindow' -> (window | null) throws error if global isn't a window object(node)
- * 'pdfjsnewwindow' -> (wind | null)
- * @param {Object|string} options An object providing some additional signalling to PDF generator.
- * Possible options are 'filename'.
- * A string can be passed instead of {filename:string} and defaults to 'generated.pdf'
- * @function
- * @instance
- * @returns {string|window|ArrayBuffer|Blob|jsPDF|null|undefined}
- * @memberof jsPDF#
- * @name output
- */
-
-
- var output = API.output = API.__private__.output = SAFE(function output(type, options) {
- options = options || {};
-
- if (typeof options === "string") {
- options = {
- filename: options
- };
- } else {
- options.filename = options.filename || "generated.pdf";
- }
-
- switch (type) {
- case undefined:
- return buildDocument();
-
- case "save":
- API.save(options.filename);
- break;
-
- case "arraybuffer":
- return getArrayBuffer(buildDocument());
-
- case "blob":
- return getBlob(buildDocument());
-
- case "bloburi":
- case "bloburl":
- // Developer is responsible of calling revokeObjectURL
- if (typeof globalObject.URL !== "undefined" && typeof globalObject.URL.createObjectURL === "function") {
- return globalObject.URL && globalObject.URL.createObjectURL(getBlob(buildDocument())) || void 0;
- } else {
- console.warn("bloburl is not supported by your system, because URL.createObjectURL is not supported by your browser.");
- }
-
- break;
-
- case "datauristring":
- case "dataurlstring":
- var dataURI = "";
- var pdfDocument = buildDocument();
-
- try {
- dataURI = btoa(pdfDocument);
- } catch (e) {
- dataURI = btoa(unescape(encodeURIComponent(pdfDocument)));
- }
-
- return "data:application/pdf;filename=" + options.filename + ";base64," + dataURI;
-
- case "pdfobjectnewwindow":
- if (Object.prototype.toString.call(globalObject) === "[object Window]") {
- var pdfObjectUrl = "https://cdnjs.cloudflare.com/ajax/libs/pdfobject/2.1.1/pdfobject.min.js";
- var integrity = ' integrity="sha512-4ze/a9/4jqu+tX9dfOqJYSvyYd5M6qum/3HpCLr+/Jqf0whc37VUbkpNGHR7/8pSnCFw47T1fmIpwBV7UySh3g==" crossorigin="anonymous"';
-
- if (options.pdfObjectUrl) {
- pdfObjectUrl = options.pdfObjectUrl;
- integrity = "";
- }
-
- var htmlForNewWindow = "" + '";
- var nW = globalObject.open();
-
- if (nW !== null) {
- nW.document.write(htmlForNewWindow);
- }
-
- return nW;
- } else {
- throw new Error("The option pdfobjectnewwindow just works in a browser-environment.");
- }
-
- case "pdfjsnewwindow":
- if (Object.prototype.toString.call(globalObject) === "[object Window]") {
- var pdfJsUrl = options.pdfJsUrl || "./examples/PDF.js/web/viewer.html";
- var htmlForPDFjsNewWindow = "" + "" + ' + options.filename + '" width="500px" height="400px" />' + "";
- var PDFjsNewWindow = globalObject.open();
- console.log(htmlForPDFjsNewWindow);
- if (PDFjsNewWindow !== null) {
- PDFjsNewWindow.document.write(htmlForPDFjsNewWindow);
- var scope = this;
-
- PDFjsNewWindow.document.documentElement.querySelector("#pdfViewer").onload = function () {
- PDFjsNewWindow.document.title = options.filename;
- PDFjsNewWindow.document.documentElement.querySelector("#pdfViewer").contentWindow.PDFViewerApplication.open(scope.output("bloburl"));
- };
- }
-
- return PDFjsNewWindow;
- } else {
- throw new Error("The option pdfjsnewwindow just works in a browser-environment.");
- }
-
- case "dataurlnewwindow":
- if (Object.prototype.toString.call(globalObject) === "[object Window]") {
- var htmlForDataURLNewWindow = "" + "" + "" + '.output("datauristring", options) + '">' + "";
-
- var dataURLNewWindow = globalObject.open();
-
- if (dataURLNewWindow !== null) {
- dataURLNewWindow.document.write(htmlForDataURLNewWindow);
- dataURLNewWindow.document.title = options.filename;
- }
-
- if (dataURLNewWindow || typeof safari === "undefined") return dataURLNewWindow;
- } else {
- throw new Error("The option dataurlnewwindow just works in a browser-environment.");
- }
-
- break;
-
- case "datauri":
- case "dataurl":
- return globalObject.document.location.href = this.output("datauristring", options);
-
- default:
- return null;
- }
- });
以上这些参数,在 CallBack 中调用 pdf.Output 时使用:
- var link = document.getElementById('linklink');
- link.target = '_blank';
- //link.href = window.URL.createObjectURL(convertBase64UrlToBlob(pdf.output('datauristring',{filename: 'A4.pdf'})));//140ms Base64 数据转 Blob
- //link.href = window.URL.createObjectURL(pdf.output('blob',{filename: 'A4.pdf'}));//77ms Base64 数据
- link.href = pdf.output('bloburi');//77ms 直接输出Blob 链接
- //link.href =
- //pdf.output('pdfobjectnewwindow',{filename: 'A41.pdf'});//弹出对象窗口,无用
- //link.href = pdf.output('dataurl',{filename: 'A4.pdf'});//数据链接无用
- //pdf.output('pdfjsnewwindow',{filename: 'A42.pdf'});//弹出窗口
- //pdf.output('dataurlnewwindow',{filename: 'A43.pdf'});//弹出窗口,无用
- link.download ="A41.pdf";
- link.text='点击这里下载';
经过对比,发现在不考虑后台生成 pdf 文件的情况下,只有生成 Blob 链接适合用于手机前台下载,但只有华为自带浏览器支持下载 blob 链接而且还必须用华为浏览器打开那个网页才行,QQ、百度等均不支持下载 blob 链接。
jspdf 中原例子里面使用的是 iframe.src = pdf.output('datauristring'); 方式,返回的是 Base64 编码的 pdf 数据,可以使用参考资料 3 中的 convertBase64UrlToBlob 转换为 Blob 链接,分析 jspdf 的 Out 方法后发现其可直接输出 Blob 链接,那么就暂时用不到参考资料 3 中的方法了。
最终决定,由网页 JS 生成 Blob 链接给 A 标签,在手机端点击这个 A 标签下载为 pdf 文档,并打开。
(刚刚写了一大段,按了下 Ctrl+z 就全没了,草稿也没了,是要我重新梳理下吗???)
那么,就重新梳理,整理一下解决方法好了,零碎的分析就不写了。
那么,项目解决方案步骤开始:
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/webfrm"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent" >
-
- <WebView
- android:id="@+id/webshow"
- android:layout_width="match_parent"
- android:layout_height="fill_parent"
- android:layout_alignParentLeft="true" />
-
- RelativeLayout>
WebView mWebView;
onCreate 中赋值 mWebView:
- mWebView=(WebView)findViewById(R.id.webshow);
- setWebStyle();
设置 mWebView 属性及重写其部分动作,扩展功能:
- @SuppressLint("SetJavaScriptEnabled")
- private void setWebStyle() {
- WebSettings webseting = mWebView.getSettings();
-
-
- webseting.setAppCachePath(getApplicationContext().getCacheDir().getAbsolutePath());
- webseting.setUseWideViewPort(true);
- webseting.setLoadWithOverviewMode(true);
- //webseting.setPluginState(WebSettings.PluginState.ON);
- webseting.setDomStorageEnabled(true);//最重要的方法,一定要设置,这就是出不来的主要原因 //webseting.setDomStorageEnabled(true);
-
- webseting.setSupportZoom(true);
- webseting.setDefaultTextEncodingName("utf-8");
- /* 下载blob准备 */
- webseting.setJavaScriptEnabled(true);
- webseting.setJavaScriptCanOpenWindowsAutomatically(true);
-
- /***********************/
- mWebView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);// 去掉底部和右边的滚动条
- mWebView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); // 去掉底部和右边的滚动条
-
- mWebView.requestFocus();
-
- webseting.setCacheMode(WebSettings.LOAD_DEFAULT); // 默认使用缓存
- // webseting.setCacheMode(WebSettings.LOAD_NO_CACHE); //默认不使用缓存!
- webseting.setAppCacheMaxSize(1024*1024*20);//设置缓冲大小,便于第一次缓存字体,否则每次下载字体需时太长。
- String appCacheDir=this.getApplicationContext().getDir("cache",Context.MODE_PRIVATE).getPath();
- webseting.setAppCachePath(appCacheDir);
- webseting.setAllowFileAccess(true);
- webseting.setAppCacheEnabled(true);
- webseting.setCacheMode(WebSettings.LOAD_DEFAULT|WebSettings.LOAD_CACHE_ELSE_NETWORK);
-
- //webview 动作重写
- mWebView.setWebViewClient(new WebViewClient(){
- @Override
- public boolean shouldOverrideUrlLoading(WebView view, String url) {
- view.loadUrl(url);
- return false;// false 显示frameset, true 不显示Frameset ,内嵌页面
- //return true;
- }
- @Override
- public void onPageStarted(WebView view, String url, Bitmap favicon) {
- //有页面跳转时被回调
- //dialog = ProgressDialog.show(webxj.this,null,"数据加载中,请稍侯...");
- super.onPageStarted(view, url,favicon);
- }
- @Override
- public void onPageFinished(WebView view, String url) {
- //页面跳转结束后被回调
- //dialog.dismiss();
- super.onPageFinished(view, url);
- }
- @Override
- public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
- super.onReceivedError(view, errorCode, description, failingUrl);//错误处理+ description
- Toast.makeText(webxj.this, "错误:请检查网络链接! " , Toast.LENGTH_SHORT).show();
- dialog.dismiss();
- new Handler().postDelayed(new Runnable() {
- public void run() {
- mWebView.loadUrl(urlw); //显示等待画面
- }
- }, 500);
- }
-
- });
- //禁用webview右键功能:长按
- mWebView.setOnLongClickListener(new OnLongClickListener(){
- public boolean onLongClick(View v) {
- // TODO Auto-generated method stub
- return true;
- }
- });
-
- //blob
- //下载支持,A 标签内需 download
- mWebView.setDownloadListener(new MyWebViewDownLoadListener()); // 设置 WebView 下载监听器
- mWebView.addJavascriptInterface(mDownloadBlobFileJSInterface, "Android"); // Blob 下载 js 定义
- mDownloadBlobFileJSInterface.setDownloadGifSuccessListener(new openDownloadfile()); // Blob 下载完成后监听器
- }
注:上面步骤 3 中已正确设置下载完成监听器:
mDownloadBlobFileJSInterface.setDownloadGifSuccessListener(new openDownloadfile()); //下载完成后监听器
参考资料 1 中的:
mDownloadBlobFileJSInterface.setDownloadGifSuccessListener(absolutePath -> Toast.makeText(MainActivity.this,"下载成功,在Download目录下",Toast.LENGTH_SHORT).show());
这句有问题,absolutePath 未定义,怀疑其是抄来的代码,没抄全,分析发现其为定义下载完成监听器,那么,就按照重写 DownloadListener 的定义,自己定义一个监听器,用于下载完成后的处理,如调用外部程序打开:
- /* 定义下载完成事件监听器 */
- private class openDownloadfile implements DownloadGifSuccessListener {
- public void ondownloadGifSuccess(String gifFile){
- String fileName=gifFile.substring(gifFile.lastIndexOf("/")+1);
- String directory=gifFile.substring(0,gifFile.lastIndexOf("/")+1);
- // System.out.println("11.下载成功,fileName:"+fileName);
- // System.out.println("12.下载成功,directory:"+directory);
-
- File File = new File(directory,fileName);
- Intent intent = getFileIntent(File);
- startActivity(intent); //调用万部程序打开
- // System.out.println("13.下载成功,在Download目录下:"+gifFile);
- }
- }
-
- public Intent getFileIntent(File file){
- // Uri uri = Uri.parse("http://m.ql18.com.cn/hpf10/1.pdf");
- Uri uri = Uri.fromFile(file);
- String type = getMIMEType(file);
- // Log.e("tag", "type="+type);
- Intent intent = new Intent("android.intent.action.VIEW");
- intent.addCategory("android.intent.category.DEFAULT");
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.setDataAndType(uri, type);
- return intent;
- }
-
- private String getMIMEType(File f){
- String type="";
- String fName=f.getName();
- // * 取得扩展名 * /
- String end=fName.substring(fName.lastIndexOf(".")+1,fName.length());//.toLowerCase();
- Locale loc = Locale.getDefault();//可以去掉这步
- end=end.toLowerCase(loc);
- // * 依扩展名的类型决定MimeType * /
- if(end.equals("pdf")){
- type = "application/pdf";//
- }
- else if(end.equals("m4a")||end.equals("mp3")||end.equals("mid")||
- end.equals("xmf")||end.equals("ogg")||end.equals("wav")){
- type = "audio/*";
- }
- else if(end.equals("3gp")||end.equals("mp4")){
- type = "video/*";
- }
- else if(end.equals("jpg")||end.equals("gif")||end.equals("png")||
- end.equals("jpeg")||end.equals("bmp")){
- type = "image/*";
- }
- else if(end.equals("apk")){
- // * android.permission.INSTALL_PACKAGES * /
- type = "application/vnd.android.package-archive";
- }
- // else if(end.equals("pptx")||end.equals("ppt")){
- // type = "application/vnd.ms-powerpoint";
- // }else if(end.equals("docx")||end.equals("doc")){
- // type = "application/vnd.ms-word";
- // }else if(end.equals("xlsx")||end.equals("xls")){
- // type = "application/vnd.ms-excel";
- // }
- else{
- // // * 如果无法直接打开,就跳出软件列表给用户选择 * /
- type="*/*"; //因注释多加了一个空格
- }
- return type;
- }
其中 getFileIntent 和 getMIMEType 来自参考资料 2 。
参考资料 1 中的 DownloadBlobFileJSInterface 可直接使用,等下 获取 Blob 文件名时才需修改其转换 Base64 及保存过程。
参考资料 1 中,是无法取得 Blob 文件名及类型的,那么就要用个变通的方法:调用网页预设的 JS 中的 getDname 函数,获取文件名,文件名已事先由 .html 方法的回调函数中预设。
注:下载监听器已在 setWebStyle(); 中设置:
mWebView.setDownloadListener(new MyWebViewDownLoadListener());
在下载监听器中判断是否为 Blob 链接,不是则启用参考资料 2 中的直接下载进程。
是的话,则启用参考资料 1 中的 Blob 下载进程,并在下载前调用网页中的 getDname js 函数获取预设的 Blob 文件名:
- // 改装blob下载
- private class MyWebViewDownLoadListener implements DownloadListener {
- public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype,long contentLength){
- if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
- Toast.makeText(webxj.this, "需要SD卡。", Toast.LENGTH_SHORT).show();
- return;
- }
- //Toast.makeText(webxj.this,"Url:"+url.lastIndexOf("data:app"), Toast.LENGTH_SHORT).show();
- // System.out.println("Url:"+url);
- // Log.e("tag", "Url="+url);
- if(url.indexOf("blob:http")==0){
- urlP=url;
- // System.out.println("打开:"+urlP);
- //mWebView.loadUrl("javascript:calljs();");
- mWebView.evaluateJavascript("javascript:getDname('"+urlP+"')", new ValueCallback
(){ - public void onReceiveValue(String value){
- // System.out.println("JS返回::"+value);
- pdffn=value.replace("\"","");
- // System.out.println("pdffn:"+pdffn);
- File directory= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
- File file=new File(directory,pdffn);
- if(file.exists()){
- // Log.e("tag", "The file has already exists."); //文件已存在
- System.out.println("文件已存在:"+directory+pdffn);
- Toast.makeText(webxj.this, "文件已保存:"+directory+"/"+pdffn,Toast.LENGTH_LONG).show();
- File File = new File(directory,pdffn);
- Intent intent = getFileIntent(File);
- startActivity(intent);
- }else{
- mWebView.loadUrl(DownloadBlobFileJSInterface.getBase64StringFromBlobUrl(urlP));
- }
- }
- });
- //mWebView.loadUrl(DownloadBlobFileJSInterface.getBase64StringFromBlobUrl(url));
- }else{
- DownloaderTask task=new DownloaderTask();
- task.execute(url);
- }
- }
- }
注意:网页 JS 返回的字符串前后有双引号 " 包裹,需要将其去除。
其中, DownloaderTask 为参考资料 2 的直接下载方法,注意,A 标签中必须加上 download ,否则报错:
<a href="./upload/dll.rar" download>dll.rara>
然后,在 onCreate 打开动态生成 pdf 文件的页面即可:
- double rndX=Math.random();
- urlx="?v="+String.valueOf(rndX);
- urlm=getString(R.string.mainUrl);//
- mWebView=(WebView)findViewById(R.id.webshow);
- setWebStyle();
- mWebView.loadUrl(urlm + urlx);
- // 定义于 extends Activity 与 onCreate 之间
- WebView mWebView; // WebView 实例变量
- private String urlx=new String(""); //存放调用首页的参数等,如 double rndX=Math.random(); urlx="?v="+String.valueOf(rndX);
-
- private String urlw=new String("file:///android_asset/index.html"); //存放等待页面url
- private String urlm;//存放首页菜单的页面
-
- DownloadBlobFileJSInterface mDownloadBlobFileJSInterface = new DownloadBlobFileJSInterface(this); // Blob 下载 js 接口定义
- private String urlP=new String(""); // 下载链接全局变量
- private static String pdffn=new String(""); // Blob 文件名全局变量
其中 pdffn 用于存放 Blob 链接预设文件名,因为我懒得其动 DownloadBlobFileJSInterface 的输入变量,就用这个传递给其中的 convertToGifAndProcess 方法,用做保存文件名:
- /**
- * 转换成file
- * @param base64
- */
- private void convertToGifAndProcess(String base64) {
- String fileName =pdffn;// UUID.randomUUID().toString() + ".pdf";
- // System.out.println("3.convertToGifAndProcess:fileName:" + fileName);
- File directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
- File gifFile = new File(directory, fileName);
- // System.out.println("4.convertToGifAndProcess:gifFile:" + gifFile);
- // Log.e("tag", "type=convertToGifAndProcess" + gifFile);
- saveFileToPath(base64, gifFile);
- // System.out.println("7.convertToGifAndProcess:mDownloadGifSuccessListener:" + mDownloadGifSuccessListener);
- // System.out.println("8.convertToGifAndProcess gifFile:" + gifFile);
- if (mDownloadGifSuccessListener != null) {
- // System.out.println("10.mDownloadGifSuccessListener 不为空到这里:" + mDownloadGifSuccessListener);
- mDownloadGifSuccessListener.ondownloadGifSuccess(gifFile.getAbsolutePath());
- }
- }
-
- /**
- * 保存文件
- * @param base64
- * @param gifFilePath
- */
- private void saveFileToPath(String base64, File gifFilePath) {
- // System.out.println("5.saveFileToPath gifFilePath.getAbsolutePath():" + gifFilePath.getAbsolutePath());
- try {
- byte[] fileBytes = Base64.decode(base64.replaceFirst(
- "data:application/pdf;base64,", ""), 0);
- FileOutputStream os = new FileOutputStream(gifFilePath, false);
- os.write(fileBytes);
- os.flush();
- os.close();
- Toast.makeText(mContext, "文件已保存:"+gifFilePath,Toast.LENGTH_LONG).show();
- // Log.e("tag", "type=saveFileToPath" + gifFilePath);
- // System.out.println("6.saveFileToPath FileOutputStream gifFilePath:" + gifFilePath);
-
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
至此,完整的流程及关键代码完成,下面是运行后的样例:
’-------------------------------
此记!
接下来还要解决下手机端显示宽度问题