RecyclerView的使用

xiasuhuei321發表於2017-12-13

今天關於是寫ListView還是RecyclerView著實糾結了一會,畢竟這二者都很重要,不過最近做的東西里用的是RecyclerView,所以還是先寫RecyclerView了,而且程式碼也擼好了。好了日常嘮嗑(1/1),下面進入正題。

本文包含以下幾個方面:

  • 使用RecyclerView實現ListView的效果
  • RecyclerView載入圖片出現OOM時的一些處理
  • RecyclerView實現瀑布流
  • RecyclerView滑動卡頓的一些優化

RecyclerView與ListView原理是類似的:都是僅僅維護少量的View並且可以展示大量的資料,不過RecyclerView相比ListView更加高階和靈活。RecyclerView本身只負責View的回收和複用,這從它名字本身也能看出一些端倪。既然RecyclerView只關心View的回收和複用,那麼這玩意到底該怎麼才能玩起來?

好問題,雖然是我自己問的……借用一句別人說的:真正牛逼的人是不需要什麼都管的,只要底下有人就行。LayoutManager負責Item佈局和展示,Item之間的間隔由ItemDecoration來解決,Item的增加與刪除的動畫由ItemAnimator來解決,Adapter用來將資料與Item介面繫結。

好了作了基本的瞭解,開始上碼吧,有關如何在Android Studio中使用RecyclerView和我的程式碼獲取方法,將在文末作相應的說明。首先第一要實現的,就是和ListView類似的效果,先看效果圖,原諒我不會做動圖……

效果1

效果2

好了,看完效果,開碼,首先是主介面的xml檔案

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    tools:context="com.example.luo_pc.testforrecyclerview.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_test"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"></android.support.v7.widget.RecyclerView>

</LinearLayout>
複製程式碼

沒啥玩意,就是一個線性佈局里加一個RecyclerView,接下來與ListView一樣,列表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="wrap_content"
    android:orientation="vertical"
    android:background="#ffffff"
    >

    <ImageView
        android:id="@+id/iv_item_img"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"
        android:adjustViewBounds="true"
        android:layout_gravity="center"
        />

    <TextView
        android:id="@+id/tv_item_desc"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:textColor="#000000"
        />
</LinearLayout>
複製程式碼

主介面程式碼:

public class SecondActivity extends AppCompatActivity{
    private ArrayList<ItemBean> itemList;
    private RecyclerView rv_test;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        rv_test = (RecyclerView) findViewById(R.id.rv_test);
        initData();
        //設定線性layoutManager
        rv_test.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
        
        mAdapter myAdapter = new mAdapter();
        //設定myAdapter的資料
        myAdapter.setData(itemList);
        //設定Adapter
        rv_test.setAdapter(myAdapter);

    }

    private void initData(){
        itemList = new ArrayList<ItemBean>();
        itemList.add(new ItemBean(R.drawable.bird, "南小鳥1"));
        itemList.add(new ItemBean(R.drawable.xiyangyang, "喜羊羊"));
        itemList.add(new ItemBean(R.drawable.bird3, "南小鳥2"));
        itemList.add(new ItemBean(R.drawable.blackcat, "黑貓"));
        itemList.add(new ItemBean(R.drawable.dog, "狗與剪刀"));
        itemList.add(new ItemBean(R.drawable.nike3, "妮可妮可"));
        itemList.add(new ItemBean(R.drawable.paojie, "炮姐"));
        itemList.add(new ItemBean(R.drawable.saber1, "吾王1"));
        itemList.add(new ItemBean(R.drawable.saber2, "吾王2"));
    }
}
複製程式碼

上面的ItemBean程式碼:

public class ItemBean {
    //R.drawable....
    private int img;
    private String desc;

    public ItemBean(int img,String desc){
        this.img = img;
        this.desc = desc;
    }

    public int getImg(){
        return img;
    }

    public void setImg(int img) {
        this.img = img;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}
複製程式碼

接下來就是重頭戲了,Adapter的程式碼。我們需要建立一個類繼承RecyclerView.Adapter,以此來建立RecyclerView的介面卡,繼承RecyclerView.Adapter需要實現 onCreateViewHolder()、onBindViewHolder()和getItemCount()方法,而onCreateView的返回值是RecyclerView.ViewHolder,所以我們需要建立一個RecyclerView.ViewHolder。而RecyclerView.ViewHolder是一個抽象類,所以我們必須新建一個類繼承該類,該抽象類中的建構函式也必須實現。說了這麼多,可能有點繞,讓我們從程式碼中學習如何操作。

    class mAdapter extends RecyclerView.Adapter{
        //資料
        private ArrayList<ItemBean> itemList;
//        private ImageResizer imgResizer = new ImageResizer();
        //設定資料
        private void setData(ArrayList<ItemBean> itemList){
            this.itemList = itemList;
        }


        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            //得到item的view
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_item, parent, false);
            //返回viewholder
            return new mViewHolder(view);
        }

        //繫結資料
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            ItemBean it = itemList.get(position);
            mViewHolder mholder = (mViewHolder)holder;
            //設定imageView的圖片
            mholder.iv_item_img.setImageResource(it.getImg());
//            mholder.iv_item_img.setImageBitmap(imgResizer.decodeSampledBitmapFromResource(getResources(),it.getImg()));
            //設定textView的文字
            mholder.tv_item_desc.setText(it.getDesc());
        }



        @Override
        public int getItemCount() {
            return itemList.size();
        }

        class mViewHolder extends RecyclerView.ViewHolder{
            ImageView iv_item_img;
            TextView tv_item_desc;

            public mViewHolder(View itemView) {
                super(itemView);
                iv_item_img = (ImageView) itemView.findViewById(R.id.iv_item_img);
                tv_item_desc = (TextView)itemView.findViewById(R.id.tv_item_desc);
            }
        }
    }
複製程式碼

我這有些地方寫的不是很好,在寫一個類繼承RecyclerView.Holder時這麼寫會更好

    class mAdapter extends RecyclerView<mViewHolder>{}
複製程式碼

如此在onBindViewHolder方法中便可以直接使用holder了,而不必再強轉。

好了程式碼也寫完了,是時候跑一波了!我的圖片都是在百度上找的,隨便下了那麼幾張。跑的時候果然不負我望,OOM!

OOM

誒?OOM?那麼我之前放的效果圖是怎麼回事?沒錯……我壓縮了一下圖片,才跑出來開頭放的效果圖……關於該如何操作,容我細細道來。

我從網上下的圖都是比較大的,大多為jpg格式,用個看圖軟體開啟顯示是1125*1674畫素在windows系統上是436K大小。對於jpg我不太瞭解,只知道這換算成ARGB_8888每個畫素佔4byte,得7M了。而且

反正是OOM了,容我說一下我的處理方法:

壓縮

圖片有不同的形狀與大小。在大多數情況下它們的實際大小都比需要呈現的尺寸大很多。考慮到在有限的記憶體下工作,理想情況是我們只需要在記憶體中載入一個低解析度的圖片即可。

BitmapFactory提供了一些解碼的方法,用來從不同的資源中建立一個Bitmap。每一種解碼方法都可以通過BitmapFactory.Options設定一些附加的標記,以此來指定解碼選項。設定inJustDecodeBounds屬性為true可以在解碼的時候獲取到圖片的原始寬/高資訊,並不會真正的去載入圖片。

獲取到圖片的寬高意義何在呢?你可能會有這樣的疑問,事實上只要指定Options的inSampleSize並在decode時傳入Options,再將inJustDecodeBounds設定為false,就可以獲得壓縮後的Bitmap。而我們獲取了原始圖片的寬高,就可以根據我們所需要的寬高計算出壓縮的比例。例如一個ARGB_8888的Bitmap解析度為2048x1536直接載入所需的記憶體是12M,而指定inSampleSize為4,那麼會得到一個512x384的Bitmap,所需的記憶體僅為0.75M。

上程式碼:

public static Bitmap decodeBitmapFromResource(Resources res,int resId,int reqWidth,int reqHeight){
        BitmapFactory.Options options = new BitmapFactory.Options();
        //可以只獲取寬高而不載入
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res,resId,options);

        //計算壓縮比例
        options.inSampleSize = calculateInSampleaSize(options,reqWidth,reqHeight);

        //解碼
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res,resId,options);
    }

    private static int calculateInSampleaSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // 圖片的高寬
        final int height = options.outHeight;
        final int width = options.outWidth;
        //預設不壓縮
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;
            while ((halfHeight / inSampleSize) > reqHeight
                    && (halfWidth / inSampleSize) > reqWidth) {
                //這裡設定inSampleSize為2的冪是因為解碼器最終還是會對非2的冪的數進行向下處理,
                // 獲取到最靠近2的冪的數
                inSampleSize *= 2;
            }
        }

        return inSampleSize;
    }
複製程式碼

當我們使用ImageView載入期望大小為100*100畫素的圖片時就可以按照如下格式寫:

mImageView.setImageBitmap(
        decodeBitmapFromResource(getResources(),R.id.drawa,100,100));
複製程式碼

壓縮的流程大概就到這了,繼續回到RecyclerView,上面的程式碼實現了和ListView類似的效果,接下來要實現的是瀑布流的效果。說的高大上,其實大致流程與之前差不多,不過改一下LayoutManager就行了,直接上完整的程式碼:

public class MainActivity extends AppCompatActivity {

