Android原生拍照及相册选取图片

前言

图片上传操作基本上是每个应用都会有的功能,但是由于Android碎片话严重,导致适配很繁琐,比如6.0以下版本、6.0的动态权限、7.0的FileProvider、8.0的特殊情况,再加上马上要出的Android Q权限变化。我个人项目中三方库能少用就少用,能自己解决,绝不用三方库,因为很多时候三方库不一定能满足自身要求,尤其是UI方面。下面记录下我项目中用到的图片操作,用Android原生方式实现,并且适配了Andrioid 7.0的FileProvider。

动态权限

6.0以上危险权限需要动态获取,那可能不知道哪些是危险权限,那么看下图:

危险权限列表

选取图片用的文件读写和相机操作都需要动态获取,我们这里依然用原生实现,代码如下:

    override fun onResume() {
        super.onResume()
        checkPermission()
    }

     /**
     * 获取权限
     * 因为是一次请求完几个权限,不是在用到的时候再请求,
     * 所以不需要重写onRequestPermissionsResult方法
     */
    private fun checkPermission() {
        val storage = arrayOf(
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.READ_EXTERNAL_STORAGE
        )
        val camera = arrayOf(Manifest.permission.CAMERA)
        //检查相机权限
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            // 之前拒绝了权限,但没有点击 不再询问 这个时候让它继续请求权限
            if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
                Log.d(TAG, "用户曾拒绝打开相机权限")
                ActivityCompat.requestPermissions(this, camera, 100)
            } else {
                //注册相机权限
                ActivityCompat.requestPermissions(this, camera, 100)
            }
        }
        //检查文件读写权限
        if (ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.READ_EXTERNAL_STORAGE
            ) != PackageManager.PERMISSION_GRANTED
            || ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
                Log.d(TAG, "用户曾拒绝打开文件读写权限")
                ActivityCompat.requestPermissions(this, storage, 101)
            } else {
                //注册相机权限
                ActivityCompat.requestPermissions(this, storage, 101)
            }
        }
    }

7.0FileProvider适配

第一步

在res目录下创建xml文件夹,在文件夹下创建一个文件,比如:file_paths.xml,文件里添加如下代码:



    
    
    
    
    

第二步

在AndroidManifest.xml的application中添加provider标签,如下图所示:

provider.png

其中${applicationId}你可以直接这么写,也可以替换成你的包名。这两步配置好了后7.0适配算是完成了三分之二,后面如何在代码中读取文件,请耐心往后看。

目录创建

相机拍照后保存在手机DCIM/Image目录下,如下代码中getFileUri方法就是对7.0的兼容

/**
 * @author limh
 * @function
 * @date 2019/4/19 9:40
 */
object FileUtils {
    val BASE_PATH = "${Environment.getExternalStorageDirectory()}/DCIM/Image"
    /**
     * 根据文件名称 生成目录
     * @param fileName 文件名称
     */
    fun getFilePath(fileName: String): String {
        val dir = File(BASE_PATH)
        //如果目录不存在 先创建目录
        if (!dir.exists()) {
            dir.mkdir()
        }
        return "$BASE_PATH/$fileName"
    }

    /**
     * 获取文件Uri
     * @param fileName 文件名称
     */
    fun getFileUri(context: Context, fileName: String): Uri {
        val filePah = getFilePath(fileName)
        return if (Build.VERSION.SDK_INT >= 24) {
            FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.fileprovider", File(filePah))
        } else {
            Uri.fromFile(File(filePah))
        }
    }
}

打开相机

    /**
     * 打开相机
     */
    private fun openCamera() {
        Log.d(TAG, "打开相机")
        imgUri = FileUtils.getFileUri(context, "{${System.currentTimeMillis()}}.jpg")
        // 创建Intent,用于启动手机的照相机拍照
        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        // 指定输出到文件uri中
        intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri)
        // 启动intent开始拍照
        startActivityForResult(intent, REQUEST_CAMERA)
    }
  • 注意:上面打开相机方式拍照后文件会保存在imgUri中,onActivityResult回调不会返回数据,当回调成功后直接拿imgUri就是你拍的照片内容。拍照完成打开DCIM/Image/Album_xxxxxx.jpg,就是你刚拍摄的图片。

打开相册

    /**
     * 打开相册
     */
    private fun openAlbum() {
        Log.d(TAG, "打开相册")
        val albumIntent = Intent(Intent.ACTION_PICK)
        albumIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*")
        startActivityForResult(albumIntent, REQUEST_ALBUM)
    }
  • 注意:这种方式打开相册 打开相册后选择的图片uri会在intent的data中,需要我们从里面取.

原生裁剪

    /**
     * 裁剪图片
     * @param uri:要裁剪的文件
     */
    private fun corpPic(uri: Uri?) {
        //裁剪后的文件名称
        val fileName = "Crop_${System.currentTimeMillis()}.jpg"
        //裁剪后文件Uri
        imgUri = FileUtils.getFileUri(context, fileName)
        val intent = Intent("com.android.camera.action.CROP")
        intent.setDataAndType(uri, "image/*")
        //以下两行添加,解决无法加载此图片的提示
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
        intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(File(FileUtils.getFilePath(fileName))))
        intent.putExtra("crop", "true")
        intent.putExtra("aspectX", 1) // 裁剪框比例
        intent.putExtra("aspectY", 1)
        intent.putExtra("outputX", 100) // 输出图片大小
        intent.putExtra("outputY", 100)
        intent.putExtra("scale", false)
        intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString())
        startActivityForResult(intent, REQUEST_CORP)
    }

