Android小白之RecyclerView的基本用法與進階

前言

recyclerView是Android 5.0 materials design 中的組件之一,是一個用來取代listView的SDK,它的靈活性與可替代性比listview更好,原理和listView相似,都是僅僅維護少量的view來加載展示大量的數據集。下面就開始對它進行介紹。介紹之前,先將實現效果展示出來:

GIF.gif

使用recyclerView的好處:

recyclerView封裝了viewHolder的回收復用機制,也就是說recyclerView標準化了viewHolder,編寫Adapter面向的是viewHolder而不再是view了,復用的邏輯被封裝了,寫起來也就更方便了。recyclerView提供了一種插拔式的體驗,高度的解耦,異常的靈活,針對一個Item的顯示RecylerView專門抽取出了相應的類,來控制Item的顯示,使其的擴展性非常強。它還可以實現多種效果,例如你想要橫向或者縱向的控制滑動列表,可以通過LinearLayoutManager這個類去控制,類似于GridView的展示效果可以用GridLayoutManager這個類,瀑布流的效果可以通過staggeredGridLayoutManager實現,另外你想控制Item的分隔線,可以通過繼承RecylerView的ItemDecoration這個類,控制Item增刪的動畫,可以通過ItemAnimator這個類進行控制,然后針對自己的業務需求去抒寫代碼。

recyclerView的簡單使用:

  1. 添加依賴:

      dependencies {
        ...
        compile 'com.android.support:recyclerview-v7:21.0.+'
      }
    

2.在xml布局文件中創建一個RecyclerView的布局和item的布局:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/my_recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbars="vertical"/>

    </RelativeLayout>

    創建item布局,通過使用CardView進行包裹達到更好的界面效果展示:

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:cardCornerRadius="10dp"
        android:layout_margin="5dp"
        android:foreground="?android:attr/selectableItemBackground"
        app:cardElevation="5dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:padding="5dp">

            <ImageView
                android:id="@+id/item_image"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@mipmap/ic_launcher" />

            <TextView
                android:id="@+id/item_textview"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginLeft="10dp"
                android:text="標題"
                android:textSize="18sp" />

        </LinearLayout>
    </android.support.v7.widget.CardView>
  1. 編寫代碼邏輯,在MainActivity中找到recyclerView,設置他的顯示樣式,并為他設置Adapter.

     mRecyclerView = (RecyclerView) findViewById(R.id.recycle_view);
     //創建默認的線性LinearLayoutManager
     LinearLayoutManager mLayoutManager = new LinearLayoutManager(this);
     //gridLayoutManager 多列表
     GridLayoutManager gridLayoutManager = new GridLayoutManager(this,3);
     //StaggeredGridLayoutManager 瀑布流
     StaggeredGridLayoutManager staggeredGridLayoutManager = new
             StaggeredGridLayoutManager(5,StaggeredGridLayoutManager.HORIZONTAL);
     mRecyclerView.setLayoutManager(mLayoutManager);
     //確定每個item的高度都是固定的,提高性能
     mRecyclerView.setHasFixedSize(true);
     //設置適配器
     mAdapter = new MyAdapter(this);
     mRecyclerView.setAdapter(mAdapter);
     //也可以設置分割線
     recyclerView.addItemDecoration(new DividerGridItemDecoration(this ));
     //設置增加或刪除條目的動畫
     recyclerView.setItemAnimator( new DefaultItemAnimator());
    
  2. 為recyclerView編寫Adapter:

    /**
     * 創建adapter需要實現三個方法:
     * 1. onCreateViewHolder():這個方法主要生成為每個Item inflater出一個View,但是該方法返回的是一個ViewHolder。該方法把View直接封裝在ViewHolder中,然后我們面向的是ViewHolder這個實例,當然這個ViewHolder需要我們自己去編寫,直接省去了當初的convertView.setTag(holder)和convertView.getTag()這些繁瑣的步驟。
     * 2. onBindViewHolder():這個方法主要用于適配渲染數據到View中。將界面與數據進行綁定,方法提供給你了一個viewHolder,而不是原來的convertView。
     * 3. getItemCount():這個方法就類似于BaseAdapter的getCount方法了,即總共有多少個條目。
     */

    public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
    
        private View view;
      
        @Override
        public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            view = View.inflate(parent.getContext(), R.layout.item_adapter, null);
            return new MyViewHolder(view);
        }
    
        @Override
        public void onBindViewHolder(MyViewHolder holder, int position) {
            holder.setData(position);
        }
    
        @Override
        public int getItemCount() {
            return 1000;
        }
    
        /**
         * 設置viewHolder
         */
    
        public class MyViewHolder extends RecyclerView.ViewHolder {
    
            TextView tvName;
            ImageView ivImage;
    
            public MyViewHolder(View itemView) {
                super(itemView);
                //初始化控件
                tvName = (TextView) view.findViewById(R.id.item_textview);
                ivImage = (ImageView) view.findViewById(R.id.item_image);
            }
        }
    }
  1. 這樣一個通過RecyclerView來實現界面的數據加載就實現了。

