Android图片加载--妹子图客户端

图片加载是几乎每个客户端都要用到的功能,这几天闲来无事,以妹子图客户端为例学习了一下android的图片加载。现在整理一下,一来便于自己理解记忆,二来和同样希望学习这方面知识的同学交流,三来贴出自己的代码希望大家能够指点一二。

好了,废话不多说,进入正题。

数据接口来自干货集中营
网上的图片框架有很多,这里使用的是volley。不熟悉的同学可以看一下郭大神的博客

首先讲一下基本思路。
图片加载最见得粗暴的方式就是直接从网上下载图片并显示。但是每次从网上下载会非常耗时,所以一般都会加入图片的缓存来优化加载速度。缓存的话一般有两种,缓存在内存和缓存在本地磁盘。具体关于缓存的知识同样可以看一下郭大神的博客。所以整个加载图片的思路就是首先发送一次请求获取图片的url,然后加载图片,首先从内存取,内存中没有则从磁盘缓存中加载,还是没有的话则从网上下载并添加到磁盘缓存。

先上效果图
Android图片加载--妹子图客户端_第1张图片
就是这样一个简单的瀑布流效果,下面的按钮我给它添加了一个返回最上面的功能。图片是默认的图片,不影响效果,我也懒得改了。大家将就一下。
好了,下面代码。
布局文件就是一个recyclerview,没什么好讲的,我就不贴了,直接上逻辑控制的代码。

mainactivity:

public class MainActivity extends AppCompatActivity
        implements Callback{

    private static final String TAG = "MainActivity";

    private RecyclerView recyclerView;
    private ImageWallAdapter imageWallAdapter;
    private StaggeredGridLayoutManager layoutManager;
    private List imageDataList;//图片url

    private GankTask gankTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //返回顶部
                layoutManager.smoothScrollToPosition(recyclerView,null,0);
            }
        });
        imageDataList = new ArrayList<>();
//        imageDataList = Arrays.asList(TestImageUrl.imageThumbUrls);
        recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        recyclerView.setHasFixedSize(true);
        layoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
        //上拉加载更多
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
            }

            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if(newState == RecyclerView.SCROLL_STATE_IDLE){
                    int[] posititons = new int[layoutManager.getSpanCount()];
                    layoutManager.findLastVisibleItemPositions(posititons);
                    for(int position : posititons){
                        if(position == imageDataList.size() - 1){
                            Log.d(TAG,"add more");
                            gankTask.nextPage();//获取下一页内容
                        }
                    }
                }
            }
        });
        recyclerView.setLayoutManager(layoutManager);
        imageWallAdapter = new ImageWallAdapter(this, imageDataList);
        recyclerView.setAdapter(imageWallAdapter);

        gankTask = new GankTask(this,this);//每页20张图
        gankTask.getData(1);//获得数据

    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    public void addData(List list) {
        //添加修改的数据
        int pos = imageDataList.size();
//        imageDataList.clear();
        imageDataList.addAll(list);
//        imageWallAdapter.notifyDataSetChanged();
        imageWallAdapter.notifyItemRangeInserted(pos,list.size());
    }
}

MyApplication:这个类用来全局控制RequestQueue

/**
 * Created by Lee on 2015/11/22.
 */
public class MyApplication extends Application {

    private static final String TAG = "MyApplication";

    private RequestQueue requestQueue;
    private static MyApplication applicationInstance;

    @Override
    public void onCreate() {
        super.onCreate();
        applicationInstance = this;
        requestQueue = Volley.newRequestQueue(getApplicationContext());
    }

    public static synchronized MyApplication getInstance(){
        return applicationInstance;
    }

    public RequestQueue getRequestQueue(){
        if (requestQueue == null){
            requestQueue = Volley.newRequestQueue(getApplicationContext());
        }
        return requestQueue;
    }

    public  void addRequest(Request request){
        request.setTag(TAG);
        requestQueue.add(request);
    }

    public void cancleRequest(){
        requestQueue.cancelAll(TAG);
    }
 }

Util:工具类,用来生成图片的key。因为图片url中会有特殊字符,直接用来作为文件名不太合适,所以通过md5加密后用来作为文件名。

/**
 * 工具类
 * Created by Lee on 2015/11/20.
 */
public class Util {

    /**
     * 通过md5 生成图片对应的key
     *
     * @param imagePath 图片路径
     * @return 图片对应的key
     */
    public static String keyForImage(String imagePath) {

        String key = null;
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("md5");
            messageDigest.update(imagePath.getBytes());
            key = byteToHex(messageDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return key;
    }

    /**
     * 将二进制数组转换成十六进制
     *
     * @param digest 二进制数组
     * @return 十六进制字符串
     */
    private static String byteToHex(byte[] digest) {

        StringBuilder builder = new StringBuilder();
        for (byte b : digest) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) {
                builder.append("0");
            }
            builder.append(hex);
        }
        return builder.toString();
    }
}

