Android-混合开发H5 能直接调起原生的相册和相机吗?

Posted Q-CODER

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android-混合开发H5 能直接调起原生的相册和相机吗?相关的知识,希望对你有一定的参考价值。

最近混合开发中出现,H5 界面调用原生的相册和相机。一开始的我,并不知道 H5 可以直接调起原生的相机和相册。ios 的同事告诉我,可以的。我很开心,因为这样才符合混合开发的意义嘛。(只要 H5 端写好了,两个移动端就可以不写)。但是万万万万万万没想到,android,好像不可以???(我爱 Android)

尝试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">

让前端的同学加了属性,发现然并卵

尝试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
    }
}

那??拿到照片后,怎么返回给 H5 呢?

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

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

整体思路如上。


那具体上怎么操作呢?

 //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()
}

//权限申请和管理,这里用了 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()
                    }
                }
        }
    }

}

最后,是操作后的回调。要将最后结果返回给 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)
    }
}

最后贴出用到的 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
    }
}

核心的内容都在上面了,如果还有一些细节上存在疑问。可以留言或者私信我,我将很乐意为您解答。

以上是关于Android-混合开发H5 能直接调起原生的相册和相机吗?的主要内容,如果未能解决你的问题,请参考以下文章

Android-混合开发奇案-上传照片至 H5 失败

Android-混合开发奇案-上传照片至 H5 失败

Android H5调起原生微信或支付宝支付

杂园日记-H5-IOS-Android混合开发

Android与H5交互 -- 点击H5跳转到 Android原生 页面

androd H5混合开发 当无网络下,android怎么加载H5界面