目录

1、已有可实用的动态 pdf 生成方案,

2、安卓编辑器中建立 Layout ,添加 WebView :

3、extends Activity 将上面的 WebView 赋值给变量(实例化?),并设置其大堆属性,以扩展其功能:

4、引入参考资料 1 的方法,定义下载完成监听器

5、获取 Blob 链接文件名及类型

6、重写 WebView 的下载监听器 DownloadListener 

7、其中的几个变量及其他说明

参考资料:

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 = "" + "" + '' + "

";

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 就全没了,草稿也没了,是要我重新梳理下吗???)

那么,就重新梳理,整理一下解决方法好了,零碎的分析就不写了。

那么,项目解决方案步骤开始:

1、已有可实用的动态 pdf 生成方案,详见:

html转pdf文件下载之最合理的方法支持中文_jessezappy的博客-CSDN博客

2、安卓编辑器中建立 Layout ,添加 WebView :

android:id="@+id/webfrm"

android:layout_width="fill_parent"

android:layout_height="fill_parent" >

android:id="@+id/webshow"

android:layout_width="match_parent"

android:layout_height="fill_parent"

android:layout_alignParentLeft="true" />

3、extends Activity 将上面的 WebView 赋值给变量(实例化?),并设置其大堆属性,以扩展其功能:

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 下载完成后监听器

}

4、引入参考资料 1 的方法,定义下载完成监听器

注:上面步骤 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 及保存过程。

5、获取 Blob 链接文件名及类型

参考资料 1 中,是无法取得 Blob 文件名及类型的,那么就要用个变通的方法:调用网页预设的 JS 中的 getDname 函数,获取文件名,文件名已事先由 .html 方法的回调函数中预设。

6、重写 WebView 的下载监听器 DownloadListener 

注:下载监听器已在 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 ,否则报错:

dll.rar

 然后,在 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);

7、其中的几个变量及其他说明

// 定义于 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();

}

}

至此,完整的流程及关键代码完成,下面是运行后的样例:

 

 

’-------------------------------

此记!

接下来还要解决下手机端显示宽度问题

好文推荐

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: