你真的會用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:[email protected]
 */
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:[email protected]
 */
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原理都是一樣。今天就到這了,拜了個拜^-^。

發佈了30 篇原創文章 · 獲贊 21 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章