当前位置:   article > 正文

Android 调用 系统选择器 选择 图片 或 文件(ACTION_PICK、ACTION_GET_CONTENT)

action_get_content

本文链接: https://blog.csdn.net/xietansheng/article/details/115763279

打开系统 APP 的资源选择器选取资源(图片/文件),通常可以使用以下 3 个 Action:

  • Intent.ACTION_PICK
  • Intent.ACTION_GET_CONTENT
  • Intent.ACTION_OPEN_DOCUMENT

一般 Android 系统内置的相关 APP 中均有实现了这 3 个 Action(如: 相册、文件管理),三的均能打开系统 APP 的资源选择器选择资源(图片、视频、文件、通讯录等)并返回,但三者的使用并不完全相同。有些第三方 APP 实现了这 3 个 Action 的,也可以用于选取相应的资源。

一般使用 ACTION_PICK 选择图片,使用 ACTION_GET_CONTENTACTION_OPEN_DOCUMENT 选择文件。

1. 使用 ACTION_PICK 选择图片

从数据中选择一个项目(不支持多选),并返回选择的内容,从返回的 intent.getData() 中获取资源,资源类型为 "content://" 开头的 Uri 资源,可通过 context.getContentResolver() 获取资源的内容和相关信息。

Intent.ACTION_PICK 的值为: "android.intent.action.PICK"

1.1 简单示例

启动的 Intent:

val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
activity.startActivityForResult(intent, REQUEST_CODE_PICK)
// 选择视频: intent.type = "video/*";
// 选择所有类型的资源: intent.type = "*/*"
  • 1
  • 2
  • 3
  • 4
  • 5

