公众号:字节数组

希望对你有所帮助

知乎的 Matisse 应该蛮多 Android 开发者有了解过或者是曾经使用过,这是知乎在 2017 年开源的一个 Android 端图片选择框架,其google商店颜值在现在看来也还是挺不错的

Jetpack Compose 实现一个图片选择框架

可惜近几年实例化对象是什么意思知乎官kotlin和java区别方已经不再对 Matisse 进行Kotlin维护更新了,上一次提交记录还停留在 2github永久回家地址019 年,累积了google翻译 400 个 issues 一直没人解答,google很多高版本系统的兼容性问题和内部 bug 也一直得不到解决。我反编译了知乎的 App,发现其内部还保留着 M类的实例化atisse 的相关代码,所以知乎应该不是完全废弃了 Matisse,而只是不再开源了

我公司的项目也使用到了 Matisse,随着 Android 系统的更新,时不时地android下载安装就会有用户来反馈问题,无奈我也只能 fork 了源码自己来维护。一直这么小修小补终究不太合适,而且如果不进行完全重写的话,Matisse 的一些交互体验问题也没法得到彻底解决,而这些问题在知乎目前的官方 App 上也一样存在,以修改个人头像时打开的图片选择页面为例:

Jetpack Compose 实现一个图片选择框架

我发现的问题有三个类的实例化

  • 知乎的用googleplay户头像不支持 Gif 格android是什么系统式,当用户点击 Gif 图片时会提示 “实例化对象不支持的文件类型”。按我的想法,既然不支持 Gif 格式,那么一开始展示的时候就应该过滤掉才gitlab对,而知乎目前的筛选逻辑应该就是来源自 Matisse ,因为 MatiKotlinsse 也不google翻译支持 只展示静态图,但又可以 只展示 Gif,这筛选逻辑我觉得十分奇怪
  • 当取消勾选静态图时,可以看到 Gif 图片会很明显git命令地闪烁了一下,此问题在 Matisse 中也存在。而如果从知乎的编辑器进入图片选择页面的话,就android的drawable类不单单是 Gif 图片会闪烁了,而是整个页面都会闪烁一下…
  • 当点击下拉实例化菜单时,可以看到 Pictures 目录中有三张图片,但打开目录又发现是空的。这是由于android是什么系统知乎没有过滤掉一些脏数实例化一个对象可以使用什么命令据导致的,后面会讲到具体原因

由于以上问题,也让我有了彻底放弃 Matisse,android是什么系统自己来实现一个新的图片选择框架的打算实例化一个对象可以使用什么命令,也实现得差不多了,最终的效果如下所示

Jetpack Compose 实现一个图片选择框架

除了支android/harmonyos持 Matisse 有的google商店基本功能外,此框架的 特点 / 优势 还有:

  • 完全用 Kotlin 实现,拒绝 Java
  • UI 层完全用 Jetpack Composandroid/harmonyose 实现,拒绝原生 View 体googleplay安卓版下载
  • 支持更加精细地自定义主kotlin什么意思题,默认提供了 日间 和 夜间kotlin和java 两种主题
  • 支持精准筛选图片类型,只会显示想要的图片类型
  • 同时支持 FileProvider 和 MediaStore 两种拍照策略
  • gitlab取到的图片信息更加丰富,google网站登录入口一共包含 uri、displayName、mimeType、width、height、orientation、sizgiteee、path、bucandroid下载安装ketId、bukotlin什么意思cketDisplayName 等十个属google中国性值
  • 已适配到 Android 12 系统,解决了几个系统kotlin语言兼容性问题,下文会提到

