书接上文 Android 设置头像 - 相册拍照,通过相册和照片的设置就可以获取到需要的头像信息,但是在通常情况下,我们还想要实现针对头像的裁剪功能和圆形头像功能。
先上截图:
图像裁剪
通常裁剪可以分为程序自动裁剪和用户选择裁剪两种方式,程序自动裁剪及程序中设置固定的裁剪比例然后系统自动进行裁剪,Glide即可实现该功能。但这种实现方式往往不能满足用户实际需求,及实际裁剪需要用户进行选择图片裁剪范围。下面将提供两种图片裁剪的实现逻辑,出了本文描述的裁剪实现逻辑外还有自定义裁剪view等其他实现逻辑,由于技术有限,再次仅仅阐述较为简单的两种实现逻辑。
第三方裁剪库
目前存在很对开源的第三方Android裁剪工具库,通过比较uCrop的裁剪工具库较为流行,本文将以该裁剪工具集成为例进行说明。uCrop的GitHub地址点这里。通过README文档可知,集成该工具需要以下操作:
- gradle引入
- AndroidManifest.xml中声明Activity
- 业务代码中调用UCrop进行裁剪
- 通过onActivityResult接收裁剪结果intent
上述四个过程在github中作者都进行了详细说明,下面进行我理解的简单说明:
采用该工具需要传入目标Uri
使用该工具进行裁剪需要输入源Uri和目标Uri,及被参见图片Uri和裁剪后图片存放的Uri,不同Android版本的Uri获取方式不同,Uri相关内容本人也一知半解,后续了解后将另文说明。
AndroidManifest.xml中声明Activity
相信使用module开发或者引入过arr资源包的都清楚需要在AndroidManifest.xml中引入第三方arr的Activity文件。根据这个说明不难推断出Ucrop为一个arr资源包并非jar包,并且结合网上了解到的自定义裁剪试图的实现逻辑不难推断出,Ucrop也实现了一个自定义Activity进行裁剪,当用户发起裁剪请求时,将跳转到Ucrop的Activity中,待裁剪完成后进行回跳并携带裁剪结果。
通过onActivityResult接收裁剪结果intent
UCrop.start()方法将进行裁剪,通过查看源码,该方法调用了activity.startActivityForResult(getIntent(activity), requestCode);方法,因此需要重写onActivityResult方法进行接收回跳携带参数。但是由于startActivityForResult即将弃用,所以这种方式我不建议使用。考虑是否可以通过重写UCrop.start方法通过ActivityResultLauncher.launch的方法进行裁剪调用。
系统默认裁剪intent调用
系统默认裁剪实现是裁剪过程中最简单的实现方式,按照百度结果只需要创建一个Intent intent = new Intent("com.android.camera.action.CROP");意图并进行相关参数配置即可。下面首先展示我的实现逻辑
- 首先配置ActivityResultContract如下
java
/**
* 裁剪照片的contract
*
* @author baiyang
* @since 2024-04-28
*/
public class CropImageResultContract extends ActivityResultContract<Intent, Intent> {
public static final String AUTHORITY = "com.ldr.imosApp.provider";
public static final String IMAGE_TYPE = "image/jpeg";
public static final String JPG_TYPE = "jpg";
private String type;
private Uri uri;
@NonNull
@Override
public Intent createIntent(@NonNull Context context, Intent input) {
input.putExtra("crop", "true");
input.putExtra("aspectX", 1);
input.putExtra("aspectY", 1);
input.putExtra("outputX", 256);
input.putExtra("outputY", 256);
input.putExtra("return-data", true);
String mimeType = null;
String fileName = null;
mimeType = IMAGE_TYPE;
fileName = System.currentTimeMillis() + "." + JPG_TYPE;
Uri mediaUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM);
uri = context.getContentResolver()
.insert(mediaUri, values);
} else {
uri = FileProvider.getUriForFile(context, AUTHORITY,
new File(context.getExternalCacheDir().getAbsolutePath(), fileName));
}
input.putExtra(MediaStore.EXTRA_OUTPUT, uri);
return input;
}
/**
* 返回拍照结果,因为在调用相机的过程中设置了EXTRA_OUTPUT,因此返回时intent=null,需要重新设置一下
*
* @param resultCode
* @param intent
* @return
*/
@Override
public Intent parseResult(int resultCode, @Nullable Intent intent) {
if (resultCode != Activity.RESULT_OK) {
return null;
}
intent = new Intent();
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
return intent;
}
}
上述代码在拍照图库文档中已经进行详细说明,在此需要补充一点疏漏是:我们在createIntent方法中创建了一个Uri,那么intent返回的时候通过getData就不能获取裁剪结果的Uri了,而且我们创建的这个Uri会导致裁剪后的图片将作为一个资源存放到了我们的通过中,这也是为什么在Android 设置头像 - 相册拍照一文从图库中选择照片的createIntent方法中我没有创建Uri的原因,如果创建了会导致图库中存在两张重复的图片。在上述代码实现初期,我并没有创建Uri,直接通过getData进行获取Uri,在模拟器上进行测试是可以通过的,但是使用真机测试的过程中依旧会出现裁剪正常进行,但是裁剪结果getData=null的情况。目前原因不详,怀疑可能是通过intent调用默认的裁剪实现后,裁剪的返回结果在不同机型上进行了不同方式的返回,因此通过创建Uri进行接收裁剪结果是最稳妥的实现方式
- 进行裁剪结果回调函数实现
java
/**
* 裁剪照片的回调函数
*/
private final ActivityResultCallback<Intent> cropCallback = result -> {
if (Objects.nonNull(result)) {
Uri uri = result.getParcelableExtra(MediaStore.EXTRA_OUTPUT);
Glide.with(PersonalInformationActivity.this).load(uri).into(binding.headSculpture);
} else {
LogUtils.e("裁剪照片回调失败,未回传相关数据");
}
};
即获取结果后直接通过Glide进行图片回显。后续的圆形图像可以通过Glide进行实现。
- intent调用
在进行intent调用之前首先需要获取ActivityResultLauncher,如下:
java
cropResultLauncher = registerForActivityResult(new CropImageResultContract(),cropCallback);
intent调用是需要进行裁剪,而裁剪过程应该是在选择图片或者拍照后进行,因此需要修改原拍照图库的回调函数进行调用裁剪的intent,代码如下:
java
/**
* 拍照或者选择图库照片后回调
*/
private final ActivityResultCallback<Intent> callback = result -> {
if (Objects.nonNull(result)) {
Uri uri = result.getParcelableExtra(MediaStore.EXTRA_OUTPUT);
// 进行裁剪
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/*");
cropResultLauncher.launch(intent);
// Glide.with(getApplicationContext()).load(uri).into(binding.headSculpture);
} else {
LogUtils.e("拍照、录像数据回调失败,未回传相关数据");
}
};
上述及使用系统默认的裁剪工具进行裁剪的实现方式,不同手机,不同android版本的系统默认裁剪工具并不相同,因此最终实现效果也不相同,由于手头真机数量有限,并不能针对更多机型进行测试,所以并不确定上述逻辑实现可以适配所以手机,而且对于没有系统默认裁剪工具的机型,上述代码无法进行裁剪,而Ucrop工具可以实现裁剪。
intent到底是什么?
Intent(意图)主要是解决Android应用的各项组件之间的通讯。
Intent负责对应用中一次操作的动作、动作涉及数据、附加数据进行描述,Android则根据此Intent的描述,负责找到对应的组件,将 Intent传递给被调用的组件,并完成组件的调用。
因此,Intent在这里起着一个媒体中介的作用,专门提供组件互相调用的相关信息,实现调用者与被调用者之间的解耦。
上面为百度词条对其的解释,intent可以实现各个组件的通信。回忆我们实现的Activity跳转逻辑,通过startActivity(intent);方法进行跳转,intent我们一般进行下面定义:
java
Intent intent = new Intent();
intent.setClass(getApplicationContext(), MainActivity.class);
上述代码表示从当前Activity跳转到MainActivity,可以看到我们并没有进行MainActivity初始化跳转等特殊设置,我没有深入研究Android底层源码,但是不难推测出当startActivity(intent);方法被调用后,intent意图交由Android底层进行处理,Android系统根据intent传参判断该意图需求,如果是Activity跳转,则从Activity注册表中获取目的Activity并进行生命周期的调用(个人揣测,不准欢迎斧正)。
上面例子为Activity跳转,一般都是同一APP内部跳转,但是当我们进行拍照、调用通讯录等系统级别的操作时也是借助Intent进行实现的,那么可以大胆推测,intent的处理时交由Android底层进行处理,他并不是某个app内部进行处理的,当底层接收到一个访问相机的intent时,将判断是否具备访问权限,如果具备则调用相机。
通过上述分析,我们可以继续推测调用系统裁剪及Intent intent = new Intent("com.android.camera.action.CROP");这个意图在交由Android底层进行处理的时候,Android底层将从它的Activity注册表中找到相关Activity进行跳转。在此引申下属代码:
java
<activity
android:name=".ui.GuideActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
这段代码写Android的都清楚,Android首界面的设置,设置Android受界面需要声明intent-filter标签,并且需要设置android:exported="true",表明该Activity允许外部访问(及允许Android底层进行调用启动)。
圆形头像
圆形头像的实现可记住Glide工具,按照如下代码:
java
RequestOptions options = new RequestOptions()
.circleCropTransform();
Glide.with(PersonalInformationActivity.this).load(uri)
.apply(options)
.into(binding.headSculpture);
在进行into到目标view的过程中Glide使用了bitmapTransform进行转化,使用的是CropCircleTransformation实现,该实现类将图片转化为圆形图片。通过借助Glide的实现圆形头像较为简单,在此不再深入Gilde的具体细节。
总结
通过裁剪和圆形头像的实现更加深入的对ActivityResultLauncher进行了学习,并在实现的过程中对Intent有了更加深入的思考,如果本文内容存在问题欢迎各位大佬进行斧正。总结本文内容。
- Ucrop工具实现图片裁剪
- Intent系统默认裁剪实现类
- intent深入思考
- Gilde圆形图片的实现