Kotlin 拍照、获取相册图片

安卓7.0以后,google修改了文件权限,不再允许app透露file://Uri来给其他app,转而使用FileProvider通过content://Uri来取代file://Uri

1.使用FileProvider必须在manifest文件中注册provider:

<application>
... ...
<provider
        android:authorities="${applicationId}.provider"
        android:name="android.support.v4.content.FileProvider"
        android:exported="false"
        android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepath"/>
    </provider>
</application>
  • android:authorities : FileProvider唯一标识
  • android:exported : 必须设置为false FileProvider不能公开
  • android:grantUriPermissions : 控制文件权限
  • android:resource : xml路径设置的filepath.xml

2.res/xml/新增 filepath.xml

<?xml version="1.0" encoding="utf-8"?>
<paths>
  <!--拍照存储路径-->
  <external-path
    name="pocket"
    path="pocket/picture/" />
  <!--访问相册路径-->
  <external-path
    name="external"
    path="." />
</paths>
  • path: 子目录名称
  • name: 取代path的别名

即原来路径名为file://xxxx/pocket/picture/x.jpg -> content://xxxx/pocket/picture/x.jpg

3.由于项目结果使用的是单activity多fragment,项目中的每个fragment都间接继承了PermissionCheckerDelegate,因此,项目所有的权限都放在此类中进行申请(先贴代码,后续再讲适配中出现的一些Bug解决方案):

abstract class PermissionCheckerDelegate : BaseDelegate() {

  //给子类用于显示的相片地址
  private lateinit var photoUri: Uri
  private lateinit var imagePath: String
  //供裁剪使用
  private lateinit var oriUri: Uri
  private val cropFile = File(Environment.getExternalStorageDirectory().absolutePath,
        "/pocket/picture/" + "crop_photo.jpg")

  companion object {
    const val WRITE_EXTERNAL_STORAGE = 1
    const val OPEN_CAMERA = 2
    const val OPEN_ALBUM = 3
    const val CROP_IMAGE = 4
  }

  /**
   * 相机读写权限申请
   */
  fun applyCameraPermission() {
    applyWritePermission(OPEN_CAMERA) {
        openCamera()
    }
  }

  /**
   * 相册读写权限申请
   */
  fun applyOpenAlbumPermission() {
    applyWritePermission(OPEN_ALBUM) {
        openAlbum()
    }
  }

  /**
   * 获取相机拍下的uri并转为bitmap
   */
  fun getBitmapByCamera() = BitmapFactory
        .decodeStream(context!!.contentResolver.openInputStream(photoUri))!!

