专栏首页LeoXu的博客让 Android 的 WebView 支持 type 为 file 的 input,同时支持拍照

让 Android 的 WebView 支持 type 为 file 的 input,同时支持拍照

Android 的 WebView 组件默认是不启用 type 为 file 的 input 的,需要在代码中做一些类似 hack 的编码(因为解决问题的目标对象的方法都是加了@hide注解的)才能召唤神龙。

目标对象:WebChromeClient

实例化一个目标对象,并重写它的几个隐藏方法(针对不同的Android系统版本,方法名和入参都不一样,所以方法有多个),然后将目标对象作为参数传递给 WebView 对象的 setWebChromeClient 方法。

目标对象隐藏方法的重写

// For Android 3.0+

public void openFileChooser( ValueCallback uploadMsg, String acceptType )...

// For Android 3.0+

public void openFileChooser(ValueCallback uploadMsg)...

//For Android 4.1

public void openFileChooser(ValueCallback uploadMsg, String acceptType, String capture)...

// For Lollipop 5.0+ Devices

public boolean onShowFileChooser(WebView mWebView, ValueCallback<Uri[]> filePathCallback,  WebChromeClient.FileChooserParams fileChooserParams)...

代码如下:

	private WebChromeClient mWebChromeClient = new WebChromeClient(){
		
		// For Android 3.0+
		@SuppressWarnings({ "rawtypes" })
		public void openFileChooser( ValueCallback uploadMsg, String acceptType ) {
			vCbFileChooser = uploadMsg;
			/*Intent i = new Intent(Intent.ACTION_GET_CONTENT);
			i.addCategory(Intent.CATEGORY_OPENABLE);
			i.setType("image/*");
			MainActivity.this.startActivityForResult(
					Intent.createChooser(i,"文件选择"), 
					FILECHOOSER_RESULTCODE
			);*/
			selPic();
		}
		
		// For Android 3.0+
		@SuppressWarnings({ "unused", "rawtypes" })
		public void openFileChooser(ValueCallback uploadMsg) {
			openFileChooser(uploadMsg, "");
		}
		
		//For Android 4.1
		@SuppressWarnings({ "unused", "rawtypes" })
		public void openFileChooser(ValueCallback uploadMsg, String acceptType, String capture){
			openFileChooser(uploadMsg, acceptType);
		}
		
		// For Lollipop 5.0+ Devices
		@SuppressWarnings("unchecked")
		@TargetApi(Build.VERSION_CODES.LOLLIPOP)
		public boolean onShowFileChooser(
				WebView mWebView, ValueCallback<Uri[]> filePathCallback, 
				WebChromeClient.FileChooserParams fileChooserParams
		) {
			if (vCbFileChooser != null) {
				vCbFileChooser.onReceiveValue(null);
				vCbFileChooser = null;
			}
			
			vCbFileChooser = filePathCallback;
			
			selPic();
			
			return true;
		}
	};

在上面的代码中:

    1、所有被重写的方法最后都会调用 selPic 方法,这个方法会显示一个对话框,让用户选择是拍照选取照片还是直接从已保存的文件中选取图片。

    2、vCbFileChooser 变量维持着向页面回传值的 ValueCallback 对象,直到 onActivityResult。

selPic 方法实现

	/**
	 * 弹出对话框,提示拍照或者选择照片文件
	 */
	@SuppressWarnings("unused")
	protected final void selPic() {
		if (!checkSDcard()){return;}
		String[] selectPicTypeStr = { "拍照","选择照片" };
		AlertDialog alertDialog = new AlertDialog.Builder(this)
			.setItems(
				selectPicTypeStr,
				new DialogInterface.OnClickListener() {
					
					@Override
					public void onClick(DialogInterface dialog, int which) {
						switch (which) {
							case 0://拍照
								chkPrivBeforeTakePhoto();
								break;
							case 1://选择图片文件
								choosePicFile();
								break;
							default:
								break;
						}
						
					}
				}
			).setOnCancelListener(
				new DialogInterface.OnCancelListener() {
					
					@SuppressWarnings("unchecked")
					@Override
					public void onCancel(DialogInterface dialog) {
						if (null != vCbFileChooser) {
							vCbFileChooser.onReceiveValue(null);
							vCbFileChooser = null;
						}
					}
				}
			).show();
	}

