H5调用本地相册/相机上传图片

在开发中有时候会用到H5调用本地图片或者相机,像第三方的实名认证,在线客服等等都需要上传图片。H5中只需要通过<input>调用即可,ios是可以的实现的,不需要自己处理,除非客户端压缩图片,但是Android中需要自己处理,比较坑,本文记录H5调用本地图片或者相机的实现过程以及遇到的问题。

先看效果图

H5

H5主要是通过input标签来获取图片

 <input class="upload-input"  accept="image/*" multiple required type="file" @change="chooseImg">

Android

通过WebView加载h5页面,监听WebView对应的方法,实现自己的逻辑。

WebView初始化,放开对应的权限
private fun initWebView() {
        val settings = mWebView.settings
        settings.javaScriptEnabled = true
        settings.javaScriptCanOpenWindowsAutomatically = true
        //自适应屏幕
        settings.layoutAlgorithm = SINGLE_COLUMN
        settings.loadWithOverviewMode = true
        settings.domStorageEnabled = true
        settings.databaseEnabled = true
        settings.setSupportZoom(false)
        mWebView.isHorizontalScrollBarEnabled = false
        mWebView.isVerticalScrollBarEnabled = false
        mWebView.webViewClient = webViewClient
        mWebView.webChromeClient = webViewChromeClient
}
设置WebChromeClient

当点击input标签的时候会调用WebChromeClientonShowFileChooser()方法(5.0+)或者openFileChooser()方法(3.0+)在改方法中处理自己逻辑,这里是弹出一个原生的选择框,选择从相册还是拍照获取图片。

private val webViewChromeClient = object : WebChromeClient() {
      override fun onShowFileChooser(webView: WebView, filePathCallback: ValueCallback<Array<Uri>>,
                                     fileChooserParams: FileChooserParams): Boolean {
          mFilePathCallback = filePathCallback
          val acceptTypes = fileChooserParams.acceptTypes
          //判断文件类型
          if (acceptTypes.contains("image/*")) {
              showSelectDialog()
          }
          return true
      }
}

ValueCallbacksWebView组件提供给我们的,它里面包含了一个或者一组Uri,然后我们在onActivityResult()里将Uri传给ValueCallbacksonReceiveValue()方法,这样就把文件回传给H5端了。

注意:这里会遇到onShowFileChooser()只调用一次的问题,也就是第二次点击按钮的时候该方法不再调用,因为每次调用该方法都需要设置一个回调,不管是不是选择了照片,没有就回传一个null
/**
 * 显示相册/拍照选择对话框
 */
private fun showSelectDialog() {
    if (mSelectPhotoDialog == null) {
        mSelectPhotoDialog = SelectDialog(this, View.OnClickListener { view ->
            when (view.id) {
                R.id.tv_camera -> startCamera()
                R.id.tv_photo -> startAlbum()
                //不管选择还是不选择,必须有返回结果,否则就会调用一次
                R.id.tv_cancel -> {
                    mFilePathCallback?.onReceiveValue(null)
                    mFilePathCallback = null
                }
            }
        })
    }
    mSelectPhotoDialog?.show()
}

拍照和打开相册功能封装到了PhotoUtils工具类中了。

/**
 * 拍照
 * @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)
}
注意:拍照需要适配7.0,否者拿不到文件,Android 7.0 以后 通过FileProvider在应用间共享文件;此外拍完照片获取文件的时候不要通过intent来获取数据,直接通过这里指定的存储路径来获取,intent获取的一直是null
<application
 ......
    <!-- Android7.0拍照权限适配 -->
    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="com.gfd.h5pic.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
</application>

file_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <paths>
        <cache-path
            name="cache"
            path="" />
        <external-path
            name="sdcard_files"
            path="" />
        <external-path
            name="external"
            path="" />
        <external-files-path
            name="external_files"
            path="" />
        <external-cache-path
            name="external_cache"
            path="" />
        <root-path
            name="ex_sdcard_files"
            path="" />
        <!--相机相册裁剪-->
        <external-files-path
            name="camera_has_sdcard"
            path="file/" />
        <files-path
            name="camera_no_sdcard"
            path="" />
    </paths>
</resources>

从相册中选择

/**
 * 打开相册
 * @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)
}

onActivityResult()方法中通过intent获取选取的照片。

onActivityResult()方法处理

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == PhotoUtils.RESULT_CODE_CAMERA && resultCode == Activity.RESULT_OK) {
        //拍照并确定
        compressPicture(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 {
            compressPicture(path)
        }
    } else {
        mFilePathCallback?.onReceiveValue(null)
    }
}

这里需要注意的是要判断resultCode,不然拍照完成,选择的不是确定而是取消,也是会上传图片的。

这里使用Luban压缩以后再上传的

/**
 * 压缩图片
 */
private fun compressPicture(path: String) {
    PhotoUtils.compressPicture(this, path, object : PhotoUtils.OnPictureCompressListener() {
        override fun onSuccess(file: File) {
            //上传图片
            mFilePathCallback?.onReceiveValue(arrayOf(Uri.fromFile(file)))
        }
        override fun onError(e: Throwable) {
            mFilePathCallback?.onReceiveValue(null)
        }
    })
}
最后附上完整的代码

github

Kotlin项目实战

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容