自从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
很简单,不是么!