此框架Google也有一些劣势:

  • 预览图片时不支持手势缩放。一开始我有尝试用 Jetpack Compose 来实现图片手势缩放,但效果不太理想,我又不想引入 View 体系中的三方库,所以此版giticomfort是什么轮胎本暂不支持图片手势缩放
  • 框架内部采用的图片加载android下载库是 Coil,且不支持替换。由于实例化是什么意思目前支持 Jetpack Compose 的图片加载库基本只能选择 Cgoogle谷歌搜索主页oil 了,因此没有提供替换图片加载库的入口
  • 图片列表的滑动性能要低于原生的 RecyclerView,debug 版本尤git命令为明显。此问题目前无解,只能等 Google 官方后续的优化了

代码我也google开源到了 Github,懒得想名字,再加上一开始的设计思路也来自于 Matisse,因此就取了一样的名字,也叫 Matisse。google网站登录入口下文如果没有特别说明,Mandroid/harmonyosatisse 指的就是此 Jetpack Comgithub永久回家地址pose 版本的图片选择框架了

用 Jetpack Compose 来实现 UI 相比原生的 View 体系实在要简单很多,在这一块除android什么意思了滑动性能之外我也没遇到其它问题。因此,本文的内容和 Jetpack Compose 无关,主要是讲 Matisse 的一些实现细节googleplay安卓版下载和遇到的系统兼容性问题

获取图片

实现一个图片选择框架的第一步自然就是要获取到相册内的所有图片了,因此需要申请 READ_EXT实例化对象ERNAL_STORAGE 权限,此外还需要依赖系统的 Mediandroid是什么系统aStore API 来读android平板电脑价格取所有图片

MediaStore 相当于一个文件系统数据库,记录了当前设备中所有文件的索Google引,我们giti可以android下载安装通过它来快速查找设备中特定类型的文件。Matisse 使用的是 MediaStoreKotlin.Image,在操作上就类似于查询数据库android下载,通过声明需要的数据库字段 projection 和排序规则 sortOrder,得到相应的数据库游标 ckotlin怎么读ursor,通过 cursorgithub中文官网网页 遍历查询出每一个字段值

val projection = arrayOf(
    MediaStore.Images.Media._ID,
    MediaStore.Images.Media.DISPLAY_NAME,
    MediaStore.Images.Media.MIME_TYPE,
    MediaStore.Images.Media.WIDTH,
    MediaStore.Images.Media.HEIGHT,
    MediaStore.Images.Media.SIZE,
    MediaStore.Images.Media.ORIENTATION,
    MediaStore.Images.Media.DATA,
    MediaStore.Images.Media.BUCKET_ID,
    MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
)
val sortOrder = "${MediaStore.Images.Media.DATE_MODIFIED} DESC"
val mediaResourcesList = mutableListOf<MediaResources>()
val mediaCursor = context.contentResolver.query(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder,
) ?: return@withContext null
mediaCursor.use { cursor ->
    while (cursor.moveToNext()) {
        val id = cursor.getLong(MediaStore.Images.Media._ID)
        val displayName =
            cursor.getString(MediaStore.Images.Media.DISPLAY_NAME)
        val mimeType = cursor.getString(MediaStore.Images.Media.MIME_TYPE)
        val width = cursor.getInt(MediaStore.Images.Media.WIDTH)
        val height = cursor.getInt(MediaStore.Images.Media.HEIGHT)
        val size = cursor.getLong(MediaStore.Images.Media.SIZE)
        val orientation = cursor.getInt(MediaStore.Images.Media.ORIENTATION)
        val data = cursor.getString(MediaStore.Images.Media.DATA)
        val bucketId = cursor.getString(MediaStore.Images.Media.BUCKET_ID)
        val bucketDisplayName =
            cursor.getString(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)
        val contentUri =
            ContentUris.withAppendedId(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                id
            )
        val mediaResources = MediaResources(
            uri = contentUri,
            displayName = displayName,
            mimeType = mimeType,
            width = width,
            height = height,
            orientation = orientation,
            path = data,
            size = size,
            bucketId = bucketId,
            bucketDisplayName = bucketDisplayName,
        )
        mediaResourcesList.add(mediaResources)
    }
    return@withContext mediaResourcesList
}

