让 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 条评论
登录 后参与评论

相关文章

来自专栏hrscy

使用 Unity 来实现 iOS 原生弹框

如果你有这些疑虑,那么现在你来对地方了。在这篇博客中,我将使用 Unity 创建 iOS 原生弹框。

2703
来自专栏ascii0x03的安全笔记

IE的BHO通过IHTMLDocument2接口获得网页源代码

参考了凤之焚的专栏:http://blog.csdn.net/lion_wing/article/details/769742 但是他的源码有些问题,即IHTM...

3425
来自专栏菩提树下的杨过

Flash在线拍摄用户头象

很多网站在上传用户头象时,除了传统方式上传外,都支持在线摄像头拍照并做简单编辑,完成之后再将图象数据提交到服务端(比如ASP.Net),这几天正好需要这个功能,...

3198
来自专栏菩提树下的杨过

Silverlight Telerik控件学习:GridView双向绑定

做过WinForm数据库开发的人,一定有类似经历:DataGrid绑定后,如果允许行编辑,数据一顿修改后,想批量保存修改后的结果,通常是将DataGrid的所有...

2245
来自专栏林冠宏的技术文章

如何独立开发一个网络请求框架

(原创出处:https://cloud.tencent.com/developer/user/1148436/activities) 目录: 前言   准备...

17410
来自专栏C#

免费开源的DotNet二维码操作组件ThoughtWorks.QRCode(.NET组件介绍之四)

    在生活中有一种东西几乎已经快要成为我们的另一个电子”身份证“,那就是二维码。无论是在软件开发的过程中,还是在普通用户的日常中,几乎都离不开二维码。...

1.5K9
来自专栏向治洪

android 自定义相机

老规矩,先上一下项目地址:GitHub:https://github.com/xiangzhihong/CameraDemo 方式: 调用Camera AP...

9466
来自专栏帘卷西风的专栏

关于cocos2dx之lua使用TableView

在手机游戏的开发中,滚动是一项非常重要的操作,而cocos2dx中使用的最广泛的就属于TableView了,不过由于cocos2dx的接口比较晦涩,所以需要一...

1532
来自专栏菩提树下的杨过

Flash/Flex学习笔记(52):使用TweenLite

TweenLite是第三方出品的专用于各种缓动动画的类库,其性能据说已经超过了Adobe官方的Tween. 从网上找到了一篇中文的说明文档:http://fil...

2055
来自专栏技术小黑屋

WebView处理网页位置请求

随着移动设备的激增,LBS(Location Based Service)已然成为趋势,其最关键的还是获取设备的位置信息。native代码获取位置信息轻轻松松可...

1551

扫码关注云+社区

领取腾讯云代金券