安卓摄影、裁切、选取图片实践

前语

最近项目里边有用到裁切功能,没弄多杂乱,便是体系自带的,顺便就总结了一下体系摄影、裁切、选取的运用。网上的资料说实话真是没什么营养,可是Android官网上的阐明也有点太简单了,真就要实践出真理。

摄影

原本摄影是没什么难度的,不便是调用intent去体系相机摄影么,可是由于文件权限问题,Uri这东西就能把人很头疼。下面是代码(onActivityResult见后文):

    private fun openCamera() {
        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        // 运用外部私有目录:files-Pictures
        val picFile = createFile("Camera")
        val photoUri = getUriForFile(picFile)
        // 保存途径,不要uri,读取bitmap时费事
        picturePath = picFile.absolutePath
        // 给目标运用一个暂时授权
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        //android11以后强制分区存储,外部资源无法拜访,所以添加一个输出保存方位,然后取值操作
        intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
        startActivityForResult(intent, REQUEST_CAMERA_CODE)
    }
    private fun createFile(type: String): File {
        // 在相册创立一个暂时文件
        val picFile = File(requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES),
                "${type}_${System.currentTimeMillis()}.jpg")
        try {
            if (picFile.exists()) {
                picFile.delete()
            }
            picFile.createNewFile()
        } catch (e: IOException) {
            e.printStackTrace()
        }
        // 暂时文件,后边会加long型随机数
//        return File.createTempFile(
//            type,
//            ".jpg",
//            requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)
//        )
        return picFile
    }
    private fun getUriForFile(file: File): Uri {
        // 转换为uri
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            //适配Android 7.0文件权限,通过FileProvider创立一个content类型的Uri
            FileProvider.getUriForFile(
                requireActivity(),
                "com.xxx.xxx.fileProvider", file
            )
        } else {
            Uri.fromFile(file)
        }
    }

简单阐明

这儿的file是运用getExternalFilesDir和Environment.DIRECTORY_PICTURES生成的,它的文件保存在运用外部私有目录:files-Pictures里边。这儿要留意不能寄存在内部的私有目录里边,不然是无法拜访的,外部私有目录虽然也是私有的,可是外面是能够拜访的,这儿拿官网上的阐明:

在搭载 Android 9(API 等级 28)或更低版别的设备上,只需您的运用具有恰当的存储权限,就能够拜访归于其他运用的运用专用文件。为了让用户更好地管理自己的文件并减少紊乱,以 Android 10(API 等级 29)及更高版别为目标平台的运用在默许情况下被颁发了对外部存储空间的分区拜访权限(即分区存储)。启用分区存储后,运用将无法拜访归于其他运用的运用专属目录。

Uri的获取

再一个比较费事的便是Uri的获取了,网上有一大堆资料,不过我这也贴一下,网上的或许有问题。

manifest.xml

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.xxx.xxx.fileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"
                />
        </provider>

res -> xml -> file_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!--1、对应内部内存卡根目录:Context.getFileDir()-->
    <files-path
        name="int_root"
        path="/" />
    <!--2、对应运用默许缓存根目录:Context.getCacheDir()-->
    <cache-path
        name="app_cache"
        path="/" />
    <!--3、对应外部内存卡根目录:Environment.getExternalStorageDirectory()-->
    <external-path
        name="ext_root"
        path="/" />
    <!--4、对应外部内存卡根目录下的APP公共目录:Context.getExternalFileDir(String)-->
    <external-files-path
        name="ext_pub"
        path="/" />
    <!--5、对应外部内存卡根目录下的APP缓存目录:Context.getExternalCacheDir()-->
    <external-cache-path
        name="ext_cache"
        path="/" />
</paths>

ps. 留意authorities这个最好填自己的包名,不然有两个运用用了同样的authorities,后边的运用会安装不上。

翻开相册

这儿翻开相册用的是SAF框架,运用intent去选取(onActivityResult见后文)。

    private fun openAlbum() {
        val intent = Intent()
        intent.type = "image/*"
        intent.action = "android.intent.action.GET_CONTENT"
        intent.addCategory("android.intent.category.OPENABLE")
        startActivityForResult(intent, REQUEST_ALBUM_CODE)
    }

裁切

