三三要成为安卓糕手
一:外部存储的权限管理
在此之前(Android 5.1 及以下),权限仅需在AndroidManifest.xml中静态声明,
API 23(Android 6.0)谷歌正式引入动态权限管理机制的版本
API 28(Android 8.0) 及以下:对存储权限要求宽松,应用可以自由访问外部存储中的文件。
API 29 (Android 10):引入了分区存储机制,应用只能访问自己的存储区域,或者使用 MediaStore API 来访问公共媒体文件。
API 30 (Android 11):进一步加强了对外部存储的控制,引入了 MANAGE_EXTERNAL_STORAGE,但建议开发者使用分区存储和 MediaStore。
API 33 (Android 13):权限管理更加精细化,引入了 READ_MEDIA_IMAGES、READ_MEDIA_VIDEO 和 READ_MEDIA_AUDIO 等特定权限。
二:MediaStore涉及权限声明
1:读写权限
译为:媒体库
之前的学习都是访问手机内部的存储空间,并不需要权限;在高版本安卓当中要使用MediaStore访问媒体文件,这就涉及一些权限的管理
Android33(13)以下的版本需要添加两个权限
//读取外部存储权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
//写入外部存储权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
如果是版本13以上,支持单独精细化的对媒体权限做申请
2:动态权限申请
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE},
100);
在清单Manifest中声明的权限是静态声明,主要告诉App需要哪些权限,但仅是 “告知”,并不能直接让 App 获得对应权限的使用能力;
在6.0后,对于“危险权限”,必须通过代码进行动态申请,也就是调用 ActivityCompat.requestPermissions()
这类方法,让用户在 App 运行时明确授予这些权限。
``
ActivityCompat.requestPermissions()
- AndroidX中的方法,用于兼容较低版本的 Android 系统(如 Android 2.3 - Android 5.1 等),虽然这些设备原本是没有动态权限管理机制的,但是通过这个方法也能模拟实现动态权限申请的逻辑
Activity.requestPermissions()
- 这是
Activity
类本身的方法,从 Android 6.0(API 23)开始引入,专门用于在运行时动态申请危险权限。 - 如果应用只针对 Android 6.0 及以上的设备开发,那么可以直接使用这个方法。
- 如果要兼容更低版本的系统,直接使用它会导致在低版本设备上出现方法找不到等错误 。
- 这是
总结:前者兼容性更高,后者适合高版本
三:公共存储空间的数据写入
方法说明:使用OkHttp下载网络中的图片,保存在公共存储空间下;
代码中OkHttp部分不再进行说明
private void downloadFile() {
String imgPath = "http://titok.fzqq.fun/uploads/20240826/b4ee80207d67072b2227034c496f7fce.jpeg";
OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder()
.url(imgPath)
.build();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(PublicFileActivity.this,"下载失败",Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
ContentValues values = new ContentValues();
//名字
values.put(MediaStore.Images.Media.DISPLAY_NAME,"my_picture.jpeg");
//类型
values.put(MediaStore.Images.Media.MIME_TYPE,"image/jpeg");
//路径
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
if(uri != null){
try(OutputStream outputStream = getContentResolver().openOutputStream(uri);
InputStream inputStream = response.body().byteStream()){
byte[] bytes = new byte[1024];
int readByte;
//响应中读到程序里是用字节读,在写到内存中也是字节
while((readByte = inputStream.read(bytes)) != -1){
outputStream.write(bytes,0,bytes.length);
}
outputStream.flush();
}
}
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(PublicFileActivity.this, "保存成功", Toast.LENGTH_SHORT).show();
}
});
}
});
}
1:公共存储空间
“emulated” 最常见的含义是 “模拟的”
使用模拟机,相机拍下来的图片也是放到了Pictures文件夹下
2:getContentResolver().insert()
插入应用共享的数据(如媒体库、联系人、短信等)数据,插入位置主要是公共存储区域
参数一: Uri url
指向要插入数据的目标地址(内容 URI),明确要操作的数据集合
MediaStore
用于访问媒体数据(图片、音频、视频等)的类,Images.Media.EXTERNAL_CONTENT_URI
明确了查询的是外部存储中的图片集合。示例:
- 向系统图片库插入图片:
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
- 向联系人添加记录:
ContactsContract.Contacts.CONTENT_URI
- 向系统图片库插入图片:
参数二: ContentValues values
- 存储要插入的字段和对应值的键值对集合(类似数据库的
Map
)。 - 需根据目标
ContentProvider
支持的字段设置,例如插入图片时需指定文件名、MIME 类型、路径等。
ContentValues values = new ContentValues();
//名字
values.put(MediaStore.Images.Media.DISPLAY_NAME,"my_picture.jpeg");
//类型
values.put(MediaStore.Images.Media.MIME_TYPE,"image/jpeg");
//路径
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
3:getContentResolver().openOutputStream(uri)
通过内容解析器(ContentResolver)打开一个指向目标 URI 的输出流,主要用于向指定 URI 对应的资源写入数据
四:公共存储空间的数据读取
使用MediaStore读取指定的图片,就读取咱们刚刚保存保存在公共存储区域的图片;
这段代码虽然短,但是非常儿豁,((20250830170544-0amnn9o “后面对Cursor的处理之前用过一次”)),不必多说
private void showPicture() {
/**
* 简记五个参数:
* 查询内容uri,外部存储中图片集合
* 查询图片哪些信息,id,name
* 筛选条件
* 排序方式
*/
Cursor query = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME},
MediaStore.Images.Media.DISPLAY_NAME + "= ?", new String[]{"my_picture.jpeg"},
MediaStore.Images.Media.DATE_TAKEN + " ASC");
if (query != null && query.moveToFirst()){
int idIndex = query.getColumnIndexOrThrow(MediaStore.Images.Media._ID);
long id = query.getLong(idIndex);
Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
imageView.setImageURI(uri);
}
}
1:getContentResolver().query()
通过内容解析器(ContentResolver)向指定的内容 URI 发送查询请求,获取符合条件的数据集合
参数二:projection
new String[]{MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME}
作用:指定要从查询结果中返回的列(字段)。
解释:数组中的每个字符串对应媒体数据库中的一个列名。
MediaStore.Images.Media._ID
是图片在媒体库中的唯一标识 ID;MediaStore.Images.Media.DISPLAY_NAME
是图片的显示名称(文件名)。通过指定这些列,查询结果只会包含这些列的数据,避免获取不必要的信息,提高效率。
获取某一个列名所在的索引,之前学习数据库的时候有学习过类似的东西,脑子里要有那张移动数据库的图
参数三:selection
MediaStore.Images.Media.DISPLAY_NAME + " = ?"
- 作用:指定查询的筛选条件(类似 SQL 中的
WHERE
子句) - 解释:
?
是占位符,防止 SQL 注入等安全问题
参数四:selectionArgs
new String[]{"my_picture.jpeg"}
- 作用:为
selection
中的占位符?
提供具体的值。 - 解释:数组中的值会按顺序替换
selection
中的?
。这里表示要查询显示名称为my_picture.jpeg
的图片。
参数五:sortOrder
null
- 作用:指定查询结果的排序方式(类似 SQL 中的
ORDER BY
子句)。 - 解释:如果为
null
,表示默认的排序方式,按照数据的添加的时间顺序来排列,新的在前旧的在后(对应DESC降序),升序为ASC。
如果要指定根据某一项数据进行排序,比如按名称
- 按图片显示名称升序排列:
"MediaStore.Images.Media.DISPLAY_NAME ASC"
- 按图片显示名称降序排列:
"MediaStore.Images.Media.DISPLAY_NAME DESC"
2:query.getLong()
getLong(int columnIndex)
:Cursor
的方法,根据 “列索引” 从当前行中获取对应列的 Long 类型值(因为 _ID
在媒体库中是长整型,避免 ID 值过大导致溢出)。
idIndex
:第一行获取到的 “图片 ID 列” 的索引。id
:当前图片在媒体库中的唯一 ID(如12345
),这个 ID 是系统分配的,不会重复,是定位单张图片的核心标识。
3:ContentUris.withAppendedId()
ContentUris 类:Android 提供的工具类,用于处理 Content URI(内容 URI),提供了一些静态方法来操作 URI。
withAppendedId () 方法:这是 ContentUris 类的静态方法,给基础 URI 追加一个 ID,生成一个指向具体资源的 URI
- 第一个参数是基础 URI(这里是
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
) - 第二个参数是资源的 ID(这里是
id
变量)
3:效果
一:需求即相关代码
打开相册显示所有的照片;照片 = 相册app本身带有的图片 + 其它app(相机)带有的图片;这里就涉及到需要获取相机的READ_MEDIA_IMAGES权限
public class AlbumActivity extends AppCompatActivity {
private RecyclerView recycleView;
ArrayList<Uri> pictures = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_album);
//如果安卓版本在11以上,对权限的申请更为细分
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_MEDIA_IMAGES},100);
}else{
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE},100);
}
recycleView = findViewById(R.id.recycler_view);
recycleView.setLayoutManager(new GridLayoutManager(this, 3));
Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME},
null, null, null);
if (cursor != null && cursor.moveToFirst()) {
do {
int idIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID);
long id = cursor.getLong(idIndex);
//通过图像的id获取图像的uri
Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
//添加到列表
pictures.add(uri);
} while (cursor.moveToNext());
//把获取到的数据显示到RecyclerView
ImageAdapter adapter = new ImageAdapter(pictures);
recycleView.setAdapter(adapter);
}
}
}
1:细分权限声明
当前使用的模拟机版本是33,在安卓11也就是api30之上,对权限做了更加细分操作,就需要单独对图像的权限进行申请
左图没有权限申请的弹窗直接进入相册界面;右图代码中进行了api的判定并进行了相应权限的声明,获取到了权限,就可以查看相册中所有的照片了
当然,现在的大部分出厂手机,最低的安卓版本都是12了,车载和pos机,可能还是用的8.0,10.0
2:RecyclerView布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView 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/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".file.AlbumActivity">
</androidx.recyclerview.widget.RecyclerView>
3:do while循环读取照片的弊端
这是没有进行分段的处理,容易出现一些问题比如卡顿,这里涉及的数据量不大,所以不做过多的展开
4:固定用法
Build.VERSION.SDK.INT 指的是当前安卓api版本
Build.VERSION.CODES.R指的是api30这个版本
二:适配器
public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> {
private final ArrayList<Uri> mPictures;
public ImageAdapter(ArrayList<Uri> mPictures){
this.mPictures = mPictures;
}
//item加载到parent的视图中,创建这样类型的ViewHolder
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_image, parent, false);
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
//显示图片
Uri uri = mPictures.get(position);
holder.imageView.setImageURI(uri);
}
@Override
public int getItemCount() {
return mPictures == null ? 0 : mPictures.size();
}
class ViewHolder extends RecyclerView.ViewHolder{
private final ImageView imageView;
public ViewHolder(@NonNull View itemView) {
super(itemView);
imageView = itemView.findViewById(R.id.image_view);
}
}
}
1: mPictures
的命名逻辑
变量命名前缀 m
是一种广泛使用的编码规范(源于早期 Android 源码风格),其中 m
代表 “member”(成员变量),用于区分成员变量和局部变量。
m
:表示这是一个类的成员变量Pictures
:描述变量的含义,这里表示它是一个存储图片 Uri 的集合
2:适配器关联的布局
<?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">
<ImageView
android:id="@+id/image_view"
android:layout_width="110dp"
android:layout_height="110dp"
android:scaleType="centerCrop"/>
</LinearLayout>
三:页面跳转中this的使用范围
在这段代码中,this
处于 View.OnClickListener
的匿名内部类里面。此时,this
指代的是 View.OnClickListener
这个匿名内部类的实例,而不是外部的 Activity
实例。