你真的会用ListView吗?

最简单也是最复杂的控件——ListView,这句话真的一点也不夸张。依稀记得大三下学期,抱着一本《Android第一行代码》,每天开开心心的学一点基础知识。UI学了没多少,就接触到了ListView。用个for循环,给ListView塞一串item,自己还可以滑,(@ο@) 哇~,这是极好的。

又扯远了,上干货。

假设一个界面就一个ListView,然后ListView的Item就是一个ImageView,额,撸代码:

activity_main.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="match_parent">

    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

image_item.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="match_parent">

    <ImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:src="@mipmap/ic_launcher" />
</LinearLayout>

然后要做的就是从网络上获取图片放在ListView中显示,有人说这不简单,直接异步加载不就收工了么,先贴出来ImageAdapter的代码:

package com.cjt_pc.listviewtest;

/**
 * Created by cjt-pc on 2015/10/13.
 * Email:879309896@qq.com
 */

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

public class ImageAdapter extends ArrayAdapter<String> {

    private int resource;

    public ImageAdapter(Context context, int resource, String[] objects) {
        super(context, resource, objects);
        this.resource = resource;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        String url = getItem(position);
        ViewHolder viewHolder;
        if (convertView == null) {
            convertView = LayoutInflater.from(getContext()).inflate(resource, null);
            viewHolder = new ViewHolder();
            viewHolder.imageView = (ImageView) convertView.findViewById(R.id.image);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }
        new BitmapTask(viewHolder.imageView).execute(url);
        return convertView;
    }

    class ViewHolder {
        ImageView imageView;
    }


    /**
     * 异步下载图片的任务。
     *
     * @author cjt-pc
     */
    class BitmapTask extends AsyncTask<String, Void, Bitmap> {

        private ImageView imageView;

        public BitmapTask(ImageView imageView) {
            this.imageView = imageView;
        }