上述代码:

    1、chkPrivBeforeTakePhoto 方法执行拍照选取流程(之所以这样取名,是因为在拍照之前,还要考虑到Android 6.0以上版本权限系统机制的变化);

    2、choosePicFile 方法执行直接从已保存文件中选取图片的流程;

   3、如果两中流程都没有,而是执行了取消操作(按下返回键或者点击了界面空白处),那么 vCbFileChooser 变量也必须调用 onReceivValue 方法回传空值,保证type=file的input能反复使用。

    4、checkSDcard 方法的作用是在拍照以前判断有没有存储。

	/**
	 * 检查SD卡是否存在
	 */
	public final boolean checkSDcard() {
		boolean flag = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
		if(!flag){Toast.makeText(this, "请插入手机存储卡再使用本功能", Toast.LENGTH_SHORT).show();}
		return flag;
	}

chkPrivBeforeTakePhoto 方法

	private static final int PERMISSIONS_REQUEST_CODE_TAKE_PHOTO = 1;
	@SuppressWarnings("unchecked")
	private void chkPrivBeforeTakePhoto(){
		if(
				ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
				ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
		) {
			if (null != vCbFileChooser) {
				vCbFileChooser.onReceiveValue(null);
				vCbFileChooser = null;
			}
			new AlertDialog
				.Builder(this)
				.setTitle("提示信息")
				.setMessage("该功能需要您接受应用对一些关键权限(拍照)的申请,如之前拒绝过,可到手机系统的应用管理授权设置界面再次设置。")
				.setPositiveButton("确认", new OnClickListener() {
		
					@Override
					public void onClick(DialogInterface dialog, int which) {
						ActivityCompat.requestPermissions(MainActivity.this, new String[]{
								Manifest.permission.CAMERA,
								Manifest.permission.WRITE_EXTERNAL_STORAGE
						}, PERMISSIONS_REQUEST_CODE_TAKE_PHOTO);
					}
				})
				.show();
		} else {
			chooseTakePhoto();
		}
	}
	
	private void chooseTakePhoto(){
		pathTakePhoto = Environment.getExternalStorageDirectory().getPath()
				+ "/mbossclient/camera/temp/"
				+ (System.currentTimeMillis() + ".jpg");
		File vFile = new File(pathTakePhoto);
		if (!vFile.exists()) {//必须确保文件夹路径存在,否则拍照后无法完成回调
			File vDirPath = vFile.getParentFile();
			vDirPath.mkdirs();
		} else {
			if (vFile.exists()) {
				vFile.delete();
			}
		}
		
		Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
		uriTakePhoto = Uri.fromFile(vFile);
		intent.putExtra(MediaStore.EXTRA_OUTPUT, uriTakePhoto);
		startActivityForResult(intent, TAKEPHOTO_RESULTCODE);
	}

上述代码:

    1、Android 6.0 及以上版本都需要就权限进行询问操作;

    2、chooseTakePhoto 方法执行实际的拍照流程;

    3、TAKEPHOTO_RESULTCODE 用于在 onActivityResult 方法中识别出是执行了拍照选取的流程。

choosePicFile 方法

	/**
	 * 选择文件
	 */
	private void choosePicFile(){
		Intent i = new Intent(Intent.ACTION_GET_CONTENT);
		i.addCategory(Intent.CATEGORY_OPENABLE);
		i.setType("image/*");
		MainActivity.this.startActivityForResult(
				Intent.createChooser(i,"文件选择"), 
				FILECHOOSER_RESULTCODE
		);
	}

FILECHOOSER_RESULTCODE 用于在onActivityResult方法中识别出是执行了从已保存文件中选取图片文件的流程。