-注意:裁剪输出目录一定要用Uri.fromFile生成,直接用imgUri的话会提示:无法保存经过裁剪的图片,图片也无法正常加载。

个人感觉原生裁剪很好用,可以设置裁剪比例和输出大小。设置完后图片很小,也不需要做压缩操作。

相机相册以及裁剪回调

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        Log.d(TAG, "返回结果:${data ?: data.toString()}")
        if (requestCode == REQUEST_CAMERA) {
            //相机结果返回
            if (resultCode == Activity.RESULT_OK) {
                imgUri?.let { corpPic(it) }
            }
        } else if (requestCode == REQUEST_ALBUM) {
            //相册返回结果
            if (resultCode == Activity.RESULT_OK) {
                //选择的图片转存 在DCIM/Image目录
                data?.let { corpPic(it.data) }
            }
        } else if (requestCode == REQUEST_CORP) {
            //裁剪回调
            Picasso.get().load(imgUri).memoryPolicy(MemoryPolicy.NO_CACHE).into(image)
        }
    }

至此,整个相机相册以及裁剪操作完成,最终的结果都保存到Uri中,但是有时候还需要解析文件路径,这个解析就涉及到版本兼容,又是另一个大坑,5.0上下获取方式都不同,好歹踩过坑的人,因此再上一个文件路径解析工具,直接调用ImagePath.getRealPathFromUri将Uri转换为String路径:

/**
 * @author limh
 * @function 解析图片路径
 * @date 2019/2/25 11:29
 */
object ImagePath {
    /**
     * 根据Uri获取图片的绝对路径
     *
     * @param context 上下文对象
     * @param uri     图片的Uri
     * @return 如果Uri对应的图片存在, 那么返回该图片的绝对路径, 否则返回null
     */
    fun getRealPathFromUri(context: Context, uri: Uri?): String? {
        if (null == uri) return ""
        val sdkVersion = Build.VERSION.SDK_INT
        return if (sdkVersion >= 19) {
            getRealPathFromUriAboveApi19(context, uri)
        } else { // api < 19
            getRealPathFromUriBelowAPI19(context, uri)
        }
    }

    /**
     * 适配api19以下(不包括api19),根据uri获取图片的绝对路径
     *
     * @param context 上下文对象
     * @param uri     图片的Uri
     * @return 如果Uri对应的图片存在, 那么返回该图片的绝对路径, 否则返回null
     */
    private fun getRealPathFromUriBelowAPI19(context: Context, uri: Uri): String? {
        return getDataColumn(context, uri, null, null)
    }

    /**
     * 适配api19及以上,根据uri获取图片的绝对路径
     *
     * @param context 上下文对象
     * @param uri     图片的Uri
     * @return 如果Uri对应的图片存在, 那么返回该图片的绝对路径, 否则返回null
     */
    @SuppressLint("NewApi")
    private fun getRealPathFromUriAboveApi19(context: Context, uri: Uri): String? {
        var filePath: String? = null
        if (DocumentsContract.isDocumentUri(context, uri)) {
            // 如果是document类型的 uri, 则通过document id来进行处理
            val documentId = DocumentsContract.getDocumentId(uri)
            if (isMediaDocument(uri)) { // MediaProvider
                // 使用':'分割
                val id = documentId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1]

                val selection = MediaStore.Images.Media._ID + "=?"
                val selectionArgs = arrayOf(id)
                filePath =
                    getDataColumn(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection, selectionArgs)
            } else if (isDownloadsDocument(uri)) { // DownloadsProvider
                val contentUri = ContentUris.withAppendedId(
                    Uri.parse("content://downloads/public_downloads"),
                    java.lang.Long.valueOf(documentId)
                )
                filePath = getDataColumn(context, contentUri, null, null)
            }
        } else if ("content".equals(uri.scheme!!, ignoreCase = true)) {
            // 如果是 content 类型的 Uri
            filePath = getDataColumn(context, uri, null, null)
        } else if ("file" == uri.scheme) {
            // 如果是 file 类型的 Uri,直接获取图片对应的路径
            filePath = uri.path
        }
        return filePath
    }

    /**
     * 获取数据库表中的 _data 列,即返回Uri对应的文件路径
     *
     * @return
     */
    private fun getDataColumn(context: Context, uri: Uri, selection: String?, selectionArgs: Array?): String? {
        var path: String? = null

        val projection = arrayOf(MediaStore.Images.Media.DATA)
        var cursor: Cursor? = null
        try {
            cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
            if (cursor != null && cursor.moveToFirst()) {
                val columnIndex = cursor.getColumnIndexOrThrow(projection[0])
                path = cursor.getString(columnIndex)
            }
        } catch (e: Exception) {
            cursor?.close()
        }

        return path
    }

    /**
     * @param uri the Uri to check
     * @return Whether the Uri authority is MediaProvider
     */
    private fun isMediaDocument(uri: Uri): Boolean {
        return "com.android.providers.media.documents" == uri.authority
    }

    /**
     * @param uri the Uri to check
     * @return Whether the Uri authority is DownloadsProvider
     */
    private fun isDownloadsDocument(uri: Uri): Boolean {
        return "com.android.providers.downloads.documents" == uri.authority
    }
}

总结

生命不息,踩坑不止,很多Android三方库追根揭底也是对原生SDK的封装,多了解下原生的写法,你就是下一个轮子大神。

最后附上github地址:
https://github.com/limhGeek/Album

你可能感兴趣的:(Android原生拍照及相册选取图片)