ListView异步加载图片方法和滚动优化

基本流程:
1.异步任务从指定的网页中获取JSON信息,解析JSON数据,自定义JAVA BEAN对象封装所需要的数据项(标题、摘要、图片URL地址等信息),并将BEAN对象组织成变长数组ArrayList。

2.自定义BaseAdapter,通过内部类ViewHolder提高ListView 的Item复用效率。

注意:
在getView方法中, 对于inflate方法的第三个参数attachToRoot应该使用false,否则将会导致UnsupportedOperationException

convertView convertView = inflater.inflate(R.layout.item_layout,parent,false);

注意,不要写成

convertView convertView = inflater.inflate(R.layout.item_layout,parent);

因为这个函数,默认第三个参数为true,也将导致发生错误。

3.BaseAdapter中为ImageView 设置图片及其优化。
因为我们解析得到的数据只包括图片的URL地址,并没有得到实际的图片,所以需要从网络上获取实际的图片,得到对应的bitmap。

图片的获取有两个思路,一个是通过多线程的方法,另一个是通过异步任务AsyncTask的方法。

优化点:
1.防止图片的错乱
这个现象主要是因为ListView的重用机制,当一个可见的item划出屏幕外时,将会放入一个回收池,新进入屏幕中的item将从回收池中取出一个item复用,而不再是重新生成一个item。这样无论一个ListView中有多少个item,在显示的过程中最多只需要生成n个item(n为一屏中可以同时显示的item的数目)。

现象分析:
当我们滚动屏幕时,假设item n新进入屏幕内,复用了item m,此时item n所要显示的图片开始下载,如果正好item m的图片下载完毕,那么就会更新item, 导致item n显示了item m的图片,当item n的图片下载完毕后,又会再次更新item,导致图片再次发生变化。这样就会导致图片的错位和闪烁(多次更新现象),其原因就是因为item m虽然不在屏幕内,但是item n复用了item m,即两者对应的item实际是内存中的同一块区域。

优化方案:
在getView方法中为ImageView绑定一个Tag标志(该标志应该能够作为标识该ImageView的唯一标识),比较简单常见的方法就是将存有图片URL地址的字符串作为该标识。

<pre><code>
img.setTag(url);</code></pre>

同时在进行异步任务下载图片时,当图片下载完毕(doInBackground方法执行完毕),执行onPostExecute方法时,不再是直接为img设置图片,而是要增加一个判断,只有当下载好的图片的URL地址和img的tag标识相同时,才对图片进行更新。

        if (img.getTag() != null && img.getTag().equals(url)) {
            img.setImageBitmap(result);
        }

2.避免图片的多次下载
如果每次滚动重新显示item时,都需要重新从网上下载图片资源,显得非常不友好。
解决方式:
为了避免每次显示item都要重新从网上下载对应的图片资源,可以引入缓存。包括一级缓存(内存缓存)LruCache,以及二级缓存(硬盘缓存)DiskCache.

    //添加缓存,
LruCache<String, Bitmap> cache;// 缓存,本质相当于一个map

public ImageLoader() {
    int maxMemory = (int) Runtime.getRuntime().maxMemory();
    int cacheSize = maxMemory / 4;
    cache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap value) {
            // TODO Auto-generated method stub
            // 每次存入时调用,返回Bitmap 的实际大小
            return value.getByteCount();
        }
    };
}

//从缓存中获取图片
Bitmap getFromCache(String url)
{
    return cache.get(url);
}

//将图片存入缓存
void addIntoCache(Bitmap bitmap, String url)
{
    if (getFromCache(url) == null) {
        cache.put(url, bitmap);
    }
}

这样,每次在加载图片时,首先判断缓存中是否已经存在该图片资源,只有当不存在时,才会从网络上去获取资源。


滚动优化:(防止滚动时因为加载而导致卡顿)
1.ListView滑动停止后才加载可见项
2.滑动时,取消所有加载项
怎么通过tag来获取对应的ImageView?
通过findViewWithTag方法。

滚动优化实现思路:
1.既然要针对滚动过程进行优化,就需要实现OnScrollListener接口,因为需要控制item的显示,所以可以在Adapter中实现该接口。(需要在构造方法中对listview注册该接口)

listView.setOnScrollListener(this);//在Adapter中实现该接口

实现该接口需要覆写两个函数

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
    // TODO Auto-generated method stub
    if(scrollState==SCROLL_STATE_IDLE)
    {//正常状态(没有滚动)时,开始加载任务
        loader_scroll.LoadImageByAsyncTask(ImgStart,ImgEnd);
    }else{
        //停止任务
        loader_scroll.cancelAllTask();
    }
    
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    // TODO Auto-generated method stub
    ImgStart=firstVisibleItem;
    ImgEnd= ImgStart+visibleItemCount;
    
    if(first_flag==1 && visibleItemCount>0)
    {//首次加载预处理
        loader_scroll.LoadImageByAsyncTask(ImgStart,ImgEnd);
    }
}

当处于滑动状态时,停止任务(不会执行onPostExecute方法,即不会导致界面的更新重绘,滚动将更为流畅);
当滚动停止时,加载当前可以显示的所有item,通过维护ImgStart和ImgEnd这两个变量(当前可见的第一个Item和可见的最后一个Item),可以控制当前可以显示的item的加载。

因为需要控制多个Item的加载,所以传入的参数不再是ImageView,而是ImgStart和ImgEnd,但是又有一个问题,怎么传递URL?我们可以在Adapter中创建一个静态数组,用以存储所有图片的URL,这样就可以在ImageLoader类中获取到URL数组,从而执行对图片的异步加载。

为了让ListView实现正常的功能,我们还需要进行一个首次加载的预处理,否则只有当listView滚动一次以后才会执行加载任务。

取消加载项时,
task.cancel(false)传参数要传false,为什么?
因为调用该方法就可以保证不执行UI线程上的onPostExecute更新界面的函数,滚动的优化关键在于不要在滚动过程中重绘界面,既不更新UI即可,并不需要强行停止图片的缓存。传入false,不会强制中断图片的下载缓存,同时保证不会因为图片下载完毕而导致UI重绘。


ImageLoader示例

1.通过多线程下载(未进行滚动优化版本)
使用说明,因为ImageView和图片的url信息是由ImageLoader_thread类封装的,所以每次使用该类加载图片时都需要使用一个新的实例。

public class ImageLoader_thread {
ImageView img;
String url;

public ImageLoader_thread(ImageView img,String url)
{
    this.img=img;
    this.url=url;
}

Handler handler= new Handler(){

    Bitmap bitmap=null;
    @Override
    public void handleMessage(Message msg) {
        // TODO Auto-generated method stub
        super.handleMessage(msg);
        bitmap = (Bitmap) msg.obj;
        if(img.getTag().equals(url))
        {
            img.setImageBitmap(bitmap);
        }
    }
    
};


void LoadImageByThread()
{
    new Thread(){

        @Override
        public void run() {
            // TODO Auto-generated method stub
            super.run();
            try {
                URL _url =new URL(url);
                InputStream is = _url.openStream();
                Bitmap bitmap = BitmapFactory.decodeStream(is);
                Message msg=Message.obtain();
                msg.obj=bitmap;
                handler.sendMessage(msg);
            } catch (MalformedURLException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }   
    }.start();
}
}

2.使用异步任务AsyncTask下载(未进行滚动优化版本)

public class ImageLoader {
//添加缓存,
LruCache<String, Bitmap> cache;// 缓存,本质相当于一个map

public ImageLoader() {
    int maxMemory = (int) Runtime.getRuntime().maxMemory();
    int cacheSize = maxMemory / 4;
    cache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap value) {
            // TODO Auto-generated method stub
            // 每次存入时调用,返回Bitmap 的实际大小
            return value.getByteCount();
        }
    };
}

//从缓存中获取图片
Bitmap getFromCache(String url)
{
    return cache.get(url);
}

//将图片存入缓存
void addIntoCache(Bitmap bitmap, String url)
{
    if (getFromCache(url) == null) {
        cache.put(url, bitmap);
    }
}

void LoadImageByAsyncTask(ImageView img, String url) {
    new MyAsyncTask(img, url).execute(url);
}

class MyAsyncTask extends AsyncTask<String, Void, Bitmap> {
    ImageView img;
    String url;

    public MyAsyncTask(ImageView img, String url) {
        this.img = img;
        this.url = url;
    }