Activity.onActivityResult(...) 中接收选取返回的图片资源:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (resultCode != RESULT_OK && requestCode == REQUEST_CODE_PICK) {
        // 获取选取返回的图片资源, 结果为 "content://" 开头的 Uri 格式的资源,
        // Uri 格式参考: content://media/external/images/media/123
        val uri = data?.data ?: return

        // 获取图片的数据, 可以使用 ContentResolver 直接打开输入流
        var imageInputStream = contentResolver.openInputStream(uri)

        // 查询图片的详细信息
        val cursor = contentResolver.query(uri, null, null, null, null)
        ...
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

获取到的 uri 资源一般只能在当前 Activity 实例没有被销毁前被访问,如果当前 Activity 实例已 onDestroy(),访问该 uri 可能会报无权限访问 Uri 资源的错误。

1.2 详细代码示例

(1)先在 AndroidManifest.xml 中添加读取外部存储器的权限:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!--
Android 6.0+ 需要动态获取存储权限。一般系统 APP 的内容提供者可以通过 content.getContentResolver()
不需要权限直接读取选择的 Uri 内容。有些则需要权限才能读取,保险起见,读取内容前建议先申请相关权限。
-->
  • 1
  • 2
  • 3
  • 4
  • 5

(2)布局文件: res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <Button
        android:id="@+id/btn_choose_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Choose Image"/>
    <ImageView
        android:id="@+id/image_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

(3)Activity 代码: MainActivity.kt

package com.xiets.demo

import android.content.Intent
import android.database.Cursor
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.view.View
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import java.io.Closeable
import java.io.InputStream
import java.util.*

class MainActivity : AppCompatActivity() {

    companion object {
        private const val TAG = "MainActivity"
        private const val REQUEST_CODE_PICK = 1000
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        findViewById<View>(R.id.btn_choose_image).setOnClickListener {
            // 点击按钮: 使用 ACTION_PICK 选择图片,启动 Activity Intent
            openSystemImageChooser(REQUEST_CODE_PICK)
        }
    }

    /**
     * 使用 ACTION_PICK 选择图片,启动 Activity Intent
     */
    private fun openSystemImageChooser(requestCode: Int) {
        val intent = Intent(Intent.ACTION_PICK)
        intent.type = "image/*"
        startActivityForResult(intent, requestCode)
        // 选择视频: intent.type = "video/*";
        // 选择所有类型的资源: intent.type = "*/*"
    }

    /**
     * 在返回的 onActivityResult 中接收选取返回的图片资源
     */
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (resultCode != RESULT_OK) {
            Log.d(TAG, "onActivityResult not ok.")
            return
        }

        if (requestCode == REQUEST_CODE_PICK) {
            // 获取选取返回的图片资源, Uri 格式
            val uri = data?.data ?: return

            // URI 格式参考: content://media/external/images/media/123
            Log.d(TAG, "选取的图片: $uri")

            // 如果需要使用图片的数据(如解析为 Bitmap 或 上传至服务端),
            // 可以使用 ContentResolver 直接打开输入流
            var imageInputStream: InputStream? = null
            try {
                // 打开 Uri 的输入流
                imageInputStream = contentResolver.openInputStream(uri)
                // 把输入流解析为 Bitmap
                val bitmap = BitmapFactory.decodeStream(imageInputStream)
                // 显示 Bitmap 到 ImageView
                findViewById<ImageView>(R.id.image_view).setImageBitmap(bitmap)
            } catch (e: Exception) {
                e.printStackTrace()
            } finally {
                closeStream(imageInputStream)
            }

            // 查询图片的详细信息
            queryUriDetail(uri)
        }
    }

    private fun queryUriDetail(uri: Uri) {
        // 如果需要选取的图片的详细信息(图片大小、路径、所在相册名称、修改时间、MIME、宽高、文件名等),
        // 则需要通过 content.getContentResolver().query(uri, ...) 查询(直接查询所有字段)
        val cursor = contentResolver.query(uri, null, null, null, null)

        // 一般查询出来的只有一条记录
        if (cursor?.moveToFirst() == true) {
            // 查看查询结果数据的的所有列, 不同系统版本列名数量和类型可能不相同, 参考:
            // [_id, _data, _size, _display_name, mime_type, title, date_added, date_modified,
            // description, picasa_id, isprivate, latitude, longitude, datetaken, orientation,
            // mini_thumb_magic, bucket_id, bucket_display_name, width, height]
            Log.d(TAG, "columnNames: " + Arrays.toString(cursor.columnNames))

            // 获取图片的 大小、文件名、路径
            // val size = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.ImageColumns.SIZE))
            // val filename = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DISPLAY_NAME))
            // val path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA))

            // 输出所有列对应的值
            for (column in cursor.columnNames) {
                val index = cursor.getColumnIndex(column)
                val valueDesc = when (cursor.getType(index)) {
                    Cursor.FIELD_TYPE_NULL      -> "$column: NULL"
                    Cursor.FIELD_TYPE_INTEGER   -> "$column: " + cursor.getInt(index)
                    Cursor.FIELD_TYPE_FLOAT     -> "$column: " + cursor.getFloat(index)
                    Cursor.FIELD_TYPE_STRING    -> "$column: " + cursor.getString(index)
                    Cursor.FIELD_TYPE_BLOB      -> "$column: BLOB"
                    else                        -> "$column: Unknown"
                }
                Log.d(TAG, valueDesc)
            }
        }
        
        cursor?.close()
    }

    private fun closeStream(c: Closeable?) {
        try {
            c?.close()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128

2. 使用 ACTION_GET_CONTENT 选择文件

ACTION_GET_CONTENT 允许用户选择一种特定类型的数据并返回(支持多选)。这与 ACTION_PICK 不同,因为这里我们只说需要哪种数据,而不是用户可以选择的现有数据的 URI。ACTION_GET_CONTENT 可以允许用户在数据运行时创建数据(例如拍照或录制声音),让他们浏览Web 并下载所需的数据,等等。获取的资源类型为 "content://" 开头的 Uri 资源,可通过 context.getContentResolver() 获取资源的内容和相关信息。

ACTION_GET_CONTENT 不单只支持文件/图片,还支持选择通讯录、录音、音频、视频等内容。

Intent.ACTION_GET_CONTENT 的值为: "android.intent.action.GET_CONTENT"

2.1 简单示例

启动的 Intent:

val intent = Intent(Intent.ACTION_GET_CONTENT)

intent.type = "*/*"
// 只选择图片: intent.type = "image/*"
// 只选择视频: intent.type = "video/*"

// 支持多选(长按多选)
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)

// 用于表示 Intent 仅希望查询能使用 ContentResolver.openFileDescriptor(Uri, String) 打开的 Uri
intent.addCategory(Intent.CATEGORY_OPENABLE)

activity.startActivityForResult(intent, REQUEST_CODE_GET_CONTENT)
// 可以包装 Intent
// activity.startActivityForResult(Intent.createChooser(intent, "选择文件"), REQUEST_CODE_GET_CONTENT)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

Activity.onActivityResult(...) 中接收选取返回的文件资源:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (resultCode != RESULT_OK && requestCode == REQUEST_CODE_GET_CONTENT) {
        // 多选的情况
        val clipData = data?.clipData
        if (clipData != null && clipData.itemCount > 0) {
            for (i in 0 until clipData.itemCount) {
                val item = clipData.getItemAt(i)
                val uri = item.uri ?: continue
                handleSelectedFile(uri)
            }
        }
        // 单选的情况
        val uri = data?.data ?: return
        handleSelectedFile(uri)
    }
}

private fun handleSelectedFile(uri: Uri) {
    // 获取选取返回的文件资源, 结果为 "content://" 开头的 Uri 格式的资源,
    // Uri 格式参考: content://com.android.providers.media.documents/document/document%3A145
    val uri = data?.data ?: return

    // 获取文件的数据, 可以使用 ContentResolver 直接打开输入流
    var fileInputStream = contentResolver.openInputStream(uri)

    // 查询文件的详细信息
    val cursor = contentResolver.query(uri, null, null, null, null)
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

2.2 详细代码示例

(1)先在 AndroidManifest.xml 中添加读取外部存储器的权限:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  • 1

(2)布局文件: res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <Button
        android:id="@+id/btn_get_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Choose Files"/>
</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

(3)Activity 代码: MainActivity.kt

package com.xiets.demo

import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import android.provider.DocumentsContract
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import java.io.Closeable
import java.io.InputStream
import java.util.*

class MainActivity : AppCompatActivity() {

    companion object {
        private const val TAG = "MainActivity"
        private const val REQUEST_CODE_GET_CONTENT = 1001
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        findViewById<View>(R.id.btn_get_content).setOnClickListener {
            // 点击按钮: 使用 ACTION_GET_CONTENT 选择文件,启动 Activity Intent
            openSystemFilesChooser(REQUEST_CODE_GET_CONTENT)
        }
    }

    /**
     * 使用 ACTION_GET_CONTENT 选择文件,启动 Activity Intent
     */
    private fun openSystemFilesChooser(requestCode: Int) {
        val intent = Intent(Intent.ACTION_GET_CONTENT)

        intent.type = "*/*"
        // 只选择图片: intent.type = "image/*"
        // 只选择视频: intent.type = "video/*"

        // 支持多选(长按多选)
        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
        
        // 用于表示 Intent 仅希望查询能使用 ContentResolver.openFileDescriptor(Uri, String) 打开的 Uri
        intent.addCategory(Intent.CATEGORY_OPENABLE)

        startActivityForResult(intent, requestCode)
        // 可以包装 Intent
        // startActivityForResult(Intent.createChooser(intent, "选择文件"), requestCode)
    }

    /**
     * 在返回的 onActivityResult 中接收选取返回的文件资源
     */
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (resultCode != RESULT_OK) {
            Log.d(TAG, "onActivityResult not ok.")
            return
        }

        if (requestCode == REQUEST_CODE_GET_CONTENT) {
            // 多选的情况
            val clipData = data?.clipData
            if (clipData != null && clipData.itemCount > 0) {
                for (i in 0 until clipData.itemCount) {
                    val item = clipData.getItemAt(i)
                    val uri = item.uri ?: continue
                    handleSelectedFile(uri)
                }
            }

            // 单选的情况
            val uri = data?.data ?: return
            handleSelectedFile(uri)
        }
    }

    private fun handleSelectedFile(uri: Uri) {
        // URI 格式参考: content://com.android.providers.media.documents/document/document%3A145
        Log.d(TAG, "选取的文件: $uri")

        // 如果需要使用文件的数据, 可以使用 ContentResolver 直接打开输入流
        var fileInputStream: InputStream? = null
        try {
            // 打开 Uri 的输入流
            fileInputStream = contentResolver.openInputStream(uri)
            // ...
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            closeStream(fileInputStream)
        }

        // 查询文件的详细信息
        queryUriDetail(uri)
    }

    private fun queryUriDetail(uri: Uri) {
        // 如果需要选取的文件的详细信息(MIME、文件名、修改时间、大小等),
        // 则需要通过 content.getContentResolver().query(uri, ...) 查询(直接查询所有字段)
        val cursor = contentResolver.query(uri, null, null, null, null)

        // 一般查询出来的只有一条记录
        if (cursor?.moveToFirst() == true) {
            // 查看查询结果数据的的所有列, 不同系统版本列名数量可能不相同, 参考:
            // [document_id, mime_type, _display_name, last_modified, flags, _size], 这里没有路径字段
            Log.d(TAG, "columnNames: " + Arrays.toString(cursor.columnNames))

            // 获取文件的 大小、文件名, 列名常量值参考: DocumentsContract.Document.COLUMN_XXX
            // val size = cursor.getLong(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE))
            // val filename = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME))

            // 输出所有列对应的值
            for (column in cursor.columnNames) {
                val index = cursor.getColumnIndex(column)
                val valueDesc = when (cursor.getType(index)) {
                    Cursor.FIELD_TYPE_NULL      -> "$column: NULL"
                    Cursor.FIELD_TYPE_INTEGER   -> "$column: " + cursor.getInt(index)
                    Cursor.FIELD_TYPE_FLOAT     -> "$column: " + cursor.getFloat(index)
                    Cursor.FIELD_TYPE_STRING    -> "$column: " + cursor.getString(index)
                    Cursor.FIELD_TYPE_BLOB      -> "$column: BLOB"
                    else                        -> "$column: Unknown"
                }
                Log.d(TAG, valueDesc)
            }
        }

        cursor?.close()
    }

    private fun closeStream(c: Closeable?) {
        try {
            c?.close()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142

3. 使用 ACTION_OPEN_DOCUMENT 选择文件

ACTION_OPEN_DOCUMENT 允许用户选择并返回一个或多个现有文档。调用时,系统显示实现了 DocumentsProvider 实例的文件选取器(APP),让用户以交互方式浏览他们。支持的文档包括本地媒体(例如照片和视频)以及已安装的云存储提供商提供的文档。

选取的每个文档都表示为由 DocumentsProvider 提供的 "content://" 开头的 Uri 资源,可以通过 ContentResolver.openInputStream(Uri) 将该 Uri 作为流打开,也可以通过 ContentResolver.openFileDescriptor(Uri, String) 查询 DocumentsContract.Document 元数据。

ACTION_OPEN_DOCUMENT 的用法与 ACTION_GET_CONTENT 基本相似,参考之。

与 ACTION_GET_CONTENT 不同的是,ACTION_OPEN_DOCUMENT 只支持选择文档(即图片、音视频、文件 等)。

Intent.ACTION_OPEN_DOCUMENT 的值为: "android.intent.action.OPEN_DOCUMENT"

简单实例:

启动的 Intent:

val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)

intent.type = "*/*"
// 只选择图片: intent.type = "image/*"
// 只选择视频: intent.type = "video/*"

// 支持多选(长按多选)
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)

activity.startActivityForResult(intent, REQUEST_CODE_OPEN_DOCUMENT)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Activity.onActivityResult(...) 中接收选取返回的文件资源:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (resultCode != RESULT_OK && requestCode == REQUEST_CODE_OPEN_DOCUMENT) {
        // 多选的情况
        val clipData = data?.clipData
        if (clipData != null && clipData.itemCount > 0) {
            for (i in 0 until clipData.itemCount) {
                val item = clipData.getItemAt(i)
                val uri = item.uri ?: continue
                handleSelectedFile(uri)
            }
        }
        // 单选的情况
        val uri = data?.data ?: return
        handleSelectedFile(uri)
    }
}

private fun handleSelectedFile(uri: Uri) {
    // 获取选取返回的文件资源, 结果为 "content://" 开头的 Uri 格式的资源,
    // Uri 格式参考: content://com.android.providers.media.documents/document/document%3A145
    val uri = data?.data ?: return

    // 获取文件的数据, 可以使用 ContentResolver 直接打开输入流
    var fileInputStream = contentResolver.openInputStream(uri)

    // 查询文件的详细信息
    val cursor = contentResolver.query(uri, null, null, null, null)
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号