美文网首页
Kotlin 拍照、获取相册图片

Kotlin 拍照、获取相册图片

作者: 512DIDIDI | 来源:发表于2018-12-28 16:36 被阅读0次

安卓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

相关文章

网友评论

      本文标题:Kotlin 拍照、获取相册图片

      本文链接:https://www.haomeiwen.com/subject/ricelqtx.html