-
Notifications
You must be signed in to change notification settings - Fork 60
Home
一个用 Jetpack Compose 实现的 Android 图片视频选择框架
- 适配到 Android 14
- 解决了多个系统兼容性问题
- 按需索取权限,遵循最佳用户实践
- 完全用 Kotlin & Jetpack Compose 实现
- 支持多种拍照策略,可以自由定义拍照逻辑
- 支持自定义图片加载框架,可以自由定义加载逻辑
- 支持同时选择图片和视频,或者单独选择两者之一
- 支持精细自定义主题,提供了日夜间两套默认主题
关联的文章:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
dependencies {
implementation("io.github.leavesczy:matisse:latestVersion")
}
Matisse 包含两种使用场景,可以组合使用或者是单独使用,这两种场景分别对应两个 ActivityResultContract
- MatisseContract。展示系统相册内的图片和视频,支持同时启用拍照功能
- MatisseCaptureContract。直接启动系统相机,拍摄照片
MatisseContract 的启动参数是 Matisse 类,在回调函数里获取用户选择的图片或视频,返回值类型为 List<MediaResource>?
Jetpack Compose:
val mediaPickerLauncher =
rememberLauncherForActivityResult(contract = MatisseContract()) { result: List<MediaResource>? ->
if (!result.isNullOrEmpty()) {
val mediaResource = result[0]
val uri = mediaResource.uri
val path = mediaResource.path
val name = mediaResource.name
val mimeType = mediaResource.mimeType
}
}
val matisse = Matisse(
maxSelectable = 1,
imageEngine = GlideImageEngine(),
mediaType = MediaType.ImageOnly
)
mediaPickerLauncher.launch(matisse)
View:
private val mediaPickerLauncher =
registerForActivityResult(MatisseContract()) { result: List<MediaResource>? ->
if (!result.isNullOrEmpty()) {
val mediaResource = result[0]
val uri = mediaResource.uri
val path = mediaResource.path
val name = mediaResource.name
val mimeType = mediaResource.mimeType
}
}
val matisse = Matisse(
maxSelectable = 1,
imageEngine = GlideImageEngine(),
mediaType = MediaType.ImageOnly
)
mediaPickerLauncher.launch(matisse)
MatisseCaptureContract 的启动参数是 MatisseCapture 类,在回调函数里获取用户拍摄的照片,返回值类型为 MediaResource?
。Matisse 通过启动一个透明 Activity 来完成 “申请权限” 和 “拍照” 等操作
Jetpack Compose:
val takePictureLauncher =
rememberLauncherForActivityResult(contract = MatisseCaptureContract()) { result ->
if (result != null) {
val uri = result.uri
val path = result.path
val name = result.name
val mimeType = result.mimeType
}
}
takePictureLauncher.launch(MatisseCapture(captureStrategy = MediaStoreCaptureStrategy()))
View:
private val takePictureLauncher =
registerForActivityResult(MatisseCaptureContract()) { result: MediaResource? ->
if (result != null) {
val uri = result.uri
val path = result.path
val name = result.name
val mimeType = result.mimeType
}
}
takePictureLauncher.launch(MatisseCapture(captureStrategy = MediaStoreCaptureStrategy()))
/**
* @param maxSelectable 用于设置最多能选择几个媒体资源
* @param imageEngine 用于自定义图片加载框架
* @param fastSelect 用于设置是否免去预览图片和确认选择的流程。值为 true 时 maxSelectable 必须为 1
* @param mediaType 用于设置要加载的媒体资源类型。默认仅图片
* @param singleMediaType 用于设置是否允许同时选择图片和视频。默认允许
* @param mediaFilter 用于设置媒体资源的筛选规则。默认不进行筛选
* @param captureStrategy 拍照策略。默认不开启拍照功能
*/
data class Matisse(
val maxSelectable: Int,
val imageEngine: ImageEngine,
val fastSelect: Boolean = false,
val mediaType: MediaType = MediaType.ImageOnly,
val singleMediaType: Boolean = false,
val mediaFilter: MediaFilter? = null,
val captureStrategy: CaptureStrategy? = null
)
/**
* @param captureStrategy 拍照策略
*/
data class MatisseCapture(val captureStrategy: CaptureStrategy)
maxSelectable 用于设置最多能选择几个媒体资源
fastSelect 用于设置是否免去预览图片和确认选择的流程,仅在 maxSelectable 为 1 时才能设置为 true。当值为 true 时,用户点击图片后就会立马返回所选图片,无需再次确认
mediaType 用于设置要加载的媒体资源类型,包含以下四种类型供开发者选择
//仅图片
val imageOnly = MediaType.ImageOnly
//仅视频
val videoOnly = MediaType.VideoOnly
//图片 + 视频
val imageAndVideo = MediaType.ImageAndVideo
//jpg + mp4
val mimeTypes = MediaType.MultipleMimeType(mimeTypes = setOf("image/jpeg", "video/mp4"))
imageEngine 用于自定义图片加载框架。考虑到引用方大概率已经集成了某个图片加载框架,因此 Matisse 通过 ImageEngine 接口来让开发者可以自由选择要使用的图片加载框架
interface ImageEngine : Parcelable {
/**
* 加载缩略图时调用
*/
@Composable
fun Thumbnail(mediaResource: MediaResource)
/**
* 加载大图时调用
*/
@Composable
fun Image(mediaResource: MediaResource)
}
目前,比较主流且支持 Jetpack Compose 的图片加载框架有两个:Glide 和 Coil。Matisse 默认集成了相应的 ImageEngine 实现:GlideImageEngine 和 CoilImageEngine,引用方可以按照自己的实际情况进行选择
引用方需要在自己的项目中手动引入 Glide 的 Jetpack Compose 扩展依赖库
dependencies {
val glideComposeVersion = "1.0.0-beta01"
implementation("com.github.bumptech.glide:compose:$glideComposeVersion")
}
然后就可以直接使用 GlideImageEngine 了
val matisse = Matisse(
maxSelectable = 1,
imageEngine = GlideImageEngine(),
mediaType = MediaType.ImageOnly
)
引用方需要在自己的项目中手动引入 Coil 的 Jetpack Compose 扩展依赖库,根据实际情况来决定是否要引入 coil-gif
和 coil-video
dependencies {
val coilVersion = "2.6.0"
//必选
implementation("io.coil-kt:coil-compose:$coilVersion")
//可选,需要展示 Gif 则引入
implementation("io.coil-kt:coil-gif:$coilVersion")
//可选,需要展示 Video 则引入
implementation("io.coil-kt:coil-video:$coilVersion")
}
初始化 Coil
private fun initCoil(context: Context) {
val imageLoader = ImageLoader.Builder(context = context)
.components {
//可选,需要展示 Gif 则引入
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
//可选,需要展示 Video 则引入
add(VideoFrameDecoder.Factory())
}
.build()
Coil.setImageLoader(imageLoader)
}
然后就可以直接使用 CoilImageEngine 了
val matisse = Matisse(
maxSelectable = 1,
imageEngine = CoilImageEngine(),
mediaType = MediaType.ImageOnly
)
假如默认的 GlideImageEngine 和 CoilImageEngine 不符合需求,或者是想要使用其它的图片加载框架的话,开发者可以自己来手动实现 ImageEngine
此时引用方的项目就需要开启 Jetpack Compose 功能并引用 kotlinCompiler 了
引用 kotlinCompiler 的方式有两种,取决于引用方使用的 Kotlin 版本:Compose to Kotlin Compatibility Map
-
在 kotlin 2.0.0 版本之前,通过 kotlinCompilerExtensionVersion 进行引用
android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "x.x.x" } }
-
从 kotlin 2.0.0 版本开始,通过 Compose Compiler Gradle Plugin 进行引用
android {
buildFeatures {
compose = true
}
}
plugins {
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0"
}
另外,由于 ImageEngine 接口继承了 Parcelable 接口,因此 ImageEngine 的任意子类也均需要实现 Parcelable。建议开发者通过引入 Kotlin 的 Parcelize 插件来实现自动序列化
假如引用方希望同时集成图片缩放功能的话,可以去引用任意适用于 Jetpack Compose 的图片缩放库,Matisse 提供了相关的示例代码,开发者可以去参照实现方式
singleMediaType 用于当 mediaType 同时包含图片和视频两种类型时,设置是否允许用户同时选择图片和视频,值为 true 时只能选择一种媒体类型
mediaFilter 用于设置媒体资源的筛选规则,其作用有两个:
- 忽略特定的媒体资源
- 默认选中特定的媒体资源
MediaFilter 本身是一个接口,有一个默认实现 DefaultMediaFilter,其包含的三个构造参数就分别对应上述两个功能
/**
* @param ignoredMimeType 包含在内的 mimeType 将会被忽略,不会展示给用户
* @param ignoredResourceUri 包含在内的 Uri 将会被忽略,不会展示给用户
* @param selectedResourceUri 包含在内的 Uri 将会被默认选中
*/
@Parcelize
class DefaultMediaFilter(
private val ignoredMimeType: Set<String> = emptySet(),
private val ignoredResourceUri: Set<Uri> = emptySet(),
private val selectedResourceUri: Set<Uri> = emptySet()
) : MediaFilter
captureStrategy 即拍照策略,用于支持两种拍照场景:
- 直接启动相机让用户进行拍照
- 在资源列表页面展示一个拍照入口,在用户点击后进行拍照
CaptureStrategy 决定了相机的启动参数、照片的命名和存储规则、权限的申请规则
Matisse 提供了三种默认实现,用户的感知各不相同
- FileProviderCaptureStrategy
- MediaStoreCaptureStrategy
- SmartCaptureStrategy
开启拍照功能,通过 FileProvider 来生成拍照所需要的 imageUri,并将图片保存到应用的私有目录内
此策略不需要权限,但需要配置 FileProvider,authorities
视自身情况而定,通过 authorities
来实例化 FileProviderCaptureStrategy
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="xxx.xxx.xxx"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths" />
</provider>
file_provider_paths.xml
中需要配置 external-files-path
路径的 Pictures 文件夹,name 可以随意命名
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path
name="Capture"
path="Pictures" />
</paths>
开启拍照功能,通过系统的 MediaStore 来生成拍照所需要的 imageUri,并将图片保存到系统相册中
此策略在不同系统版本上有着不同的权限要求
- 在 Android 10 之前,需要获取到
WRITE_EXTERNAL_STORAGE
权限后才可以向系统相册插入图片 - 从 Android 10 开始,通过 MediaStore 向系统相册插入图片无需任何权限
所以,采用此策略后,如果你的项目的 minSdkVersion 小于 29 则需要声明 WRITE_EXTERNAL_STORAGE
权限,否则不需要声明
开启拍照功能,此策略同时包含了 FileProviderCaptureStrategy 和 MediaStoreCaptureStrategy 两种策略,因此外部也需要像 FileProviderCaptureStrategy 一样配置 FileProvider,通过 authorities
来实例化 SmartCaptureStrategy
SmartCaptureStrategy 的执行策略是:
- 当系统版本小于 Android 10 时,执行 FileProviderCaptureStrategy 策略
- 当系统版本大于等于 Android 10 时,执行 MediaStoreCaptureStrategy 策略
采用此策略后,既无需申请权限,又可以在 Android 10 开始之后的系统版本将照片存入到系统相册中,同时平衡了 “隐私安全” 和 “用户体验” 两者
拍照策略 | 需要的权限 | 配置项 | 图片对用户是否可见 |
---|---|---|---|
FileProviderCaptureStrategy | 无 | 需要配置 FileProvider | 否,图片保存在应用的私有目录内,对用户不可见 |
MediaStoreCaptureStrategy | Android 10 之前需要 WRITE_EXTERNAL_STORAGE 权限,Android 10 开始不需要权限 | 无 | 是,图片保存在系统相册内,对用户可见 |
SmartCaptureStrategy | 无 | 需要配置 FileProvider | 在 Android 10 之前的系统版本不可见,和 FileProviderCaptureStrategy 一样 |
开发者根据自己的实际情况来选择拍照策略:
- 如果应用本身就拥有
WRITE_EXTERNAL_STORAGE
权限的话,建议选 MediaStoreCaptureStrategy,拍摄的图片保存到系统相册中也比较符合用户的认知 - 如果应用本身就没有
WRITE_EXTERNAL_STORAGE
权限的话,建议选 FileProviderCaptureStrategy 或 SmartCaptureStrategy,为了相册问题而多申请一个敏感权限得不偿失
CaptureStrategy 本身是一个接口,假如以上三种拍照策略均无法满足你的需求的话,开发者可以自己来实现 CaptureStrategy 接口
此外,CaptureStrategy 接口内包含一个返回值类型为 Bundle 的默认方法
getCaptureExtra
,该返回值会被添加到用于启动系统相机的 Intent 中,可用于为相机设置启动参数(例如,开启前置摄像头)。相对应的,FileProviderCaptureStrategy、MediaStoreCaptureStrategy、SmartCaptureStrategy 也都包含一个 Bundle 类型的构造参数作为getCaptureExtra
方法的返回值,引用方可以通过该参数来控制相机属性
Matisse 提供了日间和夜间两套默认主题,也支持引用方进一步自定义主题
通过在项目的 values
和 values-night
文件夹中按需声明以下属性值,以此来自定义 图片列数、系统状态栏和导航栏的背景色和图标颜色、页面背景色、文本颜色、图标颜色 等多个属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="matisse_image_span_count">3</integer>
<bool name="matisse_status_bar_dark_icons">true</bool>
<bool name="matisse_navigation_bar_dark_icons">true</bool>
<color name="matisse_status_bar_color">#FFFFFFFF</color>
<color name="matisse_navigation_bar_color">#FFFFFFFF</color>
<color name="matisse_main_page_background_color">#FFFFFFFF</color>
<color name="matisse_media_item_background_color">#66CCCCCC</color>
<color name="matisse_media_item_scrim_color_when_selected">#80000000</color>
<color name="matisse_capture_item_background_color">#66CCCCCC</color>
<color name="matisse_capture_item_icon_color">#FFFFFFFF</color>
<color name="matisse_top_bar_background_color">#FFFFFFFF</color>
<color name="matisse_top_bar_icon_color">#FF000000</color>
<color name="matisse_top_bar_text_color">#FF000000</color>
<color name="matisse_dropdown_menu_background_color">#FFFFFFFF</color>
<color name="matisse_dropdown_menu_text_color">#FF000000</color>
<color name="matisse_bottom_navigation_bar_background_color">#FFFFFFFF</color>
<color name="matisse_preview_text_color">#FF000000</color>
<color name="matisse_preview_text_color_if_disable">#FFC6CCD2</color>
<color name="matisse_sure_text_color">#FF000000</color>
<color name="matisse_sure_text_color_if_disable">#FFC6CCD2</color>
<color name="matisse_preview_page_background_color">#FF22202A</color>
<color name="matisse_preview_page_bottom_navigation_bar_background_color">#FF2B2A34</color>
<color name="matisse_preview_page_back_text_color">#FFFFFFFF</color>
<color name="matisse_preview_page_sure_text_color">#FFFFFFFF</color>
<color name="matisse_preview_page_sure_text_color_if_disable">#80FFFFFF</color>
<color name="matisse_check_box_circle_color">#FFFFFFFF</color>
<color name="matisse_check_box_circle_color_if_disable">#80FFFFFF</color>
<color name="matisse_check_box_circle_fill_color">#FF03A9F4</color>
<color name="matisse_check_box_text_color">#FFFFFFFF</color>
<color name="matisse_circular_loading_color">#FF03A9F4</color>
<color name="matisse_video_icon_color">#FFFFFFFF</color>
<color name="matisse_video_view_page_background_color">#FF22202A</color>
</resources>
此外,Matisse 中也包含了一些默认文本用于对用户进行提示,引用方可以在自己项目中的 strings.xml
文件中按需声明以下属性值,以此来覆盖 Matisse 的默认文本
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="matisse_default_bucket_name">全部</string>
<string name="matisse_read_media_permission_denied">请授予相册访问权限后重试</string>
<string name="matisse_write_external_storage_permission_denied">请授予存储写入权限后重试</string>
<string name="matisse_camera_permission_denied">请授予拍照权限后重试</string>
<string name="matisse_limit_the_number_of_media">最多只能选择 %d 个图片或视频</string>
<string name="matisse_cannot_select_both_picture_and_video_at_the_same_time">不能同时选择图片和视频</string>
<string name="matisse_no_apps_support_take_picture">没有可用于拍照的应用</string>
<string name="matisse_preview">预览</string>
<string name="matisse_sure">确定(%d/%d)</string>
<string name="matisse_back">返回</string>
</resources>
Matisse 没有主动声明任何权限,开发者需要根据实际情况在自己的项目中进行按需声明
必需权限用于读取系统相册内的图片和视频
如果应用的 targetSdkVersion 小于 33,声明 READ_EXTERNAL_STORAGE
权限即可
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
如果应用的 targetSdkVersion 大于等于 33
- 需要展示的仅是图片,声明
READ_EXTERNAL_STORAGE
和READ_MEDIA_IMAGES
两个权限 - 需要展示的仅是视频,声明
READ_EXTERNAL_STORAGE
和READ_MEDIA_VIDEO
两个权限 - 需要同时展示图片和视频,声明
READ_EXTERNAL_STORAGE
、READ_MEDIA_IMAGES
、READ_MEDIA_VIDEO
三个权限
此外,在这种情况下,READ_EXTERNAL_STORAGE
权限已无法用于 Android 13 开始之后的系统版本了,所以可以将权限的 maxSdkVersion 设为 32
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
可选权限是否需要声明取决于开发者采用的拍照策略和应用的 minSdkVersion
- SmartCaptureStrategy。无需申请此权限
- FileProviderCaptureStrategy。无需申请此权限
- MediaStoreCaptureStrategy
- 如果应用的
minSdkVersion
大于等于 29。无需申请此权限 - 如果应用的
minSdkVersion
小于 29。由于在 Android 10 之前向系统相册写入图片需要存储写入权限,所以需要声明WRITE_EXTERNAL_STORAGE
权限。此外,在这种情况下,Android 10 开始之后的系统版本不再需要此权限,因此可以将权限的maxSdkVersion
设为 28
- 如果应用的
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />