Android CameraX适配Android11的踩坑之路

 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地址:感兴趣的同学可以看看,如有问题及时提出,一起成长.

CameraXApp: Android CameraX相机Api的使用实例

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容