    @Override
    protected Bitmap doInBackground(String... params) {
        // TODO Auto-generated method stub
        Bitmap bitmap = null;
        //每次准备从网上获取资源前先判断缓存中是否存在该图片
        bitmap = getFromCache(url);
        if(bitmap!=null)
        {
            return bitmap;
        }
        // url=params[0];
        try {
            URL _url = new URL(url);
            InputStream is = _url.openStream();
            bitmap = BitmapFactory.decodeStream(is);
            //每次下载完后将图片存入缓存中
            if(bitmap!=null)
            {
                addIntoCache(bitmap,url);
            }
            return bitmap;
        } catch (MalformedURLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onPostExecute(Bitmap result) {
        // TODO Auto-generated method stub
        super.onPostExecute(result);
        if (img.getTag() != null && img.getTag().equals(url)) {
            img.setImageBitmap(result);
        }
        
    }
}
}

3.异步下载图片(滚动优化版,仅指定起始Item号和结束item号)

//对listView滚动时进行更进一步的优化
public class ImageLoader_srcoll_better {

// 添加缓存,
LruCache<String, Bitmap> cache;// 缓存,本质相当于一个map
int ImgStart,ImgEnd;
//存储URL数组
List<String> URLS=MyAdapter.URLS;

ListView mlistView;
Set<MyAsyncTask> taskSet;
public ImageLoader_srcoll_better(ListView listView) {
    this.mlistView=listView;
    taskSet = new HashSet<MyAsyncTask>();
    
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int cacheSize = maxMemory / 4;
        cache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                // TODO Auto-generated method stub
                // 每次存入时调用,返回Bitmap 的实际大小
                return value.getByteCount();
            }
        };
    }

// 从缓存中获取图片
Bitmap getFromCache(String url) {
    return cache.get(url);
}

// 将图片存入缓存
void addIntoCache(Bitmap bitmap, String url) {
    if (getFromCache(url) == null) {
        cache.put(url, bitmap);
    }
}

//更改为为从ImgStart开始到ImgEnd(包括ImgStart但不包括ImgEnd)的ImageView设置图像,而不是
//针对特定的某个ImageView
void LoadImageByAsyncTask(int ImgStart , int ImgEnd) {
    String url;
    for (int i=ImgStart; i<ImgEnd; i++)
    {
        url=URLS.get(i);
        MyAsyncTask task = new MyAsyncTask(url);
        task.execute(URLS.get(i));
        taskSet.add(task);
        
    }
}

class MyAsyncTask extends AsyncTask<String, Void, Bitmap> {
    ImageView img;
    String url;

    public MyAsyncTask(String url) {
        this.url = url;
    }

    @Override
    protected Bitmap doInBackground(String... params) {
        // TODO Auto-generated method stub
        //怎么通过tag来获取对应的ImageView
        Bitmap bitmap = null;
        // 每次准备从网上获取资源前先判断缓存中是否存在该图片
        bitmap = getFromCache(url);
        if (bitmap != null) {
            return bitmap;
        }
        // url=params[0];
        try {
            URL _url = new URL(url);
            InputStream is = _url.openStream();
            bitmap = BitmapFactory.decodeStream(is);
            // 每次下载完后将图片存入缓存中
            if (bitmap != null) {
                addIntoCache(bitmap, url);
            }
            return bitmap;
        } catch (MalformedURLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onPostExecute(Bitmap result) {
        // TODO Auto-generated method stub
        super.onPostExecute(result);
        img = (ImageView)mlistView.findViewWithTag(url);
        if (img != null && result!=null) {
            img.setImageBitmap(result);
        }
        taskSet.remove(this);
    }
}

public void cancelAllTask()
{
    if(taskSet!=null)
    {
        for(MyAsyncTask task:taskSet)
        {
            //滚动时保证不重绘即可(不执行UI线程的onPostExecute),但是允许继续下载缓存图片
            task.cancel(false);//>?????
        }
    }
}
}

下面给出一个完整的滚动优化后的示例代码:
MyAdapter.java

public class MyAdapter extends BaseAdapter implements OnScrollListener{

//不要忘记注册Listener!!!!!
List<Beans> list;
Context context;
LayoutInflater inflater;
//ViewHolder 不能放在外面
//ViewHolder holder;

int ImgStart ,ImgEnd;
ListView listView;
ImageLoader_srcoll_better loader_scroll;
//首次加载预处理
int first_flag = 1;

static List<String> URLS =new ArrayList<String>();

ImageLoader loader;

public MyAdapter(Context context,List<Beans>list,ListView listView){
    this.context=context;
    this.list=list;
    inflater = LayoutInflater.from(context);
    loader = new ImageLoader();
    //初始化URLS列表
    for(int i=0;i<list.size();i++)
    {
        URLS.add(list.get(i).img);
    }
    this.listView=listView;
    listView.setOnScrollListener(this);
    loader_scroll=new ImageLoader_srcoll_better(listView);
}
@Override
public int getCount() {
    // TODO Auto-generated method stub
    return list.size();
}

@Override
public Object getItem(int position) {
    // TODO Auto-generated method stub
    return list.get(position);
}

@Override
public long getItemId(int position) {
    // TODO Auto-generated method stub
    return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    // TODO Auto-generated method stub
    ViewHolder holder;
    if(convertView==null)
    {
        holder = new ViewHolder();
        //必须加flase否则出错,为什么???!!!
        convertView = inflater.inflate(R.layout.item_layout, parent,false);
        //也可以使用null,但不能用parent
        //convertView = inflater.inflate(R.layout.item_layout, null);
        holder.img= (ImageView)convertView.findViewById(R.id.item_img);
        holder.txt= (TextView)convertView.findViewById(R.id.item_txt);
        convertView.setTag(holder);
    }else{
        holder=(ViewHolder)convertView.getTag();
    }
    
    holder.txt.setText(list.get(position).title);
    holder.img.setImageResource(R.drawable.ic_launcher);
    holder.img.setTag(list.get(position).img);
    //loader.LoadImageByAsyncTask(holder.img, list.get(position).img);
    //new ImageLoader_thread(holder.img,list.get(position).img).LoadImageByThread();
    return convertView;
}

class ViewHolder{
    ImageView img;
    TextView txt;
}

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
    // TODO Auto-generated method stub
    if(scrollState==SCROLL_STATE_IDLE)
    {//正常状态(没有滚动)时,开始加载任务
        loader_scroll.LoadImageByAsyncTask(ImgStart,ImgEnd);
    }else{
        //停止任务
        loader_scroll.cancelAllTask();
    }
    
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    // TODO Auto-generated method stub
    ImgStart=firstVisibleItem;
    ImgEnd= ImgStart+visibleItemCount;
    
