ListView 异步加载并使用LruCache进行缓存

版权声明:本文为博主原创文章,可任意转载,但转载需注明出处。 https://blog.csdn.net/u013370358/article/details/52441485

      这篇文章是根据慕课网上的ListView的视频讲解做的练习:

1.视频链接:http://www.imooc.com/video/7871

2.json数据获取地址:http://www.imooc.com/api/teacher?type=4&num=30

数据格式截图:

                        

                                  


app效果展示:

  


一共有四个类,两个布局文件:

1.activity的布局文件:activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.admin.mylistviewdemo.MainActivity">

    <ListView
        android:id="@+id/lv_main"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</RelativeLayout>

2.具体列表项的布局:item_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:src="@mipmap/ic_launcher" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:orientation="vertical"
        android:paddingLeft="4dp">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:maxLines="1"
            android:text="title"
            android:textSize="18sp" />

        <TextView
            android:id="@+id/tv_content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:maxLines="3"
            android:text="content"
            android:textSize="13sp" />
    </LinearLayout>
</LinearLayout>

3.MainActivity.java文件

package com.example.admin.mylistviewdemo;

import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.ListView;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    public ListView mListView;
    public List<NewsBean> mNewsBeanList;
    public static String JSONPATH = "http://www.imooc.com/api/teacher?type=4&num=30";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mListView = (ListView) findViewById(R.id.lv_main);
        mNewsBeanList = new ArrayList<>();

        new NewsAsyncTask().execute(JSONPATH);
    }

    //用于根据指定的Url异步地获取json数据
    class NewsAsyncTask extends AsyncTask<String, Void, List<NewsBean>> {

        @Override
        protected void onPostExecute(List<NewsBean> newsBeen) {
            super.onPostExecute(newsBeen);

            NewsAdapter mNewsAdapter = new NewsAdapter(MainActivity.this,newsBeen,mListView);
            mListView.setAdapter(mNewsAdapter);

        }

        @Override
        protected List<NewsBean> doInBackground(String... params) {

            //这里传进来的参数就是我们需要解析的json数据的地址,因为就只有一个参数,
            // 因此取参数数组的第一个元素params[0]
            return getJsonData(params[0]);

        }
    }

    //从指定的流中读取json数据,将得到的json数据转化为String类型的字符串
    public String readStream(InputStream is) {

        String result = "", line;
        BufferedReader bf = null;
        try {
            //将输入流读入字符缓冲数组中
            bf = new BufferedReader(new InputStreamReader(is, "utf-8"));
            while ((line = bf.readLine()) != null) {
                result += line;
            }
            return result;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    //通过指定的Url来解析json数据,封装进NewsBean,并且装入mList
    public List<NewsBean> getJsonData(String url) {

        //用于将数据封装到该NewsBean中
        NewsBean mNewsBean;

        try {
            String mJsonString = readStream(new URL(JSONPATH).openStream());

            JSONObject mJSONObject  = new JSONObject(mJsonString);
            JSONArray mJSONArray = mJSONObject.getJSONArray("data");

            for (int i = 0; i < mJSONArray.length(); i++) {
                //获得json数组中的子json对象
                mJSONObject = mJSONArray.getJSONObject(i);

                //将数据封装到该NewsBean中
                mNewsBean = new NewsBean();
                mNewsBean.newsTitle = mJSONObject.getString("name");
                mNewsBean.newsContent = mJSONObject.getString("description");
                mNewsBean.newsIconUrl = mJSONObject.getString("picSmall");

                //添加该NewsBean到List中
                mNewsBeanList.add(mNewsBean);
            }
            return mNewsBeanList;
        } catch (IOException e) {
            Log.d("qc", "网络连接故障");
            System.out.println("网络连接故障");
            e.printStackTrace();
        } catch (JSONException e) {
            Log.d("qc", "json数据异常");
            System.out.println("json数据异常");
            e.printStackTrace();
        }
        return null;
    }
}

在该类中,我们主要通过在AsyncTackzhong 中异步地进行json数据的获取,主要在getJsonData(String url)方法中进行json数据的解析。

4.NewsBean.java为我们定义的用来存放数据的javabean

package com.example.admin.mylistviewdemo;

/**
 * Created by admin on 2016/9/5.
 */
public class NewsBean {
    public String newsIconUrl;
    public String newsTitle;
    public String newsContent;
}

5.我们自定义了一个类:ImageLoader,来进行图片的加载

package com.example.admin.mylistviewdemo;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;
import android.widget.ListView;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;

/**
 * Created by admin on 2016/9/5.
 */
public class ImageLoader {
    //创建cache
    private LruCache<String, Bitmap> mLruCache;
    //将传入的ListView赋给mListView,从而可以直接对列表项进行设置,比如只加载可见的列表项,或者设置bitmap等
    private ListView mListView;
    //创建set,用来存储我们的那些加载图片的线程,便于在需要的时候开启或者关闭相应的后台人任务
    private Set<MyAsyncTask> mTasks;


    //构造方法,传入我们需要加载进图片的listview,并在该构造函数中初始化缓存大小
    //我们通过传如ListView,从而可以在需要的时候对ListView直接进行设置
    public ImageLoader(ListView listView) {
        mListView = listView;
        mTasks = new HashSet<>();

        //获得系统当前最大可用内存,这里取最大缓存的1/4来作为此应用的缓存大小
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int cacheSize = maxMemory / 4;

        mLruCache = new LruCache<String, Bitmap>(cacheSize) {
            //覆写其sizeOf(String key, Bitmap value)方法,
            // 返回值就是我们每次存入数据所需缓存空间的大小
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //return super.sizeOf(key, value);

                //返回值为我们需要存入的bitmap的大小
                return value.getByteCount();
            }
        };
    }

    //增加数据到缓存中
    public void addBitmapToCache(String url, Bitmap bitmap) {
        //如果缓存中已经没有的话,就存入缓存,否则略过
        if (getBitmapFromCache(url) == null) {
            mLruCache.put(url, bitmap);
        }
    }

    //从缓存中读取数据
    public Bitmap getBitmapFromCache(String url) {
        return mLruCache.get(url);
    }

    public void cancelAllTasks() {
        for (MyAsyncTask task : mTasks) {
            //传入false表示只取消那些没有开始执行的线程,如果线程正在执行中则不取消,等待其执行完毕
            task.cancel(false);
        }
    }


    //根据传入的图片的Url去获取对应的图片bitmap
    class MyAsyncTask extends AsyncTask<String, Void, Bitmap> {
        private String mUrl;

        //        public  MyAsyncTask(String url){
//            mUrl = url;
//        }
        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);

            ImageView imageView = (ImageView) mListView.findViewWithTag(mUrl);
            if (imageView != null && bitmap != null) {
                imageView.setImageBitmap(bitmap);
            }
            mTasks.remove(this);
        }

        @Override
        protected Bitmap doInBackground(String... params) {
            mUrl = params[0];

            Bitmap bitmap = getBitmapFromUrl(mUrl);
            if (bitmap != null) {
                addBitmapToCache(mUrl, bitmap);
            }
            return  bitmap;
        }
    }

    public Bitmap getBitmapFromUrl(String url) {

        BufferedInputStream bufferedInputStream = null;
        try {
            HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
            bufferedInputStream = new BufferedInputStream(connection.getInputStream());
            Bitmap bitmap = BitmapFactory.decodeStream(bufferedInputStream);

            //及时关闭数据连接
            connection.disconnect();
            return bitmap;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                bufferedInputStream.close();
            } catch (IOException e) {
                Log.d("qc", "未能正常关闭流");
                e.printStackTrace();
            }
        }
        return null;
    }

    public void loadImages(int start, int end) {

        String url;
        Bitmap bitmap;
        MyAsyncTask task;
        ImageView imageView;
        for (int i = start; i < end; i++) {
            url = NewsAdapter.URLS[i];
            bitmap = getBitmapFromCache(url);
            if (bitmap == null) {
                task = new MyAsyncTask();
                task.execute(url);
                mTasks.add(task);
            } else {
                imageView = (ImageView) mListView.findViewWithTag(url);
                imageView.setImageBitmap(bitmap);
            }
        }
    }

}

在该类中我们使用了缓存机制:LruCache,以下是LruCache的源码

/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.camera.gallery;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

public class LruCache<K, V> {

    private final HashMap<K, V> mLruMap;
    private final HashMap<K, Entry<K, V>> mWeakMap =
            new HashMap<K, Entry<K, V>>();
    private ReferenceQueue<V> mQueue = new ReferenceQueue<V>();

    @SuppressWarnings("serial")
    public LruCache(final int capacity) {
        mLruMap = new LinkedHashMap<K, V>(16, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
                return size() > capacity;
            }
        };
    }

    private static class Entry<K, V> extends WeakReference<V> {
        K mKey;

        public Entry(K key, V value, ReferenceQueue<V> queue) {
            super(value, queue);
            mKey = key;
        }
    }

    @SuppressWarnings("unchecked")
    private void cleanUpWeakMap() {
        Entry<K, V> entry = (Entry<K, V>) mQueue.poll();
        while (entry != null) {
            mWeakMap.remove(entry.mKey);
            entry = (Entry<K, V>) mQueue.poll();
        }
    }

    public synchronized V put(K key, V value) {
        cleanUpWeakMap();
        mLruMap.put(key, value);
        Entry<K, V> entry = mWeakMap.put(
                key, new Entry<K, V>(key, value, mQueue));
        return entry == null ? null : entry.get();
    }

    public synchronized V get(K key) {
        cleanUpWeakMap();
        V value = mLruMap.get(key);
        if (value != null) return value;
        Entry<K, V> entry = mWeakMap.get(key);
        return entry == null ? null : entry.get();
    }

    public synchronized void clear() {
        mLruMap.clear();
        mWeakMap.clear();
        mQueue = new ReferenceQueue<V>();
    }
}

