Android WebView 实现文件选择、拍照、录制视频、录音
原文地址:Android WebView 实现文件选择、拍照、录制视频、录音 | Stars-One的杂货小窝
Android中的WebView如果不进行相应的设置,H5页面的上传按钮是无法触发Android弹出文件选择框的,所以,需要进行以下的设置
原理说明
Webview通过
setWebChromeClient()方法来设置一个
WebChromeClient对象,里面有相关的方法处理,我们需要将其相关的方法处理即可实现对应的效果(如弹出对话框,权限申请或弹出文件选择)
我们想要实现文件选择,只需要继承
WebChromeClient类,重写其的
onShowFileChooser()方法即可,方法如下:
boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams)
可以看到,
onShowFileChooser()方法中存在有3个参数,分别为
webview,
filePathCallback和
fileChooserParams
fileChooserParams
是文件选择的参数,我们可以利用此对象的方法fileChooserParams.getAcceptTypes()
来知道H5中的上传组件的accept
属性(即H5规定接收的文件格式)
通常情况,我们通过拿到对应的文件格式,从而弹出对应的文件选择,比如说接收的格式是图片类型,可以给出拍照或者是从图库中选择照片的两个选项
filePathCallback
是文件选择后的回调,调用filePathCallback.onReceiveValue()
方法,把我们把文件的Uri传回给H5
PS:需要考虑到用户没有选择文件的情况.filePathCallback则需要传空数组回去(null也行)
代码如下:
//注意onReceiveValue方法接收的是个Uri数组 filePathCallback.onReceiveValue(new Uri[]{}); filePathCallback.onReceiveValue(null);
之后的上传操作由前端H5实现,这里就不过多展开了,前端使用相应的上传组件即可
步骤实现
1.前提权限和配置
- 选择图片需要存储权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
- 拍照和录制视频需要相机权限
<uses-permission android:name="android.permission.CAMERA" />
记得声明和动态获取权限,这里不再赘述
同时,还要设置webview,下面给出比较全面的设置,点击展开即可查看
Webview相关的配置WebSettings webSettings = webview.getSettings(); webSettings.setAllowFileAccess(true); webSettings.setDomStorageEnabled(true); webSettings.setDatabaseEnabled(true); webSettings.setJavaScriptEnabled(true); //支持js webSettings.setUseWideViewPort(true);//设置此属性,可任意比例缩放 webSettings.setLoadWithOverviewMode(true); webSettings.setBuiltInZoomControls(false); webSettings.setDisplayZoomControls(false); webSettings.setAllowFileAccessFromFileURLs(true); // 视频播放需要使用 int SDK_INT = android.os.Build.VERSION.SDK_INT; if (SDK_INT > 16) { webSettings.setMediaPlaybackRequiresUserGesture(false); } webSettings.setSupportZoom(false);//支持缩放 requestFocusFromTouch(); //跨域取消 try { Class<?> clazz = getSettings().getClass(); Method method = clazz.getMethod( "setAllowUniversalAccessFromFileURLs", boolean.class); if (method != null) { method.invoke(getSettings(), true); } } catch (IllegalArgumentException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); }
2.重写onShowFileChooser方法
创建一个类
CustomWebViewChrome,继承
WebChromeClient,重写其的
onShowFileChooser()方法,为了方便说明,下面只给出部分代码:
@Override public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) { this.filePathCallback = filePathCallback; //姜前端H5接收的格式类型转为字符串,且后面不带分号 String[] acceptTypes = fileChooserParams.getAcceptTypes(); String acceptType = "*/*"; StringBuilder sb = new StringBuilder(); if (acceptTypes.length > 0) { for (String type : acceptTypes) { sb.append(type).append(';'); } } if (sb.length() > 0) { String typeStr = sb.toString(); acceptType = typeStr.substring(0, typeStr.length() - 1); } //根据判断,触发相关的操作,如文件选择,拍照等...详见3步讲解 //这里,也可以实现弹出个对话框供用户选择,记得在弹出对话框之后调用下回调onReceiveValue方法,否则会出现下次无法弹出对话框的Bug return true; }
这里,需要说明的是,这个回调这是在点击前端H5的上传组件(即input标签设置type属性为file)即可触发,但是,我们需要调用
filePathCallback.onReceiveValue()方法才能把文件给回前端
但文件的参数我们应该怎么获取,且调用上述所说方法?
目前的思路是:
CustomWebViewChrome类中创建个变量,存放
filePathCallback这个参数
触发文件选择等操作后面都会回调对应Activity中的
onActivityResult()方法,在
onActivityResult()方法中处理文件,得到文件对应的Uri
之后就是可以利用我们
CustomWebViewChrome对象中的
filePathCallback,进行文件的回调操作,将文件Uri传给前端H5
3.Activity获得Uri并回调
上述代码中,我特地空了一段代码,这里以图片选择为例,使用Intent跳转到选择图片的页面
Intent intent = new Intent(Intent.ACTION_PICK, null); intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*"); currentActivity.startActivityForResult(intent,15);
跳转页面都是需要Context参数,但
CustomWebViewChrome里面没有,所以得加个变量,在创建对象的时候将当前的Activity传进来(当然,我自己这边是传了个Webview对象,也可以获得对应的activity对象)
跳转页面后传有个15(即requestCode),之后得在onActivityResult判断requestCode是否为15,从而对返回的数据进行处理,得到文件的Uri,再回调
public class WebViewActivity extends AppCompatActivity { CustomWebViewChrome customWebViewChrome; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //省略对应的webview设置 WebView webview = findViewById(R.id.webview); CustomWebViewChrome customWebViewChrome = new CustomWebViewChrome(webview); webview.setWebChromeClient(customWebViewChrome); String url = "https://stars-one.site"; customWebView.loadUrl(url); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (requestCode == 15 ) { if(resultCode==Activity.RESULT_OK)[ Uri imgUri = data.getData(); filePathCallback.onReceiveValue(imgUri); ]else{ filePathCallback.onReceiveValue(new Uri[]{}); } } } }
当然这里你也可以选择使用第三库来实现文件选择,个人推荐的这个库LuckSiege/PictureSelector: 图片选择器,可以拍照和录制视频,且可以多选图片或视频文件,录音文件也支持选择(但是无法录音),而且也封装有权限的动态申请,比较方便,且代码也比较优雅
原理也是一样的,只要按照开源库的文档说明,先拿到文件Uri,之后回调
filePathCallback.onReceiveValue()即可
如果是自己要实现,则是有些麻烦,不过下面我也是研究了下拍照和录制视频如何使用Intent方式跳转,简单的补充说明下,仅供参考
补充-Intent跳转页面
下面的例子中,省略了回调filePathCallback.onReceiveValue()
代码!!!和上面的保持一致即可!!
录制视频
Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); //takeVideoIntent.putExtra(MediaStore.EXTRA_SIZE_LIMIT,10*1024*1024);//限制10M takeVideoIntent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 10);//限制录制时长 if (takeVideoIntent.resolveActivity(activity.getPackageManager()) != null) { activity.startActivityForResult(takeVideoIntent, 16); }
onActivityResult回调文件处理
Uri videoUri = data.getData(); //省略回调
拍照
拍照的话,得先定义好文件的输出路径,但需要注意的是,之后在**
onActivityResult()方法回调中的data不会携带任何数据**
所以在跳转页面前得把文件输出路径先保存一份,之后再onActivityResult,再拿之前的保存的数据回调即可
String acceptType = fileChooserParams.getAcceptTypes()[0]; File file = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "imageCapture+" + System.currentTimeMillis() + ".jpg"); //这个变量是存放在当前的Activity中 captureUri = AppUtils.getPathUri(MainActivity.this, file.getPath()); Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); intent.putExtra(MediaStore.EXTRA_OUTPUT, captureUri); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivityForResult(intent, CATURE_REQUEST);
public static Uri getPathUri(Context context, String filePath) { Uri uri; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { String packageName = context.getPackageName(); uri = FileProvider.getUriForFile(context, packageName + ".fileprovider", new File(filePath)); } else { uri = Uri.fromFile(new File(filePath)); } return uri; }
文件选择
Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("file/*"); startActivityForResult(intent, requestCode);
onActivityResult()回调中也是通过
data.getData()来获得选中文件的Uri
录音
本来录音也可以通过intent跳转的,但实际上手机提示打不开的问题,提示如下:
No Activity found to handle Intent { act=android.provider.MediaStore.RECORD_SOUND }
所以目前解决方案考虑的是直接使用H5进行录音,Android这边则是需要动态申请录音权限即可
使用的开源库框架:xiangyuecn/Recorder
研究使用其的提供的apk安装测试(也是webview套h5),可以正常申请权限并录音
但实际项目中,弹出的权限申请后,允许权限,但是h5还是拿不到权限导致无法录音,需要第二次重新进APP才可以正常录音,原因不明...
研究无果,只好在进入APP前进行录音权限的申请,而不是每次点击开始录音才申请权限
录音Vue代码首先,安装依赖
"recorder-core": "^1.1.21021500",写在
package.json文件中
引入官方提供的MP3播放的js,JS下载地址
之后在Vue文件中需要引入
import Recorder from 'recorder-core' //需要使用到的音频格式编码引擎的js文件统统加载进来 import 'recorder-core/src/engine/mp3' import 'recorder-core/src/engine/mp3-engine'
页面按顺序点击按钮即可测试录音功能,可以根据情况改造逻辑,只要记住,每次录音前必须要申请一次录音权限
下面的代码是Uni-App的实现方式,注释上也补充有Vue原生的使用方法,两者不同是,Vue原生得使用audio标签来播放录音,而Uni-App可以通过代码的方式进行创建
<template> <!--参考例子 https://github.com/xiangyuecn/Recorder/blob/master/assets/demo-vue/component/recorder.vue --> <view> <button type="default" @click="openRecord()">1.申请权限</button> <button type="default" @click="startRecord()">2.开始录音</button> <button type="default" @click="stopRecord()">3.停止录音</button> <button type="default" @click="playRecord()">4.播放录音</button> <text>{{tipText}}</text> <!-- 如果是Vue原生,需要使用audio标签来播放声音 --> <!-- <audio ref="LogAudioPlayer" :src="audioSrc" style="width:100%"></audio> --> </view> </template> <script> import Recorder from 'recorder-core' //需要使用到的音频格式编码引擎的js文件统统加载进来 import 'recorder-core/src/engine/mp3' import 'recorder-core/src/engine/mp3-engine' export default { data() { return { rec: null, tipText: "", audio: { blob: null, duration: null }, audioBase64: "" } }, created() { this.rec = Recorder(); }, methods: { openRecord() { this.rec.open(function() { //打开麦克风授权获得相关资源 console.log("授权成功") // success && success(); }, function(msg, isUserNotAllow) { //用户拒绝未授权或不支持 console.log((isUserNotAllow ? "UserNotAllow," : "") + "无法录音:" + msg); }); }, startRecord() { this.rec.start(); this.tipText = "录制中" console.log("录制中"); }, stopRecord() { let that = this this.rec.stop(function(blob, duration) { that.audio.blob = blob that.audio.duration = duration that.tipText = "停止录音" //音频文件转成base64编码 var reader = new FileReader(); reader.onloadend = function() { that.audioBase64 = reader.result; console.log(that.audioBase64) }; reader.readAsDataURL(blob) }, function(s) { console.log("结果出错!") }, true); //自动close }, playRecord() { this.tipText = "播放中" let innerAudioContext = uni.createInnerAudioContext(); innerAudioContext.autoplay = true; //base64转blob //base64数据是","后面的数据,看看是由后端处理还是前端处理 // let blob = this.dataURLtoBlob(this.audioBase64) // innerAudioContext.src = (window.URL || webkitURL).createObjectURL(blob); innerAudioContext.src = (window.URL || webkitURL).createObjectURL(this.audio.blob); innerAudioContext.onPlay(() => { console.log('开始播放'); }); innerAudioContext.onError((res) => { console.log(res.errMsg); console.log(res.errCode); }); <!-- Vue原生的播放--> // var audio=this.$refs.LogAudioPlayer; // audio.controls=true; // if(!(audio.ended || audio.paused)){ // this.tipText = "暂停" // console.log("暂停") // audio.pause(); // }; // audio.onerror=function(e){ // this.tipText = "播放失败" // console.log("播放失败") // }; // audio.src=(window.URL || webkitURL).createObjectURL(this.audio.blob) // audio.play() }, //音频的base64转blob dataURLtoBlob(dataurl) { var arr = dataurl.split(','); //注意base64的最后面中括号和引号是不转译的 var _arr = arr[1].substr(0, arr[1].length - 2); var mime = arr[0].match(/:(.*?);/)[1], bstr = atob(_arr), n = bstr.length, u8arr = new Uint8Array(n); while (n--) { u8arr
= bstr.charCodeAt(n); } return new Blob([u8arr], { type: mime }); }, }, } </script>
踩坑补充
前文也说到,我是在里面对H5的接收文件类型进行判断,从而弹出不同的选择框,在测试的时候发现存在有问题,如果在弹出对话框后不选,用户是点击了对话框之外的地方,从而取消选择,则会导致下次无法弹出对话框,原因之前也说过,就是一定要保证调用
filePathCallback.onReceiveValue()方法
要解决得用个取巧的方法,就是对dialog的消失进行监听,设置个变量去判断用户是否点击了对话框的选项
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) { this.filePathCallback = filePathCallback; String[] acceptTypes = fileChooserParams.getAcceptTypes(); String acceptType = "*/*"; StringBuilder sb = new StringBuilder(); if (acceptTypes.length > 0) { for (String type : acceptTypes) { sb.append(type).append(';'); } } if (sb.length() > 0) { String typeStr = sb.toString(); acceptType = typeStr.substring(0, typeStr.length() - 1); } final String tempType = acceptType; //权限检查 if (AndPermission.hasPermissions(currrentActivity, Permission.Group.STORAGE, Permission.Group.STORAGE)) { showChooseDialog(tempType); } else { AndPermission.with(currrentActivity).runtime() .permission(Permission.WRITE_EXTERNAL_STORAGE) .onGranted(permissions -> { //从这里开始 showChooseDialog(tempType); }) .onDenied(permission -> { Toast.makeText(currrentActivity, "拒绝权限,无法进行上传文件...", Toast.LENGTH_SHORT).show(); //拒绝权限也得加个回调 filePathCallback.onReceiveValue(null); }).start(); } return true; } private boolean isClickDialog = false; /** * 展示选择方式的对话框 */ private void showChooseDialog(String acceptType) { if (TextUtils.isEmpty(acceptType) || "*/*".equals(acceptType)) { String[] items = new String[]{"选择图片/拍照", "选择视频/录制视频", "选择音频/录制音频", "选择文件"};//创建item //添加列表 AlertDialog alertDialog = new AlertDialog.Builder(currrentActivity) .setTitle("选择方式") .setIcon(R.mipmap.ic_launcher) .setItems(items, (dialogInterface, i) -> { isClickDialog = true; chooseFileFromWay(i); }) .create(); //加个监听, alertDialog.setOnCancelListener(dialogInterface -> { if (!isClickDialog) { filePathCallback.onReceiveValue(null); } else { //重置记录的状态 isClickDialog = false; } }); alertDialog.show(); } if (acceptType.contains("image")) { chooseFileFromWay(0); } if (acceptType.contains("video")) { chooseFileFromWay(1); } if (acceptType.contains("audio")) { chooseFileFromWay(2); } if (acceptType.contains("file")) { chooseFileFromWay(3); } }
由于之后的操作,我们都会进入到另外的Activity页面,而之后会回到
onActivityResult()方法,所以最终也需要在对应的回调处理中对
isClickDialog状态进行重置(重置为false)
- Android WebView 上传各种文件(包括拍照 录像 录音 文件 音乐 等,用到图片或拍照的)
- Android WebView 网页实现选择文件
- galleryfinal 实现Android图片单选/多选、拍照、裁剪、压缩。视频选择和录制。
- 在Android浏览器中通过WebView调用相机拍照/选择文件 上传到服务器
- android webview 选择文件(拍照,本地相册) 百度定位自适应屏幕
- Android学习之实现WebView中input="file"选择文件,处理选择图片无法返回类型问题
- Android WebView 选择图片并上传(调用相机拍照/相册/选择文件)
- Android WebView 上传各种文件(包括拍照 录像 录音 文件 音乐 等,用到图片或拍照的,可以参考下)
- 在Android中通过WebView调用相机拍照/选择文件
- android仿微信录制短视频,拍照,自动聚焦,手动聚焦,滑动缩放功能(Camera+TextureView+rxjava实现)
- 在Android浏览器中通过WebView调用相机拍照/选择文件 上传到服务器
- android webview file标签点击弹出选择文件或拍照菜单
- android使用WebView来打开文件选择器(相机,相册,曲目)
- Android WebView上传文件的问题 AlertDialog取消选择
- Android中WebView使用6,js调java实现播放视频
- Android Webview 加载外部html时选择加载本地的js,css等资源文件
- android使用webview上传文件(支持相册和拍照)
- Android Webview 后台播放音视频实现
- Android Webview 加载外部html时选择加载本地的js,css等资源文件
- android使用webview上传文件(支持相册和拍照)