深坑之Webview,解决H5调用android相机拍照和录像
来源:互联网 发布:java 打印异常堆栈 编辑:程序博客网 时间:2024/05/29 12:31
最近在开发过程中遇到一个问题,主要是调用第三方的实名认证,需要拍照和录像;
办过支付宝大宝卡和腾讯的大王卡的都知道这玩意,办卡的时候就需要进行实名认证,人脸识别;
本来第三方平台(xxx流量公司)说的是直接用WebView加载这个H5界面就完事了,我心想这么简单,那不是分分钟的事,放着后面做(公司就我一个安卓,所以开发都是我说的算^_^,独立开发有的时候还是挺爽);
结果到项目快要上线的时候,只想说一句mmp,根本调不了相机,这个时候怎么搞,只有自己去实现了,才发现自己已经进了webview的深坑;
到处找资料,发现webview根本不能让h5自己调用,ios是可以的,项目经理就说是我的锅,真特么又一句mmp(关键是这个H5还特么不能改,不能提供给我调用的方法);
进入正题,首先来了解webview,这里我分享两篇大佬的博客
1,WebView开车指南
2,WebView详解
看完这两篇基本你已经可以随意操作webview了, 但是,当你调用相机的时候你才发现这只是入坑的开始
第一个坑
调用相机之后,文件回调不了,其实根本就是没有回调.这个时候你要去检查你的webview的Settings了关键代码,是否给足了权限;
WebSettings settings = webView.getSettings(); settings.setUseWideViewPort(true); settings.setLoadWithOverviewMode(true); settings.setDomStorageEnabled(true); settings.setDefaultTextEncodingName("UTF-8"); settings.setAllowContentAccess(true); // 是否可访问Content Provider的资源,默认值 true settings.setAllowFileAccess(true); // 是否可访问本地文件,默认值 true // 是否允许通过file url加载的Javascript读取本地文件,默认值 false settings.setAllowFileAccessFromFileURLs(false); // 是否允许通过file url加载的Javascript读取全部资源(包括文件,http,https),默认值 false settings.setAllowUniversalAccessFromFileURLs(false); //开启JavaScript支持 settings.setJavaScriptEnabled(true); // 支持缩放 settings.setSupportZoom(true);
其中settings.setDomStorageEnabled(true);
这个方法当时我没加,血崩了一次;
第二个坑
WebChromeClient的openFileChooser()只调用了一次
首先了解为什么这个方法只调用了一次,看这篇博客就可
Android开发深入理解WebChromeClient之onShowFileChooser或openFileChooser使用说明
看了他基本你就了解怎么回事了
第三个坑
怎么区分是要调用相机是需要拍照还是录视频
这个时候你就要看WebViewClient的 shouldOverrideUrlLoading()方法了
,我是通过拦截url来判断url里面是拍照还是录制视频来加一个flag
这样你调用你的相机的时候就可以分别处理了
第四个坑
android 7.0的FileProvider的坑
看洪阳大佬的这篇博客基本就了解了
Android 7.0 行为变更 通过FileProvider在应用间共享文件吧
最后附上我自己的代码吧
package com.sihaiwanlian.cmccnev.feature.verify.activitys;import android.annotation.TargetApi;import android.app.Activity;import android.content.ClipData;import android.content.Intent;import android.net.Uri;import android.os.Build;import android.os.Bundle;import android.os.Environment;import android.os.SystemClock;import android.provider.MediaStore;import android.support.annotation.RequiresApi;import android.support.v4.content.FileProvider;import android.support.v7.app.AppCompatActivity;import android.text.TextUtils;import android.util.Log;import android.view.KeyEvent;import android.webkit.ValueCallback;import android.webkit.WebChromeClient;import android.webkit.WebSettings;import android.webkit.WebView;import android.webkit.WebViewClient;import android.widget.Button;import android.widget.ImageView;import android.widget.TextView;import com.orhanobut.logger.Logger;import com.sihaiwanlian.cmccnev.R;import com.sihaiwanlian.cmccnev.utils.PhotoUtils;import java.io.File;import butterknife.BindView;import butterknife.ButterKnife;import butterknife.OnClick;public class Certifica extends AppCompatActivity { private final static String TAG = "villa"; @BindView(R.id.titleBar_iv_back) ImageView mTitleBarIvBack; @BindView(R.id.titleBar_btn_back) Button mTitleBarBtnBack; @BindView(R.id.titleBar_centerTV) TextView mTitleBarCenterTV; private WebView webView; private ValueCallback<Uri> mUploadMessage; private ValueCallback<Uri[]> mUploadCallbackAboveL; private final static int PHOTO_REQUEST = 100; private final static int VIDEO_REQUEST = 120; private final static String url = "your_url"; private boolean videoFlag = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_certifica); ButterKnife.bind(this); initToolBar(); initWebView(); } private void initToolBar() { mTitleBarCenterTV.setText("实名认证"); } //初始化webView private void initWebView() { //从布局文件中扩展webView webView = (WebView) this.findViewById(R.id.certifi_webview); initWebViewSetting(); } //初始化webViewSetting @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) private void initWebViewSetting() { WebSettings settings = webView.getSettings(); settings.setUseWideViewPort(true); settings.setLoadWithOverviewMode(true); settings.setDomStorageEnabled(true); settings.setDefaultTextEncodingName("UTF-8"); settings.setAllowContentAccess(true); // 是否可访问Content Provider的资源,默认值 true settings.setAllowFileAccess(true); // 是否可访问本地文件,默认值 true // 是否允许通过file url加载的Javascript读取本地文件,默认值 false settings.setAllowFileAccessFromFileURLs(false); // 是否允许通过file url加载的Javascript读取全部资源(包括文件,http,https),默认值 false settings.setAllowUniversalAccessFromFileURLs(false); //开启JavaScript支持 settings.setJavaScriptEnabled(true); // 支持缩放 settings.setSupportZoom(true); //辅助WebView设置处理关于页面跳转,页面请求等操作 webView.setWebViewClient(new MyWebViewClient()); //辅助WebView处理图片上传操作 webView.setWebChromeClient(new MyChromeWebClient()); //加载地址 webView.loadUrl(url); } @OnClick(R.id.titleBar_btn_back) public void onViewClicked() { if (webView.canGoBack()) { webView.goBack();// 返回前一个页面 } else { finish(); } } //自定义 WebViewClient 辅助WebView设置处理关于页面跳转,页面请求等操作【处理tel协议和视频通讯请求url的拦截转发】 private class MyWebViewClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { Logger.e(url); if (!TextUtils.isEmpty(url)) { videoFlag = url.contains("vedio"); } if (url.trim().startsWith("tel")) {//特殊情况tel,调用系统的拨号软件拨号【<a href="tel:1111111111">1111111111</a>】 Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(url)); startActivity(i); } else { String port = url.substring(url.lastIndexOf(":") + 1, url.lastIndexOf("/"));//尝试要拦截的视频通讯url格式(808端口):【http://xxxx:808/?roomName】 if (port.equals("808")) {//特殊情况【若打开的链接是视频通讯地址格式则调用系统浏览器打开】 Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(url)); startActivity(i); } else {//其它非特殊情况全部放行 view.loadUrl(url); } } return true; } } private File fileUri = new File(Environment.getExternalStorageDirectory().getPath() + "/" + SystemClock.currentThreadTimeMillis() + ".jpg"); private Uri imageUri; //自定义 WebChromeClient 辅助WebView处理图片上传操作【<input type=file> 文件上传标签】 public class MyChromeWebClient extends WebChromeClient { // For Android 3.0- public void openFileChooser(ValueCallback<Uri> uploadMsg) { Log.d(TAG, "openFileChoose(ValueCallback<Uri> uploadMsg)"); mUploadMessage = uploadMsg; if (videoFlag) { recordVideo(); } else { takePhoto(); } } // For Android 3.0+ public void openFileChooser(ValueCallback uploadMsg, String acceptType) { Log.d(TAG, "openFileChoose( ValueCallback uploadMsg, String acceptType )"); mUploadMessage = uploadMsg; if (videoFlag) { recordVideo(); } else { takePhoto(); } } //For Android 4.1 public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) { Log.d(TAG, "openFileChoose(ValueCallback<Uri> uploadMsg, String acceptType, String capture)"); mUploadMessage = uploadMsg; if (videoFlag) { recordVideo(); } else { takePhoto(); } } // For Android 5.0+ public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) { Log.d(TAG, "onShowFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture)"); mUploadCallbackAboveL = filePathCallback; if (videoFlag) { recordVideo(); } else { takePhoto(); } return true; } } /** * 拍照 */ private void takePhoto() { imageUri = Uri.fromFile(fileUri); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { imageUri = FileProvider.getUriForFile(Certifica.this, getPackageName() + ".fileprovider", fileUri);//通过FileProvider创建一个content类型的Uri } PhotoUtils.takePicture(Certifica.this, imageUri, PHOTO_REQUEST); } /** * 录像 */ private void recordVideo() { Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1); //限制时长 intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 10); //开启摄像机 startActivityForResult(intent, VIDEO_REQUEST); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { //如果按下的是回退键且历史记录里确实还有页面 if ((keyCode == KeyEvent.KEYCODE_BACK) && webView.canGoBack()) { webView.goBack(); return true; } return super.onKeyDown(keyCode, event); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == PHOTO_REQUEST) { if (null == mUploadMessage && null == mUploadCallbackAboveL) return; Uri result = data == null || resultCode != RESULT_OK ? null : data.getData(); if (mUploadCallbackAboveL != null) { onActivityResultAboveL(requestCode, resultCode, data); } else if (mUploadMessage != null) { mUploadMessage.onReceiveValue(result); mUploadMessage = null; } } else if (requestCode == VIDEO_REQUEST) { if (null == mUploadMessage && null == mUploadCallbackAboveL) return; Uri result = data == null || resultCode != RESULT_OK ? null : data.getData(); if (mUploadCallbackAboveL != null) { if (resultCode == RESULT_OK) { mUploadCallbackAboveL.onReceiveValue(new Uri[]{result}); mUploadCallbackAboveL = null; } else { mUploadCallbackAboveL.onReceiveValue(new Uri[]{}); mUploadCallbackAboveL = null; } } else if (mUploadMessage != null) { if (resultCode == RESULT_OK) { mUploadMessage.onReceiveValue(result); mUploadMessage = null; } else { mUploadMessage.onReceiveValue(Uri.EMPTY); mUploadMessage = null; } } } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void onActivityResultAboveL(int requestCode, int resultCode, Intent data) { if (requestCode != PHOTO_REQUEST || mUploadCallbackAboveL == null) { return; } Uri[] results = null; if (resultCode == Activity.RESULT_OK) { if (data == null) { results = new Uri[]{imageUri}; } else { String dataString = data.getDataString(); ClipData clipData = data.getClipData(); if (clipData != null) { results = new Uri[clipData.getItemCount()]; for (int i = 0; i < clipData.getItemCount(); i++) { ClipData.Item item = clipData.getItemAt(i); results[i] = item.getUri(); } } if (dataString != null) results = new Uri[]{Uri.parse(dataString)}; } } mUploadCallbackAboveL.onReceiveValue(results); mUploadCallbackAboveL = null; } @Override protected void onDestroy() { super.onDestroy(); if (webView != null) { webView.setWebViewClient(null); webView.setWebChromeClient(null); webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null); webView.clearHistory();// ((ViewGroup) webView.getParent()).removeView(webView); webView.destroy(); webView = null; } }}
这里面有个photoUtils,已经封装好了各种调用,简单好用
public class PhotoUtils { private static final String TAG = "PhotoUtils"; /** * @param activity 当前activity * @param imageUri 拍照后照片存储路径 * @param requestCode 调用系统相机请求码 */ public static void takePicture(Activity activity, Uri imageUri, int requestCode) { //调用系统相机 Intent intentCamera = new Intent(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { intentCamera.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //添加这一句表示对目标应用临时授权该Uri所代表的文件 } intentCamera.setAction(MediaStore.ACTION_IMAGE_CAPTURE); //将拍照结果保存至photo_file的Uri中,不保留在相册中 intentCamera.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); if (activity!=null){ activity.startActivityForResult(intentCamera, requestCode); } } /** * @param activity 当前activity * @param requestCode 打开相册的请求码 */ public static void openPic(Activity activity, int requestCode) { Intent photoPickerIntent = new Intent(Intent.ACTION_GET_CONTENT); photoPickerIntent.setType("image/*"); activity.startActivityForResult(photoPickerIntent, requestCode); } /** * @param activity 当前activity * @param orgUri 剪裁原图的Uri * @param desUri 剪裁后的图片的Uri * @param aspectX X方向的比例 * @param aspectY Y方向的比例 * @param width 剪裁图片的宽度 * @param height 剪裁图片高度 * @param requestCode 剪裁图片的请求码 */ public static void cropImageUri(Activity activity, Uri orgUri, Uri desUri, int aspectX, int aspectY, int width, int height, 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.setDataAndType(orgUri, "image/*"); intent.putExtra("crop", "true"); intent.putExtra("aspectX", aspectX); intent.putExtra("aspectY", aspectY); intent.putExtra("outputX", width); intent.putExtra("outputY", height); intent.putExtra("scale", true); //将剪切的图片保存到目标Uri中 intent.putExtra(MediaStore.EXTRA_OUTPUT, desUri); intent.putExtra("return-data", false); intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); intent.putExtra("noFaceDetection", true); activity.startActivityForResult(intent, requestCode); } /** * 读取uri所在的图片 * * @param uri 图片对应的Uri * @param mContext 上下文对象 * @return 获取图像的Bitmap */ public static Bitmap getBitmapFromUri(Uri uri, Context mContext) { try {// Bitmap bitmap = MediaStore.Images.Media.getBitmap(mContext.getContentResolver(), uri); Bitmap bitmapFormUri = getBitmapFormUri(mContext, uri); return bitmapFormUri; } catch (Exception e) { e.printStackTrace(); return null; } } /** * 通过uri获取图片并进行压缩 * * @param uri */ public static Bitmap getBitmapFormUri(Context ac, Uri uri) throws FileNotFoundException, IOException { InputStream input = ac.getContentResolver().openInputStream(uri); BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options(); onlyBoundsOptions.inJustDecodeBounds = true; onlyBoundsOptions.inDither = true;//optional onlyBoundsOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;//optional BitmapFactory.decodeStream(input, null, onlyBoundsOptions); input.close(); int originalWidth = onlyBoundsOptions.outWidth; int originalHeight = onlyBoundsOptions.outHeight; if ((originalWidth == -1) || (originalHeight == -1)){ return null; } //图片分辨率以480x800为标准 float hh = 800f;//这里设置高度为800f float ww = 480f;//这里设置宽度为480f //缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可 int be = 1;//be=1表示不缩放 if (originalWidth > originalHeight && originalWidth > ww) {//如果宽度大的话根据宽度固定大小缩放 be = (int) (originalWidth / ww); } else if (originalWidth < originalHeight && originalHeight > hh) {//如果高度高的话根据宽度固定大小缩放 be = (int) (originalHeight / hh); } if (be <= 0){ be = 1; } //比例压缩 BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); bitmapOptions.inSampleSize = be;//设置缩放比例 bitmapOptions.inDither = true;//optional bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;//optional input = ac.getContentResolver().openInputStream(uri); Bitmap bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions); input.close(); return compressImage(bitmap);//再进行质量压缩 } /** * 质量压缩方法 * * @param image * @return */ public static Bitmap compressImage(Bitmap image) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); image.compress(Bitmap.CompressFormat.JPEG, 100, baos);//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中 int options = 100; while (baos.toByteArray().length / 1024 > 100) { //循环判断如果压缩后图片是否大于100kb,大于继续压缩 baos.reset();//重置baos即清空baos //第一个参数 :图片格式 ,第二个参数: 图片质量,100为最高,0为最差 ,第三个参数:保存压缩后的数据的流 image.compress(Bitmap.CompressFormat.JPEG, options, baos);//这里压缩options%,把压缩后的数据存放到baos中 options -= 10;//每次都减少10 } ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());//把压缩后的数据baos存放到ByteArrayInputStream中 Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);//把ByteArrayInputStream数据生成图片 return bitmap; } /** * @param context 上下文对象 * @param uri 当前相册照片的Uri * @return 解析后的Uri对应的String */ @SuppressLint("NewApi") public static String getPath(final Context context, final Uri uri) { final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; String pathHead = "file:///"; // DocumentProvider if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider if (isExternalStorageDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; if ("primary".equalsIgnoreCase(type)) { return pathHead + Environment.getExternalStorageDirectory() + "/" + split[1]; } } // DownloadsProvider else if (isDownloadsDocument(uri)) { final String id = DocumentsContract.getDocumentId(uri); final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); return pathHead + getDataColumn(context, contentUri, null, null); } // MediaProvider else if (isMediaDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; Uri contentUri = null; if ("image".equals(type)) { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; } else if ("video".equals(type)) { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } else if ("audio".equals(type)) { contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } final String selection = "_id=?"; final String[] selectionArgs = new String[]{split[1]}; return pathHead + getDataColumn(context, contentUri, selection, selectionArgs); } } // MediaStore (and general) else if ("content".equalsIgnoreCase(uri.getScheme())) { return pathHead + getDataColumn(context, uri, null, null); } // File else if ("file".equalsIgnoreCase(uri.getScheme())) { return pathHead + uri.getPath(); } return null; } /** * Get the value of the data column for this Uri. This is useful for * MediaStore Uris, and other file-based ContentProviders. * * @param context The context. * @param uri The Uri to query. * @param selection (Optional) Filter used in the query. * @param selectionArgs (Optional) Selection arguments used in the query. * @return The value of the _data column, which is typically a file path. */ private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; final String column = "_data"; final String[] projection = {column}; try { cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); if (cursor != null && cursor.moveToFirst()) { final int column_index = cursor.getColumnIndexOrThrow(column); return cursor.getString(column_index); } } finally { if (cursor != null){ cursor.close(); } } return null; } /** * @param uri The Uri to check. * @return Whether the Uri authority is ExternalStorageProvider. */ private static boolean isExternalStorageDocument(Uri uri) { return "com.android.externalstorage.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is DownloadsProvider. */ private static boolean isDownloadsDocument(Uri uri) { return "com.android.providers.downloads.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is MediaProvider. */ private static boolean isMediaDocument(Uri uri) { return "com.android.providers.media.documents".equals(uri.getAuthority()); }}
- 深坑之Webview,解决H5调用android相机拍照和录像
- Android 调用系统相机拍照、录像
- Android 调用系统照相机拍照和录像
- Android开发之调用相机拍照和调用系统相册
- Atititjs h5调用摄像头视频聊天 拍照功能 相机功能 录像attilax总结
- android调用系统相机拍照之获取原图和缩略图
- android之 h5调用系统相机和相册并显示
- 在Android中通过WebView调用相机拍照/选择文件
- Android 仿相机拍照 录像功能
- Android调用系统相册和相机拍照
- android -调用系统相机录像
- Android调用系统相机拍照,从图库中选择照片,调用系统摄像机录像
- Android中Webviw加载H5页面调用本地相机拍照并显示在H5页面上
- Android WebView中打开相机拍照和选择相册
- 关于android webview调用相册和相机
- 关于android webview调用相册和相机
- android 录像和拍照功能
- android 录像和拍照功能
- java-判断字符串是否为数字
- LINUX分区
- linux u-boot,/spl/u-boot-spl.lds:2: syntax error
- 中企动力与中国质量万里行消费投诉平台达成战略合作
- Python语法二 (人生苦短,我用Python)
- 深坑之Webview,解决H5调用android相机拍照和录像
- Java解析xml、解析xml四种方法、DOM、SAX、JDOM、DOM4j、XPath
- 完美解决json循环问题(使用javassist增强):Spring MVC中使用jackson的MixInAnnotations方法动态过滤JSON字段
- css实现宽高比固定小技巧
- 多线程
- 给定二维空间中四点的坐标,返回四点是否可以构造一个正方形。
- centos安装python3,并与python2并存
- docker生成tomcat镜像
- 在构造方法中初始化Universal-Image-Loader并做全局配置图片为圆形