        @Override
        protected Bitmap doInBackground(String... strings) {
            String url = strings[0];
            InputStream is = null;
            Bitmap bitmap = null;
            try {
                is = new URL(url).openStream();
                // 注意这里没有直接用decodeStream,是因为由于本身的原因这样极其不稳定
                byte[] bytes = PictureCompressUtil.readStream(is);
                bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            } finally {
                try {
                    if (is != null) {
                        is.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            imageView.setImageBitmap(bitmap);
        }
    }

}

图片地址的封装类:

package com.cjt_pc.listviewtest;

/**
 * Created by cjt-pc on 2015/10/13.
 * Email:879309896@qq.com
 */
public class Images {

    public final static String[] imageUrls = new String[] {
            "http://b.hiphotos.baidu.com/zhidao/pic/item/241f95cad1c8a7862039afc56609c93d71cf50ee.jpg",
            "http://d.hiphotos.baidu.com/zhidao/pic/item/09fa513d269759ee5eb29a00b3fb43166c22df1f.jpg",
            "http://h.hiphotos.baidu.com/zhidao/pic/item/fd039245d688d43f78756b397c1ed21b0ff43b19.jpg",
            "http://e.hiphotos.baidu.com/zhidao/pic/item/30adcbef76094b3608b1fe26a2cc7cd98c109dde.jpg",
            "http://a.hiphotos.baidu.com/zhidao/pic/item/42a98226cffc1e17bae869fa4b90f603728de9a2.jpg",
            "http://f.hiphotos.baidu.com/zhidao/pic/item/b7003af33a87e950b86616a711385343faf2b48c.jpg",
            "http://a.hiphotos.baidu.com/zhidao/pic/item/9922720e0cf3d7ca0b84dabdf31fbe096a63a98f.jpg",
            "http://c.hiphotos.baidu.com/zhidao/pic/item/838ba61ea8d3fd1fa30f47f1314e251f94ca5ff3.jpg",
            "http://a.hiphotos.baidu.com/zhidao/pic/item/d4628535e5dde71128bea63da6efce1b9c166189.jpg",
            "http://f.hiphotos.baidu.com/zhidao/pic/item/e7cd7b899e510fb31a77925cd833c895d0430c8a.jpg",
            "http://www.lipu.net/Photo_Max/200981321538.jpg",
            "http://t1.niutuku.com/960/58/58-421244.jpg",
            "http://a2.att.hudong.com/32/06/01300542212415138174064802766.jpg",
            "http://static.bbs.sgnet.cc/attachment/forum/201211/17/110951koommdjbo7rbrr5r.jpg",

            "https://img-my.csdn.net/uploads/201508/05/1438760758_3497.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760758_6667.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760757_3588.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760756_3304.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760755_6715.jpeg",
            "https://img-my.csdn.net/uploads/201508/05/1438760726_5120.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760726_8364.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760725_4031.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760724_9463.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760724_2371.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760707_4653.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760706_6864.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760706_9279.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760704_2341.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760704_5707.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760685_5091.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760685_4444.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760684_8827.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760683_3691.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760683_7315.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760663_7318.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760662_3454.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760662_5113.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760661_3305.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760661_7416.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760589_2946.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760589_1100.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760588_8297.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760587_2575.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760587_8906.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760550_2875.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760550_9517.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760549_7093.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760549_1352.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760548_2780.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760531_1776.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760531_1380.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760530_4944.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760530_5750.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760529_3289.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760500_7871.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760500_6063.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760499_6304.jpeg",
            "https://img-my.csdn.net/uploads/201508/05/1438760499_5081.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760498_7007.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760478_3128.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760478_6766.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760477_1358.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760477_3540.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760476_1240.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760446_7993.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760446_3641.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760445_3283.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760444_8623.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760444_6822.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760422_2224.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760421_2824.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760420_2660.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760420_7188.jpg",
            "https://img-my.csdn.net/uploads/201508/05/1438760419_4123.jpg"
    };
}

额,主Activity代码就简单了:

package com.cjt_pc.listviewtest;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ListView;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ListView listView = (ListView) findViewById(R.id.list_view);
        ImageAdapter adapter = new ImageAdapter(this, R.layout.image_item, Images.imageUrls);
        listView.setAdapter(adapter);
    }
}

是的,没有什么难度,但是运行之后就会直接闪退,OOM,何为OOM?out of memory!!内存溢出!!直观点说就是图片太大了,手机内存不够!!也是,用浏览器看看这些地址的图片,都是我精挑细选的,超大超级大有木有!!这样我们就遇到了第一个问题,OOM!!

仔细想想,一张3000*2000的图片,要放在300*200的ImageView中显示,直接加载有意义吗?答案当然是NO。这个时候我们就要想到压缩了,怎么压,借助Options这个属性,先看看这个工具类:

package com.cjt_pc.listviewtest;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;

/**
 * Created by cjt-pc on 2015/10/13.
 * Email:879309896@qq.com
 */
public class PictureCompressUtil {
    /**
     * 获取适当的压缩比率
     *
     * @param options   解析参赛
     * @param tarWidth  目标宽度
     * @param tarHeight 目标高度
     * @return 适当的InSampleSize
     */
    public static int calculateInSampleSize(BitmapFactory.Options options, int tarWidth, int tarHeight) {
        // 获取源图片的实际宽度和高度
        final int realWidth = options.outWidth;
        final int realHeight = options.outHeight;
        int inSampleSize = 1;
        if (realHeight > tarHeight || realWidth > tarWidth) {
            // 计算出实际宽高和目标宽高的比率
            final int heightRatio = Math.round((float) realHeight / (float) tarHeight);
            final int widthRatio = Math.round((float) realWidth / (float) tarWidth);
            // 注意:网上郭大神说去较小的那一个,实测选较小的那一个图片大了多了滑动起来会比较卡,选较大的那一个完美解决
            inSampleSize = heightRatio > widthRatio ? heightRatio : widthRatio;
        }
        return inSampleSize;
    }