    if(first_flag==1 && visibleItemCount>0)
    {//首次加载预处理
        loader_scroll.LoadImageByAsyncTask(ImgStart,ImgEnd);
    }
}
}

MainActivity.java

public class MainActivity extends Activity {
ListView listview;
List<Beans> list;
String url="http://www.imooc.com/api/teacher?type=4&num=30";

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    listview = (ListView)findViewById(R.id.listview);
    getBeansList();
}

void getBeansList(){
    new MyAsyncTask().execute(url);
    Log.i("logcat", "zhixingle!");
}

List<Beans> parseData(String json)
{
    List<Beans> list = new ArrayList<Beans>();
    //String title,img;
    try {
        JSONObject jb= new JSONObject(json);
        JSONArray array=jb.getJSONArray("data");
        for (int i=0;i<array.length();i++)
        {
            jb=array.getJSONObject(i);
            list.add(new Beans(jb.getString("name"),jb.getString("picSmall")));
        }
        return list;
    } catch (JSONException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    
    return null;
    
}

class MyAsyncTask extends AsyncTask<String, Void, List<Beans>>{

    @Override
    protected List<Beans> doInBackground(String... params) {
        // TODO Auto-generated method stub
        List<Beans> list=null;
        String line="",json="";
        try {
            URL _url = new URL(params[0]);
            InputStream is = _url.openStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            while((line=br.readLine())!=null)
            {
                json+=line;//获取到了json数据
            }
            list=parseData(json);
            return list;
        } catch (MalformedURLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return list;
    }

    @Override
    protected void onPreExecute() {
        // TODO Auto-generated method stub
        super.onPreExecute();
    }

    @Override
    protected void onPostExecute(List<Beans> result) {
        // TODO Auto-generated method stub
        super.onPostExecute(result);
        
        MyAdapter adapter = new MyAdapter(MainActivity.this, result, listview);
        listview.setAdapter(adapter);
        
    }

    @Override
    protected void onProgressUpdate(Void... values) {
        // TODO Auto-generated method stub
        super.onProgressUpdate(values);
    }
    
}


@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();
    if (id == R.id.action_settings) {
        return true;
    }
    return super.onOptionsItemSelected(item);
}
}

Beans.java

public class Beans {

public String title;
public String img;

public Beans(String title,String img)
{
    this.title=title;
    this.img=img;
}
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,423评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,339评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,241评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,503评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,824评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,262评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,615评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,337评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,989评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,300评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,829评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,193评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,753评论 3 230
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,970评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,708评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,295评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,207评论 2 258

推荐阅读更多精彩内容