前提条件
安装并配置好Android Studio
build.gradle(:Project)
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.3.1' apply false
id 'com.android.library' version '7.3.1' apply false
id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
}
setting.gradle
pluginManagement {
repositories {
maven { url 'https://maven.aliyun.com/repository/public' }
google()
mavenCentral()
gradlePluginPortal()
maven { url 'https://jitpack.io' }
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
maven { url 'https://maven.aliyun.com/repository/public' }
google()
mavenCentral()
gradlePluginPortal()
maven { url 'https://jitpack.io' }
}
}
rootProject.name = "FeChat"
include ':app'
build.gralde(:app)
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-android'
id 'kotlin-kapt'
}
android {
namespace 'com.example.fechat'
compileSdk 33
defaultConfig {
applicationId "com.example.fechat"
minSdk 26
targetSdk 33
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.cardview:cardview:1.0.0'
implementation("androidx.activity:activity-ktx:1.7.1")
// 沉浸式状态栏 https://github.com/gyf-dev/ImmersionBar
implementation 'com.gyf.immersionbar:immersionbar:3.0.0'
implementation 'com.gyf.immersionbar:immersionbar-components:3.0.0' // fragment快速实现(可选)
implementation 'com.gyf.immersionbar:immersionbar-ktx:3.0.0' // kotlin扩展(可选)
implementation 'com.google.code.gson:gson:2.8.9'
implementation "androidx.room:room-runtime:2.4.2"
implementation "androidx.room:room-ktx:2.4.2"
kapt "androidx.room:room-compiler:2.4.2"
implementation 'org.apache.commons:commons-csv:1.5'
implementation 'com.permissionx.guolindev:permissionx:1.4.0'
implementation 'com.blankj:utilcodex:1.30.0' // 无
implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'com.github.bumptech.glide:compiler:4.12.0'
implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.3'
implementation 'com.github.li-xiaojun:XPopup:latest.release'
}
对Kotlin语言有基本了解
使用 Jetpack activity 协定
为了简化照片选择器的集成,请添加 1.7.0 版或更高版本的 androidx.activity 库。
您可以使用以下 activity 结果协定来启动照片选择器:
如果照片选择器在设备上不可用,该库会自动调用 ACTION_OPEN_DOCUMENT intent 操作。搭载 Android 4.4(API 级别 19)或更高版本的设备支持此 intent。您可以通过调用 isPhotoPickerAvailable() 来验证照片选择器在给定设备上是否可用。
图片选择
照片选择器提供了一个可浏览、可搜索的界面,其中按日期(从最近到最早)顺序向用户呈现其媒体库中的文件。您可以指定用户只能看到照片或只能看到视频,并且默认情况下,允许的媒体选择量上限设置为 1
单图选择器
ActivityResultContracts.PickVisualMedia()
多图选择器
ActivityResultContracts.PickMultipleVisualMedia("自定义选择上限")
平台会限制您可以让用户在照片选择器中选择的文件数量上限。如需访问此限制,请调用 getPickImagesMaxLimit()。 在不支持照片选择器的设备上,系统会忽略此上限。
适用的设备
照片选择器适用于符合以下条件的设备:
- 搭载 Android 11(API 级别 30)或更高版本
- 通过 Google 系统更新接收对模块化系统组件的更改
搭载 Android 4.4(API 级别 19)到 Android 10(API 级别 29)的旧款设备,以及搭载 Android 11 或 12 且支持 Google Play 服务的 Android Go 设备,都可以安装向后移植的照片选择器版本。如需通过 Google Play 服务自动安装向后移植的照片选择器模块,请将以下条目添加到应用清单文件的 <application>
标记中:
<!-- Trigger Google Play services to install the backported photo picker module. -->
<service android:name="com.google.android.gms.metadata.ModuleDependencies"
android:enabled="false"
android:exported="false">
<intent-filter>
<action android:name="com.google.android.gms.metadata.MODULE_DEPENDENCIES" />
</intent-filter>
<meta-data android:name="photopicker_activity:0:required" android:value="" />
</service>
保留媒体文件访问权限
默认情况下,系统会授予应用对媒体文件的访问权限,直到设备重启或应用停止运行。如果您的应用执行长时间运行的工作(例如在后台上传大型文件),您可能需要将此访问权限保留更长时间。为此,请调用 takePersistableUriPermission() 方法:
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, flag)
实现方案
根据官方文档介绍,于是有了以下的初始化方式(此处在context的create生命周期中调用):
private var media: ActivityResultLauncher<PickVisualMediaRequest>? = null
fun initPicker(
activity: AppCompatActivity,
maxFiles: Int = 1,
listener: PickFileResultListener
) {
val contract = if (maxFiles == 1)
ActivityResultContracts.PickVisualMedia()
else
ActivityResultContracts.PickMultipleVisualMedia(
maxFiles
)
media =
activity.registerForActivityResult(contract) { uri ->
if (uri != null) {
val uris = uri.toString().substring(1, uri.toString().length - 1).split(",")
listener.onPick(uris)
Log.e("PhotoPicker", "Selected URI: $uris")
} else {
Log.e("PhotoPicker", "No media selected")
}
}
}
选择回调监听器
interface PickFileResultListener {
fun onPick(uris: List<String>)
}
调用如下
fun pickMedia(fileType: String) {
media?.launch(
PickVisualMediaRequest(
ActivityResultContracts.PickVisualMedia.SingleMimeType(
fileType
)
)
)
}
其中选择类型定义为:
const val PICK_IMAGE_VIDEO = "*/*"
const val PICK_IMAGE = "image/*"
const val PICK_VIDEO = "video/*"
const val PICK_GIF = "image/gif"
转化路径
这样得到的Uri如果想要使用,需要先转化为绝对路径
fun Uri2Path(context: Context, uri: Uri?): String? {
if (uri == null) {
return null
}
if (ContentResolver.SCHEME_FILE == uri.scheme) {
return uri.path
} else if (ContentResolver.SCHEME_CONTENT == uri.scheme) {
val authority: String = uri.authority!!
if (authority.startsWith("com.android.externalstorage")) {
return "${Environment.getExternalStorageDirectory()}/${
uri.path?.split(":")?.get(1)
}"
} else {
try {
var idStr = ""
if (authority == "media") {
idStr = uri.toString().substring(uri.toString().lastIndexOf('/') + 1)
} else if (authority.startsWith("com.android.providers")) {
idStr = DocumentsContract.getDocumentId(uri).split(":".toRegex())
.dropLastWhile { it.isEmpty() }
.toTypedArray()[1]
}
val contentResolver = context.contentResolver
val cursor: Cursor? = contentResolver.query(
MediaStore.Files.getContentUri("external"),
arrayOf(MediaStore.Files.FileColumns.DATA),
"_id=?",
arrayOf(idStr),
null
)
if (cursor != null) {
cursor.moveToFirst()
val idx: Int = cursor.getColumnIndex(MediaStore.Files.FileColumns.DATA)
val path = cursor.getString(idx)
cursor.close()
return path
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
return null
}
大图预览
预览框架采用XPopup
需要添加依赖
implementation 'com.github.li-xiaojun:XPopup:latest.release'
private fun showImageView(imageView: ImageView, currentPosition: Int) {
XPopup.Builder(context)
.isDestroyOnDismiss(true)
.asImageViewer(
imageView, currentPosition, uris,
{ popupView, position ->
/*Toast.makeText(
context, "切换到第" + position + "个图片", Toast.LENGTH_SHORT
).show()*/
},
SmartGlideImageLoader(R.mipmap.ic_launcher)
)
.show()
}