    public static Bitmap decodeSampledBitmapFromByteArray(byte[] bytes, int tarWidth, int tarHeight) {
        // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
        // 调用上面定义的方法计算inSampleSize值
        options.inSampleSize = calculateInSampleSize(options, tarWidth, tarHeight);
        // 使用获取到的inSampleSize值再次解析图片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
    }

    public static byte[] readStream(InputStream inStream) throws Exception {
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = inStream.read(buffer)) != -1) {
            outStream.write(buffer, 0, len);
        }
        outStream.close();
        inStream.close();
        return outStream.toByteArray();
    }
}

关于压缩就不细讲了,不是本篇的重点,代码也不是太难懂,需要指出的是,先设置options.inJustDecodeBounds为true,代表只解析边界,得到源图片的宽度和高度,然后根据需要需要放置的目标ImageView的大小计算一个适当的压缩比率,然后将options.inJustDecodeBounds设置为false即可完成解析图片,最后返回一个Bitmap对象。

修改哈ImageAdapter中部分代码:

package com.cjt_pc.listviewtest;

/**
 * Created by cjt-pc on 2015/10/13.
 * Email:879309896@qq.com
 */

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

public class ImageAdapter extends ArrayAdapter<String> {

    ...


    /**
     * 异步下载图片的任务。
     *
     * @author cjt-pc
     */
    class BitmapTask extends AsyncTask<String, Void, Bitmap> {

        ...
                is = new URL(url).openStream();
                // 注意这里没有直接用decodeStream,是因为由于本身的原因这样极其不稳定
                byte[] bytes = PictureCompressUtil.readStream(is);
                // bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
                bitmap = PictureCompressUtil.decodeSampledBitmapFromByteArray(bytes, 1440, 300);
            ...
    }
}

改动的地方非常少,只是把解析byte数组的方法封装成了一个工具类,然后再运行看看,没有闪退了出现OOM了吧,但是懊恼的是,为什么图片好乱,感觉有些图被加载了几遍,而且出现在了不该出现的位置。

是的,基于ListView的工作原理,离开屏幕的view转为convertView,能够重用提高效率,但试想,离开的view已经加载好的图片,然后在下一个重用该view的item上初始化的时候就会显示上一张图片,然后在异步加载图片完成后会显示本该显示的图片,这样就出现了闪屏、乱序的样子。

怎么解决呢?看看一般情况下采用ViewHolder提高效率时,是采用的convertView.setTag(viewHolder)形式,然后读的时候用viewHolder = (ViewHolder) convertView.getTag(),这样做的好处就是不用每次都用findViewById。

ImageView继承自View,当然也具有setTag(Object object)方法,一般情况下要是tag唯一,就设置成图片的url地址。那么怎么拿到绑定的ImageView呢?其实,除了findViewById之外,还有一个findViewByTag方法,这样思路就屡清楚了。

看看改进后的ImageAdapter源代码:

package com.cjt_pc.listviewtest;

/**
 * Created by cjt-pc on 2015/10/13.
 * Email:879309896@qq.com
 */

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

public class ImageAdapter extends ArrayAdapter<String> {

    private int resource;
    private ListView mListView;

    public ImageAdapter(Context context, int resource, String[] objects) {
        super(context, resource, objects);
        this.resource = resource;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        mListView = (ListView) parent;
        String url = getItem(position);
        ViewHolder viewHolder;
        if (convertView == null) {
            convertView = LayoutInflater.from(getContext()).inflate(resource, null);
            viewHolder = new ViewHolder();
            viewHolder.imageView = (ImageView) convertView.findViewById(R.id.image);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }
        viewHolder.imageView.setTag(url);
        viewHolder.imageView.setImageResource(R.mipmap.ic_launcher);
        new BitmapTask().execute(url);
        return convertView;
    }

    class ViewHolder {
        ImageView imageView;
    }


