通过调用 registerForActivityResult
-
单选弹框
ActivityResultContracts.PickVisualMedia() -
多选弹框,参数可选张数
ActivityResultContracts.PickMultipleVisualMedia(5) -
拍照前定义文件路径
ActivityResultContracts.TakePicture() -
拍照后定义文件路径
ActivityResultContracts.TakePicturePreview() -
共享查询本地图片视频文件
contentResolver -
定义获取存储路径
FileProvider
代码展示
xml/file_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 允许Photos轻拍后传回调用端之应用内部拍照工具。 -->
<external-path
name="my_images"
path="Android/data/cn.camera.selectpicture/cache/images" />
</paths>
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">
<uses-feature
android:name="android.hardware.camera"
android:required="true" />
<queries>
<package android:name="${applicationId}" />
<intent>
<action android:name="android.media.action.IMAGE_CAPTURE"></action>
</intent>
<intent>
<action android:name="android.media.action.ACTION_VIDEO_CAPTURE"></action>
</intent>
</queries>
<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_AUDIO" />
<uses-permission
android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"
android:minSdkVersion="34" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Selectpicture"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.media.action.IMAGE_CAPTURE" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- <!– 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"-->
<!-- tools:ignore="MissingClass">-->
<!-- <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>-->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
布局文件 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:maxHeight="300dp"
android:overScrollMode="ifContentScrolls"
android:nestedScrollingEnabled="true"
android:scrollbars="vertical"
android:text="Hello World!"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/iv_image"
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_text"
app:round="8dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btSingle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="单选"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btMut"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="多选"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btSingle" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btTakePicture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="拍照"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btMut" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btTakePicturePreview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="拍照预览"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btTakePicture" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btQueryLocalVideo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="查询本地视频文件"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btTakePicturePreview" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btQueryLocalMedia"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="查询本地图片文件"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btQueryLocalVideo" />
</androidx.constraintlayout.widget.ConstraintLayout>
活动页面 MainActivity
package cn.camera.selectpicture
import android.Manifest
import android.content.ContentUris
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.text.method.ScrollingMovementMethod
import android.util.Log
import android.util.Size
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.utils.widget.ImageFilterView
import androidx.core.content.FileProvider
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import com.google.android.material.button.MaterialButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
class MainActivity : AppCompatActivity() {
private val tempData = MutableSharedFlow<File>(replay = 1)
private val liveDataText = MutableLiveData<String>()
private var takePictureType = 1
private var queryType = 1
private val permissions = arrayOf(
Manifest.permission.READ_MEDIA_AUDIO,
Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
)
private val permissionsLow = arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
)
// Registers a photo picker activity launcher in single-select mode.
val pickMedia = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
// Callback is invoked after the user selects a media item or closes the
// photo picker.
if (uri != null) {
Log.d("PhotoPicker", "Selected URI: $uri")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val thumbnail: Bitmap = applicationContext.contentResolver.loadThumbnail(
uri, Size(640, 480), null
)
findViewById<ImageFilterView>(R.id.iv_image).setImageBitmap(thumbnail)
} else {
findViewById<ImageFilterView>(R.id.iv_image).setImageURI(uri)
}
findViewById<TextView>(R.id.tv_text).text = "Selected URI: $uri"
} else {
Log.d("PhotoPicker", "No media selected")
}
}
// Registers a photo picker activity launcher in multi-select mode.
// In this example, the app lets the user select up to 5 media files.
val pickMultipleMedia =
registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia(5)) { uris ->
// Callback is invoked after the user selects media items or closes the
// photo picker.
if (uris.isNotEmpty()) {
Log.d("PhotoPicker", "Number of items selected: ${uris.size}")
findViewById<TextView>(R.id.tv_text).text = "Number of items selected: ${uris.size}"
} else {
Log.d("PhotoPicker", "No media selected")
}
}
val takePicture =
registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
if (success) {
if (tempData.replayCache.isNotEmpty()) {
findViewById<TextView>(R.id.tv_text).text =
"take picture: ${tempData.replayCache[0].absolutePath}"
findViewById<ImageFilterView>(R.id.iv_image).setImageBitmap(
BitmapFactory.decodeFile(
tempData.replayCache[0].absolutePath
)
)
}
Log.d("PhotoPicker", "take picture success")
} else {
Log.d("PhotoPicker", "take picture failed")
}
}
// 注册启动拍照预览的ActivityResultLauncher
val takePicturePreview = registerForActivityResult(
ActivityResultContracts.TakePicturePreview(),
ActivityResultCallback<Bitmap?>() { bitmap ->
bitmap?.let {
if (bitmap != null) {
// 处理得到的Bitmap,比如显示在ImageView上或者上传
findViewById<ImageFilterView>(R.id.iv_image).setImageBitmap(bitmap);
}
}
});
// 注册权限请求的ActivityResultLauncher
val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
// 用户授权了特定权限
dispatchTakePictureIntent() {
if (takePictureType == 1) {
takePicture()
} else if (takePictureType == 2) {
// 启动拍照预览
takePicturePreview.launch(null)
}
}
} else {
// 用户拒绝了权限,可根据需要进行处理,例如提示用户访问设置中的权限页面
}
}
// 注册权限请求的ActivityResultLauncher
val requestMutPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions(),
ActivityResultCallback<Map<String, Boolean>> { map ->
if (map.filter { !it.value }.isEmpty()) {
if (queryType == 1) {
queryImages()
} else {
queryVideosNoPermission()
queryVideos()
}
}
})
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
findViewById<TextView>(R.id.tv_text).setMovementMethod(ScrollingMovementMethod.getInstance());
findViewById<MaterialButton>(R.id.btSingle).setOnClickListener {
selector()
}
findViewById<MaterialButton>(R.id.btMut).setOnClickListener {
mutSelector()
}
findViewById<MaterialButton>(R.id.btTakePicture).setOnClickListener {
takePictureType = 1
requestPermissionLauncher.launch(android.Manifest.permission.CAMERA)
}
findViewById<MaterialButton>(R.id.btTakePicturePreview).setOnClickListener {
takePictureType = 2
requestPermissionLauncher.launch(android.Manifest.permission.CAMERA)
}
findViewById<MaterialButton>(R.id.btQueryLocalMedia).setOnClickListener {
queryType = 1
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestMutPermissionLauncher.launch(permissions)
} else {
requestMutPermissionLauncher.launch(permissionsLow)
}
}
findViewById<MaterialButton>(R.id.btQueryLocalVideo).setOnClickListener {
queryType = 2
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestMutPermissionLauncher.launch(permissions)
} else {
requestMutPermissionLauncher.launch(permissionsLow)
}
}
liveDataText.observe(
this
) {
findViewById<TextView>(R.id.tv_text).append(it)
findViewById<TextView>(R.id.tv_text).append("\n")
}
}
private fun selector() {
// Include only one of the following calls to launch(), depending on the types
// of media that you want to let the user choose from.
// Launch the photo picker and let the user choose images and videos.
pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo))
//// Launch the photo picker and let the user choose only images.
// pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
//
//// Launch the photo picker and let the user choose only videos.
// pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly))
//
//// Launch the photo picker and let the user choose only images/videos of a
//// specific MIME type, such as GIFs.
// val mimeType = "image/gif"
// pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.SingleMimeType(mimeType)))
}
private fun mutSelector() {
// For this example, launch the photo picker and let the user choose images
// and videos. If you want the user to select a specific type of media file,
// use the overloaded versions of launch(), as shown in the section about how
// to select a single media item.
pickMultipleMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo))
}
private fun takePicture() {
try {
val photoFile: File = createFile()
Log.d("PhotoPicker", photoFile.absolutePath)
val uri =
FileProvider.getUriForFile(this, application.packageName + ".provider", photoFile)
takePicture.launch(uri)
tempData.tryEmit(photoFile)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun createFile(): File {
val rootFile: File = File(externalCacheDir, "images")
if (!rootFile.exists()) {
rootFile.mkdir()
}
val photoFile: File = File(rootFile, "${System.currentTimeMillis()}.png")
return photoFile
}
// Container for information about each video.
data class Video(
val uri: Uri,
val name: String,
val duration: Int,
val size: Int
) {
override fun toString(): String {
return "Video(uri=$uri, name='$name', duration=$duration, size=$size)"
}
}
private fun queryVideos() {
runBlocking {
flow<MutableList<Video>> {
// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didn't create.
val videoList = mutableListOf<Video>()
val collection =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Video.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL
)
} else {
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
val projection = arrayOf(
MediaStore.Video.Media._ID,
MediaStore.Video.Media.DISPLAY_NAME,
MediaStore.Video.Media.DURATION,
MediaStore.Video.Media.SIZE
)
// Show only videos that are at least 5 minutes in duration.
val selection = "${MediaStore.Video.Media.DURATION} <= ?"
val selectionArgs = arrayOf(
TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES).toString()
)
// Display videos in alphabetical order based on their display name.
val sortOrder = "${MediaStore.Video.Media.DISPLAY_NAME} ASC"
val query = contentResolver.query(
collection,
projection,
selection,
selectionArgs,
sortOrder
)
query?.use { cursor ->
// Cache column indices.
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
val nameColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
val durationColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)
while (cursor.moveToNext()) {
// Get values of columns for a given video.
val id = cursor.getLong(idColumn)
val name = cursor.getString(nameColumn)
val duration = cursor.getInt(durationColumn)
val size = cursor.getInt(sizeColumn)
val contentUri: Uri = ContentUris.withAppendedId(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
id
)
// Stores column values and the contentUri in a local object
// that represents the media file.
videoList += Video(contentUri, name, duration, size)
}
emit(videoList)
}
}.flowOn(Dispatchers.IO).collectLatest {
for (video in it) {
Log.d("PhotoPicker", video.toString())
liveDataText.postValue(video.toString() + "\n")
}
}
}
}
private fun queryVideosNoPermission() {
lifecycleScope.launch {
findViewById<TextView>(R.id.tv_text).text = null
flow<String> {
val retriever = MediaMetadataRetriever()
val context = applicationContext
val collection =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Video.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL
)
} else {
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
val projection = arrayOf(
MediaStore.Video.Media._ID,
MediaStore.Video.Media.DISPLAY_NAME,
MediaStore.Video.Media.DURATION,
MediaStore.Video.Media.SIZE,
MediaStore.Video.Media.DATA,
)
// Show only videos that are at least 5 minutes in duration.
val selection = "${MediaStore.Video.Media.DURATION} <= ?"
val selectionArgs = arrayOf(
TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES).toString()
)
// Display videos in alphabetical order based on their display name.
val sortOrder = "${MediaStore.Video.Media.DISPLAY_NAME} ASC"
// Find the videos that are stored on a device by querying the video collection.
val query = contentResolver.query(
collection,
projection,
selection,
selectionArgs,
sortOrder
)
query?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val videoUri: Uri = ContentUris.withAppendedId(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
id
)
try {
retriever.setDataSource(context, videoUri)
} catch (e: RuntimeException) {
Log.e("PhotoPicker", "Cannot retrieve video file", e)
}
// Metadata uses a standardized format.
val locationMetadata: String? =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
val locationDate: String? =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE)
val duration: String? =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
val mimeType: String? =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
Log.e(
"PhotoPicker",
"meta file $videoUri ,${locationMetadata},$locationDate,$duration,$mimeType"
)
emit(videoUri.toString())
}
}
}.flowOn(Dispatchers.IO).collectLatest {
liveDataText.postValue(it)
}
}
}
private fun queryImages() {
lifecycleScope.launch {
findViewById<TextView>(R.id.tv_text).text = null
flow<String> {
val collection =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Images.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL
)
} else {
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.MIME_TYPE,
MediaStore.Images.Media.DATE_TAKEN,
MediaStore.Images.Media.SIZE,
MediaStore.Images.Media.DATA,
)
val selection = "${MediaStore.Images.Media.SIZE} >= ?"
val selectionArgs = arrayOf(
(1024.0.times(50.0)).toString()
)
val sortOrder = MediaStore.Images.Media.DATE_TAKEN + " DESC"
// val sortOrder = "${MediaStore.Images.Media.DISPLAY_NAME} ASC"
val query = contentResolver.query(
collection,
projection,
selection,
selectionArgs,
sortOrder
)
query?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val imageUri: Uri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
id
)
val displayName =
cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME))
val data =
cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA))
val dateModified =
cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_TAKEN))
val modifyDate =
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(
Date(
dateModified
)
)
val sizeInBytes =
cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE))
Log.e(
"PhotoPicker",
"meta file $imageUri ,$displayName ,$data ,$modifyDate ,${(sizeInBytes / 1024.0).toInt()}kb"
)
emit(imageUri.toString())
}
}
}.flowOn(Dispatchers.IO).collectLatest {
liveDataText.postValue(it)
}
}
}
private fun dispatchTakePictureIntent(can: () -> Unit) {
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
// 确认该设备具有相机功能
if (takePictureIntent.resolveActivity(packageManager)?.also {
can.invoke()
} == null) {
Log.e(
"PhotoPicker",
"不备具有相机功能"
)
}
}
}
}
效果展示
总结
弹框样式可能跟想要的风格不搭,关于是否需要权限可以实践,还有版本兼容需要自行实践