?11月份Google發(fā)通知上架的應(yīng)用必須適配到Android30描孟,要不然提交到google play的app不能發(fā)布更新鸠儿,用戶(hù)就只能使用舊版本潦闲。
CameraX適配Android11的整個(gè)流程圖如下:
?1.我們來(lái)看看Google的通知說(shuō)明:
自 2021 年 11 月 1 日起挤悉,針對(duì) Google Play 上的應(yīng)用和游戲的更新必須以 Android 11(API 級(jí)別 30)或更高版本為目標(biāo)運(yùn)行環(huán)境泣刹。此日期過(guò)后辈讶,您將無(wú)法上傳targetSdkVersion低于 30 的新 app bundle 和 APK命浴。
請(qǐng)注意,Wear OS應(yīng)用不受關(guān)于 API 級(jí)別 30 的要求限制贱除。
將您的應(yīng)用配置為使用新近的 API 級(jí)別能使安全性和性能上的顯著改進(jìn)惠及用戶(hù)生闲,同時(shí)仍然允許您的應(yīng)用在較低版本的 Android(低至minSdkVersion)上運(yùn)行。
2.目標(biāo)版本:
compileSdkVersion30
buildToolsVersion"30.0.3"
defaultConfig{
? ? applicationId"com.example.cameraxapp"
? ? minSdkVersion21
? ? targetSdkVersion30
? ? versionCode1
? ? versionName"1.0"
? ? testInstrumentationRunner"androidx.test.runner.AndroidJUnitRunner"
}
3.我們把sdk的版本改為30之后出現(xiàn)的錯(cuò)誤如下:
?以上錯(cuò)誤信息具體意思就是在Android11及以上的手機(jī)讀寫(xiě)文件失敗
4.先看沒(méi)有適配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.適配之后的正確代碼:
/**
? * 開(kāi)始拍照
? */
? 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.創(chuàng)建一個(gè)文件:
?6.3.文件創(chuàng)建成功后把圖片插入媒體庫(kù):
val metadata = ImageCapture.Metadata().apply {
? ? // Mirror image when using the front camera
? ? isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
}
6.4.構(gòu)建圖片輸出對(duì)象outputOptions:
val outputOptions =
? ? ImageCapture.OutputFileOptions.Builder(photoFile).setMetadata(metadata).build()
6.5.拍照成功后通過(guò)MediaScannerConnection.scanFile刷新圖庫(kù)照片
?7.拍照成功后的日志如下:
?拍照成功后的截圖:
8.拍照適配Android11步驟:
8.1 請(qǐng)求文件讀寫(xiě)權(quán)限月幌,這里在首頁(yè)已經(jīng)請(qǐng)求過(guò)了直接上代碼碍讯,實(shí)際項(xiàng)目根據(jù)需要每個(gè)界面都要?jiǎng)討B(tài)請(qǐng)求權(quán)限
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("請(qǐng)您打開(kāi)必要權(quán)限")
? ? ? ? ? ? finish()
? ? ? ? }
? ? }
}
8.2 調(diào)起系統(tǒng)相機(jī)拍照
/**
* 調(diào)起系統(tǒng)相機(jī)拍照
*/
private fun startSystemCamera() {
? ? val takeIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
? ? val values = ContentValues()
? ? //根據(jù)uri查詢(xún)圖片地址
? ? 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)
? ? //調(diào)起拍照
? ? startActivityForResult(
? ? ? ? takeIntent,
? ? ? ? REQUEST_CODE_CAMERA
? ? )
}
8.3 拍照和裁剪回調(diào),由于加了系統(tǒng)裁剪扯躺,所以在拍照成功后會(huì)調(diào)用裁剪方法
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
? ? super.onActivityResult(requestCode, resultCode, data)
? ? if (resultCode == RESULT_OK) {
? ? ? ? if (requestCode == REQUEST_CODE_CAMERA) {//拍照回調(diào)
? ? ? ? ? ? workCropFun(photoUri)
? ? ? ? } else if (requestCode == REQUEST_CODE_CROP) {//裁剪回調(diào)
? ? ? ? ? ? setAvatar()
? ? ? ? }
? ? }
}
8.4 圖片裁剪方法冲茸,適配Android11
/**
* 系統(tǒng)裁剪方法
*/
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) // 裁剪框?qū)挾?/p>
? ? ? ? ? ? putExtra("outputY", 150) // 裁剪框高度
? ? ? ? ? ? putExtra("scale", true) // 縮放
? ? ? ? ? ? putExtra("return-data", false) // true-返回縮略圖-data屯阀,false-不返回-需要通過(guò)Uri
? ? ? ? ? ? putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()) // 保存的圖片格式
? ? ? ? ? ? putExtra("noFaceDetection", true) // 取消人臉識(shí)別
? ? ? ? ? ? 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
? ? ? ? )
? ? }
}
從上圖紅框內(nèi)容可以看到當(dāng)系統(tǒng)版本為Android11及以上時(shí)裁剪后直接獲取url和文件路徑的方式會(huì)報(bào)錯(cuò),提示讀寫(xiě)失敗轴术,解決方法為在Android11及以上的手機(jī)上通過(guò)MediaStore把uri插入到file中难衰,從而得到文件路徑.
?8.5 裁剪成功后設(shè)置用戶(hù)頭像,這里需要注意一下裁剪完之后這個(gè)路徑在Android11上面是不能直接獲取到的逗栽,也是需要MediaStore查詢(xún)媒體庫(kù)然后轉(zhuǎn)為file盖袭,最后才能把路徑設(shè)置為用戶(hù)頭像
/**
* 設(shè)置用戶(hù)頭像
*/
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 打印拍照成功后的圖片路徑為:
?總結(jié):
在Google11月份要求必須適配到30后,我們查閱很多資料彼宠,第一時(shí)間進(jìn)行了適配鳄虱,但是一路坎坷,所有文件權(quán)限可以解決文件讀寫(xiě)問(wèn)題凭峡,但是這個(gè)權(quán)限若應(yīng)用不是殺毒或文件管理類(lèi)這個(gè)權(quán)限是不允許隨便申請(qǐng)的拙已,即使你申請(qǐng)了上架google play的時(shí)候?qū)徍艘矔?huì)被拒絕,Android11外部文件不允許隨便讀寫(xiě)和刪除摧冀,今天只是講解了拍照和錄像時(shí)適配內(nèi)外部存儲(chǔ)權(quán)限倍踪,還有應(yīng)用可見(jiàn)性、Toast索昂、后臺(tái)運(yùn)行權(quán)限等等一些列的適配建车,在后面會(huì)寫(xiě)一篇文章全面總結(jié)一下最近的Android11適配工作。
最后給出最新的demo地址:感興趣的同學(xué)可以看看椒惨,如有問(wèn)題及時(shí)提出缤至,一起成長(zhǎng).