    /**
     * 异步下载图片的任务。
     *
     * @author cjt-pc
     */
    class BitmapTask extends AsyncTask<String, Void, Bitmap> {

        private String url;

        @Override
        protected Bitmap doInBackground(String... strings) {
            url = strings[0];
            InputStream is = null;
            Bitmap bitmap = null;
            try {
                is = new URL(url).openStream();
                // 注意这里没有直接用decodeStream,是因为由于本身的原因这样极其不稳定
                byte[] bytes = PictureCompressUtil.readStream(is);
                // bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
                bitmap = PictureCompressUtil.decodeSampledBitmapFromByteArray(bytes, 1440, 300);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            } finally {
                try {
                    if (is != null) {
                        is.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            ImageView imageView = (ImageView) mListView.findViewWithTag(url);
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }

}

这里显示定义了一个全局变量mListView,然后在getView中初始化,修改BitmapTask的构造方法,不需传入ImageView,直接在异步加载完成后onPostExecute中拿到ImageView:ImageView imageView = (ImageView) mListView.findViewWithTag(url);如果得到的不为空,说明该item还没有离开屏幕,即刻给ImageView赋值。

还有,在getView方法中,加了两句话:viewHolder.imageView.setTag(url); viewHolder.imageView.setImageResource(R.mipmap.ic_launcher);这两句话十分重要,setTag是给ImageView绑定一个Tag,便于后面获取;setImageResource则是在item刚刚加载的时候给ImageView设置一个默认图片,这次是个android机器人,可自行设置。

哇咔咔,试试运行,大功告成有木有。但是出现了一个不好的用户体验,每次移出屏幕的图片再次显示必须得二次加载,这是极为不好的。因此我们又接触到了图片缓存技术——lruCache。这里就略微介绍一下,通过lruCache,分配一个合理的缓存空间,然后每加载一张就往里面塞一张,超过设置的大小后释放掉最不常用的缓存。有了这个算法,我们就可以轻易解决上述问题。

完善ImageAdapter:

package com.cjt_pc.listviewtest;

/**
 * Created by cjt-pc on 2015/10/13.
 * Email:879309896@qq.com
 */

import android.content.Context;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.support.v4.util.LruCache;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

public class ImageAdapter extends ArrayAdapter<String> {

    private int resource;
    private ListView mListView;
    // 图片缓存,以键值的方式存储
    private LruCache<String, Bitmap> mMemoryCache;

    public ImageAdapter(Context context, int resource, String[] objects) {
        super(context, resource, objects);
        this.resource = resource;
        // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。
        // LruCache通过构造函数传入缓存值,以KB为单位。
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        // 使用最大可用内存值的1/8作为缓存的大小。
        int cacheSize = maxMemory / 8;
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                // 重写此方法来衡量每张图片的大小,默认返回图片数量。
                return bitmap.getByteCount() / 1024;
            }
        };
    }

    // 添加bitmap到缓存中
    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    // 从缓存中获取bitmap
    public Bitmap getBitmapFromMemCache(String key) {
        return mMemoryCache.get(key);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        mListView = (ListView) parent;
        String url = getItem(position);
        ViewHolder viewHolder;
        if (convertView == null) {
            convertView = LayoutInflater.from(getContext()).inflate(resource, null);
            viewHolder = new ViewHolder();
            viewHolder.imageView = (ImageView) convertView.findViewById(R.id.image);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }
        viewHolder.imageView.setTag(url);
        viewHolder.imageView.setImageResource(R.mipmap.ic_launcher);
        loadBitmap(url, viewHolder.imageView);
        return convertView;
    }

    // 通过图片缓存技术,快速重新加载和处理图片
    public void loadBitmap(String imageKey, ImageView imageView) {
        Bitmap bitmap = getBitmapFromMemCache(imageKey);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
        } else {
            imageView.setImageResource(R.mipmap.ic_launcher);
            new BitmapTask().execute(imageKey);
        }
    }

    class ViewHolder {
        ImageView imageView;
    }