从源码中我们可以清楚的看到,LruCache的本质就是一个hashmap.在这里我们根据指定的tag来设置对应的bitmmap,这里bitmap和其tag(具体为图片的Url)关联了起来。

6.自定义一个adapter:NewsAdapter,来为listview提供数据

package com.example.admin.mylistviewdemo;

import android.content.Context;
import android.graphics.Bitmap;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import java.util.List;

/**
 * Created by admin on 2016/9/5.
 */
public class NewsAdapter extends BaseAdapter implements AbsListView.OnScrollListener {

    private List<NewsBean> mNewsBeanList;
    private Context mContext;
    private ListView mListView;
    private LayoutInflater mInflater;
    private ImageLoader mImageLoader;
    private boolean isJsutEnter;
    private int mStart, mEnd;
    public static String[] URLS;

    /**
     * 传入context对象,从而可以生成LayoutInflater来解析布局文件
     * 根据传进来的mNewsBeanList来为listview提供数据
     * 根据传进来的ListView,可以在需要的时候直接对列表项的数据进行修改,比如设置bitmap等
     */
    public NewsAdapter(Context context, List<NewsBean> mNewsBeanList, ListView listView) {
        mContext = context;
        mInflater = LayoutInflater.from(mContext);
        this.mNewsBeanList = mNewsBeanList;
        mListView = listView;
        mImageLoader = new ImageLoader(listView);
        isJsutEnter = true;

        URLS = new String[mNewsBeanList.size()];
        for (int i = 0;i< URLS.length;i++){
            URLS[i]= mNewsBeanList.get(i).newsIconUrl;
        }
        //为listView添加滚动监听器
        mListView.setOnScrollListener(this);

    }