    private RecyclerView rv_test;
    private ArrayList<ItemBean> itemList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();
        rv_test = (RecyclerView) findViewById(R.id.rv_test);
        //設定layoutManager
        rv_test.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));

        myAdapter mAdapter = new myAdapter();
        mAdapter.setItemList(itemList);
        rv_test.setAdapter(mAdapter);

    }

    private void initData() {
        itemList = new ArrayList<ItemBean>();
        itemList.add(new ItemBean(R.drawable.bird, "南小鳥1"));
        itemList.add(new ItemBean(R.drawable.xiyangyang, "喜羊羊"));
        itemList.add(new ItemBean(R.drawable.bird3, "南小鳥2"));
        itemList.add(new ItemBean(R.drawable.blackcat, "黑貓"));
        itemList.add(new ItemBean(R.drawable.dog, "狗與剪刀"));
        itemList.add(new ItemBean(R.drawable.nike3, "妮可妮可"));
        itemList.add(new ItemBean(R.drawable.paojie, "炮姐"));
        itemList.add(new ItemBean(R.drawable.saber1, "吾王1"));
        itemList.add(new ItemBean(R.drawable.saber2, "吾王2"));

    }

    class myAdapter extends RecyclerView.Adapter {
        private ArrayList<ItemBean> ItemList;
        ImageResizer imgResizer = new ImageResizer();
        BitmapMemoryCache lruBitmapCache = new BitmapMemoryCache();

        public void setItemList(ArrayList<ItemBean> ItemList) {
            this.ItemList = ItemList;
        }

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_item, parent, false);

            return new myViewHolder(view);
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            myViewHolder mholder = (myViewHolder) holder;
            ItemBean it = itemList.get(position);
            if(lruBitmapCache.getBitmapFromMemory(it.getDesc()) != null){
                mholder.iv_item_img.setImageBitmap(lruBitmapCache.getBitmapFromMemory(it.getDesc()));

            }else{
                //快取圖片
                lruBitmapCache.addBitmapToMemory(it.getDesc(),imgResizer.decodeSampledBitmapFromResource(getResources(),
                        it.getImg()));

                mholder.iv_item_img.setImageBitmap(imgResizer.decodeSampledBitmapFromResource(getResources(),
                        it.getImg()));
            }

            mholder.tv_item_desc.setText(it.getDesc());
        }

        @Override
        public int getItemCount() {
            return ItemList.size();
        }

        class myViewHolder extends RecyclerView.ViewHolder {
            ImageView iv_item_img;
            TextView tv_item_desc;

            public myViewHolder(View itemView) {
                super(itemView);
                iv_item_img = (ImageView) itemView.findViewById(R.id.iv_item_img);
                tv_item_desc = (TextView) itemView.findViewById(R.id.tv_item_desc);
            }

        }

    }
}
複製程式碼

效果圖:

瀑布流

其中有兩個類是之前沒有的,ImageResizer和BitmapMemoryCache,前者是用來壓縮的,後者是用來快取的。因為是瀑布流,所以我沒有設定期望的寬高,直接設定inSampleSize為4,壓縮之後圖片還是大了,在滑動的時候列表有卡頓現象,所以做了LruCache。大致就是這樣,程式碼如下:

ImageResizer

public class ImageResizer {
    public Bitmap decodeSampledBitmapFromResource(Resources res, int resId) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        //解析圖片而不會真正的載入
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
        options.inSampleSize = 4;
        options.inJustDecodeBounds = false;

        return BitmapFactory.decodeResource(res,resId,options);
    }

}
複製程式碼

BitmapMemoryCache

public class BitmapMemoryCache {
    private final String TAG = "BitmapMemoryCache";
    int maxMemory = (int)(Runtime.getRuntime().maxMemory() / 1024);
    int catchSize = maxMemory / 8;
    LruCache<String,Bitmap> lruCache = new LruCache<String,Bitmap>(catchSize){
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getRowBytes() * value.getHeight() / 1024;
        }
    };

    public void addBitmapToMemory(String key,Bitmap bitmap){
        if(getBitmapFromMemory(key) == null){
            lruCache.put(key,bitmap);
            Log.i(TAG," "+maxMemory);
        }
    }

    public Bitmap getBitmapFromMemory(String key){

        return lruCache.get(key);
    }
}

複製程式碼

做了快取之後,第一次滑動……說實話還是有點卡,但是之後的確沒有卡頓的現象了。果然人不能作死,沒事載入大圖作死不好!這裡因為我的圖比較少,從打的log來看我手機為一個app分配的記憶體是128M,1/8的記憶體16M來做快取夠了。如果圖片比較多,記憶體做快取不夠的話,可以考慮用DisLruCache,不過sdk裡是沒有的,需要去下。關於滑動的優化還沒有說完,不過今天暫時不打算繼續了,明天或者有空的時候繼續回來填這個坑。還有本來想寫個RecyclerView上拉重新整理的例子,可惜扯著扯著扯歪了,給忘了……有空再來來補上。

以後有機會再寫一篇關於快取的文吧,這裡就不多寫了。

最後本文原始碼可以在我的github獲取

在Android Studio中使用RecyclerView請在build.gradle中新增

compile 'com.android.support:recyclerview-v7:23.4.0'
複製程式碼

eclipse中請自行百度……

資料來源:《Android開發藝術探索》       Google官方培訓文件中文版

相關文章