每一张图片都存放于特定的相册文件夹内,因此可以通过 bucketId 来对每一张图片进行归类,从而得到 Matigiticomfort是什么轮胎sse 中的下拉菜单

suspend fun groupByBucket(resources: List<MediaResources>): List<MediaBucket> {
    return withContext(context = Dispatchers.IO) {
        val resourcesMap = linkedMapOf<String, MutableList<MediaResources>>()
        resources.forEach { res ->
            val bucketId = res.bucketId
            val list = resourcesMap[bucketId]
            if (list == null) {
                resourcesMap[bucketId] = mutableListOf(res)
            } else {
                list.add(res)
            }
        }
        val allMediaBucketResource = mutableListOf<MediaBucket>()
        resourcesMap.forEach {
            val resourcesList = it.value
            if (resourcesList.isNotEmpty()) {
                val bucketId = it.key
                val bucketDisplayName = resourcesList[0].bucketDisplayName
                allMediaBucketResource.add(
                    MediaBucket(
                        bucketId = bucketId,
                        bucketDisplayName = bucketDisplayName,
                        bucketDisplayIcon = resourcesList[0].uri,
                        resources = resourcesList,
                        displayResources = resourcesList
                    )
                )
            }
        }
        return@withContext allMediaBucketResource
    }
}

android下载照策略

一般的应用对于拍照功能不会有太多的自定义需求,因此大多是Google通过直接调起系统相机来实现拍照,优点是实现简单,且不用申请 CAMERA 权限

实现代码大致如google翻译下所示,最终kotlin是什么图片就会保存在 imageUri 指向的文件中

class MatisseActivity : ComponentActivity() {
    private var tempImageUri: Uri? = null
    private fun takePicture(imageUri: Uri) {
        tempImageUri = imageUri
        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri)
        startActivityForResult(intent, 1)
    }
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == 1 && resultCode == Activity.RESULT_OK) {
            val mTempImageUri = tempImageUri
            if (mTempImageUri != null) {
                //TODO
            }
        }
    }
}

以上代码属于通用流程,当判断到完成拍照后,将以上的 imageUri 返回即可

但生成 imageUri 却有着很多学问:不kotlin为什么流行不起来同的生成规则对应着不同的权限,甚至同种方式在不同系统版本上对权限的要求也不一样,对用户的感知也不一样。此外,如果用户在相机页面取消拍照的话,此时 imageUri 指向的图片文件就没有用了,我们还需要主动删除该文件

Matissekotlin为什么流行不起来 通过 CaptureSkotlin和javatrategy 接口来抽象以上逻辑

/**
 * 拍照策略
 */
interface CaptureStrategy {
    /**
     * 是否启用拍照功能
     */
    fun isEnabled(): Boolean
    /**
     * 是否需要申请读取存储卡的权限
     */
    fun shouldRequestWriteExternalStoragePermission(context: Context): Boolean
    /**
     * 获取用于存储拍照结果的 Uri
     */
    suspend fun createImageUri(context: Context): Uri?
    /**
     * 获取拍照结果
     */
    suspend fun loadResources(context: Context, imageUri: Uri): MediaResources?
    /**
     * 当用户取消拍照时调用
     */
    suspend fun onTakePictureCanceled(context: Context, imageUri: Uri)
    /**
     * 生成图片文件名
     */
    fun createImageName(): String {
        return UUID.randomUUID().toString() + ".jpg"
    }
}

Matisse 实现了三种拍照策略供开发者选择:

  • Nothandroid下载ingCaptureStrategy
  • FileProviderCaptureStrategy
  • MediaStoreCaptureStrategy

Ngoogleplay安卓版下载othingCaptureStrategy

NothingCaptureStrategy 代表的是不开启拍照功能,也是 Matisse 默认的拍照策略

/**
 *  什么也不做,即不开启拍照功能
 */