裁切这儿比较费事,参数比较多,而且Uri那里有坑,不能运用provider,再一个便是图片传递那由于安卓版别变更,不会传略缩图了,很坑。

    private fun cropImage(path: String) {
        cropImage(getUriForFile(File(path)))
    }
    private fun cropImage(uri: Uri) {
        val intent = Intent("com.android.camera.action.CROP")
        // Android 7.0需求暂时添加读取Url的权限
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
//        intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
        intent.setDataAndType(uri, "image/*")
        // 使图片处于可裁剪状况
        intent.putExtra("crop", "true")
        // 裁剪框的比例(根据需求显现的图片比例进行设置)
//        if (Build.MANUFACTURER.contains("HUAWEI")) {
//            //硬件厂商为华为的,默许是圆形裁剪框,这儿让它无法成圆形
//            intent.putExtra("aspectX", 9999)
//            intent.putExtra("aspectY", 9998)
//        } else {
//            //其他手机一般默许为方形
//            intent.putExtra("aspectX", 1)
//            intent.putExtra("aspectY", 1)
//        }
        // 设置裁剪区域的形状,默许为矩形,也可设置为圆形,或许无效
        // intent.putExtra("circleCrop", true);
        // 让裁剪框支撑缩放
        intent.putExtra("scale", true)
        // 特点控制裁剪完毕,保存的图片的巨细格局。太大会OOM(return-data)
//        intent.putExtra("outputX", 400)
//        intent.putExtra("outputY", 400)
        // 生成暂时文件
        val cropFile = createFile("Crop")
        // 裁切图片时不能运用provider的uri,否则无法保存
//        val cropUri = getUriForFile(cropFile)
        val cropUri = Uri.fromFile(cropFile)
        intent.putExtra(MediaStore.EXTRA_OUTPUT, cropUri)
        // 记录暂时方位
        cropPicPath = cropFile.absolutePath
        // 设置图片的输出格局
        intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString())
        // return-data=true传递的为缩略图,小米手机默许传递大图, Android 11以上设置为true会闪退
        intent.putExtra("return-data", false)
        startActivityForResult(intent, REQUEST_CROP_CODE)
    }

回调处理

下面是对上面三个操作的回调处理,一开始我觉得uri没什么用,还制作费事,后边发现能够通过流翻开uri,再去获取bitmap,如同又不是那么费事了。

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == RESULT_OK) {
            when(requestCode) {
                REQUEST_CAMERA_CODE -> {
                    // 通知体系文件更新
//                    requireContext().sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
//                        Uri.fromFile(File(picturePath))))
                    if (!enableCrop) {
                        val bitmap = getBitmap(picturePath)
                        bitmap?.let {
                            // 显现图片
                            binding.image.setImageBitmap(it)
                        }
                    }else {
                        cropImage(picturePath)
                    }
                }
                REQUEST_ALBUM_CODE -> {
                    data?.data?.let { uri ->
                        if (!enableCrop) {
                            val bitmap = getBitmap("", uri)
                            bitmap?.let {
                                // 显现图片
                                binding.image.setImageBitmap(it)
                            }
                        }else {
                            cropImage(uri)
                        }
                    }
                }
                REQUEST_CROP_CODE -> {
                    val bitmap = getBitmap(cropPicPath)
                    bitmap?.let {
                        // 显现图片
                        binding.image.setImageBitmap(it)
                    }
                }
            }
        }
    }
    private fun getBitmap(path: String, uri: Uri? = null): Bitmap? {
        var bitmap: Bitmap?
        val options = BitmapFactory.Options()
        // 先不读取,仅获取信息
        options.inJustDecodeBounds = true
        if (uri == null) {
            BitmapFactory.decodeFile(path, options)
        }else {
            val input = requireContext().contentResolver.openInputStream(uri)
            BitmapFactory.decodeStream(input, null, options)
        }
        // 预获取信息,大图紧缩后加载
        val width = options.outWidth
        val height = options.outHeight
        Log.d("TAG", "before compress: width = " +
                options.outWidth + ", height = " + options.outHeight)
        // 尺度紧缩
        var size = 1
        while (width / size >= MAX_WIDTH || height / size >= MAX_HEIGHT) {
            size *= 2
        }
        options.inSampleSize = size
        options.inJustDecodeBounds = false
        bitmap = if (uri == null) {
            BitmapFactory.decodeFile(path, options)
        }else {
            val input = requireContext().contentResolver.openInputStream(uri)
            BitmapFactory.decodeStream(input, null, options)
        }
        Log.d("TAG", "after compress: width = " +
                options.outWidth + ", height = " + options.outHeight)
        // 质量紧缩
        val baos = ByteArrayOutputStream()
        bitmap!!.compress(Bitmap.CompressFormat.JPEG, 80, baos)
        val bais = ByteArrayInputStream(baos.toByteArray())
        options.inSampleSize = 1
        bitmap = BitmapFactory.decodeStream(bais, null, options)
        return bitmap
    }

这儿还做了一个图片的质量紧缩和采样紧缩,需求留意的是采样紧缩的采样率只能是2的倍数,假如需求按恣意比例采样,需求用到Matrix,不是很难,读者能够研讨下。

权限问题

假如你发现你没有请求权限,那你的去请求一下相机权限;假如你发现你还请求了贮存权限,那你能够试一下去掉贮存权限,实践还是能够运用的,由于这儿并没有用到外部贮存,都是运用的私有贮存内,详细关于贮存的适配,能够看我转载的这几篇文章,我觉得写的非常好:

Android 存储基础

Android 10、11 存储彻底适配(上)

Android 10、11 存储彻底适配(下)

结语

以上代码都通过我这儿实践了,确认了可用,或许写法不是最优,能够防止运用绝对途径,只运用Uri。至于请求码、布局什么的,读者自己改一下加一个就行,中心部分已经在这了。假如需求完好代码,能够看下篇文章末尾!

Android 不请求权限贮存、删除相册图片