前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >让 Android 的 WebView 支持 type 为 file 的 input,同时支持拍照

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

作者头像
LeoXu
发布2018-08-15 14:31:45
1.5K0
发布2018-08-15 14:31:45
举报
文章被收录于专栏:LeoXu的博客LeoXu的博客

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)...

代码如下:

代码语言:javascript
复制
	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 方法实现

代码语言:javascript
复制
	/**
	 * 弹出对话框,提示拍照或者选择照片文件
	 */
	@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 方法的作用是在拍照以前判断有没有存储。

代码语言:javascript
复制
	/**
	 * 检查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 方法

代码语言:javascript
复制
	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 方法

代码语言:javascript
复制
	/**
	 * 选择文件
	 */
	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 方法

代码语言:javascript
复制
	@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 方法的作用是将拍照生成的图片(不是缩略图)添加到相册,保证后续还能从系统中索取到。

代码语言:javascript
复制
	/**
	 * 解决拍照后在相册中找不到的问题 
	 */
	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);
	}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017/04/23 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档