sq_Hayden's blog

Picasso+OkHttp动态加载网络图片

2018/06/14 Share

分页刷新的网络图片资源加载

  • 引言:在我们的日常开发中,网络图片资源的加载十分常见。优美的图片结合我们强大的功能,才能在性能和感官上带给用户双重的美好体验。然而,若是将资源图片全部下载到本地,又会使我们的apk十分庞大,令人望而却步。那么今天,就一起来通过两个优秀的开源库,实现一下网络图片的加载吧。

1.开源库介绍

1.Picasso介绍

  Picasso是由大名鼎鼎的Square公司开发的一款优秀的开源库,主要用于加载网络图片,是一款使用简单,功能强大的图片加载库。
  Picasso有什么优势呢?主要有以下的几点:
  [1] 提供传入url获取网络图片的方法,使用便捷。
  [2] 采用LRU算法来完成图片加载的二级缓存,从而不必每次都从网络获取。
  [3] 线程与图片的自动回收销毁。
  [4] 自动对网络高清图进行压缩及像素的改变,从而适配对应的机型。

2.Picasso的使用

  (1)添加包依赖

1
2
//图片加载依赖
compile 'com.squareup.picasso:picasso:2.5.2'

  (2)使用

1
2
3
4
5
6
7
8
/**
* 参数说明
* 1.with(Context context):这个不用多说,全局上下文对象,一般都会用到。
* 2.load(String url):此方法用来传入我们已知的url地址,从而获取图片。
* 3.placeholder(int resId):此方法让我们在网络图片未返回时,显示一张本地默认图片。
* 4.into(ImageView target):用来将网络获取的图片填充到指定的ImagerView控件中。
*/
Picasso.with(mContext).load(mResultList.get(position).getUrl()).placeholder(R.mipmap.avatar).into(holder.imageView);

  到了这一步,就已经大功告成了!是不是很简单,这就是它独特的魅力。下面我们来看看网络通信服务的开源库OkHttp。

3.OkHttp的介绍

  OkHttp,又是Square公司的得意之作,是一款android中用于网络服务请求的轻量级框架,它替代了google原生的HttpUrlConnection,并支持了很多的功能。有以下的几方面优势:
  [1] 支持HTTP/2 协议,允许连接到同一个主机地址的所有请求共享Socket。
  [2] 路由节点管理,提升访问速度。
  [3] GZip透明压缩减少传输的数据包大小,节省网络流量。
  [4] 默认启用长连接,使用连接池管理,支持Cache(目前仅支持GET请求的缓存)。
  在本次的网络请求中,我们只是利用它最简单的功能,即通过它去服务取到数据,并以流的形式返回给我们去处理。

4.OkHttp的使用

  (1)添加包依赖

1
2
//网络请求
compile 'com.squareup.okhttp3:okhttp:3.4.1'

  (2)使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 该方法用来获取网络返回的Json
*/
public void sendRequestWithOkHttp() {
new Thread(new Runnable() {
@Override
public void run() {
try {
//构建OkhttpClient对象
OkHttpClient okHttpClient = new OkHttpClient();
//这里是我们的请求网址
String send_url = url + page_number;
//创建一个Request对象用来发起http请求()
Request request = new Request.Builder().url(send_url).build();
//数据接收(这里的接收可以看出请求了服务器一次,若要再次请求需要重新构建对象)
Response response = okHttpClient.newCall(request).execute();
String json = response.body().string();
dealWithJson(json);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}

  到了这里,我们的依赖库就介绍完毕了,接下来进入编码阶段。

2.实现网络图片的加载

  首先需要定义出我们的布局,使用RecyclerView在主界面list出获取到的网络图片资源:

<FrameLayout
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/img_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </android.support.v7.widget.RecyclerView>

</FrameLayout>

  接着,定义子布局list_item.xml

<?xml version="1.0" encoding="utf-8"?>
//采用卡片布局实现图片圆角化
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    //设置圆角
    app:cardCornerRadius="2dp"
    //设置Y轴阴影
    app:cardElevation="2dp"
    //padding设置
    app:cardUseCompatPadding="true"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:adjustViewBounds="true"
        />

</android.support.v7.widget.CardView>

  RecyclerView的Adapter:

public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

 private Context mContext;
 private List<Result> mResultList;
 private static final String TAG = "MyAdapter";

 public MyAdapter(Context context, List<Result> list) {
     mContext = context;
     mResultList = list;
 }

 @Override
 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
     View view = LayoutInflater.from(mContext).inflate(R.layout.list_item,parent,false);
     return new MyHolder(view);
 }

 @Override
 public void onBindViewHolder(RecyclerView.ViewHolder holderBack, int position) {
     MyHolder holder = (MyHolder)holderBack;
     //使用Picasso来加载网络图片赋值给Adapter中对应控件
     Picasso.with(mContext).load(mResultList.get(position).getUrl()).placeholder(R.mipmap.avatar).into(holder.imageView);
 }


 @Override
 public int getItemCount() {
     return mResultList.size();
 }

 //定义View
 private class MyHolder extends RecyclerView.ViewHolder{
     ImageView imageView;
     MyHolder(View itemView) {
         super(itemView);
         imageView = itemView.findViewById(R.id.image);
     }
 }

  我们定义好布局以及Adapter,接下来就是我们的逻辑代码实现了。代码如下:

/**
 * 该方法用来获取网络返回的Json
 */
public void sendRequestWithOkHttp(final boolean isLoad) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                OkHttpClient okHttpClient = new OkHttpClient();
                if(!isLoad) {
                    page_number = 1;
                }
                String send_url = url + page_number;
                Request request = new Request.Builder().url(send_url).build();
                Response response = okHttpClient.newCall(request).execute();
                String json = response.body().string();
                //子线程等待1s
                Thread.sleep(1000);
                dealWithJson(json);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
}

/**
 * 处理网络获取的Json
 *
 * @param json 获取的Json
 */
private void dealWithJson(String json) {
    //利用Gson进行解析
    Gson gson = new Gson();
    Data data = gson.fromJson(json, Data.class);
    //拿到网络数据
    returnResults = data.getResults();
    //主线程更新视图
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            //初始化适配器
            if (myAdapter == null) {
                myAdapter = new MyAdapter(MainActivity.this, results);
                //设置适配器
                mRecyclerView.setAdapter(myAdapter);
            }
        }
    });
}

  在这里,我们开启子线程,使用OkHttp去网络上请求并返回了资源Json,然后通过解析拿到对应的图片资源,封装给Adapter用RecyclerView进行显示。看看最终的实现效果:
   