    /**
     * 异步下载图片的任务。
     *
     * @author cjt-pc
     */
    class BitmapTask extends AsyncTask<String, Void, Bitmap> {

        private String url;

        @Override
        protected Bitmap doInBackground(String... strings) {
            url = strings[0];
            InputStream is = null;
            Bitmap bitmap = null;
            try {
                is = new URL(url).openStream();
                // 注意这里没有直接用decodeStream,是因为由于本身的原因这样极其不稳定
                byte[] bytes = PictureCompressUtil.readStream(is);
                // bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
                bitmap = PictureCompressUtil.decodeSampledBitmapFromByteArray(bytes, 1440, 300);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            } finally {
                try {
                    if (is != null) {
                        is.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            addBitmapToMemoryCache(url, bitmap);
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            ImageView imageView = (ImageView) mListView.findViewWithTag(url);
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }

}

这样在滑动浏览的时候,移出屏幕的图片会加载到缓存中,若该图片在下一次加载的时候没有被释放掉,则直接读取设置到ImageView上,既加强了用户体验,也使程序更为的流畅。

啧啧,到了这里,是不是就真的会用ListView了呢,我可以放心的告诉你,快了T T。

对于这个程序的选材,我可是花了一番功夫,图片要大,又要多,还要好看。。。然而在快速滚动的时候仍显不足,只要是触发了getView并且缓存中没有该图片的会开启线程异步加载,这样的话快速滚动的话,会触发很多的异步加载线程,但是滚动过去的图片在加载完没有多大的意义,当屏的图片并没有显示,每个线程会分些网速,显示图片会越来越慢。所以我们要让它在滑动的时候不进行任何操作,在停止滚动的时候加载当前屏幕内的图片。

最终的ImageAdapter:

package com.cjt_pc.listviewtest;

/**
 * Created by cjt-pc on 2015/10/13.
 * Email:879309896@qq.com
 */

import android.content.Context;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.support.v4.util.LruCache;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;

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

public class ImageAdapter extends ArrayAdapter<String> implements AbsListView.OnScrollListener {

    // 记录异步加载任务的集合
    List<BitmapTask> taskList = new ArrayList<>();
    private int resource;
    private ListView mListView;
    // 图片缓存,以键值的方式存储
    private LruCache<String, Bitmap> mMemoryCache;
    private int mFirstVisibleItem;
    private int mVisibleItemCount;
    private boolean isFirstEnter = true;

    public ImageAdapter(Context context, int resource, String[] objects) {
        super(context, resource, objects);
        this.resource = resource;
        // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。
        // LruCache通过构造函数传入缓存值,以KB为单位。
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        // 使用最大可用内存值的1/8作为缓存的大小。
        int cacheSize = maxMemory / 8;
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                // 重写此方法来衡量每张图片的大小,默认返回图片数量。
                return bitmap.getByteCount() / 1024;
            }
        };
    }

    // 添加bitmap到缓存中
    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    // 从缓存中获取bitmap
    public Bitmap getBitmapFromMemCache(String key) {
        return mMemoryCache.get(key);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        mListView = (ListView) parent;
        String url = getItem(position);
        ViewHolder viewHolder;
        if (convertView == null) {
            convertView = LayoutInflater.from(getContext()).inflate(resource, null);
            viewHolder = new ViewHolder();
            viewHolder.imageView = (ImageView) convertView.findViewById(R.id.image);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }
        viewHolder.imageView.setTag(url);
        viewHolder.imageView.setImageResource(R.mipmap.ic_launcher);
        loadBitmap(url, viewHolder.imageView);
        return convertView;
    }

    // 通过图片缓存技术,快速重新加载和处理图片
    public void loadBitmap(String imageKey, ImageView imageView) {
        Bitmap bitmap = getBitmapFromMemCache(imageKey);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
        }
    }