    //只有在滑动状态发生变化的时候才会回调该onScrollStateChanged方法,比如由滑动状态变为停止状态
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        //当滑动停止的时候就加载可见项,否则不加载,停止所有执行加载任务的线程
        if (scrollState == SCROLL_STATE_IDLE) {
            mImageLoader.loadImages(mStart, mEnd);
        } else{
            mImageLoader.cancelAllTasks();
        }
    }

    //每一次滚动都会回调该onScroll方法
    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {

        mStart = firstVisibleItem;
        mEnd = firstVisibleItem + visibleItemCount;

        //预加载第一页,进行visibleItemCount > 0判断是因为当列表项还没有加载出来的时候,
        // 也就是当visibleItemCount为0的时候,onScroll方法就已经被回调了
        if (isJsutEnter && visibleItemCount > 0) {
            mImageLoader.loadImages(mStart, mEnd);
            isJsutEnter = false;
        }
    }

    @Override
    public int getCount() {
        return mNewsBeanList.size();
    }

    @Override
    public Object getItem(int position) {
        return mNewsBeanList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        ViewHolder viewHolder ;

        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.item_layout, null);
            viewHolder = new ViewHolder();

            //从布局文件中取出对应控件的引用,并赋值给ViewHolder中的相应的控件
            viewHolder.tvTitle = (TextView) convertView.findViewById(R.id.tv_title);
            viewHolder.tvContent = (TextView) convertView.findViewById(R.id.tv_content);
            viewHolder.ivIcon = (ImageView) convertView.findViewById(R.id.iv_icon);

            //为convertView设置viewHolder标签
            convertView.setTag(viewHolder);
        } else {
            //通过getTag()取出与convertView关联的viewHolder
            viewHolder = (ViewHolder) convertView.getTag();
        }

        //设置viewHolder的tvTitle和tvContent,但是不能直接设置ivIcon,
        // 因为在我们需要开启线程去获取图片,这样在滑动的时候会出现图片错位的问题
        //因此我们需要为ivIcon设置标签(这里我们将bitmap的url设置为它的标签,从而将ivIcon与其对应的图片地址关联起来)
        viewHolder.tvTitle.setText(mNewsBeanList.get(position).newsTitle);
        viewHolder.tvContent.setText(mNewsBeanList.get(position).newsContent);

        //为viewHolder的ivIcon设置tag
        //该设置实际上是对每个ivIcon设置唯一标签,就是该ivIcon只能显示由newsIconUrl唯一标识的bitmap
        String url = mNewsBeanList.get(position).newsIconUrl;
        viewHolder.ivIcon.setTag(url);

        //如果缓存中没有我们需要的bitmap,就显示默认图片,否则就将该bitmap设置为ivIcon显示的图片
        //之所以可以这样做,是因为我们是从缓存中取bitmap,而不是从网络加载,不会差生错位。
        //当到缓存中没有的时候,我们虽然加载了默认图片,看似不符合要求,
        // 但是我们可以在其他地方改成我们需要的正确的图片就好了(在滑动监听器中获取我们需要的bitmap并修改)
        Bitmap bitmap = mImageLoader.getBitmapFromCache(url);
        if (bitmap == null) {
            viewHolder.ivIcon.setImageResource(R.mipmap.ic_launcher);
        } else {
            viewHolder.ivIcon.setImageBitmap(bitmap);
        }
        return convertView;
    }

    class ViewHolder {
        private ImageView ivIcon;
        private TextView tvTitle;
        private TextView tvContent;
    }
}
这里我们通过自定义一个ViewHolder,并将其设置为convertView的Tag,从而实现重用,并且之后通过为ImageView设置Tag来避免了出现图片的错位,通过实现 AbsListView.OnScrollListener接口,我们在需要覆写的方法中实现了ListView的预加载,并且子滚动过程中不进行图片的获取,只有当滚动停止时才进行可见列表项的设置。


PS:表示CSDN的编辑功能真的太难用了,用一会就什么也不想写了,图片插入大小还要手动设置,不可以直接拉伸。。。还是觉得博客园的好用点





猜你喜欢

转载自blog.csdn.net/u013370358/article/details/52441485