- 移动开发
- UI组件
【免费下载链接】AndroidPicker
安卓选择器类库,包括日期及时间选择器(可用于出生日期、营业时间等)、单项选择器(可用于性别、民族、职业、学历、星座等)、二三级联动选择器(可用于车牌号、基金定投日期等)、城市地址选择器(分省级、地市级及区县级)、数字选择器(可用于年龄、身高、体重、温度等)、日历选日期择器(可用于酒店及机票预定日期)、颜色选择器、文件及目录选择器、图片选择器等……WheelPicker/DatePicker/TimePicker/OptionPicker/NumberPicker/LinkagePicker/AddressPicker/CarPlatePicker/CalendarPicker/ColorPicker/FilePicker/ImagePicker etc.
本篇技术指南围绕 AndroidPicker 项目中的 ImagePicker 模块 展开,讲解如何通过ImagePicker单例快速完成「相机拍照」「相册选取」与「图片裁剪」三合一流程,包括权限处理、结果回调、裁剪参数定制,以及底层FileProvider、content://URI 转换等实现原理。读完本文,你将能够在一个FragmentActivity或Fragment中独立集成完整的图片选取与裁剪能力,并掌握每个裁剪配置项的默认值与取值范围。
模块定位:一个方法搞定「选图 + 裁剪」
AndroidPicker 的 ImagePicker 模块是一套基于startActivityForResult的图片选择组件,整合了两种经典开源方案的能力:图片选择(相机与相册)改造自 ImagePicker,裁剪 UI 与手势交互改造自 Android-Image-Cropper。模块对外暴露的核心 API 只有ImagePicker一个类,通过ImagePicker.getInstance()获取单例后,即可分别调用startCamera(...)、startGallery(...)、startChooser(...)启动三类来源,再配合一个抽象的 PickCallback 完成回调闭环。
在仓库中,ImagePicker 已被注册为独立模块(见 settings.gradle 中的include ':ImagePicker'),可通过 Gradle 依赖方式引入;示例工程中的 ImagePickerActivity 是它的完整可运行范例。
第一步:声明权限与合规注意
模块自身的 AndroidManifest.xml 已声明三项权限:
android.permission.CAMERA:调用相机拍照时需要;android.permission.READ_EXTERNAL_STORAGE:读取相册/外置存储中的图片;android.permission.WRITE_EXTERNAL_STORAGE:写入裁剪结果等文件。
集成时需注意两点:
- 这三个权限属于 Android 6.0(API 23)及以上的运行时权限,除在 manifest 中声明外,还必须在运行时向用户动态申请;
- 调用
startCamera/startGallery/startChooser之前,要确保用户已授予对应权限,否则系统可能直接抛出安全异常或返回无数据。
好消息是:ImagePicker 内部会自动处理一部分运行时权限请求(详见下文「底层实现」小节),你只需要在Activity/Fragment的回调方法里把结果转发给ImagePicker即可。
第二步:最小可用示例(源自模块 README)
ImagePicker/README.md 给出了一个可直接照搬的完整示例,使用FragmentActivity承载:
public class ImagePickerActivity extends FragmentActivity { private final PickCallback cropCallback = new PickCallback() { @Override public void onPermissionDenied(String[] permissions, String message) { Toast.makeText(ImagePickerActivity.this, message, Toast.LENGTH_SHORT).show(); } @Override public void cropConfig(ActivityBuilder builder) { builder.setMultiTouchEnabled(true) .setGuidelines(CropImageView.Guidelines.ON_TOUCH) .setCropShape(CropImageView.CropShape.OVAL) .setRequestedSize(400, 400) .setFixAspectRatio(true) .setAspectRatio(1, 1); } @Override public void onCropImage(@Nullable Uri imageUri) { Toast.makeText(ImagePickerActivity.this, String.valueOf(imageUri), Toast.LENGTH_SHORT).show(); } }; public void onCamera(View view) { ImagePicker.getInstance().startCamera(this, true, cropCallback); } public void onGallery(View view) { ImagePicker.getInstance().startGallery(this, true, cropCallback); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); ImagePicker.getInstance().onActivityResult(this, requestCode, resultCode, data); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); ImagePicker.getInstance().onRequestPermissionsResult(this, requestCode, permissions, grantResults); } }示例工程中的 ImagePickerActivity 与上述写法一致:两个按钮分别触发相机与相册(见布局文件 activity_picker_image.xml),并在onActivityResult、onRequestPermissionsResult两个生命周期回调中把结果原样转发给ImagePicker.getInstance()。
其中第二、三个参数的含义需要区分:
startCamera(this, true, callback)的true表示拍照后进入裁剪;startGallery(this, true, callback)的true表示选图后进入裁剪;- 如果只想取原图不裁剪,传
false即可(示例工程中的相册按钮正是如此,见 ImagePickerActivity.java#L80)。
第三步:理解 PickCallback 的五个回调时机
PickCallback 是一个抽象类(非接口,空方法全部可选覆写),按调用顺序包含五个回调:
| 回调方法 | 触发时机 | 说明 |
|---|---|---|
onCanceled() | 用户取消选择/拍照 | 内部在resultCode != RESULT_OK时触发 |
onPermissionDenied(String[] permissions, String message) | 用户拒绝相机或存储权限 | 内部会传入中文提示,如「没有相机权限」「没有外部存储权限」 |
onPickImage(@Nullable Uri imageUri) | 图片已选定 | 在裁剪之前回调;若cropEnabled=false,此回调即最终结果 |
cropConfig(ActivityBuilder builder) | 即将进入裁剪页 | 在此统一配置裁剪参数,默认实现为 OVAL、640×640、5:5 宽高比 |
onCropImage(@Nullable Uri imageUri) | 裁剪完成 | 最终结果以Uri形式返回,可直接用于显示或上传 |
onCropError(@NonNull Exception error) | 裁剪出错 | 例如图片 URI 为空时抛出的IllegalArgumentException("Uri is null or empty") |
值得注意的是回调的顺序设计:onPickImage会先于onCropImage触发(源码见 ImagePicker.java#L236-L248),因此即使cropEnabled=true,你也可以在onPickImage中拿到裁剪前的原始图片 URI;若cropEnabled=false,onPickImage收到的就是最终结果。
第四步:启动入口 API 全览
ImagePicker.java 是单例类(内部使用静态内部类Holder实现懒加载单例),对外提供三类启动方法,且每个方法都同时支持Activity与Fragment两个重载:
// 相机拍照,cropEnabled 控制是否继续裁剪 ImagePicker.getInstance().startCamera(activity, true, callback); ImagePicker.getInstance().startCamera(fragment, true, callback); // 相册/图库选取,内部即 startChooser(..., "image/*", ...) ImagePicker.getInstance().startGallery(activity, true, callback); ImagePicker.getInstance().startGallery(fragment, true, callback); // 自定义 MIME 的文件选择(例如 "image/*"、"video/*"),默认不裁剪 ImagePicker.getInstance().startChooser(activity, "image/*", callback); ImagePicker.getInstance().startChooser(fragment, "image/*", callback);从 HybridityUtils.java#L33-L42 的源码可见:
- 相册选取底层发送的是
Intent.ACTION_GET_CONTENT并附带Intent.CATEGORY_OPENABLE,类型为image/*; startChooser允许传入任意 MIME 类型,且实现中强制cropEnabled = false(见 ImagePicker.java#L93);- 相机拍照发送的是
MediaStore.ACTION_IMAGE_CAPTURE,并通过MediaStore.EXTRA_OUTPUT指定输出文件(见 HybridityUtils.java#L26-L31)。
第五步:ActivityBuilder 裁剪参数完全参考
当进入裁剪流程后,cropConfig(ActivityBuilder builder)中拿到的 ActivityBuilder 承载了所有裁剪外观与输出参数。它内部持有一个 CropImageOptions,所有 setter 均为链式调用并最终写入mOptions,由 CropImageOptions.validate() 在启动前统一校验取值范围。
裁剪窗口与手势
| 方法 | 默认值 | 说明 |
|---|---|---|
setCropShape(CropShape) | RECTANGLE | 裁剪窗形状;枚举见 CropImageView.java#L1466-L1469,支持RECTANGLE(矩形)、OVAL(圆形/椭圆,通常配合 1:1 宽高比实现圆形头像) |
setGuidelines(Guidelines) | ON_TOUCH | 辅助参考线显示策略:OFF永不显示、ON_TOUCH拖动时显示、ON始终显示(见 CropImageView.java#L1518-L1533) |
setScaleType(ScaleType) | FIT_CENTER | 图片初始缩放适配方式 |
setMultiTouchEnabled(boolean) | false | 是否允许双指多点触控缩放/旋转 |
setMaxZoom(int) | 4 | 最大缩放倍数 |
setTouchRadius(float) | 24dp | 手柄可触摸区域半径(依据 48dp Rhythm 设计规范) |
setSnapRadius(float) | 3dp | 裁剪框边缘吸附到边界的触发距离 |
setAutoZoomEnabled(boolean) | true | 拖动/缩放时是否自动缩放适配 |
setShowCropOverlay(boolean) | true | 是否显示裁剪窗外部的半透明遮罩 |
宽高比与结果尺寸
| 方法 | 默认值 | 说明 |
|---|---|---|
setFixAspectRatio(boolean) | false | 是否锁定宽高比;注意setAspectRatio(...)内部会将其强制置为true |
setAspectRatio(int x, int y) | 1 / 1 | 裁剪窗宽高比,例如setAspectRatio(1, 1)配合OVAL即圆形裁剪;x、y都必须大于 0,否则validate()抛出异常 |
setRequestedSize(int w, int h) | 0, 0(不缩放) | 裁剪输出的目标尺寸,默认使用RESIZE_INSIDE策略(等比缩放至不超过该尺寸,图片小于目标尺寸时不变) |
setRequestedSize(int w, int h, RequestSizeOptions) | — | 可指定缩放策略:NONE不缩放、SAMPLING仅采样、RESIZE_INSIDE等比缩小、RESIZE_FIT等比适配、RESIZE_EXACT拉伸填满(见 CropImageView.java#L1541-L1578) |
setMinCropWindowSize(w, h) | 42dp, 42dp | 裁剪窗允许的最小尺寸 |
setMinCropResultSize(w, h) | 40, 40 | 裁剪结果允许的最小宽高 |
setMaxCropResultSize(w, h) | 99999, 99999 | 裁剪结果允许的最大宽高,必须不小于最小值 |
边框、辅助线与遮罩外观
| 方法 | 默认值 | 说明 |
|---|---|---|
setBorderLineThickness(float) | 3dp | 裁剪框边框线宽 |
setBorderLineColor(int) | Color.argb(170, 255, 255, 255) | 裁剪框边框颜色 |
setBorderCornerThickness(float) | 2dp | 四角边线粗细 |
setBorderCornerOffset(float) | 5dp | 角线与裁剪框边缘的偏移 |
setBorderCornerLength(float) | 14dp | 角线的长度 |
setBorderCornerColor(int) | WHITE | 角线颜色 |
setGuidelinesThickness(float) | 1dp | 辅助参考线线宽 |
setGuidelinesColor(int) | Color.argb(170, 255, 255, 255) | 辅助参考线颜色 |
setBackgroundColor(int) | Color.argb(119, 0, 0, 0) | 裁剪窗外遮罩背景色 |
输出与旋转
| 方法 | 默认值 | 说明 |
|---|---|---|
setOutputUri(Uri) | Uri.EMPTY | 裁剪结果的保存位置;不设置时自动创建临时文件 |
setOutputCompressFormat(Bitmap.CompressFormat) | JPEG | 输出压缩格式 |
setOutputCompressQuality(int) | 90 | 压缩质量(0–100) |
setNoOutputImage(boolean) | false | 设为true时不保存裁剪位图,仅返回裁剪矩形与旋转数据(高级用法) |
setInitialCropWindowRectangle(Rect) | null | 裁剪窗初始位置,不设置则由 padding 比例推算 |
setInitialRotation(int) | -1 | 初始旋转角度(0–360),默认读取图片 EXIF |
setAllowRotation(boolean) | true | 是否允许在裁剪过程中旋转 |
setRotationDegrees(int) | 90 | 单次旋转步长(0–360,必须在此范围内) |
setInitialCropWindowPaddingRatio(float) | 0.1 | 初始裁剪窗相对图片边缘的内边距比例,取值必须小于 0.5 |
第六步:底层实现与关键调用链
6.1 运行时权限的自动申请逻辑
startCamera启动前会调用 HybridityUtils.isExplicitCameraPermissionRequired:当系统为 Android 6.0+、manifest 中声明了CAMERA权限、且当前未授予时,自动通过ActivityCompat.requestPermissions请求相机权限;授权成功后才真正拉起相机 Intent。
选图结果回来后,onActivityResultInner 会先尝试用ContentResolver.openInputStream探测 URI 是否可读(见 HybridityUtils.isUriRequiresPermissions),若不可读则自动请求READ_EXTERNAL_STORAGE。整个过程对调用方透明,你只需要在两个权限回调中把结果转发给ImagePicker.getInstance()。
6.2 content:// 到 file:// 的自动转换
handleUri 会将回调中的content://协议 URI 通过 getRealPathFromUri(查询MediaStore.Images.Media.DATA列)转换为file://路径,避免后续读写时抛出FileNotFoundException;若转换失败则保留原始 URI 原样返回。
6.3 FileProvider 与 Android 7.0 兼容
相机拍照输出文件默认位于context.getExternalCacheDir()下的pickImageResult.jpeg(见 HybridityUtils.java#L53-L60)。由于 Android 7.0(API 24)起禁止在 Intent 中传递file://URI,getIntentUri 会自动改用FileProvider生成的content://URI,其 authority 固定为${applicationId}.imagepicker.provider(见 AndroidManifest.xml),且grantUriPermissions="true"允许临时授权给相机应用。路径映射配置在 crop_image_paths.xml 中,覆盖了external-path、external-cache-path、external-files-path三类根目录。
6.4 裁剪页与结果回传
裁剪由 CropImageActivity 承担(manifest 中已声明竖屏)。ActivityBuilder通过 getCropIntent 把源图片 URI 与CropImageOptions(Parcelable)打包进 Intent(请求码CROP_IMAGE_ACTIVITY_REQUEST_CODE = 203,见 CropImageConsts.java)。结果通过CROP_IMAGE_EXTRA_RESULT键以 ActivityResult 返回,ImagePicker 解析出其中的错误或图片 URI 后分发给onCropError/onCropImage。
第七步:集成到自己的项目
ImagePicker 以独立 Gradle 模块存在,主仓库 README 中给出的引入方式为:
dependencies { implementation 'com.github.gzu-liyujiang.AndroidPicker:ImagePicker:<version>' }若以源码方式集成,则需在 settings.gradle 中include ':ImagePicker'并在依赖中加入project(':ImagePicker')。模块自带的 consumer-rules.pro 是专用于该库的混淆规则文件(内容为空,说明无需额外 keep 规则),随 AAR 打包发布。
集成清单总结如下:
- 在
AndroidManifest.xml中声明CAMERA、READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE权限(库模块已自带,宿主工程声明后合并生效); - 在
Activity/Fragment中调用startCamera/startGallery/startChooser; - 重写
onActivityResult与onRequestPermissionsResult并转发给ImagePicker.getInstance(); - 在
PickCallback中按需覆写cropConfig配置裁剪参数,在onCropImage中消费最终结果; - 处理得到
Uri后,即可配合任意图片加载库进行显示或上传。
常见问题与注意事项
- 只想要原图,不想裁剪:将
startCamera/startGallery的第二个参数传false,此时最终结果在onPickImage中返回(见 ImagePicker.java#L246-L248); - 圆形头像:
setCropShape(CropImageView.CropShape.OVAL)配合setAspectRatio(1, 1)、setRequestedSize(400, 400),即 README 示例与 demo 中相机入口的配置; - 参数非法会直接抛异常:宽高比不能小于等于 0、
initialCropWindowPaddingRatio必须在[0, 0.5)、rotationDegrees必须在[0, 360]、最大结果尺寸不得小于最小结果尺寸,这些约束在CropImageOptions.validate()中强制执行,配置时需注意取值范围; - 相机权限兼容:Android 6.0 以下系统无需动态申请相机权限,
isExplicitCameraPermissionRequired会直接返回false并跳过申请步骤; - 临时输出文件:相机拍照的原始输出固定为外置缓存目录下的
pickImageResult.jpeg,属于应用缓存,系统可能随时清理,如需要持久化请及时处理裁剪结果。
至此,从权限声明、入口调用、回调闭环到裁剪参数定制与底层原理,ImagePicker 模块的完整使用路径已全部打通;你也可以直接运行示例工程中的 ImagePickerActivity 体验「相机拍照」与「相册选取」两个入口的实际效果。
- 移动开发
- UI组件
【免费下载链接】AndroidPicker
安卓选择器类库,包括日期及时间选择器(可用于出生日期、营业时间等)、单项选择器(可用于性别、民族、职业、学历、星座等)、二三级联动选择器(可用于车牌号、基金定投日期等)、城市地址选择器(分省级、地市级及区县级)、数字选择器(可用于年龄、身高、体重、温度等)、日历选日期择器(可用于酒店及机票预定日期)、颜色选择器、文件及目录选择器、图片选择器等……WheelPicker/DatePicker/TimePicker/OptionPicker/NumberPicker/LinkagePicker/AddressPicker/CarPlatePicker/CalendarPicker/ColorPicker/FilePicker/ImagePicker etc.
相关推荐
Flutter图片选择器终极指南:相机拍照与相册选择功能详解
Flutter图片选择器终极指南:相机拍照与相册选择功能详解 想要在Flutter应用中实现强大的图片选择功能吗?flutter_deer项目提供了一个完整的解
移动开发电商UI组件图片选择与裁剪:AndroidPicker ImagePicker的完整解决方案
图片选择与裁剪:AndroidPicker ImagePicker的完整解决方案 本文详细解析了AndroidPicker ImagePicker模块的完整技术
移动开发UI组件Mage-VL API完全指南:图像理解、视频分析与主动流模式调用示例
Mage VL API完全指南:图像理解、视频分析与主动流模式调用示例 Mage VL是一款功能强大的多模态AI模型,专注于图像理解、视频分析和主动流模式处理。
人工智能基础模型多模态计算机视觉视频NLP
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考