object NothingCaptureStrategy : CaptureStrategy {
    override fun isEnabled(): Boolean {
        return false
    }
    override fun shouldRequestWriteExternalStoragePermission(context: Context): Boolean {
        return false
    }
    override suspend fun createImageUri(context: Context): Uri? {
        return null
    }
    override suspend fun loadResources(context: Context, imageUri: Uri): MediaResources? {
        return null
    }
    override suspend fun onTakePictureCanceled(context: Context, imageUri: Uri) {
    }
}

FileProviderCaptureStrategy

顾名思义,android下载此策略通过 FileProvider 来生成所需要google的 imageUri

从 Android 7.git命令0 开始,系统禁止应用通过 file://URI 来访问其他应用的私有目录文件,要在应用间共kotlin为什么流行不起来享私有文件,必须通过 contegithubnt://URIgoogle服务框架 并授予 URI 临时访问权限来实现,否则将直接抛出异常。而将 File 转换为 content://URI 的操作就需要依靠 FileProvider 来实例化实现了。Matisse 传递给系统相机的 imageUri 也需要满足此规则

FileProviderCaptureStrategy 采用的策略就是:实例化是什么意思

  • 在 ExternalFilesDir 的 Pkotlin匿名函数ictures 目录中创建一个图片临时文件用于存储拍照结果,通过 FileProvider 得到该文件对应的 content:kotlin和java//URI ,从而得到待写入的 imageUri
  • 假如用户giteegiti终取消拍照,则直接删除创建的临时文件
  • kotlin怎么读如用户最终完成拍照,则通过 Bgoogle谷歌搜索主页itmapFactorykotlin怎么读 获取图片google的详细信息
  • 由于图片是保存在应用自身的私有目录中,因此不需要申请任何权限,也正因为是私有目录,所以图片不会出现在系统相册中
/**
 *  通过 FileProvider 来生成拍照所需要的 ImageUri
 *  无需申请权限
 *  所拍的照片不会保存在系统相册里
 *  外部必须配置 FileProvider,并在此处传入 authority
 */
class FileProviderCaptureStrategy(private val authority: String) : CaptureStrategy {
    private val uriFileMap = mutableMapOf<Uri, File>()
    override fun isEnabled(): Boolean {
        return true
    }
    override fun shouldRequestWriteExternalStoragePermission(context: Context): Boolean {
        return false
    }
    override suspend fun createImageUri(context: Context): Uri? {
        return withContext(context = Dispatchers.IO) {
            return@withContext try {
                val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
                val tempFile = File.createTempFile(
                    createImageName(),
                    "",
                    storageDir
                )
                val uri = FileProvider.getUriForFile(
                    context,
                    authority,
                    tempFile
                )
                uriFileMap[uri] = tempFile
                return@withContext uri
            } catch (e: Throwable) {
                e.printStackTrace()
                null
            }
        }
    }
    override suspend fun loadResources(context: Context, imageUri: Uri): MediaResources {
        return withContext(context = Dispatchers.IO) {
            val imageFile = uriFileMap[imageUri]!!
            uriFileMap.remove(imageUri)
            val imageFilePath = imageFile.absolutePath
            val option = BitmapFactory.Options()
            option.inJustDecodeBounds = true
            BitmapFactory.decodeFile(imageFilePath, option)
            return@withContext MediaResources(
                uri = imageUri,
                displayName = imageFile.name,
                mimeType = option.outMimeType ?: "",
                width = max(option.outWidth, 0),
                height = max(option.outHeight, 0),
                orientation = 0,
                size = imageFile.length(),
                path = imageFile.absolutePath,
                bucketId = "",
                bucketDisplayName = ""
            )
        }
    }
    override suspend fun onTakePictureCanceled(context: Context, imageUri: Uri) {
        withContext(context = Dispatchers.IO) {
            val imageFile = uriFileMap[imageUri]!!
            uriFileMap.remove(imageUri)
            if (imageFile.exists()) {
                imageFile.delete()
            }
        }
    }
}