RecyclerView的進階

給RecyclerView的item添加點擊事件:

1.由于RecyclerView沒有提供直接添加item點擊事件的方法,因此需要通過自定義接口實現item的點擊事件。

/**
 * 自定義接口,給item設置點擊事件
 */

//聲明接口的變量
private onRecycleViewItemClickListener mOnItemClickListener = null;

public interface onRecycleViewItemClickListener {

    void onItemClick(View view, int position);
}

public void setOnItemClickListener(onRecycleViewItemClickListener onItemClickListener) {
    this.mOnItemClickListener = onItemClickListener;
}

2.在MyAdapter類中,通過實現view的onClickListener方法,重寫它的onClick()方法;

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> implements View.OnClickListener {

....

@Override
public void onClick(View view) {
    
    //在該處先判空。
    if (mOnItemClickListener != null) {
        //通過getTag的方式獲取到position
        mOnItemClickListener.onItemClick(view, (Integer) view.getTag());
    }
}

3.在Adapter的實現方法:onCreateViewHolder()中給view添加點擊事件,onBindViewHolder()通過setTag的方式設置item的position。

@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    view = View.inflate(parent.getContext(), R.layout.item_adapter, null);

    //給新創建的view注冊點擊事件
    view.setOnClickListener(this);

    return new MyViewHolder(view);
}

@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
    holder.setData(position);
    //將position保存在viewItem的tag中,以便在點擊的時候獲取。
    holder.itemView.setTag(position);
}

4.在MainActivity中,通過給adapter設置暴露出來的setOnItemClickListener()方法,來響應點擊事件。

 //給item設置點擊事件
    mAdapter.setOnItemClickListener(new MyAdapter.onRecycleViewItemClickListener() {
        @Override
        public void onItemClick(View view, int position) {
            Toast.makeText(MainActivity.this,"第"+position+"個item被點擊了",Toast.LENGTH_SHORT).show();
        }
    });

給recyclerView的item添加分割線:

recyclerView有提供添加分割線的方法:通過addItemDecoration()實現。

    //添加分割線
    mRecyclerView.addItemDecoration(new RecycleViewItemDecoration());

2.自定義類RecycleViewItemDecoration繼承RecyclerView.ItemDecoration,并實現它的兩個方法getItemOffsets()和onDraw()方法。

/**
 * 設置item的分割線
 */
private class RecycleViewItemDecoration extends RecyclerView.ItemDecoration {

    private Paint mPaint;
    public RecycleViewItemDecoration() {
        //構建畫筆
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.GRAY);
    }

    /**
     * 實現兩個方法
     * 確定分割線的位置
     * @param outRect
     * @param view
     * @param parent
     * @param state
     */
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        //先確定位置,在每個item的底部留出5px的位置
        int position = parent.getChildAdapterPosition(view);
        //判斷當前item不是第0個位置,就在item的頂部繪制分割線
        if(position != 0) {
            outRect.top = 5;
        }
    }

    /**
     * 繪制分割線,用Canvas在每一個item的頭部繪制
     * @param c
     * @param parent
     * @param state
     */
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {

        int childCount = parent.getChildCount();

        //指定繪畫的矩形區域
        Rect rect = new Rect();
        rect.left = parent.getPaddingLeft();
        rect.right = parent.getWidth() - parent.getPaddingRight();
        for (int i = 1; i < childCount; i++) {
            //分割線的底部是itemView的頂部
            rect.bottom = parent.getChildAt(i).getTop();
            rect.top = rect.bottom - 5;
            c.drawRect(rect,mPaint);
        }
    }
}

給RecyclerView添加頭部和底部:

listview添加頭部和底部有具體的方法實現,通過addHeaderView()和addFooterView()兩個方法就能實現。而recyclerView沒有提供具體的方法實現,需要自定義來實現。通過對listview添加頭部的方法addHeaderView()的源碼進行分析,發現它是在對Adapter進行判空處理之后,判斷當前的listview是否已經添加Adapter,然后在原來的adapter的基礎上進行了一層包裝,其實listview本身也是不支持添加頭部和底部的,只是系統為我們設定好了。最主要是需要 清楚HeaderViewListAdapter是如何實現的。

