近期使用Android拍照功能时发现,一些Android11版本及以上的手机拍照时无法将图片保存到应用程序指定目录中。只会创建文件但是文件的大小为0KB。这个问题会导致应用中一些更换图片的逻辑失效比如用户头像替换、背景图片的替换等。如何解决这个问题给大家提供一个兼容Android 24版本以上的拍照、视频及打开相册的兼容工具类。工具类如下:

class PhotoUtils private constructor() {
    private val mTag: String by lazy {
        TTLog.makeLogTag(PhotoUtils::class.java)
    companion object {
        private const val GALLERY_REQUEST_CODE = 105    // 相册选图标记
        private const val CAMERA_REQUEST_CODE = 106    // 相机拍照标记
        private const val VIDEO_REQUEST_CODE = 107    //相册选择视频
        //拍照图片地址
        private var mTempPhotoPath: String = ""
        //工具列对象
        private var mInstance: PhotoUtils? = null
        //上下文
        private lateinit var mContext: Context
         * 获取工具列对象方法
        fun getInstance() = mInstance ?: PhotoUtils().also {
            mInstance = it
     * 打开相册
     * @param activity 所在Activity对象
    fun openPhotoPick(activity: Activity) {
        //设置对象
        mContext = activity
        val pickIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
        // 如果限制上传到服务器的图片类型时可以直接写如:"image/jpeg 、 image/png等的类型"
        pickIntent.type = "image/*"
        activity.startActivityForResult(pickIntent, GALLERY_REQUEST_CODE)
     * 打开视频
     * @param activity 所在Activity对象
    fun openVideoPick(activity: Activity) {
        //设置对象
        mContext = activity
        val pickIntent = Intent(Intent.ACTION_PICK, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
        pickIntent.type = CONTENT_TYPE
        activity.startActivityForResult(pickIntent, VIDEO_REQUEST_CODE)
     * 开启照相机
     * @param activity 所在Activity对象
    fun openPhotograph(activity: Activity) {
        val takeIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        //设置对象
        mContext = activity
        //生成图片路径
        mTempPhotoPath = activity.getExternalFilesDir(Environment.DIRECTORY_DCIM)?.absolutePath + File.separator + "${System.currentTimeMillis()}.jpeg"
        //解决Android11无法拍照存储问题
        val imageUri = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
            FileProvider.getUriForFile(activity, "${activity.packageName}.fileprovider", File(mTempPhotoPath))
        } else {
            Uri.fromFile(File(mTempPhotoPath))
        //下面这句指定调用相机拍照后的照片存储的路径
        takeIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri)
        activity.startActivityForResult(takeIntent, CAMERA_REQUEST_CODE)
     * 该方法必须放置在对应Activity的onActivityResult方法中
     * @param requestCode 请求码
     * @param resultCode 响应码
     * @param data 数据传输对象
     * @param method 图片或视频地址回调方法
    fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, method: ((type: MediaTypeEnum, uri: Uri, path: String) -> Unit)? = null) {
        if (resultCode == BaseOtherActivity.RESULT_OK) {
            when (requestCode) {
                CAMERA_REQUEST_CODE -> {// 调用相机拍照
                    val temp = File(mTempPhotoPath)
                    TTLog.e(mTag, "image_temp:${temp.absoluteFile}")
                    method?.let {
                        it(MediaTypeEnum.IMAGE_TYPE, Uri.fromFile(temp), temp.absolutePath)
                GALLERY_REQUEST_CODE -> {// 直接从相册获取图片地址
                    data?.let { dataIntent ->
                        TTLog.e(mTag, "image_temp:${dataIntent.data}")
                        getFilePath(MediaTypeEnum.IMAGE_TYPE, dataIntent.data
                                ?: Uri.parse(""), method)
                VIDEO_REQUEST_CODE -> {// 直接从相册获取视频地址
                    data?.let { dataIntent ->
                        TTLog.e(mTag, "video_temp:${dataIntent.data}")
                        getFilePath(MediaTypeEnum.VIDEO_TYPE, dataIntent.data
                                ?: Uri.parse(""), method)
                else -> {
                    //todo:未知类型
     * 获得文件sd卡路径
     * @param type 多媒体类型[MediaTypeEnum]
     * @param uri 获取ContentProvider共享数据地址
     * @param method 回到方法
    @SuppressLint("CheckResult")
    private fun getFilePath(type: MediaTypeEnum, uri: Uri, method: ((type: MediaTypeEnum, uri: Uri, path: String) -> Unit)? = null) {
        Single.create<String> {
            val cursor = mContext.contentResolver.query(uri, null, null, null, null)
            if (cursor != null && cursor.count > 0) {
                if (cursor.moveToFirst()) {
                    val data = cursor.getString(cursor.getColumnIndex(MediaStore.Files.FileColumns.DATA))
                    it.onSuccess(data)
                cursor.close()
            } else {
                it.onError(Throwable("query uri is null"))
        }.subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ path ->
                    method?.let {
                        it(type, uri, path ?: "")
                    method?.let {
                        it(type, uri, "")
     * 多媒体类型枚举
    enum class MediaTypeEnum {
        IMAGE_TYPE,
        VIDEO_TYPE,
        DEFAULT_TYPE
}

该工具类中定义了三个常量分别是GALLERY_REQUEST_CODED、CAMERA_REQUEST_CODE、VIDEO_REQUEST_CODE依次代表相册选图标记、相册拍照标记及相册选择视频标记。也提供了三个open方法对应打开相应的功能。在onActivityResult方法中获取到的相册中的图片或者视频信息往往都是uri地址(content开头),而我们需要的并不是uri而是具体的图片地址。这样的话就需要通过ContentProvider进行数据库查询,所以在工具类中提供了一个getFilePath()方法对其进行数据库查询获取对应图片或者视频的绝对地址数据。当获取到这个地址就可以进行下一步操作,比如上传、发送文件等。