外部需要在自身项目中声Git明 FileProvider,authorities 视自身情况而定,通过 authorities 来实例化 FileProviderCaptureStraandroid是什么手机牌子tegy

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="github.leavesczy.matisse.samples.FileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

file_paths.xml 中需要配置 external-files-path 路径的 Picandroidstudio安装教程tures 文件夹,nagitime 可以随意命名

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-files-path
        name="Capture"
        path="Pictures" />
</paths>

MediaStoreCaptureStrategy

顾名思义,此策略通过 MediaStore 来生成gitlab所需要的 imagandroid下载eUri

在 Android 10 系统之前,应用需要获取到 WRITE_EXTERNAL_STORAGE 权限android的drawable类后才可以向共享存储空间中写入文件。从 Android 10 开始,应用通giti轮胎过 MediaStore 向共享存储空间中写入文件无kotlin匿名函数需任何权限,且对于应用自身创建的文件,无需 READ_EXTERNAL_STORAGE 权限就可以直接访问和删kotlin为什么流行不起来

MediaStoreCaptureStrategy 采用的策略就是:

  • 在大于等于 10 的系统版本中,不申请 WRgitiITgiteeE_EXTERNAL_STORAGE 权限,其它系统版本则进行申请
  • 通过 MediaStore 向系统预创建一张图片,从而得到待写入的 iandroid手机mageUri
  • 假如用户最终取消拍照,则通过 MediaStore 删除 imagegoogleUri 指向的脏数android是什么手机牌子
  • 假如用户最终完成拍照,则通过 MediaStore 去查询 imageUri 对应图片的详细信息
  • 由于图片一开始gitee就保存在android下载安装 MediaStore 中,因此图片会显示在系统相册中
/**
 *  通过 MediaStore 来生成拍照所需要的 ImageUri
 *  根据系统版本决定是否需要申请 WRITE_EXTERNAL_STORAGE 权限
 *  所拍的照片会保存在系统相册里
 */
class MediaStoreCaptureStrategy : CaptureStrategy {
    override fun isEnabled(): Boolean {
        return true
    }
    override fun shouldRequestWriteExternalStoragePermission(context: Context): Boolean {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            return false
        }
        return ActivityCompat.checkSelfPermission(
            context,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
        ) == PackageManager.PERMISSION_DENIED
    }
    override suspend fun createImageUri(context: Context): Uri? {
        return MediaProvider.createImage(context = context, fileName = createImageName())
    }
    override suspend fun loadResources(context: Context, imageUri: Uri): MediaResources? {
        return MediaProvider.loadResources(
            context = context,
            uri = imageUri
        )
    }
    override suspend fun onTakePictureCanceled(context: Context, imageUri: Uri) {
        MediaProvider.deleteImage(context = context, imageUri = imageUri)
    }
}

总结

所以说,除google服务框架了 NothingCaptureStrategy 代表不开启kotlin怎么读拍照功能外,其他两种策略所需要的权限和图片存android是什么手机牌子储的位置都不一样,对用户的感知也不一样

拍照策略 所需权限 配置项 对用户是否可见
NothingCaptureStrategy
FileProviderCaptureStrategy google部需要配置 FileProvider 否,图片存储在应用私有目录内,对用户不可见
MediaStorekotlin为什么流行不起来CaptureStrategy Android 10 之前需要 WRITE_EXTERNAL_giteeSTORgiticomfort是什么轮胎AGE 权限,Android 10 开始不需要权限 是,图片存储在系统相册内,对用户可见

开发者根据自己的实际情况来决定选择哪一种策略:Android

  • kotlin为什么流行不起来果应用本身就需要申请 WRITE_EXTERNAL_STORAGE 权限的话,选 MediaStoreCapturgithub中文官网网页eStrategy,拍照github永久回家地址后的图片保存在系统相册中也比较实例化对象是什么意思符合用户的认知
  • 如果应用本身就不需要申请 WRITE_EXTERNAgoogleplay安卓版下载L_STORAGE 权限的话,选 FileProviderCaptureStrategy,为了相册问题而多申请一个敏感权限得不偿失

