H5调用选择多媒体文件

视频资源选择哪家强

Posted by River on June 15, 2022

前言

本周支持兄弟租车部门,实现一个H5唤起native获取视频资源的需求;要同时支持拍摄视频和选择视频文件; 唤起后效果如下。

本文讨论的实现针对选择单一多媒体资源的需求,多选可以通过innerIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)实现。 对于多选文件的数量限制的参数设置,有知道的朋友可以email我。

我的email: 755087768river@gmail.com Google官方文档参考: https://developer.android.com/training/camera/photobasics?hl=zh-cn

选择视频效果图

选择视频效果图

1、H5唤起原生视频选择实现分析

Android的WebView有个WebChromeClient,前端选择媒体资源的时候会调用 WebChromeClient的onShowFileChooser;kotlin的简略实现如下

class MyWebChromeClient(
    var progressView: View?,
    var screenWidth: Int,
    var showProgressView: Boolean,
    var activity: Activity?
) : WebChromeClient() {

    // 一些属性定义,这里acceptTypes只有video/* audio/* image/* 三种,部分手机(一加6可以传video/capture之类的参数,但不通用)
    override fun onShowFileChooser(
        webView: WebView?,
        filePathCallback: ValueCallback<Array<Uri>>?,
        fileChooserParams: FileChooserParams?
    ): Boolean{ 
        this.umUploadMessages = filePathCallback
        this.fileChooserParams = fileChooserParams
        val acceptTypes = fileChooserParams?.acceptTypes
        if (acceptTypes != null && acceptTypes.isNotEmpty()) {
            val acceptType = acceptTypes[0]
            when {
                acceptType.contains(”image“) -> {
                    onImageChooseListener?.invoke(null, filePathCallback, fileChooserParams.mode)
                }
                acceptType.contains(“video”) -> {
                    onCameraChooseListener?.invoke(null, filePathCallback)
                }
                else -> {
                    onFileChooseListener?.invoke(null, filePathCallback)
                }
            }
        }
        return true
  }
  
}

typealias onImageChooseListener = (ValueCallback<Uri>?, ValueCallback<Array<Uri>>?, Int) -> Unit
typealias onCameraChooseListener = (ValueCallback<Uri>?, ValueCallback<Array<Uri>>?) -> Unit
typealias onFileChooseListener = (ValueCallback<Uri>?, ValueCallback<Array<Uri>>?) -> Unit

1.2、native测的核心实现

由上诉说代码可知核心代码在onCameraChooseListener中,这里无需自定视频存储路径

open class WebFragment : BaseFragment() {

        override fun init(view: View) {
            
            ...
            
            webChromeClient.onCameraChooseListener = { uri, uriArray ->
                startVideoCaptureForOpen(uriArray)
            }
            ...
        }
        
              open fun startVideoCaptureForOpen(uriArray: ValueCallback<Array<Uri>>?) {
                umUploadMessages = uriArray
                startSystemVideoCaptureViewByAction(ActivityRequestCodeConfig.PictureProcessor.CAPTURE)
            }
                
                // 无需视频资源的保存路径 cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
                open fun startSystemVideoCaptureViewByAction(requestCode: Int) {
                    try {
                        //拍照图片保存到指定的路径
                        val innerIntent = Intent(Intent.ACTION_PICK)
                        innerIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "video/*")
                        val cameraIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
                        cameraIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            

                        val wrapperIntent = Intent.createChooser(innerIntent, "选择视频")
                        wrapperIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf<Parcelable>(cameraIntent))
                        startActivityForResult(wrapperIntent, requestCode)
                    } catch (e: Exception) {    
                        e.printStackTrace()
                    }
                }
        
}

1.3、 native侧选中后资源后回传

所有资源获取都在onActivityResult的Intent中,视频获取比较简单

open class WebFragment : BaseFragment()  {
        
        
        ...
        
        protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
            super.onActivityResult(requestCode, resultCode, data);
            
                if (requestCode == ActivityRequestCodeConfig.PictureProcessor.CAPTURE) {
                    if (null == uploadMessage && null == umUploadMessages) return
                    val result = if (data == null || resultCode != Activity.RESULT_OK) {
                        null
                    } else {
                        data.data
                    }
                if (null != result) {
                    if (umUploadMessages != null) {
                        umUploadMessages?.onReceiveValue(arrayOf(result))
                    }
                } else {
                    umUploadMessages?.onReceiveValue(null)
                }
            }
        }

        ... 
}

2、H5唤起原生视频选择实现分析