下面是两个比较重要的类。
GankTask:从干货集中营获取url并通过回调函数添加到数据集中

/**
 * 获取数据 数据来自干货集中营 http://gank.io/
 * Created by Lee on 2015/11/21.
 */
public class GankTask {

    private static final String TAG = "GankTask";
    //数据类型: 福利 | Android | iOS | 休息视频 | 拓展资源 | 前端 | all
    //目前仅支持福利  剩下的日后扩充
    private static final String TYPE_FL = "福利";
    //分类数据: http://gank.avosapps.com/api/data/数据类型/请求个数/第几页
    private static final String DATA_URL = "http://gank.avosapps.com/api/data/%s/%d/%d";
    private static final int DEFAULT_COUNT = 10;//默认每页10个
    private static final int DEFAULT_TIMEOUT = 5000;//默认超时请求

    private List imageUrlList;//用于存放图片url
    private int count = DEFAULT_COUNT;//每页数量
    private static int currentPage = 1;//当前页数
    private String dataType = TYPE_FL;//数据格式

    private Context context;
    private Callback callback;//回调函数

    public GankTask(Context context, Callback callback) {
        this(context, DEFAULT_COUNT, callback);

    }

    public GankTask(Context context, int count, Callback callback) {
        imageUrlList = new ArrayList<>();
        this.count = count;
        this.context = context;
        this.callback = callback;

        try {
            dataType = URLEncoder.encode(dataType, "utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获得第page页图片url
     * @param page 页数
     */
    public void getData(int page) {

        String dataUrl = String.format(DATA_URL, dataType, count, page);
        StringRequest request = new StringRequest(dataUrl, new Response.Listener() {
            @Override
            public void onResponse(String response) {

                try {
                    JSONObject obj = new JSONObject(response);
                    JSONArray array = obj.getJSONArray("results");
                    imageUrlList.clear();
                    for (int i = 0; i < array.length(); i++) {
                        String url = array.getJSONObject(i).getString("url");//获得图片url
                        imageUrlList.add(url);
                    }
                    callback.addData(imageUrlList);

                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                //加载出错
                Log.e(TAG, "error:" + error.getMessage());
                Toast.makeText(context, "加载出错", Toast.LENGTH_SHORT).show();
            }
        });
        request.setRetryPolicy(new DefaultRetryPolicy(DEFAULT_TIMEOUT, 1, 1.0f));//设置请求超时
        MyApplication.getInstance().addRequest(request);//将消息添加到消息队列
    }


    /**
     * 获取下一页内容
     */
    public void nextPage(){
        currentPage += 1;
        getData(currentPage);
    }

    /**
     * 回调函数
     * 向数据集中添加新增的数据
     */
    public interface Callback {
        void addData(List list);
    }
}

GankImageTask:用来加载图片

/**
 * 下载图片
 * Created by Lee on 2015/11/22.
 */
public class GankImageTask {

    private static final String TAG = "GankImageTask";

    private LruCache lruCache;//内存缓存
    private DiskLruCache diskLruCache;//磁盘缓存
    private DiskLruCache.Editor editor;
    private ImageView imageView;
    private Callback callback;

    public GankImageTask(DiskLruCache diskLruCache, ImageView imageView, LruCache lruCache, Callback callback) {
        this.callback = callback;
        this.diskLruCache = diskLruCache;
        this.imageView = imageView;
        this.lruCache = lruCache;
    }

    public void loadImage(String imageUrl) {
        Bitmap bitmap = null;
        final String key = Util.keyForImage(imageUrl);//获得url对应的key
        try {
            //没有缓存图片  下载图片并缓存
            if (diskLruCache.get(key) == null) {
                ImageRequest request = new ImageRequest(imageUrl, new Response.Listener() {
                    @Override
                    public void onResponse(Bitmap response) {
                        try {
                            editor = diskLruCache.edit(key);
                            final OutputStream os = editor.newOutputStream(0);
                            response.compress(Bitmap.CompressFormat.JPEG, 100, os);//保存图片到本地
                            editor.commit();
                            os.flush();
                            os.close();
                            addImageToCache(key, response);//将图片加入缓存
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                        callback.setImage(imageView, response);
                    }
                }, 0, 0, ImageView.ScaleType.CENTER_CROP, Bitmap.Config.RGB_565, new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        Log.e(TAG, "load image fail:" + error.getMessage());
                    }
                });

                MyApplication.getInstance().addRequest(request);//将请求添加到请求队列
            }
            //再查询一次图片是否存在
            if (diskLruCache.get(key) != null) {
                //将图片解析出来
                FileInputStream fis = (FileInputStream) diskLruCache.get(key).getInputStream(0);
                bitmap = BitmapFactory.decodeFileDescriptor(fis.getFD());
                if (bitmap != null)
                    addImageToCache(key, bitmap);//将图片加入缓存
                callback.setImage(imageView,bitmap);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

//    /**
//     * 将记录写入journal文件
//     */
//    void flushCache() {
//        if (diskLruCache != null) {
//            try {
//                diskLruCache.flush();
//            } catch (IOException e) {
//                e.printStackTrace();
//            }
//        }
//    }

    /**
     * 将图片加入缓存
     *
     * @param key
     * @param bitmap
     */
    private void addImageToCache(String key, Bitmap bitmap) {
        if (lruCache.get(key) == null) {
            lruCache.put(key, bitmap);
        }
    }

    public interface Callback {
        void setImage(ImageView imageView, Bitmap bitmap);
    }
}

最后就是适配器
ImageWallAdapter:recyclerview适配器

public class ImageWallAdapter extends RecyclerView.Adapter<MyViewHolder>
        implements GankImageTask.Callback {

    private static final String TAG = "ImageWallAdapter";
    private static final int MAX_SIZE = 10 * 1024 * 1024;

    private Context context;
    private LruCache lruCache;//内存缓存
    private DiskLruCache diskLruCache;//磁盘缓存
    private List dataList;//图片数据

    public ImageWallAdapter(Context context, List dataList) {
        this.context = context;
        this.dataList = dataList;

        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int memorySize = maxMemory / 8;//内存的1/8作为缓存
        lruCache = new LruCache(memorySize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount();
            }
        };

        //图片缓存路径
        File cacheFile = getCacheFile(context, "thumb");
//        Log.d(TAG, "cacheFile:" + cacheFile.getPath());
        if (!cacheFile.exists()) {
            cacheFile.mkdirs();
        }
        //设置磁盘缓存
        try {
            diskLruCache = DiskLruCache.open(cacheFile, getAppVersion(context), 1, MAX_SIZE);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 得到缓存文件夹
     * @param context
     * @param fileName
     * @return
     */
    private File getCacheFile(Context context, String fileName) {
        String filePath;
        if (!Environment.isExternalStorageRemovable()
                || Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {

            filePath = context.getExternalCacheDir().getPath();
        } else {
            filePath = context.getCacheDir().getPath();
        }
        return new File(filePath + File.separator + fileName);
    }

    /**
     * 获得版本
     * @param context
     * @return 当前版本
     */
    private int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            return info.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.image_item, parent, false);
        return new MyViewHolder(view);
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {

        String key = Util.keyForImage(dataList.get(position));
        //缓存中不存在图片  下载图片
        if (lruCache.get(key) == null) {
            new GankImageTask(diskLruCache, holder.imageView, lruCache, this).loadImage(dataList.get(position));
//            new ImageLoadTask(context,holder.imageView,diskLruCache,lruCache,this).execute(dataList.get(position));
        } else {
            holder.imageView.setImageBitmap(lruCache.get(key));
        }

    }

    @Override
    public int getItemCount() {
        return dataList.size();
    }
    @Override
    public void setImage(ImageView imageView, Bitmap bitmap) {
        imageView.setImageBitmap(bitmap);
    }
}
class MyViewHolder extends RecyclerView.ViewHolder {
    ImageView imageView;
    public MyViewHolder(View itemView) {
        super(itemView);
        imageView = (ImageView) itemView.findViewById(R.id.image_item_image);
    }
}

好了,主要代码就是以上这些。

总结

代码写完了,用的主要是volley框架的知识,还有图片缓存的知识。不得不说volley确实是个很好用的框架,之前没有用这个框架之前每次网络请求都要自己写httpurlconnection或者httpclient确实很麻烦,现在用了volley框架之后代码简化了不少,而且volley中自带imageloader,为图片加载也做了不少简化,用的真心方便。
虽然整个客户端还是比较简单的就是,就是一个recyclerview加载网络图片,再加上缓存和上拉加载而更多(下拉刷新这里没加,有空补上)。不过还是有一些问题。

  1. 虽然加上了缓存,但是在快速滑动的时候图片加载还是会卡顿,不够流畅
  2. 瀑布流加载图片的时候有时候会出现只有一列显示图片另外一列不显示的情况
  3. 代码不够优雅还值得修改

代码中关键的部分都有注释,应该基本看得懂,如果不明白的地方欢迎留言。还有代码写的可能比较丑陋,不足之处还希望大家能够指点。

你可能感兴趣的:(android)