Android开发笔记[9]-使用图片选择器
摘要
选择本机的图片并获取图片资源;使用XXPermissions库处理动态权限申请;使用matisse库进行图片选择和拍照.
关键信息
- Android Studio: Electric Eel | 2022.1.1 Patch 2
- jvmTarget = '1.8'
- minSdk 21
- targetSdk 34
- compileSdk 34
- 开发语言:Kotlin,Java
- ndkVersion = '21.1.6352462'
- kotlin版本:1.9.20
- gradle版本:1.8.1
- kotlinCompilerExtensionVersion '1.5.4'
原理简介
Android6.0运行时权限简介
[https://www.jianshu.com/p/6a4dff744031]
Android M 对权限管理系统进行了改版,之前我们的 App 需要权限,只需在 manifest 中申明即可,用户安装后,一切申明的权限都可来去自如的使用。但是 Android M 把权限管理做了加强处理,在 manifest 申明了,在使用到相关功能时,还需重新授权方可使用。当然不是所有权限都需重新授权,所以就把这些需要重新授权方可使用的权限称之为运行时权限。
Android 出于系统稳定性以及用户隐私方面的考虑,将应用程序访问权限限制在各自的沙盒内。程序可以随意访问所在沙盒内部的资源或者信息,访问沙盒外部的则必须明确的申请相关访问权限。应用程序所需要的权限需要在 AndroidManifest.xml 文件中申明。
系统权限根据敏感程度分为普通权限和危险权限两类。两类权限都需要在 AndroidManifest.xml 文件中申明。在 Android 5.1 (API level 22) 及其以下,系统在 App 安装时要求用户授权所有权限,否则 App 不能安装,而在 Android 6.0 及其以上版本上,系统在APP安装时授权所有普通权限,危险权限需要在使用时动态让用户授权。这使得 Android 的权限管理更加灵活,用户可以根据需要在设置应用中对应用的各个危险权限授予不同的权限。
危险权限:涉及日历,摄像头,联系人,位置,话筒,电话,传感器,短信,存储
XXPermission库简介
[https://github.com/getActivity/XXPermissions]
- PermissionX 1.7发布,全面支持Android 13运行时权限
- XXPermission:一句代码搞定权限请求,从未如此简单
Android中的权限申请是通过应用程序与操作系统之间的交互实现的。在Android中,每个应用程序都被赋予了一组预定义的权限,这些权限定义了应用程序可以执行的操作范围,例如访问摄像头、读取联系人、发送短信等。
Material库与Material3库区别
[https://juejin.cn/post/7055303009605484551]
Material库和Material3库主要的区别在于它们的设计理念和使用方式。
Material库,也被称为Material Design,是Google于2014年发布的一种设计语言,旨在为应用程序提供一致的外观和行为。Material Design提供了一种模拟实体世界的方式,让用户能够更好地理解应用程序的工作原理。
相比之下,Material3库,也称为Material You,是Material Design的一个全新版本,于2021年发布。Material3的最大特点是对个性化的注重,如其名称“You”所示。其中一个显著的特点是动态配色 (Dynamic Color),它允许设备根据壁纸颜色动态改变应用程序的主题。此外,Material3还更新了许多Material组件,使其更具表现力。
总的来说,Material3库相较于Material库,提供了更多的个性化选项和更新的组件,使应用程序的视觉效果更加丰富和生动。
Matisse库简介
[https://github.com/leavesCZY/Matisse]
- 注意:Matisse库有Zhihu版本和leavesCZY版本,其中Zhihu版本已经很久没有维护了.
- 一个用 Jetpack Compose 实现的 Android 图片/视频 选择框架,支持打开相机
- 适配到 Android 13
- 按需索取权限,自由声明权限类型
实现
核心代码
- 移植XXPermission动态权限库
- 1.1 配置gradle
settings.gradle
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
rootProject.name = "grape-yolov5-detect-android"
include ':app'
build.gradle
// 权限请求框架:https://github.com/getActivity/XXPermissions
implementation 'com.github.getActivity:XXPermissions:18.6' // 动态权限库
gradle.properties
# 表示将第三方库迁移到 AndroidX
android.enableJetifier = true
如果项目已经适配了 Android 10 分区存储特性,请在 AndroidManifest.xml 中加入
<manifest>
<application>
<!-- 告知 XXPermissions 当前项目已经适配了分区存储特性 -->
<meta-data
android:name="ScopedStorage"
android:value="true" />
</application>
</manifest>
需要注意的是:这个选项是框架用于判断当前项目是否适配了分区存储,需要注意的是,如果你的项目已经适配了分区存储特性,可以使用 READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE 来申请权限,如果你的项目还没有适配分区特性,就算申请了 READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE 权限也会导致无法正常读取外部存储上面的文件,如果你的项目没有适配分区存储,请使用 MANAGE_EXTERNAL_STORAGE 来申请权限,这样才能正常读取外部存储上面的文件。
- 1.2 声明权限
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- start 存储相关权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- end 存储相关权限 -->
<!-- start 多媒体相关权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<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_MEDIA_VISUAL_USER_SELECTED" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/grape_icon_round"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/Theme.Grapeyolov5detectandroid"
tools:targetApi="31">
<activity
android:name=".BootActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Grapeyolov5detectandroid">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity> <!-- 告知 XXPermissions 当前项目已经适配了分区存储特性 -->
<meta-data
android:name="ScopedStorage"
android:value="true" />
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Grapeyolov5detectandroid.NoActionBar" />
<activity
android:name=".ResultActivity"
android:exported="true" />
<receiver
android:name=".ResultActivity$DetectionCompleteReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="cn.qsbye.grape_yolov5_detect_android.DETECTION_COMPLETE" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="cn.qsbye.grape_yolov5_detect_android.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
- 移植Matisse图片选择器库
- 2.1 编辑gradle文件
build.gradle(app)
buildscript {
ext {
compose_ui_version = '1.5.4'
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.4.2' apply false
id 'com.android.library' version '7.4.2' apply false
id 'org.jetbrains.kotlin.android' version '1.9.20' apply false
}
注意引入id 'kotlin-parcelize'
build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-parcelize' // 注意这里
}
buildFeatures {
viewBinding true
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.4'
}
dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.compose.material3:material3:1.1.2'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation 'androidx.activity:activity-compose:1.8.2'
implementation "androidx.compose.ui:ui:$compose_ui_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
/* start 相机相册相关 */
implementation "androidx.recyclerview:recyclerview:1.3.2"
implementation 'com.github.bumptech.glide:glide:4.16.0'
implementation "com.github.chrisbanes:PhotoView:2.3.0"
implementation "io.github.leavesczy:matisse:1.1.4"
def glideComposeVersion = "1.0.0-beta01"
implementation "com.github.bumptech.glide:compose:$glideComposeVersion"
/* end 相机相册相关 */
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
}
- 2.2 手动实现GlideImageEngine
package cn.qsbye.grape_yolov5_detect_android
import android.os.Parcel
import android.os.Parcelable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import com.bumptech.glide.integration.compose.GlideImage
import github.leavesczy.matisse.ImageEngine
import github.leavesczy.matisse.MediaResource
import kotlinx.parcelize.Parcelize
@Parcelize
class GlideImageEngine : ImageEngine {
@OptIn(ExperimentalGlideComposeApi::class)
@Composable
override fun Thumbnail(mediaResource: MediaResource) {
GlideImage(
modifier = Modifier.fillMaxSize(),
model = mediaResource.uri,
contentScale = ContentScale.Crop,
contentDescription = mediaResource.name
)
}
@OptIn(ExperimentalGlideComposeApi::class)
@Composable
override fun Image(mediaResource: MediaResource) {
if (mediaResource.isVideo) {
GlideImage(
modifier = Modifier.fillMaxWidth(),
model = mediaResource.uri,
contentScale = ContentScale.Fit,
contentDescription = mediaResource.name
)
} else {
GlideImage(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(state = rememberScrollState()),
model = mediaResource.uri,
contentScale = ContentScale.Fit,
contentDescription = mediaResource.name
)
}
}
}
- 2.3 新建res/xml/file_paths.xml
file_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="cache" path="/" />
<!-- 拍照保存目录 -->
<external-files-path
name="Capture"
path="Pictures" />
</paths>
- 编辑kotlin代码
MainActivity.kt
package cn.qsbye.grape_yolov5_detect_android
import android.content.Context
import android.content.Intent
import android.content.res.AssetManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Button
import android.widget.ProgressBar
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import cn.qsbye.grape_yolov5_detect_android.databinding.ActivityMainBinding
import com.hjq.permissions.OnPermissionCallback
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import github.leavesczy.matisse.*
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import kotlin.random.Random
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding
private lateinit var imagePickerLauncher: ActivityResultLauncher<Intent>
/* start 图片选择器相关 */
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
// 启动ResultActivity
val imageUri = uri
val intent = Intent(this@MainActivity, ResultActivity::class.java)
intent.putExtra("original_grape_bitmap_uri", imageUri)
startActivity(intent)
}
}
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
// 启动ResultActivity
val imageUri = uri
val intent = Intent(this@MainActivity, ResultActivity::class.java)
intent.putExtra("original_grape_bitmap_uri", imageUri)
startActivity(intent)
}
}
val matisse = Matisse(
maxSelectable = 1,
mediaFilter = DefaultMediaFilter(supportedMimeTypes = MimeType.ofImage(hasGif = false)),
imageEngine = GlideImageEngine(),
singleMediaType = true,
captureStrategy = null
)
/* end 图片选择器相关 */
// toast函数
fun Context.toast(message: CharSequence) =
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
companion object {
@JvmStatic
external fun initAssetManager(assetManager: AssetManager)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 初始化assets文件夹实例
val assetManager: AssetManager = assets
initAssetManager(assetManager)
/* start 监听幸运按钮 */
val luckyButton = findViewById<Button>(R.id.lucky_btn)
luckyButton.setOnClickListener {
val s_grape_file_str = String.format("grape_img/G%04d.jpg", Random.nextInt(1, 10))
try {
// 从assets目录中读取位图数据
val inputStream = assets.open(s_grape_file_str)
val bitmap = BitmapFactory.decodeStream(inputStream)
Log.e("检测", "当前识别的图片为:$s_grape_file_str")
// 将位图保存到应用的缓存目录中
val file = File(cacheDir, "grape_image.jpg")
val outputStream = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
outputStream.flush()
outputStream.close()
// 创建一个 Intent 对象,指定当前活动(this)和目标活动(ResultActivity::class.java)
val intent = Intent(this@MainActivity, ResultActivity::class.java)
// 将位图的 URI 作为附加参数传递给 ResultActivity
val imageUri = FileProvider.getUriForFile(this, "cn.qsbye.grape_yolov5_detect_android.fileprovider", file)
intent.putExtra("original_grape_bitmap_uri", imageUri)
// 启动 ResultActivity
startActivity(intent)
// 不会执行:在解析位图完成后发送广播通知 ResultActivity
val broadcastIntent = Intent("cn.qsbye.grape_yolov5_detect_android.DETECTION_COMPLETE")
sendBroadcast(broadcastIntent)
} catch (e: IOException) {
e.printStackTrace()
}
} // end setOnClickListener
/* end 监听幸运按钮 */
/* start 监听拍照按钮 */
val cameraButton = findViewById<Button>(R.id.camera_btn)
cameraButton.setOnClickListener {
/* start 检查相机权限 */
XXPermissions.with(this)
// 申请单个权限
.permission(Permission.CAMERA)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) {
if (!allGranted) {
// toast("相机权限正常")
return
}
toast("获取相机权限成功")
}
override fun onDenied(permissions: MutableList<String>, doNotAskAgain: Boolean) {
if (doNotAskAgain) {
toast("被永久拒绝授权,请手动授予相机权限")
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(this@MainActivity, permissions)
} else {
toast("获取相机权限失败")
}
}
})
/* end 检查相机权限 */
// 使用Matisse库
runOnUiThread{
takePictureLauncher.launch(MatisseCapture(captureStrategy = MediaStoreCaptureStrategy()))
}
} // end setOnClickListener
/* end 监听拍照按钮 */
/* start 监听相册按钮 */
val selectFromAlbumButton = findViewById<Button>(R.id.select_from_album_btn)
selectFromAlbumButton.setOnClickListener{
/* start 检查相册(存储)权限 */
XXPermissions.with(this)
// 申请多个权限
.permission(Permission.CAMERA)
.permission(Permission.READ_MEDIA_IMAGES)
.permission(Permission.READ_MEDIA_VIDEO)
.permission(Permission.READ_MEDIA_VISUAL_USER_SELECTED)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) {
if (!allGranted) {
toast("有权限没有获取成功")
return
}
toast("获取所需权限成功")
}
override fun onDenied(permissions: MutableList<String>, doNotAskAgain: Boolean) {
if (doNotAskAgain) {
toast("被永久拒绝授权,请手动授予相机、相册权限")
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(this@MainActivity, permissions)
} else {
toast("获取相机、相册权限失败")
}
}
})
/* end 检查相册(存储)权限 */
Log.e("图片", "进入图片选择器!")
// 使用Matisse库
mediaPickerLauncher.launch(matisse)
}// end setOnClickListener
/* end 监听相册按钮 */
} // end onCreate
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_content_main)
return navController.navigateUp(appBarConfiguration)
|| super.onSupportNavigateUp()
}
}
效果
图片选择器 |
---|
![]() |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
2023-01-31 esp32笔记[3]-mpu6050
2023-01-31 esp32笔记[2]-串口打印