Android :使用Loader加载本地图片
基于qiujuer老师的视频,还有自己的一些摸索做出的笔记。下面内容有的是自己理解,如果有问题,希望大家能够指出来。学渣在此拜谢(✿◕‿◕✿)。
@[TOC]
最终效果:
一、如何得到图片地址?
要把一个图片显示在布局中,首先肯定要知道图片在手机中的位置。我们都知道平时保存一些图片,打开相册之后会自动分类:QQ,微信,截屏,微博……这些图片都是在不同的文件夹里面,我们肯定不可能一个应用一个应用的去找。
安卓系统的提供ContentProvider可以实现不同应用程序的数据共享。通过Android Studio的File Explore我们看到这些数据所在位置。在**/data/data/**目录下,我们可以找到com.android.provider.***这些目录,里面存放的就是我们共享的数据们。共享的视频,图片,音乐,录音……这些文件的信息会在com.android.provider.media这个文件夹下面。我们找到里面的数据库,里面有external.db,和internal.db。
具体external和internal的区别,可以查看:Android存储。这里只简单说一下,可以共享的数据存储信息是在external.db里面的。
使用SQLiteSpy打开external.db文件,我们可以看到媒体文件信息,这里我们关注image,可以看到一共有20列,就是说有20列属性,其中有_id, _data, _size……等等。
-
我们要怎么得到这些数据呢?
要访问 ContentProvider 中的这些数据,我们需要借助 ContentResolver 。当然还需要得到相应的访问权限。
-
如何具体到得到每一个数据?
学过数据库的孩纸应该知道可以定义一个游标Cursor,用游标来遍历这些数据。在Android中已经为数据的增删改查做了封装,我们不需要用SQL语句。
1、权限
获得权限——AndroidManifest.xml:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
还需要运行时的权限——Activity中在onCreate中调用:
public static final int PERMISSION_REQUEST_CODE = 1;
private void checkPermission() {
int readExStoragePermissionRest = ActivityCompat.checkSelfPermission(this,Manifest.permission.READ_EXTERNAL_STORAGE);
if(readExStoragePermissionRest!= PackageManager.PERMISSION_GRANTED){
requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},PERMISSION_REQUEST_CODE);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(requestCode == PERMISSION_REQUEST_CODE){
if(grantResults.length ==0 && grantResults[0]==PackageManager.PERMISSION_GRANTED){
//TODO有权限
}else{
//TODO没有权限
}
}
}
2、得到数据
ContentResolver contentResolver = getContentResolver();
Uri imageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Cursor cursor = contentResolver.query(imageUri, null, null, null, null);
String[] columnNames = cursor.getColumnNames();
cursor.moveToFirst();
while (cursor.moveToNext()) {
for (String columnName : columnNames) {
Log.d(TAG, columnName + "==" + cursor.getString(cursor.getColumnIndex(columnName)));
}
}
cursor.close();
上面imgeUri具体的值为:
content://media/external/images/media
,其实我这里有个疑问还没解决,为什么uri地址是这个,加载到的就是那个表格里面的内容呢?这到底是怎么联系起来的呢?
通过以上可以在Logcat中得到所有的信息。也就是那个表格中的所有内容。
现在得到了数据,我们就可以进行处理了。要加载图片是比较费时的操作,尤其是图片比较多的时候,我们不能直接在主线程中加载,这里用Loader来加载。
二、用什么加载到界面?
Loader是一个强大的异步加载器。强大在于它的数据更新回调。
在使用Loader之前先要了解以下概念:
LoaderManager管理器,依附于Activity,Fragment;
Loader 任务执行者,AsyncTaskLoader,CursorLoader;
LoaderCallbacks 加载回调。
1、如何使用Loader呢?
-
首先需要得到显示图片的Activity或Fragment的
LoaderManager
,这个Activity或Fragment是LoaderManager的拥有者。例如:LoaderManager loaderManager = LoaderManager.getInstance(PhotoActivity.this);
-
LoaderManager 初始化一个 Loader,调用initLoader()。如果这个Loader不存在,就会被创建。 initLoader(int,Bundle,LoaderCallbacks)需要三个参数,第一个是id,是这个Loader唯一的标识;第二个是可选的参数,可以直接为空;第三个是LoaderCallbacks,是LoaderManager得到Loader信息状态的一个接口。
-
当Loader发生变化时,会调用上面第三个参数LoaderCallbacks里的一些方法,所以这个LoaderCallbacks需要我们自己去写实现类。这里我要用Cursor来遍历数据,所以传入的< D>为< Cursor>。
- onCreateLoader(),用来创建Loader,只调用一次;在这里面指明Cursor要怎么查询,查询哪些数据。
- onLoadFinished(),当Loader加载完所有的数据的时候回调本方法;Cursor已经找到了那些数据。
- onLoaderReset(),当已存在的Loader被重新创建的时候调用。
private class DataCallback implements LoaderManager.LoaderCallbacks<Cursor>{ @NonNull @Override public Loader<Cursor> onCreateLoader(int id, @Nullable Bundle args) {return null;} @Override public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {} @Override public void onLoaderReset(@NonNull Loader<Cursor> loader) {} }
2、加载到什么界面?
用Loader能够得到数据(图片的一些id,地址,大小等信息),我们只要知道地址就可以加载到界面的控件中,比如【ImageView】,这里使用RecyclerView
的GridLayout
布局。
①我们会将得到的数据封装在一个Bean中,这里写一个具体的类(省略了Getter,Setter方法)
public class PhotoBean {
private int id;
private String path;
private long date;
public PhotoBean() {
}
public PhotoBean(int id, String path, long date) {
this.id = id;
this.path = path;
this.date = date;
}
}
②RecyclerView的适配器:(对应每一项的布局是image_item.xml,里面是一个自己写的自定义控件SquareLayout,继承自FrameLayout,里面有一个ImageView,关于这部分,我放到最后去解释,这里我们先直接使用)
public class WallAdapter extends RecyclerView.Adapter<WallAdapter.Holder> {
private final List<PhotoBean> mData = new ArrayList<>();
private Callback mCallback;
public WallAdapter(Callback mCallback) {
this.mCallback = mCallback;
}
@NonNull
@Override
public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
//不需要在初始化的时候加载到父布局parent中去,而是由适配器和recyclerview进行管理的时候加载
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.image_item,parent,false);
return new Holder(view);
}
@Override
public void onBindViewHolder(@NonNull Holder holder, int position) {
holder.bind(mData.get(position),mCallback.getGlideManager());
}
@Override
public int getItemCount() {
return mData.size();
}
public void setmData(List<PhotoBean> beanList){
mData.clear();
if(beanList!=null&&beanList.size()>0)
mData.addAll(beanList);
notifyDataSetChanged();
}
public interface Callback{
RequestManager getGlideManager();
}
static class Holder extends RecyclerView.ViewHolder{
private ImageView photo;
public Holder(@NonNull View itemView) {
super(itemView);
photo = (ImageView)itemView.findViewById(R.id.img_photo);
}
public void bind(PhotoBean bean,RequestManager manager){
manager.load(bean.getPath())
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.into(photo);
}
}
}
这里使用了Glide来把图片加载到控件ImageView中。Glide.with(),返回的是一个RequestManager,这里我们是将图片都显示在一个Activity中的(下面即将说的PhotoActivity)。所以我们可以在PhotoActivity中新建一个变量来保存这个RequestManager,这样就不用每次都去调用资源获得。
这里我们新建一个接口Callback,通过这个接口来获得Activity中的RequestManager。当然这个接口的实现是在Activity中的。所以在这里我们要获得 Activity中实现的接口对象。这里我们是通过构造函数来传递参数的。
还看到有一个setmData(),这个方法是我们我们重新设置所有的数据的时候使用的。在之后的LoaderManager.LoaderCallbacks<D>
实现类的onLoaderReset()中进行了调用。
3、PhotoActivity.java
我们要把数据加载到PhotoActivity所对应的布局中。
(代码中用到了ButterKnife。下面代码中DataCallback是PhotoActivity的内部类,是 LoaderManager.LoaderCallbacks<D>
的实现类。本身implememts的是刚刚 WallAdapter 中定义的接口,用来给 WallAdapter 提供 RequestManager。)
public class PhotoActivity extends AppCompatActivity implements WallAdapter.Callback {
private final int mId = 100; //是Loader的标识,前面提到过的id。
private WallAdapter mAdapter; //recyclerView的适配器
private RequestManager mGlideManager; //Glide的管理器RequestManager
private DataCallback mDataCallback; //自定义的实现LoaderManager获得Loader信息的接口的实现类
@BindView(R.id.recycler_view)
RecyclerView recyclerView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_photo);
setTitle("Photo");
initRecycler(); //初始化recyclerView的相关内容
initLoader(); //初始化Loader的一些内容
}
@Override
public RequestManager getGlideManager() {
/*Glide框架是使用多线程的,为了防止多线程抢占,我们需要进行同步操作,一定要再次判断*/
if (mGlideManager == null) {
synchronized (this) {
if (mGlideManager == null) {
mGlideManager = Glide.with(this);
}
}
}
return mGlideManager;
}
}
可以看到上面对实现了接口WallAdapter.Callback的方法。上面的代码只有一些最基本的实现,为了容易看,将一些具体方法和内部类分开整理在了下面。
initRecycler()
上面我们提到,在适配器 WallAdapter 中要获得 Activity中的 RequestManager。我们的PhotoActivity 实现了WallAdapter.Callback 接口,这样就可以将本身传入 WallAdapter 的构造函数中,从而得到 RequestManager。
private void initRecycler() {
recyclerView.setLayoutManager(new GridLayoutManager(this, 4));
mAdapter = new WallAdapter(this);
recyclerView.setAdapter(mAdapter);
}
initLoader()
这里的Bundle可以传递为空,这里我们为了进行下面的一些测试,传入了一个key~200。
private void initLoader() {
Bundle bundle = new Bundle();
bundle.putString("Key","200");
mDataCallback = new DataCallback();
LoaderManager loaderManager = LoaderManager.getInstance(PhotoActivity.this);
loaderManager.initLoader(mId, bundle, mDataCallback);
}
class DataCallback
注意一下这里的Log.d(),之后测试Loader加载数据的方法过程的时候会用到!
private class DataCallback implements LoaderManager.LoaderCallbacks<Cursor> {
@NonNull
@Override
public Loader<Cursor> onCreateLoader(int id, @Nullable Bundle args) {
String argStr = args==null?"null":args.toString();
Log.d("PhotoActivity","onCreateLoader id:"+id+" args:"+argStr);
CursorLoader cursorLoader = new CursorLoader(
PhotoActivity.this,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{
MediaStore.Images.Media._ID, //id
MediaStore.Images.Media.DATA, //路径
MediaStore.Images.Media.DATE_MODIFIED //日期
}, null, null,
MediaStore.Images.Media.DATE_MODIFIED + " DESC"
);
return cursorLoader;
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
if (data != null) {
if (data.getCount() > 0) {
List<PhotoBean> beans = new ArrayList<>();
data.moveToFirst();
final int mid = data.getColumnIndexOrThrow(MediaStore.Images.Media._ID);
final int mpath = data.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
final int mdate = data.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_MODIFIED);
do {
int id = data.getInt(mid);
String path = data.getString(mpath);
long date = data.getLong(mdate);
PhotoBean bean = new PhotoBean(id, path, date);
beans.add(bean);
//当data.moveToNext()为true,继续循环
} while (data.moveToNext());
//设置到界面中
mAdapter.setmData(beans);
Log.d("PhotoActivity","onLoadFinished:"+beans.size());
return;
}
}
//下面这两行是在没有数据的时候才执行的,也就是手机中没有任何图片的时候。
Log.d("PhotoActivity","onLoadFinished: data is null !!");
mAdapter.setmData(null);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
Log.d("PhotoActivity","onLoaderReset");
mAdapter.setmData(null);
}
}
三、关于Loader,我还想说
好了,现在图片应该已经顺利加载到界面了吧,接下来我们在界面底部增加三个按钮,分别用来创建Loader,重启Loader,删除Loader,在PhotoActivity中设置三个按钮的点击事件:
@OnClick({R.id.init, R.id.restart, R.id.des})
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.init:
LoaderManager.getInstance(this).initLoader(mId, null, mDataCallback);
break;
case R.id.restart:
Bundle bundle = new Bundle();
bundle.putString("Key","300");
LoaderManager.getInstance(this).restartLoader(mId, null, mDataCallback);
break;
case R.id.des:
LoaderManager.getInstance(this).destroyLoader(mId);
break;
}
}
我做了以下测试:
直接加载
首先,直接加载之后,在Activity的onCreate方法中,我们初始化了一个Loader,并传入了数据Bundle,这时候Activity中还没有Loader。调用LoaderManager的initLoader()方法,对应于调用LoaderManager.LoaderCallbacks<Cursor>
中的onCreateLoader()
方法和onLoadFinished()
方法。
上面也说了 onCreateLoader()
表示创建一个Loader,如果是第一次创建,就会调用onCreateLoader()
方法,在这个方法中,我们开始输出了新建的时候的Bundle中所带的数据。接着就会调用onLoadFinished()
。这个方法中输出了所获得信息的个数。
【init】按钮
连续点击了两次【init】按钮,都是只调用onLoadFinished()
方法。如果Loader已经存在,再次调用initLoader(),就会对应于直接调用onLoadFinished()
方法,而不会再调用onCreateLoader()
。
【restart】按钮
当点击restart的时候,调用LoaderManager的restartLoader()方法,也是对应于调用LoaderManager.LoaderCallbacks<Cursor>
中的onCreateLoader()
方法和onLoadFinished()
方法。但是和上面的有什么区别呢?
我们发现每次都会调用onCreateLoader()
方法和onLoadFinished()
方法!
【reset按钮】
对应于调用LoaderManager.LoaderCallbacks<Cursor>
中的onLoaderReset()
方法。
最后的最后,重新画一张qiujuer老师的图:
四、补充:每个图片的布局
前面说RecyclerView的适配器:(对应每一项的布局是image_item.xml,里面是一个自己写的自定义控件SquareLayout,继承自FrameLayout,里面有一个ImageView)
image_item.xml:
<!--SquareLayout的layout_width="match_parent"-->
<com.yuki.viewpage.recycler.SquareLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp">
<ImageView
android:id="@+id/img_photo"
android:contentDescription="@string/app_name"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.yuki.viewpage.recycler.SquareLayout>
注意:SquareLayout的layout_width="match_parent",这个一定得是match_parent,否则无法显示图片。因为GridLayoutManager是根据手机的宽来划分每一项图片的宽的,并且在使用适配器Adapter加载界面的时候,会为这每一项的布局加一个父布局,所以这个得和这个父布局的宽度一样。
SquareLayout.java
自定义的布局对应于java中:
以下修改了 onMeasure()
方法,将super.onMeasure(widthMeasureSpec, heightMeasureSpec)
;改为了super.onMeasure(widthMeasureSpec, widthMeasureSpec);
public class SquareLayout extends FrameLayout {
public SquareLayout(@NonNull Context context) {
super(context);
}
public SquareLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public SquareLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public SquareLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
}
ok!结束,写的如果有什么不对的地方希望大家能提出来啊!