拍照权限

Android 系统的 CAMERA 权限用于自定义实现相机功能的业务场景,也即如果使用到了 Camera API 的话,应用就必须声明和申请 Cgoogle浏览器AMERgiteeA 权限

而调起系统相机进行拍照不android平板电脑价格属于自定义实android是什么系统现,因此该操作本身是不要androidstudio安装教程求 CAMERA 权限的,但是googleplay否真的不需要申请权限要根据实际情况而定

Android 系统对于 CAMERA 权限有着比较奇怪的要kotlin语言求:

  • 应用如果没有声明 CAMERA 权github限,此时调起系统相机不需要申请任何权限
  • 应用如果有声明kotlin为什么流行不起来 CAMERA 权限,就必须等到用户同意了 CAMERA 权限后才能调起系统相机,否则将直接抛出 SecurityException

因此,虽然 Matisse 本身是通过调起系统相机来实现拍照的,但如果引用方声明了 CAMERA 权限的话,将连锁导致 Matisse 也必须申请 CAME实例化一个对象可以使用什么命令RA 权限

为了解决这android下载安装个问题,实例化servlet类异常Matisse 通过检查应用的 Manifest 文件中是否包含 CAMERA 权限来决定是否需要进行申请,避免由于意外而奔溃

private fun requestCameraPermissionIfNeed() {
    if (PermissionUtils.containsPermission(
            context = this,
            permission = Manifest.permission.CAMERA
        )
        &&
        !PermissionUtils.checkSelfPermission(
            context = this,
            permission = Manifest.permission.CAMERA
        )
    ) {
        requestCameraPermission.launch(Manifest.permission.CAMERA)
    } else {
        takePicture()
    }
}
internal object PermissionUtils {
    /**
     * 检查是否已授权指定权限
     */
    fun checkSelfPermission(context: Context, permission: String): Boolean {
        return ActivityCompat.checkSelfPermission(
            context,
            permission
        ) == PackageManager.PERMISSION_GRANTED
    }
    /**
     * 检查应用的 Manifest 文件是否声明了指定权限
     */
    fun containsPermission(context: Context, permission: String): Boolean {
        val packageManager: PackageManager = context.packageManager
        try {
            val packageInfo = packageManager.getPackageInfo(
                context.packageName,
                PackageManager.GET_PERMISSIONS
            )
            val permissions = packageInfo.requestedPermissions
            if (!permissions.isNullOrEmpty()) {
                return permissions.contains(permission)
            }
        } catch (e: Throwable) {
            e.printStackTrace()
        }
        return false
    }
}

取消拍照导致的脏数据

在文章开头给出来的知乎github官方 App 示例中可以类的实例化看到,Pictures 目录明kotlin和java区别明显示有三张图片,但点击进去又发现目录是空的。这是由于 MediaStore 中存在Kotlin脏数据导致的

当应用通过 MediaStoreCaptureStrategy 来启动相机时,已经先向 Mekotlin怎么读diaStore 插入一条图片数据了,但如果用户此时又取消了拍照,就会导致 MediaStore 中存在一条脏数据:该数据有 id、uri、path、displayName 等信息,但对应的图片文件实际上并不存在。知乎 App 应该是一开始在归类图片目录的时候没有检查图片是否真的kotlin匿名函数存在,等google浏览器到要加载图片的时候才发现图片不可用

虽然 MediaStoreCapt实例化servlet类异常ureStrategy 会主动删除自己生成的脏数据,但我们没法确保其它应用就不会向 MediaStore 插入脏数据。因此,Matisse 会在遍历查询所有图片的过程中实例化对象是什么意思,同时判断该图片指向的文件是否真的存在,有的话才进行展示github永久回家地址

mediaCursor.use { cursor ->
    while (cursor.moveToNext()) {
        val data = cursor.getString(MediaStore.Images.Media.DATA)
        if (data.isBlank() || !File(data).exists()) {
            continue
        }
        //TODO
    }
}