circle

3.添加动态上拉刷新和下拉加载更多的效果

1.下拉加载更多的实现思路

  (1) 定义一个refresh_footer表示加载中的布局,让其当做RecyclerView的最后一项。
  (2) 监听RecyclerView的滑动事件,如果滑动到数据的最后一项则显示定义的View,同时访问网络获取数据。
  (3) 将数据重新封装好,填充Adapter动态更新,实现下拉加载。

2.实现过程:

  首先需要定义一个表示加载中的布局,这里布局如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="40dp">

    <ProgressBar
        style="?android:attr/progressBarStyle"
        android:layout_gravity="center"
        android:layout_width="30dp"
        android:layout_height="30dp"/>

</FrameLayout>

  接着我们需要修改Adapter,用来将我们的布局添加进去,那么第一步就是改变长度:

@Override
public int getItemCount() {
    return mResultList.size()+1;
}

  第二步,改变ViewType的类型,用以将我们定义的布局与子Item布局视图进行区分:

@Override
public int getItemViewType(int position) {
    if (position < mResultList.size()) {
        return super.getItemViewType(position);
    } else {
        return -1;
    }
}

  第三步,创建自定义视图的ViewHolder:

// 定义底部刷新View对应的ViewHolder
private class FooterHolder extends RecyclerView.ViewHolder {
    FooterHolder(View view){
        super(view);
    }
}

  最后一步,实现视图的加载工作,并且只有在不是尾部的时候才去绑定控件:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    // 如果是底部刷新View,则加载底部刷新View布局,并创建底部刷新View对应的ViewHolder
    if (viewType == -1) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.refresh_footer, parent, false);
        return new FooterHolder(view);
    }
    // 如果是其他类型的View,则按照正常流程创建普通的ViewHolder
    else {
        View view = LayoutInflater.from(mContext).inflate(R.layout.list_item,parent,false);
        return new MyHolder(view);
    }


@Override
public void onBindViewHolder(RecyclerView.ViewHolder holderBack, int position) {
    if (!(holderBack instanceof FooterHolder)) {
        MyHolder holder = (MyHolder) holderBack;
        //使用Picasso来加载网络图片赋值给Adapter中对应控件
        Picasso.with(mContext).load(mResultList.get(position).getUrl()).placeholder(R.mipmap.avatar).into(holder.imageView);
    }
} 

  这样,当我们下拉到最后一项时,就实现了刷新View显示在我们list尾部的效果。接下来看主Activity怎么去实现:

//滑动监听
 RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {
     //此变量用于判断我们是否已经加载过一遍了
     private boolean mAlreadyRefreshed = false;

     /*
      * onScrollStateChanged()方法会在每次滑动状态发生改变时调用。
      * 例如,由静止状态变为滑动状态,或者由滑动状态变为静止状态时,onScrollStateChanged()方法都会被调用。
      */
     @Override
     public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
         super.onScrollStateChanged(recyclerView, newState);
         if (newState == RecyclerView.SCROLL_STATE_IDLE) {
             //如果滑动停止且刷新过了,就恢复原态
             if (mAlreadyRefreshed) {
                 mAlreadyRefreshed = false;
             }
         }
     }

     /*
      *  此方法会在RecyclerView滑动时被调用
      *  即使手指离开了屏幕,只要RecyclerView仍然在滑动onScrolled()就会被不断调用。
      */
     @Override
     public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
         super.onScrolled(recyclerView, dx, dy);
         //如果在刷新或者刷新过了,则不再执行刷新
         if (isBottomRefreshing() || mAlreadyRefreshed) {
             return;
         }
         //否则执行刷新
         if (isBottomViewVisible()) {
             //改变页数
             page_number++;
             //执行刷新
             sendRequestWithOkHttp(true);
             mBottomRefreshing = true;
             mAlreadyRefreshed = true;
         }
     }
 };

 //获取当前最后一个可见Item(即我们的底部View)在list中的位置
 private int getLastVisibleItemPosition() {
     RecyclerView.LayoutManager manager = mRecyclerView.getLayoutManager();
     if (manager instanceof LinearLayoutManager) {
         return ((LinearLayoutManager) manager).findLastVisibleItemPosition();
     }
     return -1;
 }

 //判断底部View是否可见
 private boolean isBottomViewVisible() {
     int lastVisibleItem = getLastVisibleItemPosition();
     return lastVisibleItem != -1 && lastVisibleItem == myAdapter.getItemCount() - 1;
 }

 //是否正在刷新
 public boolean isBottomRefreshing() {
     return mBottomRefreshing;
 }

 //执行完刷新应该调用此方法
 public void onBottomRefreshComplete() {
     mBottomRefreshing = false;
 }

  以上就是主要的逻辑,我们来分析一下。
  首先,我们将加载更多的功能放在了onScrolled方法中,表示只要滑动过程中显示出来了我们的加载更多视图,就去请求加载数据。那如何得知此视图已经被加载出来了呢?借用LinearLayoutManager的findLastVisibleItemPosition去获取加载出来的最后一个视图的位置,接着去判断此位置是不是最后一个,是的话就返回true去请求网络加载视图。
  其次,由于这两个监听方法在滑动过程中必然存在着多次执行的情况,我们为了避免资源的消耗,采用两个变量去控制。一是mBottomRefreshing表示此时是否已经在刷新了(初始值为false),是的话就不再去请求刷新了。二是mAlreadyRefreshed表示是否已经刷新过了,
如果刷新过了的话,就不再进行刷新了。
  最后,RecyclerView的数据更新,当我们请求了数据后,让其实时更新UI,代码如下:

//拿到网络数据
returnResults = data.getResults();
//添加到集合中
Boolean add = results.addAll(returnResults);
if (add) {
    //主线程更新视图
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            //初始化适配器
            if (myAdapter == null) {
                myAdapter = new MyAdapter(MainActivity.this, results);
                //设置适配器
                mRecyclerView.setAdapter(myAdapter);
            } else {
                //每次请求都是10张图
                myAdapter.notifyItemRangeInserted(results.size(), 10);
            }
            //重置
            onBottomRefreshComplete();
        }
    });
}

3.上拉刷新的实现

  相对于下拉加载更多来说,上拉刷新就比较简单了,我们直接将RecyclerView包裹在SwipeRefreshLayout控件中,通过监听其刷新事件,进行请求就行。
  布局代码:

<android.support.v4.widget.SwipeRefreshLayout
         android:id="@+id/refresh"
         android:layout_width="match_parent"
         android:layout_height="match_parent">

         <FrameLayout
             android:id="@+id/container"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:background="@android:color/white">

             <android.support.v7.widget.RecyclerView
                 android:id="@+id/img_list"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent">
             </android.support.v7.widget.RecyclerView>

         </FrameLayout>

 </android.support.v4.widget.SwipeRefreshLayout>

  逻辑处理:

refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
        @Override
        public void onRefresh() {
            //清空缓存
            results.clear();
            //请求最新
            sendRequestWithOkHttp(false);
        }
    });

 好了,到了这里,我们已经将本次图片的加载操作完整的进行了一遍,让我们来看看最终的实现效果吧。
  

circle
 

  

CATALOG
  1. 1. 分页刷新的网络图片资源加载
    1. 1.1. 1.开源库介绍
      1. 1.1.1. 1.Picasso介绍
      2. 1.1.2. 2.Picasso的使用
      3. 1.1.3. 3.OkHttp的介绍
      4. 1.1.4. 4.OkHttp的使用
    2. 1.2. 2.实现网络图片的加载
    3. 1.3. 3.添加动态上拉刷新和下拉加载更多的效果
      1. 1.3.1. 1.下拉加载更多的实现思路
      2. 1.3.2. 2.实现过程:
      3. 1.3.3. 3.上拉刷新的实现