11月份Google发通知上架的应用必须适配到Android30,要不然提交到google play的app不能发布更新,用户就只能使用旧版本。
CameraX适配Android11的整个流程图如下:
1.我们来看看Google的通知说明:
自 2021 年 11 月 1 日起,针对 Google Play 上的应用和游戏的更新必须以 Android 11(API 级别 30)或更高版本为目标运行环境。此日期过后,您将无法上传targetSdkVersion低于 30 的新 app bundle 和 APK。
请注意,Wear OS应用不受关于 API 级别 30 的要求限制。
将您的应用配置为使用新近的 API 级别能使安全性和性能上的显著改进惠及用户,同时仍然允许您的应用在较低版本的 Android(低至minSdkVersion)上运行。
2.目标版本:
compileSdkVersion30
buildToolsVersion"30.0.3"
defaultConfig{
applicationId"com.example.cameraxapp"
minSdkVersion21
targetSdkVersion30
versionCode1
versionName"1.0"
testInstrumentationRunner"androidx.test.runner.AndroidJUnitRunner"
}
3.我们把sdk的版本改为30之后出现的错误如下:
以上错误信息具体意思就是在Android11及以上的手机读写文件失败
4.先看没有适配Android11之前的代码:
private fun takePhoto() {
val imageCapture =imageCamera ?:return
/* val photoFile = createFile(outputDirectory, DATE_FORMAT, PHOTO_EXTENSION)
val metadata = ImageCapture.Metadata().apply {
// Mirror image when using the front camera
isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
}*/
val mFileForMat = SimpleDateFormat(DATE_FORMAT, Locale.US)
val file = File(FileManager.getAvatarPath(mFileForMat.format(Date()) +".jpg"))
val outputOptions =
ImageCapture.OutputFileOptions.Builder(file).build()
imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG,"Photo capture failed: ${exc.message}", exc)
ToastUtils.shortToast(" 拍照失败 ${exc.message}")
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = output.savedUri ?: Uri.fromFile(file)
ToastUtils.shortToast(" 拍照成功 $savedUri")
Log.d(TAG, savedUri.path.toString())
val mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(savedUri.toFile().extension)
MediaScannerConnection.scanFile(
this@MainActivity,
arrayOf(savedUri.toFile().absolutePath),
arrayOf(mimeType)
){ _, uri->
Log.d(TAG,"Image capture scanned into media store: $uri")
}
}
})
}
5.适配之后的正确代码:
/**
* 开始拍照
*/
private fun takePhoto() {
val imageCapture =imageCamera ?:return
val photoFile = createFile(outputDirectory,DATE_FORMAT,PHOTO_EXTENSION)
val metadata = ImageCapture.Metadata().apply {
// Mirror image when using the front camera
isReversedHorizontal =lensFacing == CameraSelector.LENS_FACING_FRONT
}
/* val mFileForMat = SimpleDateFormat(DATE_FORMAT, Locale.US)
val file = File(FileManager.getAvatarPath(mFileForMat.format(Date()) + ".jpg"))*/
val outputOptions =
ImageCapture.OutputFileOptions.Builder(photoFile).setMetadata(metadata).build()
imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG,"Photo capture failed: ${exc.message}", exc)
ToastUtils.shortToast(" 拍照失败 ${exc.message}")
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
ToastUtils.shortToast(" 拍照成功 $savedUri")
Log.d(TAG, savedUri.path.toString())
val mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(savedUri.toFile().extension)
MediaScannerConnection.scanFile(
this@MainActivity,
arrayOf(savedUri.toFile().absolutePath),
arrayOf(mimeType)
){ _, uri->
Log.d(TAG,"Image capture scanned into media store: $uri")
}
}
})
}
6.适配步骤:
6.1 初始化文件和图片输出路径
6.2.创建一个文件:
6.3.文件创建成功后把图片插入媒体库:
val metadata = ImageCapture.Metadata().apply {
// Mirror image when using the front camera
isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
}
6.4.构建图片输出对象outputOptions:
val outputOptions =
ImageCapture.OutputFileOptions.Builder(photoFile).setMetadata(metadata).build()
6.5.拍照成功后通过MediaScannerConnection.scanFile刷新图库照片
7.拍照成功后的日志如下:
拍照成功后的截图:
8.拍照适配Android11步骤:
8.1 请求文件读写权限,这里在首页已经请求过了直接上代码,实际项目根据需要每个界面都要动态请求权限
if (allPermissionsGranted()) {
// ImageCapture
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, Constants.REQUEST_CODE_PERMISSIONS
)
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults:
IntArray
) {
if (requestCode == Constants.REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
ToastUtils.shortToast("请您打开必要权限")
finish()
}
}
}
8.2 调起系统相机拍照
/**
* 调起系统相机拍照
*/
private fun startSystemCamera() {
val takeIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
val values = ContentValues()
//根据uri查询图片地址
photoUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
Log.w("lzq", "photoUri:" + photoUri?.authority + ",photoUri:" + photoUri?.path)
//放入拍照后的地址
takeIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
//调起拍照
startActivityForResult(
takeIntent,
REQUEST_CODE_CAMERA
)
}
8.3 拍照和裁剪回调,由于加了系统裁剪,所以在拍照成功后会调用裁剪方法
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK) {
if (requestCode == REQUEST_CODE_CAMERA) {//拍照回调
workCropFun(photoUri)
} else if (requestCode == REQUEST_CODE_CROP) {//裁剪回调
setAvatar()
}
}
}
8.4 图片裁剪方法,适配Android11
/**
* 系统裁剪方法
*/
private fun workCropFun(imgPathUri: Uri?) {
mUploadImageUri = null
mUploadImageFile = null
if (imgPathUri != null) {
val imageObject: Any = FileUtil.getHeadJpgFile()
if (imageObject is Uri) {
mUploadImageUri = imageObject
}
if (imageObject is File) {
mUploadImageFile = imageObject
}
val intent = Intent("com.android.camera.action.CROP")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
intent.run {
setDataAndType(imgPathUri, "image/*")// 图片资源
putExtra("crop", "true") // 裁剪
putExtra("aspectX", 1) // 宽度比
putExtra("aspectY", 1) // 高度比
putExtra("outputX", 150) // 裁剪框宽度
putExtra("outputY", 150) // 裁剪框高度
putExtra("scale", true) // 缩放
putExtra("return-data", false) // true-返回缩略图-data,false-不返回-需要通过Uri
putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()) // 保存的图片格式
putExtra("noFaceDetection", true) // 取消人脸识别
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putExtra(MediaStore.EXTRA_OUTPUT, mUploadImageUri)
} else {
val imgCropUri = Uri.fromFile(mUploadImageFile)
putExtra(MediaStore.EXTRA_OUTPUT, imgCropUri)
}
}
startActivityForResult(
intent, REQUEST_CODE_CROP
)
}
}
从上图红框内容可以看到当系统版本为Android11及以上时裁剪后直接获取url和文件路径的方式会报错,提示读写失败,解决方法为在Android11及以上的手机上通过MediaStore把uri插入到file中,从而得到文件路径.
8.5 裁剪成功后设置用户头像,这里需要注意一下裁剪完之后这个路径在Android11上面是不能直接获取到的,也是需要MediaStore查询媒体库然后转为file,最后才能把路径设置为用户头像
/**
* 设置用户头像
*/
private fun setAvatar() {
val file: File? = if (mUploadImageUri != null) {
FileManager.getMediaUri2File(mUploadImageUri)
} else {
mUploadImageFile
}
Glide.with(this).load(file).into(iv_avatar)
Log.d("filepath", file!!.absolutePath)
}
8.6 打印拍照成功后的图片路径为:
总结:
在Google11月份要求必须适配到30后,我们查阅很多资料,第一时间进行了适配,但是一路坎坷,所有文件权限可以解决文件读写问题,但是这个权限若应用不是杀毒或文件管理类这个权限是不允许随便申请的,即使你申请了上架google play的时候审核也会被拒绝,Android11外部文件不允许随便读写和删除,今天只是讲解了拍照和录像时适配内外部存储权限,还有应用可见性、Toast、后台运行权限等等一些列的适配,在后面会写一篇文章全面总结一下最近的Android11适配工作。
最后给出最新的demo地址:感兴趣的同学可以看看,如有问题及时提出,一起成长.