resolveActivity API 的兼容性

当我们要隐式启动一个 Activity 的时候,为了避免由于目标 Activity 不存在而导致应用崩溃,我们就需要在 startActivity 前先判断该隐式启动是否有接收者,有的话才去调用 stagithub永久回家地址rtActivity

Matisse 在启动系统相机kotlin匿名函数的时候也是如此,会先通过 resolveActivity 方法查询系统中是否有应用可以处理拍照请求,有的话才去启动相机,避免由于设备没有摄像头而导致应用崩溃

private fun takePicture() {
    lifecycleScope.launch {
        val imageUri = captureStrategy.createImageUri(context = this@MatisseActivity)
        tempImageUri = imageUri
        if (imageUri != null) {
            val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
            if (captureIntent.resolveActivity(packageManager) != null) {
                takePictureLauncher.launch(imageUri)
            }
        }
    }
}

resolveActivity 方法在 Android 11 和更高的系统上gitlab也有着一个兼容性问题:软件包可见性过滤

如果应用的目标平台是 Android 11 或更高版本,那么当应用通过 queryIntentAcandroid什么意思tivities()、getPackageInfo()、git命令geGittInstalledApplications() 等方法查询设备上已安装的其它应用相关信息时,系统会默认对返回结果进行过滤。也就是说,通过这些方法查询到的应用信息会少于设备上真实安装的应用数。resolveActivity 方法也受到此影响,经测试,在 Android 11 和 Android 12 的模拟器上,resolveActivity 方法均会返回 null,但在一台 Android 12 的真机上返回值git命令则不为 null,因为不同设备会根据自己的实际情况来决定哪些实现 Android 核心功能的系统服务对所有应用均可见

Magithub中文官网网页tisse 的解决方案是:在 Manifest 文件中通过 queries 主动声明 IMAGE_CAgoogle网站登录入口Pkotlin和javaTURE,从而提高对此 action 的可见性

<queries>
    <intent>
        <action android:name="android.media.action.IMAGE_CAPTURE" />
    </intent>
</queries>

File API 的兼容性

严格来说,File APGitI 的兼容性并不属于 Matisse 遇到的google商店问题,而是外部使用者会遇到的问题

从 Android 10 开始,系统推出了分区存储的特google商店性,限制了应用读写共享文件的方式。当应用开启分区存储特性后,对共享文件的读写需要通过 MediaStore 来实现,而不能使用以前常用的 File API,否则将直接抛出异常:FileNogit教程tFoundException open failed: EAandroid是什么系统CCES (Permission denied)

例如,像 Glide、Coil 等图片框架均支持通过 ByteArray 来加载图片,对于开启了分区存储特性的应用,在github中文官网网页 Android 10 系统之前,以下方式是完全可用的,但在 Android 10 系统上就会直接崩溃

val filePath: String = xxx
imageView.load(File(filePath).readBytes())

而到giti轮胎了 Android 11 后,Google 可能觉得这种限制对于实例化应用来说过于google中国严格,因此又取消了限制,允许应用继续通过 File API 来读写共kotlin和java享文件,系统会自动将 File API 重定向为 MediaStore API =_=

因此,虽然 Matisse 的返回值中包含了图片的绝对路径android/harmonyos path,但如果外部开启了分区存储特性的话,在 Android 10 设备上google商店是不能直接通过 File API 来读写共享文件的,在其它系统版本上则可以继续使用

Github

以上就是 Matisse 的一些实现细节和遇到的系统兼容性问题,更多实现细节请看 Github:Mkotlin为什么流行不起来atisse

Matisse 同时也kotlin为什么流行不起来发布到了 Jitpack,方便开发者直接远程依赖使用:

allprojects {
    repositories {
        maven { url "https://jitpack.io" }
    }
}
dependencies {
    implementation 'com.github.leavesCZY:Matisse:0.0.1'
}