图片选择和视频选择略有差异,主要体现在以下两点: 1、图片选择拍摄选中,在onActivityResult中没人任何数据返回;需要从之前指定的Uri中获取。 2、图片选择的单选和多选,在回调方法onActivityResult通过不同方式获取; 单选是getDataString和多选是getClipData

先看两张效果图

选择图片效果图

选择图片效果图

2.1、打开系统的图片选择Activity

WebView中的访问方式在开篇H5唤起原生视频选择实现分析已说明。

      private void startSystemImageCaptureViewByAction() {

        try {
            //拍照图片保存到指定的路径

            imageUri = getUriForFile(this, getPhotoSaveFile());

            Intent innerIntent = new Intent(Intent.ACTION_PICK);
            innerIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);

//            innerIntent.addCategory(Intent.CATEGORY_OPENABLE);
            innerIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");

            Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            cameraIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            //拍照图片保存到指定的路径
            cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);

            Intent wrapperIntent = Intent.createChooser(innerIntent, "选择图片");
            wrapperIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Parcelable[]{cameraIntent});

            startActivityForResult(wrapperIntent, 101);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

2.2、Uri的设置,参考文档 https://developer.android.com/training/camera/photobasics?hl=zh-cn

    
       private Uri getUriForFile(Context context, File file) {
        Uri fileUri = null;
        try {
            if (Build.VERSION.SDK_INT >= 29) {
                ContentValues contentValues = new ContentValues();
                contentValues.put(MediaStore.Images.Media.DATA, file.getAbsolutePath());
                contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, file.getName());
                contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
                fileUri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                fileUri = getUriForFile24(context, file);
            } else {
                fileUri = Uri.fromFile(file);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return fileUri;
    }
    
        private File getPhotoSaveFile() throws IOException {
        File imageStorageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
        final File tempFile = File.createTempFile(
                "IMG_" + String.valueOf(System.currentTimeMillis()),  /* prefix */
                ".jpg",         /* suffix */
                imageStorageDir      /* directory */
        );
        currentPhotoPath = tempFile.getAbsolutePath();
        return tempFile;
    } 
    

2.3、获取选中的图片

简单解释三种case
1、无论单选多选,拍照指定了Uri那么data就是null;
2、单选下纯粹选择图片,数据在data.dataString中;String的格式;
3、多选在data.clipData中,需要进一步解析出Uri。


     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
            
        var results: Array<Uri> = arrayOf()
        when (resultCode) {
            Activity.RESULT_OK -> {
                if (data == null) { // 拍照返回
                    results = arrayOf(imageUri)
                } else {
                    val dataString = data.dataString
                    val clipData = data.clipData
                    if (clipData != null) { // 多选图片返回
                        results = Array(clipData.itemCount) { k ->
                            clipData.getItemAt(k).uri
                        }
                    }
                    if (dataString != null) { // 单选图片返回
                        results = arrayOf(Uri.parse(dataString))
                    }
                }
                umUploadMessages?.onReceiveValue(results)
            }
            Activity.RESULT_CANCELED -> {
                umUploadMessages?.onReceiveValue(null)
            }
            else -> {
                umUploadMessages?.onReceiveValue(null)
            }
        }
     }

3、H5中全屏播放视频

实现关键也是WebChromeClient这个类两个方法的重写, 核心代码如下。

class MyWebChromeClient(
    var progressView: View?,
    var screenWidth: Int,
    var showProgressView: Boolean,
    var activity: Activity?
) : WebChromeClient() {
    
    private var customView: View? = null
    private var customViewCallback: CustomViewCallback? = null
    
    
        override fun onShowCustomView(view: View?, callback: CustomViewCallback?) {
            super.onShowCustomView(view, callback)
            if (null != customView) {
                return
            }
            val decorView = activity?.window?.decorView as? FrameLayout ?: return
            decorView.addView(
                view, FrameLayout.LayoutParams(
                FrameLayout.LayoutParams.MATCH_PARENT,
                FrameLayout.LayoutParams.MATCH_PARENT
                )
            )
            customView = view
            customViewCallback = callback
        }
        
        override fun onHideCustomView() {
            super.onHideCustomView()
            val decorView = activity?.window?.decorView as? FrameLayout ?: return
            customView?.let {
                decorView.removeView(it)
            }
            customView = null
            customViewCallback?.onCustomViewHidden()
        }  
    
}
    

总结

纸上得来终觉浅,绝知此事要躬行。

  1. H5通过WebView和原生的交互,其实系统做了非常丰富的封装;实现也异常精美;
  2. 图片选择的实现有一定的兼容判断代码,针对拍照的case需要指定下储存文件的Uri。