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 能直接调起原生的相册和相机吗?的主要内容,如果未能解决你的问题,请参考以下文章