Android 自制照片选择器

自制照片选择器

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

看起来确实不错,本来想直接调用 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 尺寸进行加载和缓存以避免界面卡顿:

scss 复制代码
Glide.with(context)
        .load(imageUrls.get(position))
        .override(imageSize)
        .error(errorPhotoDrawableRes)
        .into((PhotoSelectImageView) holder.itemView);

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

scss 复制代码
canvas.drawRect(0 + getBorderWidth() / 2, 0 + getBorderWidth() / 2, getWidth() - getBorderWidth() / 2, getHeight() - getBorderWidth() / 2, paint);

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

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

最后绘制标记:

less 复制代码
//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 的呈现效果:

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

ini 复制代码
@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 进行显示,区别就在于内容还需要考虑到相册名字的呈现:

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

权限处理

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

ini 复制代码
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

使用代码申请:

arduino 复制代码
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 和传统以文件方式读取照片的形式有所区别,它是一个媒体数据库,这意味着需要用读取数据库的思路去操作它。

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

ini 复制代码
List<String> 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 即可:

ini 复制代码
sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC"

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

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

ini 复制代码
//读取相册列表
List<String> 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 仓库:

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

额外的还需引入:

bash 复制代码
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"
相关推荐
代码拯救不了世界7 分钟前
若依框架中的上传图片后如何实现回显到页面的
java·图片上传·若依
heirui_Oooo9 分钟前
利用两种方式分别实现单例模式(懒汉式、饿汉式)
java·开发语言
wy02_11 分钟前
Linux下载RabbitMQ,并解决Github拒绝访问443的问题
linux·rabbitmq·github
天使day17 分钟前
Maven
java·maven
汇匠源19 分钟前
共享无人系统,从出行到生活全面覆盖
java·生活
Vincent(朱志强)24 分钟前
设计模式详解(十二):单例模式——Singleton
android·单例模式·设计模式
mmsx1 小时前
android 登录界面编写
android·登录界面
姜毛毛-JYM1 小时前
【JetPack】Navigation知识点总结
android
Domain-zhuo1 小时前
Git常用命令
前端·git·gitee·github·gitea·gitcode
aPurpleBerry1 小时前
解决 fatal: detected dubious ownership in repository at ‘XXXX‘ 问题
github