一、目标
1、解决从纯H5开发且带拍照功能的App迁移至Android平台时,保证拍照和裁剪功能正常;
2、解析实现过程中碰到的各种问题;
二、实现效果
为了达成上述目标,先大体介绍下思路:
1)Android平台下支持h5的input拍照标签,但是需要在Android侧对拍照过程做一些特殊处理才能正常获取到此拍照图片;
2)Android平台存在版本碎片化问题,不同版本的拍照接口不同,需要做好兼容处理;
三、步骤
1、拍照需要用到Activity对象这个上下文,所以在初始化WebView时,需要把Activity对象(this)引用,传递给WebView,继而传递给WebView中的WebCromeClient。如下是WebView中的代码(WebChromeClient中的代码详见github):
/**
* 绑定activity
*
* @param activity
*/
public void bind(Activity activity)
{
// this.activity = activity;
this.webChromeClient.bind(activity);
}
2、Android中的拍照核心代码:
1)WebChromeClient通用拍照代码:
/**
* 拍照
*/
private void takePicture()
{
try
{
cameraFile = new File(path, SystemClock.currentThreadTimeMillis() + ".jpg");
Log.i(TAG, "camera file path=" + cameraFile.toString());
cameraUri = Uri.fromFile(cameraFile);
Log.i(TAG, "camera uri=" + cameraUri.toString());
PictureUtil.takePicture(this.activity, cameraUri, Constant.TAKE_PICTURE_CODE);
}
catch (Throwable t)
{
Log.e(TAG, "ERROR:", t);
}
}
2)上述只是封装了一个Uri,真正的拍照代码在PictureUtil中:
/**
* 拍照
*
* @param activity 当前activity
* @param imageUri 拍照后照片存储路径
* @param requestCode 调用系统相机请求码
*/
public static void takePicture(Activity activity, Uri imageUri, int requestCode)
{
Log.i(TAG, "start to take a picture.");
// 调用系统相机
Intent cameraIntent = new Intent();
cameraIntent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
activity.startActivityForResult(cameraIntent, requestCode);
Log.i(TAG, "end to take a picture.");
}
这里是调用了拍照的Activity,拍照完成后,需要在指定的Activity中,根据约定的requestCode来接收拍照完成后的结果。
3)上述拍照传入的是MainActivity,其处理结果应在onActivityResult中,代码如下:
@Override
protected void onActivityResult(int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
// 拍照
case Constant.TAKE_PICTURE_CODE: {
Log.i(TAG, "camera result:" + resultCode);
Log.i(TAG, "camera data:" + (data == null));
// 取消拍照
if (resultCode == Activity.RESULT_CANCELED) {
Log.i(TAG, "abort to take a picture.");
} else {
Log.i(TAG, "successfully to take a picture.");
}
this.webView.cropPicture(resultCode);
break;
}
}
}
4)上述Activity拍照成功的逻辑中,调用了图片的裁剪功能。
WebChromeClient中的裁剪代码如下:
/**
* 开始裁剪照片
*
* @param activity
* @param cameraUri
*/
private void cropPicture(Activity activity, Uri cameraUri)
{
File cropFile = new File(path, SystemClock.currentThreadTimeMillis() + ".jpg");
Log.i(TAG, "crop file path=" + cropFile.toString());
cropUri = Uri.fromFile(cropFile);
Log.i(TAG, "crop uri=" + cropUri.toString());
PictureUtil.Crop crop = new PictureUtil.Crop(1, 1, 250, 250);
PictureUtil.cropPicture(activity, cameraUri, cropUri, crop, Constant.CROP_PICTURE_CODE);
}
PictureUtil中真正的裁剪代码为:
/**
* 裁剪图片
*
* @param activity 当前activity
* @param orgUri 剪裁原图的Uri
* @param desUri 剪裁后的图片的Uri
* @param crop:{aspectX X方向的比例;aspectY:Y方向的比例;width:剪裁图片的宽度;height:剪裁图片高度}
* @param requestCode 剪裁图片的请求码
*/
public static void cropPicture(Activity activity, Uri orgUri, Uri desUri, Crop crop, int requestCode)
{
Intent intent = new Intent("com.android.camera.action.CROP");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
{
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
intent.setDataAndType(orgUri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", crop.getAspectX());
intent.putExtra("aspectY", crop.getAspectY());
intent.putExtra("outputX", crop.getWidth());
intent.putExtra("outputY", crop.getHeight());
intent.putExtra("scale", true);
// 将剪切的图片保存到目标Uri中
intent.putExtra(MediaStore.EXTRA_OUTPUT, desUri);
// 取消人脸识别
intent.putExtra("noFaceDetection", true);
// true:返回bitmap,false:返回bitmap
intent.putExtra("return-data", true);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
activity.startActivityForResult(intent, requestCode);
}
裁剪的过程和拍照的过程类似,也会在指定的Activity中,根据约定的requestCode来接收裁剪完成后的结果。
6)上述裁剪传入的Activity是MainActivity,其处理结果应在onActivityResult中,代码如下:
@Override
protected void onActivityResult(int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case Constant.CROP_PICTURE_CODE: {
Log.i(TAG, "crop result:" + resultCode);
Log.i(TAG, "crop data:" + data);
Log.i(TAG, "crop data.getData:" + data.getData());
Bitmap photo = data.getParcelableExtra("data");
Log.i(TAG, "crop picture:" + photo);
// String base64Pic = PictureUtil.getPictureBase64(photo);
// Log.i(TAG, "base64Pic:" + base64Pic);
this.webView.cropCallback(resultCode);
break;
}
}
}
注意:此处的data中是可以拿到裁剪的Bitmap对象的,如果有这种诉求,可以在这里加获取的业务逻辑。
7)裁剪完成后,需要把处理完成的照片结果,返回给拍照接口中指定的回调对象,然后由Android返回给调用方(即我们的H5拍照组件),WebChromeClient中回调的核心代码如下所示,其中valueCallback为回调结果的处理接口,由Android系统接管。
/**
* 拍照异步回调
*
* @param resultCode
* @param uri
*/
public void cropCallback(int resultCode, Uri uri)
{
if (resultCode == Activity.RESULT_CANCELED)
{
takePictureCancel();
}
else
{
if (null != oldValueCallback)
{
oldValueCallback.onReceiveValue(cropUri);
}
else if (null != valueCallback)
{
valueCallback.onReceiveValue(new Uri[] {cropUri});
}
oldValueCallback = null;
valueCallback = null;
}
if (cameraFile != null)
{
Log.i(TAG, "delete cached camera file:" + cameraFile.getPath());
cameraFile.delete();
}
}
注意:上述代码末尾有删除拍照文件的操作,是因为拍照和裁剪会分别产生图片,而拍照的图片大小又特别大(在我真机上测试占有3M左右),所以裁剪完成后,需要把原图从手机中删掉。这个因具体业务而已。
3、上面一步详细讲了Android系统中的拍照和裁剪流程,下面再来列下Android版本碎片化的拍照入口,同样在WebChromeClient中:
public void openFileChooser(ValueCallback<Uri> filePathCallback)
{
Log.d(TAG, "call openFileChooser01");
oldValueCallback = filePathCallback;
takePicture();
}
// For Android 3.0+
public void openFileChooser(ValueCallback filePathCallback, String acceptType)
{
Log.d(TAG, "call openFileChooser02");
oldValueCallback = filePathCallback;
takePicture();
}
// For Android 4.1
public void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType, String capture)
{
Log.d(TAG, "call openFileChooser03");
oldValueCallback = filePathCallback;
takePicture();
}
// For Android 5.0+
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
FileChooserParams fileChooserParams)
{
// super.onShowFileChooser(webView, filePathCallback, fileChooserParams);
Log.d(TAG, "call openFileChooser04");
valueCallback = filePathCallback;
takePicture();
return true;
}
经过上面3步,基本上就完成了拍照的全过程。再次重申,完整代码见github。由于时间关系,本github代码仅在本人真机和模拟器中验证OK(Android 8.0+),低版本有待各位自己去验证。
四、总结
1、拍照是当今手机一个非常核心的功能,掌握好这个知识点非常有用,网上资料也相当多,而且还出现了几种不同的处理方案(有人说涉及拍照的权限升级,只能在高版本拍照时使用FileProvider得到的Uri,有人说必须设置XX权限);实际验证却并不 完全一致。至于原因,是不是当时的版本或者Android API差异导致的,无从得知;
2、在查找资料的过程中,发现有拍照和裁剪封装成一个startActivityResult的,但由于时间问题,当时并没有深入去研究,作为一个遗留问题,留给爱思考爱动脑筋的你:)
五、参考资料
[1]Android7.0完美适配——FileProvider拍照裁剪全解析 - 擦肩的阳光的专栏
[2]Android webView拍照与展示相册图片 - 追风筝的摆渡人
[3]Android WebView实现选择本地图片拍照功能 - Ho博客
[4]深坑之Webview,解决H5调用android相机拍照和录像 - villa_mou的博客
上一篇:Vue.js实战——开发Android H5 App之Webview高级配置_13