public void addHeaderView(View v, Object data, boolean isSelectable) {
     //第一步 數據的初始化和賦值 并添加到頭部視圖list中
    final FixedViewInfo info = new FixedViewInfo();
    info.view = v;
    info.data = data;
    info.isSelectable = isSelectable;
    //添加頭部集合
    mHeaderViewInfos.add(info);
    mAreAllItemsSelectable &= isSelectable;

    // Wrap the adapter if it wasn't already wrapped.
    if (mAdapter != null) {
        //第二步 關鍵點
        //給mAdapter賦值成HeaderViewListAdapter對象 并把head、foot視圖列表和原來的adapter傳進去 
        //所以這里就是關鍵 從這里進入看HeaderViewListAdapter類的源碼
        if (!(mAdapter instanceof HeaderViewListAdapter)) {
            mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
        }
        //第三步 被觀察者發出修改通知
        // In the case of re-adding a header view, or adding one later on,
        // we need to notify the observer.
        if (mDataSetObserver != null) {
            mDataSetObserver.onChanged();
        }
    }
}

接下來,就自己實現一下,實現的基本原理是通過getItemViewType()方法返回不同的類型來添加頭部和底部。
貼上實現該功能的整段代碼,對上面的代碼塊有部分改動:

public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements View.OnClickListener {

    private View view;
    private Context mContext;
    private LayoutInflater mLayoutInflater;
    //item類型
    public static final int ITEM_TYPE_HEADER = 0;
    public static final int ITEM_TYPE_CONTENT = 1;
    public static final int ITEM_TYPE_BOTTOM = 2;
    //頭部View個數
    private int mHeaderCount = 1;
    //底部View個數
    private int mBottomCount = 1;

    int icons[] = {R.drawable.g1, R.drawable.g2, R.drawable.g3, R.drawable.g4, R.drawable.g5, R.drawable.g6, R.drawable.g7, R.drawable.g9,
            R.drawable.g10, R.drawable.g11, R.drawable.g12, R.drawable.g13, R.drawable.g14, R.drawable.g15, R.drawable.g16, R.drawable.g17, R.drawable.g18, R.drawable.g19,
            R.drawable.g20, R.drawable.g21, R.drawable.g22, R.drawable.g23, R.drawable.g24, R.drawable.g25, R.drawable.g26, R.drawable.g27, R.drawable.g28, R.drawable.g29,};

    String names[] = {"瀏覽器", "輸入法", "健康", "效率", "教育", "理財",
            "閱讀", "個性化", "購物", "資訊", "生活", "工具", "出行", "通訊", "拍照", "社交",
            "影音", "安全", "休閑", "棋牌", "益智", "射擊", "體育", "兒童", "網游", "角色", "策略",
            "經營", "競速"};

    public MyAdapter(Context context) {
        this.mContext = context;
        mLayoutInflater = LayoutInflater.from(context);
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        if (viewType == ITEM_TYPE_HEADER) {
            return new HeaderViewHolder(mLayoutInflater.inflate(R.layout.rv_headerview, parent, false));
        } else if (viewType == mHeaderCount) {
            view = mLayoutInflater.inflate(R.layout.item_adapter, parent, false);
            view.setOnClickListener(this);
            return new MyViewHolder(view);
        } else if (viewType == ITEM_TYPE_BOTTOM) {
            return new BottomViewHolder(mLayoutInflater.inflate(R.layout.rv_footerview, parent, false));
        }
        return null;

    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

        if (holder instanceof HeaderViewHolder) {

        } else if (holder instanceof MyViewHolder) {
            ((MyViewHolder) holder).setData(position);
            //將position保存在viewItem的tag中,以便在點擊的時候獲取。
            holder.itemView.setTag(position);
        } else if (holder instanceof BottomViewHolder) {

        }
    }

    @Override
    public int getItemCount() {
        return getContentItemCount() + mHeaderCount + mBottomCount;
    }

    //填充內容的長度
    public int getContentItemCount() {
        return names.length;
    }

    //判斷當前item類型
    @Override
    public int getItemViewType(int position) {
        int dataItemCount = getContentItemCount();
        if (mHeaderCount != 0 && position < mHeaderCount) {
            //頭部View
            return ITEM_TYPE_HEADER;
        } else if (mBottomCount != 0 && position >= (mHeaderCount + dataItemCount)) {
            //底部View
            return ITEM_TYPE_BOTTOM;
        } else {
            //內容View
            return ITEM_TYPE_CONTENT;
        }
    }

    /**
     * 這兩個方法是用來判斷當前item是頭部還是底部,在使用gridLayoutManager的時候調用
     * @param position
     * @return
     */
    public boolean isHeaderView(int position) {
        return mHeaderCount != 0 && position < mHeaderCount;
    }

    public boolean isBottomView(int position) {
        return mBottomCount != 0 && position >= (mHeaderCount + getContentItemCount());
    }

    /**
     * 自定義接口,給item設置點擊事件
     */

    //聲明接口的變量
    private onRecycleViewItemClickListener mOnItemClickListener = null;

    public interface onRecycleViewItemClickListener {

        void onItemClick(View view, int position);
    }

    public void setOnItemClickListener(onRecycleViewItemClickListener onItemClickListener) {
        this.mOnItemClickListener = onItemClickListener;
    }

    @Override
    public void onClick(View view) {

        if (mOnItemClickListener != null) {
            //通過getTag的方式獲取到position
            mOnItemClickListener.onItemClick(view, (Integer) view.getTag());
        }
    }

    /**
     * 設置viewHolder
     */
    
    //中間內容的viewHolder
    public class MyViewHolder extends RecyclerView.ViewHolder {

        TextView tvName;
        ImageView ivImage;

        public MyViewHolder(View itemView) {
            super(itemView);
            //初始化控件
            tvName = (TextView) view.findViewById(R.id.item_textview);
            ivImage = (ImageView) view.findViewById(R.id.item_image);
        }

        public void setData(int position) {
            //給控件賦值
            tvName.setText(names[position % names.length]);
            ivImage.setImageDrawable(mContext.getResources().getDrawable(icons[position % icons.length]));
        }
    }

    //頭部的ViewHolder
    public static class HeaderViewHolder extends RecyclerView.ViewHolder {
        public HeaderViewHolder(View itemView) {
            super(itemView);
        }
    }

    //底部的ViewHolder
    public static class BottomViewHolder extends RecyclerView.ViewHolder {
        public BottomViewHolder(View itemView) {
            super(itemView);
        }
    }
}

