对于这张来说,是来了解Looper和Handler和HandlerThread
上面的几章中,我们学习的是下载并解析Json数据,接下来的任务就是下载和显示图片
一.配置RecyclerView来显示图片
上面的完成是已经是有了用TextView来显示标题,那么要显示图片就应该在PhotoHolder中去添加ImageView.最后每一个ImageView都显示一张从GalleryItem的mUrl地址下载的图片
(1)首先需要为GalleryItem创建一个名为list_item_gallery.xml的布局文件,该文件包含一个ImageView组件
ImageView由RecyclerView的GridLayoutManager赋值管理,这意味着其宽度会变,而高度固定,为最大化地使用ImageView的空间,应该设置它的scaleType属性值为centerCrop,这个属性的作用是先居中放置图片,然后放大较小的图片,剪切较大的图片
(2)更新PhotoHolder类,用ImageView来替换TextView,让其保存ImageView,同时也来更新bindGalleryItem的方法,用来设置ImageView的Drawable
private class PhotoHolder extends RecyclerView.ViewHolder {
private ImageView mItemIageView;
public PhotoHolder(View itemView){
super(itemView);
mItemImageView = (ImageView)itemView.findViewById(R.id.item_image_view);//上面那个布局里面的ImageView的id
}
public void bindDrawable(Drawable drawable) {
mItemImageView.setImageDrawable(drawable);
}
}
(3)更新PhotoHolder的onCreateViewHolder()方法,实例化list_item_gallery布局,然后将1结果返回给PhotoAdapter的构造器
LayoutInflater inflater = LayoutInflater.from(getActivity());
View view = inflater.inflate(R.layout.list_item_gallery,viewGroup,false);
return new PhotoHolder(view);
(4)现在需要为每一个ImageView设置占位图,等下载成功后再做替换,在onBindViewHolder方法中
Drawable placeholder = getResources().getDrawable(R.drawable.bill_up_close);//我们需要去设置一张默认的图片
photoHolder.bindDrawable(Placeholder);
二.批量下载缩图
我们现在这个应用联网代码的工作方式如下面:
这个应用先执行一个AsyncTask,该AsyncTask会在后台线程上去从网址获取json数据,然后解析json并将解析结果存在GalleryItem数组,最终每一个GalleryItem都将得到一个指向某张图片的Url
而对于下载图片,我们应该选择让它在需要显示图片的时候才会去下载图片,RecyclerView和它的Adapter应该负责实现按需求来下载,adapter触发图片下载就放在onBindViewHolder()方法中实现
(1)对于AsyncTask来说,它是执行后台线程的最简单的方式,但是它不适合那些重复和长时间运行的任务,所以对此我们应该放弃它,自己创建一个专用的后台线程
(2)与主线程进行通信
Android中,线程使用的收件箱叫做消息队列,使用了消息队列的线程叫做消息循环,消息循环会循环检查队列上是不是有新信息。
而消息循环由线程和looper组成,Looper对象管理着线程的消息队列,主线程也是一个消息循环,因此也拥有looper,主线程上的所有工作都由其looper来完成的,looper不断从消息队列中捉取消息然后完成消息指定的任务
(3)接下来我们将创建一个消息循环作为后台线程作为后台线程,准备需要的looper时,我们就会使用名为HandlerThread的类
继承HandlerThread类,
//这边使用了泛型,这是因为这个类的使用者需要使用某些对象来识别每次的下载,并确定该使用已下载图片更新哪一个UI元素。
public class ThumbnailDownLoader<T> extends HandlerThread {
private static final String TAG = "Th..";
private Boolean mHasQuit = false;
public ThumbnaiDownLoader(){
super(TAG);
}
//线程退出通知方法
public boolean quit(){
mHashQuit = true;
return super.quit();
}
//需要有一个T对象(标记具体哪一次下载)和一个String参数(URL下载连接)
public void queueThumbnail(T target,String url) {
Log.i(TAG,"" + url);
}
}
(4)然后我们打开PhotoGalleryFragment,在里面添加一个上面的类的成员变量,然后在onCreate()方法中去创建和启动线程,最后覆盖onDestroy()方法退出线程
类中
//泛型支持各种类型,但是这里PhotoHolder最合适,因为该视图是最终显示图片的地方
private ThumnbailDownLoader<PhotoHolder> mThumbnailDownloader;
onCreate方法中
mThumbnailDownloader = new ThumbnailDownloader<>();
mThumbnailDownloader.start();
mThumbnailDownloader.getLooper();//在start方法后调用,保证线程就绪避免竞争得存在,
public void onDestroy(){
super.onDestroy();
mThumbnailDownloader.quit();//这个很重要,如果不停止那么它就会一直运行下去,成为僵尸
}
(5)让ThumbnailDownloader跑起来,我们在onBindViewHolder方法中去添加下面得代码
mThumbnailDownloader.queueThumbnail(photoHolder,galleryItem.getUrl());
接下来的任务就是:使用传入queueThumbnail()方法的信息创建消息,并放置在ThumbnailDownloader的消息队列中
三.Message与message handler
消息是Message类的一个实例,它有好几个实例变量,其中有三个需要你去定义
1.what:用户定义的一个int型的消息代码,用来描述消息
2.obj:用户指定,随消息发送的对象
3.target:处理消息的Handler
Message的目标(target)是一个Handler类的实例,Handler可以看成message handler的简称,创建Message的时候,它会自动与一个Handle相关联,Message待处理时,Handler对象负责触发消息处理事件了
(1)要处理消息和消息指定的任务,首先需要一个Handler实例,Handler不仅仅是处理Message的目标,也是创建和发布Message的接口
(2)Looper拥有Message对象的收件箱,所以Message必须在Looper上发布和处理,这也就是协同工作,handler总是引着looper
(3)一个Handler仅仅与一个looper相关联,一个Message也仅仅与一个目标Handler相关联,而looper拥有整个Message队列,多个Message可以引用着同一个目标handler,多个Handler也可以与一个looper相关联
四.使用handler
(1)正常来说,不应该手动设置消息的目标Handler,创建信息时,最好调用Handler.obtainMessage()方法,传入其他必要的消息字段后,该方法就会自动设置的目标Handler,这个方法可以从公有的回收池里获取消息,这样可以避免反复创建新的Message对象
(2)一旦取得Message,就可以调用sendToTarget()方法将其发送给它的handler,然后handler就会将这个Message放置在Looper消息队列的尾部
(3)对于这个应用,我们在queueThumbnail方法中获取并发送消息给它的目标,这边消息的what是一个定义为MESSAGE_DOWNLOAD的常量,obj是指photoHolder,用于标记下载
(4)Looper取得消息队列中特定的消息后,会将它发送给消息的目标Handler去处理,消息正常是在目标Handler的handler.handlerMessage()里面实现方法中进行处理的2
下面的代码是要在handleMessage()实现方法中将会使用FlickrFetchr从url下载图片字节数据,然后转发为位图
在ThumbnailDownloader填写下面的代码
private static final int MESSAGE_DOWNLOAD = 0;//标记下载请求消息
private boolean mHasQuit = false;
private Handler mRequestHandler;//用来存储对Handler的引用,负责在后台线程上管理下载请求消息队列,还负责从消息队列中取出和处理下载请求消,在这边这个成员变量是它发送东西出去,然后又由它接收
private ConcurrentMao<T,String> mRequestMap = new ConcurrentHashMap<>();//一种安全的HashMap,这里用一个标记下载请求的T作为key,我们可以存取和请求关联的URI下载链接
(1)在queueThumbnail()方法中添加代码,更新mRequestMap并把下载消息放到后台线程的消息队列中
public void queueThumbnail(T target,String url) {
if(url == null) {
mRequestMap.remove(target);
}else{
mRequestMap.put(target,url);
mRequestHandler.obtainMessage(MESSAGE_DOWNLOAD,target).sendToTarget();
}
}
然后在ThumbnailDownloader中覆盖onLooperPrepared()
protected void onLooperPrepared(){
mRequestHandler = new Handler(){
public void handleMessage(Message msg) {
if(msg.what == MESSAGE_DOWNLOAD) {
T target = (T)msg.obj;
handleRequest(target);//下面要建立的一个方法
}
}
}
}
在上面的代码中,我们初始化mRequestHandler并定义该Handler在得到消息队列中的下载消息后应执行的任务,首先监察消息的类型,再获取obj的值,然后将其传递给handleRequest(target)处理
在这边消息自身不包含URL信息,这边是使用PhotoHolder和url的对应关系更新mRequesMap,随后我们会从mRequestMap中取出图片URL,用来保证总是比配到Photo实例的最新下载URL
//这边是下载的地方
private void handleRequest(final T target) {
try{
final String url = mRequestMap.get(target);
if(url == null){
return;
}
byte[] bit = new FlickrFetchr().getUrlByte(url);
final Bitmap bitmap = BitmapFactory.decodeByteArray(bit,0,bit.length);//把返回的字节组转换为位图
}catch(IOException ioe) {
}
}
在上面的代码中,我们是在onLooperPrepared()方法中实现Handler.handlerMessage(),HandlerThread.onLooperPrepared是在Looper首次检查消息队列之前调用的,所以该方法是实现创建Handler实现的好地方
到了这边,我们就需要将将位图设置给PhotoHolder视图,不过这个是UI的工作,所以我们必须回到主线程上去完成它,上面的作用等于如何把消息放到自己的收件箱子里面
五.ThumbnailDownloader如何使用Handler向主线程发起请求
当前使用ThumbnailDownloader中的mRequestHandler,我们已经可以从主线程安排后台线程,反过来也可以从后台线程使用与主线程关联的Handler安排主线程任务,主线程任务拥有handler和一个looper的消息循环,主线程上创建的Handler会自动与它的Looper相关联,主线程上创建的这个handler也可以传递给另一个线程
已传入的Handler负责处理所有的消息都将在主线程的消息队列处理
所以我们需要在ThumbnailDownloader中去新添加一个mResponseHandler变量,用来存放来自主线程的的Handler,然后用一个能接受Handler的构造方法来替换原来的构造器,并设置变量然后这边也需要新增加一个监听器接口用于响应请求(主线程发起请求,然后结果是下载图片)代码如下
在ThumbnailDownloader中
private Handler mResponseHandler;
private ThumbnailDownloadListener<T> mThumbnailDownloadListener;
punlic interface ThumbnailDownloadListener<T>{
void onThumbnailDownloader(T target,Bitmap thumbnail);
}
public void setThumbnailDownloader(ThumbnailDownloadListener<T> listener){
mThumbnailDownloadListener = listener;
}
public ThumnailDownloader(Handler responseHandler) {
super(handler);
mResponseHandler = responseHandler;
}
此时在图片下载完成,就可以交给UI去显示,定义在ThumbnailDownloadListener新接口的onThumbnailDownloader()方法就会调用
我们会使用这个监听器方法把处理已经下载的图片的任务委给另一个类(这边指PhotonailGalleryFragment),这样ThumbnailDownloader就可以把下载的结果传给其他视图对象
(1)现在修改PhotoGalleryFragment类,将与主线程关联的Handler传递给ThumbnailDownLoader,然后再设置一个ThumnailDownloadListener处理已下载的图片
在所说的这个类的onCreate方法中添加下面的代码
Handler responsehandler = new Handler();
mThumbnailDownLoader = new ThumbnailDownLoader<>(responsehandler);
mThumbnailDownLoader.setThumbnailDownLoadListener{
new ThumbnailDownloader.ThumbnailDownloadListener<PhotoHolder>(){
public void onThumbnailDownloaded(PhotoHolder photoHolder,Bitmap bitmap) {
Drawable drawable = new BitmapDrawable(getResources(),bitmap);
photoHolder.bindDrawable(drawable);
}
}
}
Handler默认与当前线程的Looper相关联,这个Handler是在onCreate()方法中创建的,所以它会与主线程的Looper相关联
六.现在通过responsehandler,ThumbnailDownlLoader就可以使用与主线程Looper绑定的Handler,同时这样就可以通过onThumbnailDownloader实现,使用新下载的bitmap来设置PhotoHolder的Drawable
在ThumbnailDownloader.handlleRequest()方法中添加下面的代码
mResponseHandler.post(new Runnable(){
public void run(){
if(mRequestMap.get(target) !=null || mHasQuit){
return;
}
mRequestMap.remove(target);//最后从requestMap中删除了配对的Photo-URL,然后将位图设置到目标PhotoHolder上面
mThumbnailDownloadListener.onThumbnailDownLodered(target,bitmap);
}
})
post()这个方法就是,当Message设有方法属性后,取出队列的消息是不会发给target Handler的,相反的存在回调方法中的Runnable的run()就会立刻执行
因为mResponseHandler已经与主线相接了,所以此时的代码就会在主线程上去展示UI。
(1)上面的代码,它会再次检查requestMap,这很重要,因为RecyclerView会循环使用其视图,在ThumbnailDownLoader这个类下载完Bitmap之后,RecyclerView可能会循环使用PhotoHolder并请求一个不同的url,该检查看可以确保每一个PhotoHolder都能获取到正确的图片,即使中间发生了其他请求也没事
七.添加一个清理的方法
在运行应用并看图片时,我们得考虑一个危险,如果用户旋转屏幕,因PhotoHolder视图的失效,那么此时ThumbnailDownLoader可能会挂起来,如果此时点击这些ImageView,那么就会发送异常,所以添加下面的方法
public void clearQueue(){
mResquestHandler.removeMessages(MESSAGE_DOWNLOAD);
mResquestHandler.clear();
}
所以当视图销毁后,就应该在PhotoGalleryFragment的onDestroyView()方法中调用上面的方法
public void onDestroyView(){
super.onDestroyView();
mThumbnailDownloader.clearQueue();
}
现在应用就有了下载并显示图片的基本功能
八.对于上面的显示图片的方法都是运用官方的库来编写的,步骤会有一些麻烦,所以如果想要开发图片的展示,那么我们可以使用下面的几个第三方库
1.Picasso库,追求小而美,不能支持动态图片
2.Glide,可以动态的图片