在开发中有时候会用到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
标签的时候会调用WebChromeClient的onShowFileChooser()
方法(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
}
}
ValueCallbacks
是WebView组件提供给我们的,它里面包含了一个或者一组Uri,然后我们在onActivityResult()
里将Uri传给ValueCallbacks
的onReceiveValue()
方法,这样就把文件回传给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)
}
})
}