注册

Android 自制照片选择器

自制照片选择器

Android 从 11 版本后提供了照片选择器

image-20231221130815793.png

看起来确实不错,本来想直接调用 Android 提供的照片选择器的,不用自己再去做缩略图缓存也不用处理麻烦的 API 结果定睛一看发现几个大问题

  1. Android 提供的照片选择器必须升级 App 的 androidx.activity 库到 1.7.0 版本,这可能意味着 app 的 targetSdkVersion 也得升级,同时需要处理好其他兼容性问题;
  2. Android 提供的照片选择器仅限搭载 Android 11(API 级别 30)或更高版本使用,其他的版本需要通过 Google 系统更新接收对模块化系统组件的更改,如果在低版本使用可能会调用 ACTION_OPEN_DOCUMENT 的 intent 操作来实现,这意味着很多现在例如限制选择几张照片可能不生效,这与需求严重不符。
  1. 能从网上找到的资料可以发现 Android 提供的照片选择器的 API 在变化,实际使用确实很难受。

综上,还不如自己做一个咯🤷‍♂️

开始动手

UI

UI 方面就照着 Google 的抄就好,图片加载用 Glide 来完成,参考微信的照片选择一列默认显示 4 个缩略图就好,然后用 RecyclerView 实现网格状列表容器,基于 DialogX 的 FullScreenDialog 对话框打底实现 activity 界面下沉效果以及从屏幕底部上移的对话框,准备就绪,开干!

复写 RecyclerView.Adapter 实现 PhotoAdapter,在其中用 Glide 加载照片并 override 尺寸进行加载和缓存以避免界面卡顿:

Glide.with(context)
      .load(imageUrls.get(position))
      .override(imageSize)
      .error(errorPhotoDrawableRes)
      .int0((PhotoSelectImageView) holder.itemView);

当照片被选中时,为了实现选中状态的图片缩小,增加边框和对钩图示,自定义了一个 PhotoSelectImageView 作为缩略图呈现使用,图片缩小效果直接用 padding 实现,边框绘制代码:

canvas.drawRect(0 + getBorderWidth() / 2, 0 + getBorderWidth() / 2, getWidth() - getBorderWidth() / 2, getHeight() - getBorderWidth() / 2, paint);

图库部分带圆角,边框的绘制代码调整为:

RectF rect = new RectF(0 + getBorderWidth() / 2, 0 + getBorderWidth() / 2, getWidth() - getBorderWidth() / 2, getHeight() - getBorderWidth() / 2);
canvas.drawRoundRect(rect, radius, radius, paint);

最后绘制标记:

//init 初始化部分代码:
//从图片资源加载
selectFlagBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.album_dialog_img_selected);
//按照主题色染色
Bitmap tintedBitmap = Bitmap.createBitmap(selectFlagBitmap.getWidth(), selectFlagBitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(tintedBitmap);
Paint paint = new Paint();
paint.setColorFilter(new PorterDuffColorFilter(getResources().getColor(R.color.albumDefaultThemeDeep), PorterDuff.Mode.SRC_IN));

//...

//onDraw 部分代码
canvas.drawBitmap(selectFlagBitmap, null, selectFlagRect, paint);

PhotoSelectImageView 的呈现效果:

image-20231221132323779.png

RecyclerView 设置一个间隔装饰器 GridSpacingItemDecoration,指定 item 的间距:

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
   int position = parent.getChildAdapterPosition(view);
   int column = position % spanCount;
   if (column >= 1) {
       outRect.left = spacing;
  }
   if (position >= spanCount) {
       outRect.top = spacing;
  }
}

基本上界面主体就完活了,额外的实现了一个相册列表的 Adapter,复用 RecyclerView 进行显示,区别就在于内容还需要考虑到相册名字的呈现:

image-20231221132715672.png

接下来就是相册的读取了,在开始之前首先需要申请权限。

权限处理

API-33 以前使用存储文件读取权限 READ_EXTERNAL_STORAGE 即可,API - 33 以后则需要使用 READ_MEDIA_IMAGES 权限,因此需要先在 AndroidManifest 声明这两个权限:

name="android.permission.READ_EXTERNAL_STORAGE"/>
name="android.permission.READ_MEDIA_IMAGES" />