在activity當中如果RecyclerView使用Grid類型列表在設置Adapter后需要調用這個方法mLayoutManager.setSpanSizeLookup(),根據當前Item類型來判斷占據的橫向格數。

    /**
     * 如果RecyclerView使用Grid類型列表在設置Adapter后需要調用這個方法,
     * 根據當前Item類型來判斷占據的橫向格數
     */
    mLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
        @Override
        public int getSpanSize(int position) {
            return (mAdapter.isHeaderView(position) ||
                    mAdapter.isBottomView(position)) ? mLayoutManager.getSpanCount() : 1;
        }
    });

RecyclerView實現列表的上拉加載和下拉刷新:

要實現列表的下拉刷新可以使用Android提供的swipeRefreshLayout來實現下拉刷新或者上拉加載,也可以自定義上拉加載和下拉刷新,
使用swipeRefreshLayout來進行控件的下拉刷新。需要在布局文件中對RecyclerView進行包裹。

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/srf_refresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycle_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbars="vertical">

        </android.support.v7.widget.RecyclerView>
    </android.support.v4.widget.SwipeRefreshLayout>

接下來就是activity中的代碼實現,需要先找到控件,然后刷新的監聽方法,該處使用了handler來模擬網絡請求操作。

//利用swipeRefreshLayout設置下拉刷新
private void setPullToRefresh() {
    //設置圈圈的顏色
    mRefreshLayout.setColorSchemeColors(Color.RED,Color.BLUE,Color.BLACK);
    //設置圈圈的顏色,里面放的是顏色的資源值
    // refreshLayout.setColorSchemeColors(R.color.cardview_dark_background,R.color.cardview_light_background);

    mRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
        @Override
        public void onRefresh() {
            Toast.makeText(MainActivity.this,"正在拼命加載中...",Toast.LENGTH_SHORT).show();

            Handler handler = new Handler();
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    //停止刷新
                    mRefreshLayout.setRefreshing(false);
                }
            },3000);
        }
    });
}   

SwipeRefreshLayout里面需要注意的Api:
1)setOnRefreshListener(OnRefreshListener listener) 設置下拉監聽,當用戶下拉的時候會去執行回調
2)setColorSchemeColors(int... colors) 設置 進度條的顏色變化,最多可以設置4種顏色
3)setProgressViewOffset(boolean scale, int start, int end) 調整進度條距離屏幕頂部的距離
4)setRefreshing(boolean refreshing) 設置SwipeRefreshLayout當前是否處于刷新狀態,一般是在請求數據的時候設置為true,在數據被加載到View中后,設置為false。

總結:

到此關于RecyclerView的一些常見使用方式就介紹完了,總的來說RecyclerView的使用比起listview還是要便捷的多。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容