Jetpack 之 ActivityResultContract的使用,调用相机、相册并裁剪的优雅实现

自从Google官方发布Jetpack以来,我们Android开发的很多开发习惯都发生了巨大的变化,最近又双叒叕在实现更换头像的功能。发现以前startActivityForResult + onActivityResult 那一套做法又有了新的实现方式。略微找了下相关资料,动手实现了出来,发现代码能够更简洁也更优雅了,感觉收获了一点小惊喜。所以这里给大家分享一下。

以前请求权限以及startActivityForResult,都免不了要实现onActivityResult,代码分离,逻辑不连贯,还要申明一连串code,一个不留神就功能对应不上,浪费时间排查。现在使用registerForActivityResult + ActivityResultContract来实现就更加方便了。

官方已经帮我们封装好了一部分可以直接使用,列在下面就不多做介绍了:

ActivityResultContracts.CaptureVideo, 
ActivityResultContracts.CreateDocument, 
ActivityResultContracts.GetContent, 
ActivityResultContracts.GetMultipleContents, 
ActivityResultContracts.OpenDocumentTree, 
ActivityResultContracts.OpenDocument, 
ActivityResultContracts.OpenMultipleDocuments,
ActivityResultContracts.PickContact,
ActivityResultContracts.RequestMultiplePermissions,
ActivityResultContracts.RequestPermission,
ActivityResultContracts.StartActivityForResult,
ActivityResultContracts.StartIntentSenderForResult,
ActivityResultContracts.TakePicturePreview,
ActivityResultContracts.TakePicture,
ActivityResultContracts.TakeVideo,
ActivityResultContracts.WatchFaceEditorContract

这里主要介绍自定义Contract来实现打开相机拍照、打开相册选择照片,然后进行裁剪是如何实现的。

一、首先,先继承ActivityResultContract实现自定义的Contract,如下:

SelectPhotoContract:

/**
 * 选择照片的协定
 */
class SelectPhotoContract : ActivityResultContract<Unit?, Uri?>() {
    companion object {
        private const val TAG = "SelectPhotoContract"
    }

    override fun createIntent(context: Context, input: Unit?): Intent {
        return Intent(Intent.ACTION_PICK).setType("image/*")
    }

    override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
        Logger.d(TAG, "Select photo uri: ${intent?.data}")
        return intent?.data
    }
}

TakePhotoContract:

/**
 * 拍照协定
 */
class TakePhotoContract : ActivityResultContract<Unit?, Uri?>() {
    companion object {
        private const val TAG = "TakePhotoContract"
    }

    private var uri: Uri? = null

    override fun createIntent(context: Context, input: Unit?): Intent {
        val mimeType = "image/jpeg"
        val fileName = "IMG_${System.currentTimeMillis()}.jpg"
        uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            // Android 10 及以上获取图片uri
            val values = contentValuesOf(
                Pair(MediaStore.MediaColumns.DISPLAY_NAME, fileName),
                Pair(MediaStore.MediaColumns.MIME_TYPE, mimeType),
                Pair(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
            )
            context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
        } else {
            // Android 9 及以下获取图片uri
            FileProvider.getUriForFile(
                context, "${context.packageName}.provider",
                File(context.externalCacheDir, "/$fileName")
            )
        }
        return Intent(MediaStore.ACTION_IMAGE_CAPTURE).putExtra(MediaStore.EXTRA_OUTPUT, uri)
    }

    override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
        Logger.d(TAG, "Take photo, resultCode: $resultCode, uri: $uri")
        if (resultCode == Activity.RESULT_OK) return uri
        return null
    }
}

CropPhotoContract:

/**
 * 裁剪照片的协定
 */
class CropPhotoContract : ActivityResultContract<Uri, CropPhotoContract.CropOutput?>() {
    companion object {
        private const val TAG = "CropPhotoContract"
    }

    private var output: CropOutput? = null

    override fun createIntent(context: Context, input: Uri): Intent {
        // 获取输入图片uri的媒体类型
        val mimeType = context.contentResolver.getType(input)
        // 创建新的图片名称
        val fileName = "IMG_${System.currentTimeMillis()}.${
            MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
        }"
        val outputUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            // Android 10 及以上获取图片uri
            val values = contentValuesOf(
                Pair(MediaStore.MediaColumns.DISPLAY_NAME, fileName),
                Pair(MediaStore.MediaColumns.MIME_TYPE, mimeType),
                Pair(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
            )
            context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
        } else {
            Uri.fromFile(File(context.externalCacheDir!!.absolutePath, fileName))
        }
        output = CropOutput(outputUri!!, fileName)
        return Intent("com.android.camera.action.CROP")
            .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            .setDataAndType(input, "image/*")
            .putExtra("outputX", 300)
            .putExtra("outputY", 300)
            .putExtra("aspectX", 1)
            .putExtra("aspectY", 1)
            .putExtra("scale", true)
            .putExtra("crop", true)
            .putExtra("return-data", false) // 在小米手机部分机型中 如果直接返回Data给Intent,图片过大的时候会有问题
            .putExtra("noFaceDetection", true)
            .putExtra(MediaStore.EXTRA_OUTPUT, outputUri)
            .putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString())
    }

    override fun parseResult(resultCode: Int, intent: Intent?): CropOutput? {
        Logger.d(TAG, "Crop photo, resultCode: $resultCode output: $output")
        if (resultCode == Activity.RESULT_OK) return output
        return null
    }

    data class CropOutput(val uri: Uri, val fileName: String) {
        override fun toString(): String {
            return "{ uri: $uri, fileName: $fileName }"
        }
    }
}

二、然后,在Activity或者Fragment中注册:

/**
     * 打开相机拍照,并前往裁剪
     */
    private val takePhotoLauncher = (this as ComponentActivity).registerForActivityResult(TakePhotoContract()) { uri ->
        uri?.let {
            cropPhotoLauncher.launch(it)
        }
    }

    /**
     * 前往相册选择照片,并前往裁剪
     */
    private val selectPhotoLauncher = (this as ComponentActivity).registerForActivityResult(SelectPhotoContract()) { uri ->
        uri?.let {
            cropPhotoLauncher.launch(it)
        }
    }

    /**
     * 前往裁剪
     */
    private val cropPhotoLauncher = (this as ComponentActivity).registerForActivityResult(CropPhotoContract()) { output ->
        output?.let {
            Logger.d(TAG, "裁剪完成,开始上传头像到服务器, output: $it")
            // 上传头像
            showLoading(getString(R.string.base_uploading))
            mViewModel.fetchUpload(ContentUriRequestBody(contentResolver, it.uri), it.fileName)
        }
    }

三、调用:

// 打开相机
takePhotoLauncher.launch(null)
// 打开相册
selectPhotoLauncher.launch(null)

OVER

很简单,不是么!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值