• 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,filePathCallbackfileChooserParams

    • 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="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[n] = 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)


    提问之前,请先看提问须知 点击右侧图标发起提问 联系我 或者加入QQ群一起学习 Stars-One安卓学习交流群 TornadoFx学习交流群:1071184701
  • 相关阅读:
    spring cloud配置中心
    网关中自定义登陆验证过滤器
    spring cloud网关
    Hystrix断路器 熔断器Hystrix的在Fegin的集成
    Hystrix断路器 熔断器Hystrix的在Ribbon的集成
    负载均衡二Feign
    Eureka负载均衡Ribbon
    Eureka高可用注册中心(解决单点故障)
    Eureka多服务调用
    input错误提示,点击提交,提示有未填项,屏幕滑到input未填项的位置
  • 原文地址:https://www.cnblogs.com/stars-one/p/15498004.html
Copyright © 2020-2023  润新知