使用代码申请:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
   if (ContextCompat.checkSelfPermission(activityContext, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) {
       ActivityCompat.requestPermissions(activityContext, new String[]{Manifest.permission.READ_MEDIA_IMAGES}, PERMISSION_REQUEST_CODE);
       return false;
  }
} else {
   if (ContextCompat.checkSelfPermission(activityContext, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
       ActivityCompat.requestPermissions(activityContext, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
       return false;
  }
}

本来想用 registerForActivityResult,至于为啥没用?别提那玩意了基本上就是一坨...

接下来有了权限,就只需要使用 MediaStore 读取所有相册和照片就可以完成实现了。

MediaStore 读取照片

MediaStore 和传统以文件方式读取照片的形式有所区别,它是一个媒体数据库,这意味着需要用读取数据库的思路去操作它。

首先是依据相册名称读取照片,如果相册名称为空则认为是所有照片,核心代码如下:

List photos = new ArrayList<>();
Uri images = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
String[] projection = new String[]{
       MediaStore.Images.Media.DATA,
       MediaStore.Images.Media.DATE_ADDED
};
String selection;
String[] selectionArgs;
if (isNull(albumName)) {
   selection = null;
   selectionArgs = null;
} else {
   selection = MediaStore.Images.Media.BUCKET_DISPLAY_NAME + " = ?";
   selectionArgs = new String[]{albumName};
}
Cursor cur = context.getContentResolver().query(images,
       projection,
       selection,
       selectionArgs,
       null);
if (cur != null && cur.moveToFirst()) {
   int dataColumn = cur.getColumnIndex(MediaStore.Images.Media.DATA);
   do {
       String photoPath = cur.getString(dataColumn);
       photos.add(photoPath);
  } while (cur.moveToNext());
}
if (cur != null) {
   cur.close();
}

photos 即查询到的所有照片列表了,但还需要处理为按照最近时间倒序,添加 sortOrder 即可:

sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC"

添加 sortOrder 到 query 最后一个参数即可。这里的 MediaStore.Images.Media.DATE_ADDED 代表着按照添加到媒体库的时间排序,另外也可以选择 MediaStore.MediaColumns.DATE_TAKEN 按照拍摄时间排序,至于 DESC 就是倒序的意思了。

然后还需要查询所有相册,查询到的相册名称可能有重复的需要剔重。

//读取相册列表
List albums = new ArrayList<>();
String[] projection = new String[]{
       MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
       MediaStore.Images.Media.BUCKET_ID
};
Uri images = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Cursor cur = context.getContentResolver().query(images,
       projection,
       null,      
       null,      
       null      
);
if (cur != null && cur.moveToFirst()) {
   int bucketColumn = cur.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME);
   do {
       String albumName = cur.getString(bucketColumn);
       if (!albums.contains(albumName) && !isNull(albumName)) albums.add(albumName);
  } while (cur.moveToNext());
}
if (cur != null) {
   cur.close();
}

在 UI 呈现时按照相册名称读取最后一张图片作为封面图即可。

至此,自制照片选择器就基本上完成了,相关完整代码已经开源到 Github 上,欢迎参考学习 github.com/kongzue/Dia…,DialogXSample 是基于 DialogX 对话框框架的一系列功能模块扩展包,目前也提供了 地址滚动选择对话框、日期/日历(区间)选择对话框、分享选择对话框、自定义联动滚动选择对话框、底部弹出的评论输入对话框、选择(多选/筛选)文件对话框、抽屉对话框和照片选择器的 Demo 代码。

一键使用

照片选择器直接引入的 gradle 配置如下:

在 build.gradle(Project)(新版本 Android Studio 请在 settings.gradle)添加 jitpack 仓库:

allprojects {
  repositories {
      ...
      maven { url 'https://jitpack.io' }
  }
}
def dialogx_sample_version = "0.0.10"
implementation 'com.github.kongzue.DialogXSample:AlbumDialog:${dialogx_sample_version}'

额外的还需引入:

def DIALOGX_VERSION = "0.0.50.beta2"
implementation "com.github.kongzue.DialogX:DialogX:${DIALOGX_VERSION}"
implementation 'com.github.bumptech.glide:glide:4.12.0'
implementation "androidx.recyclerview:recyclerview:1.2.1"

如果默认的就能满足你的业务需求,直接引入对应功能的包即可,如果不能,请自行拉取代码集成到自己的项目里修改使用


作者:Kongzue
来源:juejin.cn/post/7314642642868715554

0 个评论

要回复文章请先登录注册