我正在参加「启航方案」
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">
于是,让前端的同学加了相应的特点,成果发现并没有什么效果。
测验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);
}
全体思路如上。
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()
}
//权限申请和办理,这儿用了 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
}
}
04. 结语
中心的内容都在上面了,假如还有一些细节上存在疑问。能够留言或者私信我,我将很乐意为您回答。假如呈现图片上传失败,不妨看看我另一篇文章——【Android】混合开发 – 奇案 – 上传相片至 H5 失败,或许能为你处理相关问题。