我正在参加「启航方案」

00. 前言

混合开发中,由 H5 界面调用原生的相册和相机,也是一种常见的需求场景。

刚入坑混合开发的我,当在需求评定会上,被说到这样的功用时,我还并不知道怎么去实现。不过 iOS 的搭档告诉我:“H5 那儿能够直接引发的,小问题”。我很高兴,因为这样才契合混合开发的意义嘛。可是万万万万万万没想到,Android,能够又不完全能够???让我们一起来看看这条探索之路吧。

01. 测验

测验1

抱着希望在 Google 上一顿查找,有文章说要在 H5 标签中,添加capture特点。

<input type="file" accept="image/*" capture="camera">
<input type="file" accept="video/*" capture="camcorder"> 
<input type="file" accept="audio/*" capture="microphone">

【Android】混合开发- H5 能直接调起原生的相册和相机吗?
于是,让前端的同学加了相应的特点,成果发现并没有什么效果。

测验2

经过研究(google)发现,当 <input> 标签润饰的控件被点击,我们这边是能够收到这个事件的。会经过 WebChromClient 中的 onShowFileChooser() 回调用来。

private val webViewChromeClient = object : WebChromeClient() {
    override fun onShowFileChooser(webView: WebView, 
                                   filePathCallback:ValueCallback<Array<Uri>>,
                                   fileChooserParams: FileChooserParams): Boolean {
        //这个拿到,将成果回来给 H5 的
        mFilePathCallback = filePathCallback
        val acceptTypes = fileChooserParams.acceptTypes
        if (acceptTypes.contains("image/*")) {
            //todo 调起挑选框
        }
        return true
    }
}

【Android】混合开发- H5 能直接调起原生的相册和相机吗?
那??拿到相片后,怎么回来给 H5 呢?

看到上面的 ValueCallback 了吗?点击源码瞅一眼,哦?就一个办法,那就调它回来文件的 path 给到 H5。

public interface ValueCallback<T> {
    void onReceiveValue(T var1);
}

【Android】混合开发- H5 能直接调起原生的相册和相机吗?
全体思路如上。

03.全体流程

那具体上怎么操作呢?

 //1.调起挑选框
 //2.权限申请和办理
 //3.操作完的回调
 //其实便是基本调起原生相册,相机的操作。不过,需求特别注意的便是,在回调回来值给H5的时分,
 //无论是否有值都要回调给 H5,不然下次就调不起来了。believe it or not, you can try it.
 //mFilePathCallback?.onReceiveValue(null)
/**
 * 显现相册/摄影挑选对话框
 */
private fun showSelectDialog() {
    if (mSelectPhotoDialog == null) {
        //简略写个Dialog
        mSelectPhotoDialog = SelectDialog(this, View.OnClickListener { view ->
            when (view.id) {
                //不同挑选的,不同权限申请
                R.id.tv_camera -> requestPermissions(SELECT_CAMERA)
                R.id.tv_photo -> requestPermissions(SELECT_ALBUM)
                //♨♨♨不论挑选仍是不挑选,必须有回来成果,不然就只会调用一次。(不理解的话,能够试试不写下面的代码)
                R.id.tv_cancel -> {
                    mFilePathCallback?.onReceiveValue(null)
                    mFilePathCallback = null
                }
            }
        })
    }
    mSelectPhotoDialog?.show()
}

【Android】混合开发- H5 能直接调起原生的相册和相机吗?
//权限申请和办理,这儿用了 PermissionX 库,假定你知道(ResonDialog,是我自定义的,这儿不贴代码了)

private fun requestPermissions(type: Int) {
    when (type) {
        SELECT_CAMERA -> {
            PermissionX.init(this)
                .permissions(
                    Manifest.permission.CAMERA,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE
                )
                .onExplainRequestReason { scope, deniedList ->
                    scope.showRequestReasonDialog(
                        ReasonDialog(
                            this,
                            "该功用需求摄影和存储权限",
                            deniedList
                        )
                    )
                }
                .request { allGranted, _, deniedList ->
                    //若被回绝的权限不为0,需求回来空数据给 H5
                    if (deniedList.size != 0) {
                        mFilePathCallback?.onReceiveValue(null)
                    }
                    //所有权限都被授权后的操作
                    if (allGranted) {
                        startCamera()
                    }
                }
        }
        SELECT_ALBUM -> {
            PermissionX.init(this)
                .permissions(
                    Manifest.permission.WRITE_EXTERNAL_STORAGE
                )
                .onExplainRequestReason { scope, deniedList ->
                    scope.showRequestReasonDialog(
                        ReasonDialog(
                            this,
                            "该功用需求拜访您的相册,需求存储权限",
                            deniedList
                        )
                    )
                }
                .request { allGranted, _, deniedList ->
                    //若被回绝的权限不为0,需求回来空数据给 H5
                    if (deniedList.size != 0) {
                        mFilePathCallback?.onReceiveValue(null)
                    }
                    //所有权限都被授权后的操作
                    if (allGranted) {
                        startAlbum()
                    }
                }
        }
    }
}