  /**
   * 获取相册的图片转为bitmap
   */
  fun getBitmapByAlbum(data: Intent): Bitmap {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        handleImageAfterKitKat(data)
    } else {
        handleImageBeforeKitKat(data)
    }
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        //7.0适配
        oriUri = FileProvider.getUriForFile(context!!, "com.dididi.pocket.provider", File(imagePath))
    }
    return MediaStore.Images.Media.getBitmap(context!!.contentResolver, oriUri)
  }

  /**
   * 打开相机
   */
  private fun openCamera() {
    //创建file于sdcard/pocketPicture/ 以当前时间命名的jpg图像
    File(Environment.getExternalStorageDirectory().absolutePath,
            "/pocket/picture/" + System.currentTimeMillis() + ".jpg").apply {
        parentFile.mkdirs()
        photoUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            //android7.0之后,不再允许app透露file://Uri给其他app
            //转而使用FileProvider来生成content://Uri取代file://Uri
            FileProvider
                    .getUriForFile(context!!, "com.dididi.pocket.provider", this)
        } else {
            //7.0之前 直接获取Uri
            Uri.fromFile(this)
        }
    }
    Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
        //将uri存进intent,供相机回调使用 data.getData中获取
        putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
        startActivityForResult(this, OPEN_CAMERA)
    }
  }

  /**
   * 打开相册
   */
  private fun openAlbum() {
    Intent(Intent.ACTION_GET_CONTENT).apply {
        type = "image/*"
        startActivityForResult(this, OPEN_ALBUM)
    }
  }

  /**
   * 裁剪Uri
   * @param oriUri 原始Uri
   * @param desUri 目标Uri
   */
  fun cropImageUri(oriUri: Uri, desUri: Uri, aspectX: Int, aspectY: Int, width: Int, height: Int) {
    Intent("com.android.camera.action.CROP").apply {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        }
        setDataAndType(oriUri, "image/*")
        putExtra("crop", "true")
        putExtra("aspectX", aspectX)
        putExtra("aspectY", aspectY)
        putExtra("outputX", width)
        putExtra("outputY", height)
        putExtra("scale", true)
        //将剪切的图片保存到目标Uri中
        putExtra(MediaStore.EXTRA_OUTPUT, desUri)
        putExtra("return-data", false)
        putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString())
        putExtra("noFaceDetection", true)
        this@PermissionCheckerDelegate.startActivityForResult(this, CROP_IMAGE)
    }
  }

  override fun onRequestPermissionsResult(requestCode: Int,
                                        permissions: Array<out String>,
                                        grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    //权限请求结果
    when (requestCode) {
        WRITE_EXTERNAL_STORAGE -> {
            permissionHint(grantResults, "没有读写权限") {}
        }
        OPEN_CAMERA -> {
            permissionHint(grantResults, "没有读写权限") {
                openCamera()
            }
        }
        OPEN_ALBUM -> {
            permissionHint(grantResults, "没有读写权限") {
                openAlbum()
            }
        }
        else -> {
            Toast.makeText(context, "没有权限", Toast.LENGTH_SHORT).show()
        }
    }
  }

  /**
   * 权限结果处理lambda函数
   * @param grantResults 请求结果
   * @param msg toast内容
   * @param target 权限拿到要做什么
   */
  private fun permissionHint(grantResults: IntArray, msg: String, target: () -> Unit) {
    if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        target()
    } else {
        Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
    }
  }

  /**
   * 请求读写权限
   * @param requestCode 请求码
   * @param target 要做什么
   */
  private fun applyWritePermission(requestCode: Int, target: () -> Unit) {
    val permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
    //android6.0之后,需要动态申请读写权限
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        //读写是否已经授权
        val check = ContextCompat.checkSelfPermission(context!!, permissions[0])
        if (check == PackageManager.PERMISSION_GRANTED) {
            target()
        } else {
            //如果未发现授权,则请求权限
            requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
                    requestCode)
        }
    } else {
        target()
    }
  }

  /**
   * android4.4之后,需要解析获取图片真实路径
   */
  @TargetApi(Build.VERSION_CODES.KITKAT)
  private fun handleImageAfterKitKat(data: Intent) {
    val uri = data.data
    //document类型的Uri
    when {
        DocumentsContract.isDocumentUri(context, uri) -> {
            //通过documentId处理
            val docId = DocumentsContract.getDocumentId(uri)
            when (uri?.authority) {
                "com.android.externalstorage.documents" -> {
                    val type = docId.split(":")[0]
                    if ("primary".equals(type, ignoreCase = true)) {
                        imagePath = Environment.getExternalStorageDirectory()
                                .toString() + "/" + docId.split(":")[1]
                    }
                }
                //media类型解析
                "com.android.providers.media.documents" -> {
                    val id = docId.split(":")[1]
                    val type = docId.split(":")[0]
                    val contentUri: Uri? = when (type) {
                        "image" -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
                        "video" -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
                        "audio" -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
                        else -> null
                    }
                    val selection = "_id=?"
                    val selectionArgs: Array<String> = arrayOf(id)
                    imagePath = getImagePath(contentUri!!, selection, selectionArgs)!!
                }
                //downloads文件解析
                "com.android.providers.downloads.documents" -> {
                    ContentUris.withAppendedId(
                            Uri.parse("content://downloads/public_downloads"), docId.toLong()
                    ).apply {
                        imagePath = getImagePath(this, null, null)!!
                    }
                }
                else -> {
                }
            }
        }
        "content".equals(uri?.scheme, ignoreCase = true) ->
            //content类型数据不需要解析,直接传入生成即可
            imagePath = getImagePath(uri!!, null, null)!!
        "file".equals(uri?.scheme, ignoreCase = true) ->
            //file类型的uri直接获取图片路径即可
            imagePath = uri!!.path!!
    }
  }

  /**
   * android4.4之前可直接获取图片真实uri
   */
  private fun handleImageBeforeKitKat(data: Intent) {
    val uri = data.data
    imagePath = getImagePath(uri!!, null, null)!!
  }

  /**
   * 解析uri及selection
   * 获取图片真实路径
   */
  private fun getImagePath(uri: Uri, selection: String?, selectionArgs: Array<String>?): String? {
    var cursor: Cursor? = null
    try {
        cursor = context!!.contentResolver.query(uri, null, selection, selectionArgs, null)
        if (cursor?.moveToFirst()!!) {
            return cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA))
        }
    } finally {
        cursor?.close()
    }
    return null
  }
}
  1. 子类ChatDelegate间接继承PermissionCheckerDelegate,因此,子类只需要复写onActivityResult()方法即可,具体代码省略如下:

     ... ...
     //上拉页面的按钮
     //打开相机
     val moreCamera = morePagerView?.findViewById<MoreButtonItem>(R.id.item_msg_chat_more_camera)
     moreCamera?.setOnClickListener {
         applyCameraPermission()
     }
     //打开相册
     val moreOpenAlbum = morePagerView?.findViewById<MoreButtonItem>(R.id.item_msg_chat_more_album)
     moreOpenAlbum?.setOnClickListener {
         applyOpenAlbumPermission()
     }
     ... ...
     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
       super.onActivityResult(requestCode, resultCode, data)
       when (requestCode) {
         OPEN_CAMERA -> {
             if (resultCode == ISupportFragment.RESULT_OK) {
                 insertBitmapToList(getBitmapByCamera())
             }
         }
         OPEN_ALBUM -> {
             if (resultCode == ISupportFragment.RESULT_OK) {
                 insertBitmapToList(getBitmapByAlbum(data!!))
             }
         }
       }
     }
    