onActivityResult 方法

	@SuppressLint("NewApi")
	@SuppressWarnings("unchecked")
	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
		if (requestCode == FILECHOOSER_RESULTCODE) {//从文件选择器选择照片
			if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
				if(null == vCbFileChooser) {return;}
				vCbFileChooser.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, intent));
				vCbFileChooser = null;
			} else {
				if(null == vCbFileChooser) {return;}
				Uri result = (intent == null || resultCode != RESULT_OK)? null:intent.getData();
				vCbFileChooser.onReceiveValue(result);
				vCbFileChooser = null;
			}
		} else if(requestCode == TAKEPHOTO_RESULTCODE){
			if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
			    if(null == vCbFileChooser) {return;}
				if(null == uriTakePhoto) {
					vCbFileChooser.onReceiveValue(null);
					vCbFileChooser = null;
					return;
				}
				addImageGallery(pathTakePhoto);
				Uri[] uris = new Uri[1];
				uris[0] = uriTakePhoto;
				vCbFileChooser.onReceiveValue(uris);
				vCbFileChooser = null;
				uriTakePhoto = null;
				pathTakePhoto = null;
		    } else {
				if(null == vCbFileChooser) {return;}
				if(null == uriTakePhoto) {
					vCbFileChooser.onReceiveValue(null);
					vCbFileChooser = null;
					return;
				}
				addImageGallery(pathTakePhoto);
				vCbFileChooser.onReceiveValue(uriTakePhoto);
				vCbFileChooser = null;
				uriTakePhoto = null;
				pathTakePhoto = null;
			}
		}
		
		super.onActivityResult(requestCode, resultCode, intent);
	}

上述代码:

    1、以Android Lollipop版本为届,低于该版本的系统与等于或高于该版本的系统处理方式不一样,表面上看主要是使用API获取uri数据的方法不同;

    2、无论取没取到 uri 数据,只要 vCbFileChooser 变量不为空,都必须调用一次 onReceiveValue 方法,而且这之后要将它以及相关变量置为null,以保证type=file的input能反复使用。

    3、addImageGallery 方法的作用是将拍照生成的图片(不是缩略图)添加到相册,保证后续还能从系统中索取到。

	/**
	 * 解决拍照后在相册中找不到的问题 
	 */
	private void addImageGallery(String path) {
		if (null == path || "".equals(path)) {
			return;
		}
		File file = new File(pathTakePhoto);
		ContentValues values = new ContentValues();
		values.put(MediaStore.Images.Media.DATA, file.getAbsolutePath());
		values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
		getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
	}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • [翻译]Ext JS 教程-ExtJS 4中的数据包(Package)

    数据包(data package)是用来加载和保存你应用程序中的数据的东西,包含41个类,但是其中有三个类比所有其他类更加重要——Model,Store和Ext...

    LeoXu
  • iText的使用

    首先要getInstance并open一个Document对象,该对象也就代表了这个文件:

    LeoXu
  • SpringMVC 项目中 Quartz 定时任务的设置纪要

    项目使用的是SpringMVC, 以前就已经集成了 1.x 版本的 Quartz,有专门的配置文件定义了需要的bean。

    LeoXu
  • 关于蘑菇数据集的探索分析数据集描述读取数据集直观分析——颜色鲜艳的蘑菇都有毒?相关性分析——判断各指标与毒性相关性模型训练——使用决策树模型

    数据集描述 来源于kaggle的蘑菇数据集,包括毒性,大小,表面,颜色等,所有数据均为字符串类型,分析毒性与其他属性的关系 读取数据集 dataset = pd...

    月见樽
  • redis 存储对

    如果需要用到Redis存储List对象,而list又不需要进行操作,可以按照MC的方式进行存储,不过Jedis之类的客户端没有提供API,可以有两种思路实现: ...

    半条命专刊
  • Flutter 1.17 对列表图片的优化解析

    相信 Flutter 的开发者应该遇到过,对于大量数据的列表进行图片加载时,在 iOS 上很容易出现 OOM的问题,这是因为 Flutter 特殊的图片加载流程...

    恋猫
  • cors跨域探讨

    前端跨域方案很多,jsonp、iframe等等,但是个人觉得,最正宗,最无损的跨域方式还是CORS。 CORS(Cross-origin resource sh...

    用户1394570
  • 爬虫 0030~ requests利刃出鞘

    requests第三方封装的模块,通过简化请求和响应数据的处理,简化繁琐的开发步骤和处理逻辑、统一不同请求的编码风格以及高效的数据处理特性等而风靡于爬虫市场。

    大牧莫邪
  • 【Jfinal源码】第一章 com.jfinal.core.JFinalFilter(1)

    前言: 首先在gitosc获取到jfinal的源码,本学习笔记使用的是jfinal2.2版本。 ---- 从web.xml开始,我们去学习jfinal是怎么从...

    冷冷
  • Java 程序该怎么优化?技巧篇

    研发过程中,String 的 API 用的应该是最多,创建 String 对象,以及字符串分割处理那是常有的事儿。

    一猿小讲

扫码关注云+社区

领取腾讯云代金券