【Android】混合开发- H5 能直接调起原生的相册和相机吗?
最终,是操作后的回调。要将最终成果回来给 H5

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == PhotoUtils.RESULT_CODE_CAMERA && resultCode == Activity.RESULT_OK) {
        //摄影并确认
        //能够考虑--紧缩图片(这儿因为我司H5那儿做了紧缩,所以客户端就能够不做了)
        mFilePathCallback?.onReceiveValue(arrayOf(Uri.parse(PhotoUtils.PATH_PHOTO)))
    } else if (requestCode == PhotoUtils.RESULT_CODE_PHOTO && resultCode == Activity.RESULT_OK) {
        //相册挑选并确认
        val result = data?.data
        val path = result?.let { PhotoUtils.getPath(this, it) }
        if (path == null) {
            mFilePathCallback?.onReceiveValue(null)
        } else {
            mFilePathCallback?.onReceiveValue(arrayOf(Uri.parse(path)))
        }
    } else {
        mFilePathCallback?.onReceiveValue(null)
    }
}

【Android】混合开发- H5 能直接调起原生的相册和相机吗?
最终贴出用到的 PhotoUtils,感谢郭富东大佬共享的东西类。 里边用了,假如要用到,能够参加依赖。不然,注释掉就好了。

//紧缩算法
implementation 'top.zibin:Luban:1.1.8'
/**
 * @Author :郭富东
 * @Date:2019/2/1:10:37
 * @descriptio:
 */
