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!结束,写的如果有什么不对的地方希望大家能提出来啊!






