    @Override
    public void onScrollStateChanged(AbsListView absListView, int i) {
        // 仅当ListView静止时才去下载图片,GridView滑动时取消所有正在下载的任务
        if (i == SCROLL_STATE_IDLE) {
            loadBitmaps(mFirstVisibleItem, mVisibleItemCount);
        } else {
            cancelAllTasks();
        }
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
                         int totalItemCount) {
        mFirstVisibleItem = firstVisibleItem;
        mVisibleItemCount = visibleItemCount;
        // 下载的任务应该由onScrollStateChanged里调用,但首次进入程序时onScrollStateChanged并不会调用,
        // 因此在这里为首次进入程序开启下载任务。
        if (isFirstEnter && visibleItemCount > 0) {
            loadBitmaps(firstVisibleItem, visibleItemCount);
            isFirstEnter = false;
        }
    }

    /**
     * 加载Bitmap对象。此方法会在LruCache中检查所有屏幕中可见的ImageView的Bitmap对象,
     * 如果发现任何一个ImageView的Bitmap对象不在缓存中,就会开启异步线程去下载图片。
     *
     * @param firstVisibleItem 第一个可见的ImageView的下标
     * @param visibleItemCount 屏幕中总共可见的元素数
     */
    private void loadBitmaps(int firstVisibleItem, int visibleItemCount) {
        try {
            for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) {
                String imageUrl = Images.imageUrls[i];
                Bitmap bitmap = getBitmapFromMemCache(imageUrl);
                if (bitmap == null) {
                    BitmapTask task = new BitmapTask();
                    taskList.add(task);
                    task.execute(imageUrl);
                } else {
                    ImageView imageView = (ImageView) mListView.findViewWithTag(imageUrl);
                    if (imageView != null) {
                        imageView.setImageBitmap(bitmap);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void cancelAllTasks() {
        if (!taskList.isEmpty()) {
            // 遍历任务集合,取消正在进行的异步任务,然后清空任务列表
            for (BitmapTask task : taskList) {
                task.cancel(true);
            }
            taskList.clear();
        }
    }

    class ViewHolder {
        ImageView imageView;
    }


    /**
     * 异步下载图片的任务。
     *
     * @author cjt-pc
     */
    class BitmapTask extends AsyncTask<String, Void, Bitmap> {

        private String url;

        @Override
        protected Bitmap doInBackground(String... strings) {
            url = strings[0];
            InputStream is = null;
            Bitmap bitmap = null;
            try {
                is = new URL(url).openStream();
                // 注意这里没有直接用decodeStream,是因为由于本身的原因这样极其不稳定
                byte[] bytes = PictureCompressUtil.readStream(is);
                // bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
                bitmap = PictureCompressUtil.decodeSampledBitmapFromByteArray(bytes, 1440, 300);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (is != null) {
                        is.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bitmap != null) {
                addBitmapToMemoryCache(url, bitmap);
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            ImageView imageView = (ImageView) mListView.findViewWithTag(url);
            if (imageView != null && bitmap != null) {
                imageView.setImageBitmap(bitmap);
            }
            // 要记得在异步加载过程完成后将本次任务移出
            taskList.remove(this);
        }
    }

}

主要的就是让ImageAdapter实现了OnScorllListener的接口,然后要记住在Activity中加上监听:

package com.cjt_pc.listviewtest;

import android.app.Activity;
import android.os.Bundle;
import android.widget.AbsListView;
import android.widget.ListView;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ListView listView = (ListView) findViewById(R.id.list_view);
        ImageAdapter adapter = new ImageAdapter(this, R.layout.image_item, Images.imageUrls);
        listView.setAdapter(adapter);
        listView.setOnScrollListener(adapter);
    }
}

这里写图片描述

额,差不多了。总的来说,就四个方面

  1. 图片压缩
  2. 放置乱序
  3. 缓存技术
  4. 滑动监听

具体的上面都有,好了,就是这些了,我们终于敢说我会用ListView了额,GridView原理都是一样。今天就到这了,拜了个拜^-^。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值