object PhotoUtils {
    const val RESULT_CODE_CAMERA = 0x02
    const val RESULT_CODE_PHOTO = 0x04
    const val RESULT_CODE_CROP = 0x05
    lateinit var PATH_PHOTO: String
    fun photoClip(context: Activity, uri: Uri) {
        // 调用系统中自带的图片取舍
        val intent = Intent("com.android.camera.action.CROP")
        intent.setDataAndType(uri, "image/*")
        // 下面这个crop=true是设置在开启的Intent中设置显现的VIEW可裁剪
        intent.putExtra("crop", "true")
        // aspectX aspectY 是宽高的比例
        intent.putExtra("aspectX", 1)
        intent.putExtra("aspectY", 1)
        // outputX outputY 是裁剪图片宽高
        intent.putExtra("outputX", 150)
        intent.putExtra("outputY", 150)
        intent.putExtra("return-data", true)
        context.startActivityForResult(intent, RESULT_CODE_CROP)
    }
    /**
     * 摄影
     * @param context Activity
     */
    fun startCamera(context: Activity) {
        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        PATH_PHOTO = getSdCardDirectory(context) + "/temp.png"
        val temp = File(PATH_PHOTO)
        if (!temp.parentFile.exists()) {
            temp.parentFile.mkdirs()
        }
        if (temp.exists()) {
            temp.delete()
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            //添加这一句表明对方针使用暂时授权该Uri所代表的文件
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
            // 经过FileProvider创立一个content类型的Uri
            val uri: Uri =
                FileProvider.getUriForFile(context, context.packageName + ".fileprovider", temp)
            intent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
        } else {
            intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(temp))
        }
        context.startActivityForResult(intent, RESULT_CODE_CAMERA)
    }
    /**
     * 翻开相册
     * @param context Activity
     */
    fun startAlbum(context: Activity) {
        val albumIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
        albumIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        albumIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*")
        context.startActivityForResult(albumIntent, RESULT_CODE_PHOTO)
    }
    abstract class OnPictureCompressListener {
        fun onStart() {}
        abstract fun onSuccess(file: File)
        abstract fun onError(e: Throwable)
    }
    /**
     * 紧缩图片
     * @param context Context
     * @param path String
     * @param listener OnPictureCompressListener?
     */
    fun compressPicture(context: Context, path: String, listener: OnPictureCompressListener?) {
        Luban.with(context)
                .load(path)
                .ignoreBy(1000)
                .setTargetDir(getSdCardDirectory(context))
                .filter { path -> !(TextUtils.isEmpty(path) || path.toLowerCase().endsWith(".gif")) }
                .setCompressListener(object : OnCompressListener {
                    override fun onStart() {
                        //紧缩开端前调用,能够在办法内启动 loading UI
                        listener?.onStart()
                    }
                    override fun onSuccess(file: File) {
                        //紧缩成功后调用,回来紧缩后的图片文件
                        listener?.onSuccess(file)
                    }
                    override fun onError(e: Throwable) {
                        //当紧缩进程呈现问题时调用
                        listener?.onError(e)
                    }
                }).launch()
    }
    fun getSdCardDirectory(context: Context): String {
        var sdDir: File? = null
        if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
            sdDir = Environment.getExternalStorageDirectory()
//            sdDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        } else {
            sdDir = context.cacheDir
        }
        val cacheDir = File(sdDir, "h5pic")
        if (!cacheDir.exists()) {
            cacheDir.mkdirs()
        }
        return cacheDir.path
    }
    @RequiresApi(Build.VERSION_CODES.KITKAT)
    fun getPath(context: Context, uri: Uri): String? {
        val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
        // DocumentProvider
        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
            // ExternalStorageProvider
            if (isExternalStorageDocument(uri)) {
                val docId = DocumentsContract.getDocumentId(uri)
                val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
                val type = split[0]
                if ("primary".equals(type, ignoreCase = true)) {
                    return Environment.getExternalStorageDirectory().path + "/" + split[1]
                }
            } else if (isDownloadsDocument(uri)) {
                val id = DocumentsContract.getDocumentId(uri)
                val contentUri = ContentUris.withAppendedId(
                    Uri.parse("content://downloads/public_downloads"),
                    java.lang.Long.valueOf(id)
                )
                return getDataColumn(context, contentUri, null, null)
            } else if (isMediaDocument(uri)) {
                val docId = DocumentsContract.getDocumentId(uri)
                val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
                val contentUri = when (split[0]) {
                    "image" -> {
                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI
                    }
                    "video" -> {
                        MediaStore.Video.Media.EXTERNAL_CONTENT_URI
                    }
                    "audio" -> {
                        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
                    }
                    else -> {
                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI
                    }
                }
                val selection = "_id=?"
                val selectionArgs = arrayOf(split[1])
                return getDataColumn(context, contentUri, selection, selectionArgs)
            }
        } else if ("content".equals(uri.scheme, ignoreCase = true)) {
            return getDataColumn(context, uri, null, null)
        } else if ("file".equals(uri.scheme, ignoreCase = true)) {
            return uri.path
        }
        return null
    }
    private fun getDataColumn(
        context: Context,
        uri: Uri,
        selection: String?,
        selectionArgs: Array<String>?
    ): String? {
        var cursor: Cursor? = null
        val column = "_data"
        val projection = arrayOf(column)
        try {
            cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
            if (cursor != null && cursor.moveToFirst()) {
                val columnIndex = cursor.getColumnIndexOrThrow(column)
                return cursor?.getString(columnIndex)
            }
        } finally {
            if (cursor != null) cursor!!.close()
        }
        return null
    }
    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is ExternalStorageProvider.
     */
    private fun isExternalStorageDocument(uri: Uri): Boolean {
        return "com.android.externalstorage.documents" == uri.authority
    }
    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is DownloadsProvider.
     */
    private fun isDownloadsDocument(uri: Uri): Boolean {
        return "com.android.providers.downloads.documents" == uri.authority
    }
    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is MediaProvider.
     */
    private fun isMediaDocument(uri: Uri): Boolean {
        return "com.android.providers.media.documents" == uri.authority
    }
}

04. 结语

中心的内容都在上面了,假如还有一些细节上存在疑问。能够留言或者私信我,我将很乐意为您回答。假如呈现图片上传失败,不妨看看我另一篇文章——【Android】混合开发 – 奇案 – 上传相片至 H5 失败,或许能为你处理相关问题。