碰到了以下几个Error:

  1. java.lang.IllegalStateException: Couldn't read row 0, col -1 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it.
    注意检查android4.4之后Uri的解析是否正确是否有遗漏项没解析,参考如handleImageAfterKitKat()方法。
    2.java.io.FileNotFoundException: No content provider: /storage/emulated/0/.../xxx.jpg,这个问题出现在访问相册获取相册照片时,android7.0以上设备需要通过FileProvider来获得访问权限,检查一下filepath.xml文件,如果是使用getExternalStorageDirectory()需要加上:

     <!--访问相册路径-->
     <external-path
       name="external"
       path="." />
    

然后拿到处理后的imagePath之后,需要通过FileProvider来获取图片Uri,具体如方法getBitmapByAlbum():

/**
 * 获取相册的图片转为bitmap
 */
fun getBitmapByAlbum(data: Intent): Bitmap {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        handleImageAfterKitKat(data)
    } else {
        handleImageBeforeKitKat(data)
    }
    oriUri = Uri.parse(imagePath)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        //7.0适配(此处的authority为${applicationId}.provider)
        oriUri = FileProvider.getUriForFile(context!!, "com.dididi.pocket.provider", File(imagePath))
    }
    return MediaStore.Images.Media.getBitmap(context!!.contentResolver, oriUri)
}

至此。放张效果图吧。。。

sendPhoto.gif

PermissionCheckerDelegate.kt

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

推荐阅读更多精彩内容