那年的RecyclerView我們從頭擼一下
1. RecyclerView是什么?
根據(jù)Google官方給出的說(shuō)明:A flexible view for providing a limited window into a large data set。能夠在有限的窗口中展示大數(shù)據(jù)集合的靈活視圖。
所以我們能夠理解為,RecyclerView的一個(gè)恰當(dāng)?shù)氖褂脠?chǎng)景是:由于尺寸限制,用戶的設(shè)備不能一次性展現(xiàn)所有條目,用戶需要上下滾動(dòng)以查看更多條目。滾出可見(jiàn)區(qū)域的條目將被回收,并在下一個(gè)條目可見(jiàn)的時(shí)候被復(fù)用。
對(duì)于減少內(nèi)存開(kāi)銷(xiāo)和CPU的計(jì)算,緩存條目是一個(gè)非常有用的方法,因?yàn)檫@意味著我們不必每次都創(chuàng)建新的條目,從而減小內(nèi)存開(kāi)銷(xiāo)和CPU的計(jì)算,而且還能夠有效降低屏幕的卡頓,保證滑動(dòng)的順滑。
RecyclerView不關(guān)心視覺(jué)效果(visuals)
但是和ListView有什么區(qū)別呀?我們已經(jīng)使用ListView很長(zhǎng)一段時(shí)間了呀,它一樣可以做到呀。從它的類(lèi)名上看,RecyclerView代表的意義是,我只管Recycler View,也就是說(shuō)RecyclerView只管回收與復(fù)用View,其他的開(kāi)發(fā)者可以自己去設(shè)置。你想要另一個(gè)布局?插入另一個(gè)LayoutManager。你想要不同的動(dòng)畫(huà)嗎?插入一個(gè)ItemAnimator,等等。可以看出其高度的解耦,給予你充分的定制自由(所以你才可以輕松的通過(guò)這個(gè)控件實(shí)現(xiàn)ListView,GirdView,瀑布流等效果)。
2. 引入RecyclerView
RecyclerView 是Support Library的一部分。所以只需要在app/build.gradle中添加以下依賴,便能立即使用:
- dependencies { compile 'com.android.support:recyclerview-v7:25.3.1'}
在布局文件中加入:
- <android.support.v7.widget.RecyclerView
- android:id="@+id/recycler_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
然后在頁(yè)面中引入RecyclerView即可:
- RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
OK,從現(xiàn)在開(kāi)始,讓我們一步一步,開(kāi)始了解它。
3. 使用RecyclerView
下面的表格中就是使用RecyclerView來(lái)顯示數(shù)據(jù)中用到的幾個(gè)最重要的類(lèi),這些類(lèi)都是RecyclerView的內(nèi)部類(lèi),如果你想使用RecyclerView,需要做以下操作:
Class | 功能 |
---|---|
Adapter | 處理數(shù)據(jù)集合并負(fù)責(zé)綁定視圖 |
ViewHolder | 持有所有的用于綁定數(shù)據(jù)或者需要操作的View |
LayoutManager | 負(fù)責(zé)擺放視圖等相關(guān)操作 |
ItemDecoration | 負(fù)責(zé)繪制Item附近的分割線 |
ItemAnimator | 為Item的一般操作添加動(dòng)畫(huà)效果,如,增刪條目等 |
我們可以從下圖更直觀的了解到RecyclerView的基本結(jié)構(gòu):
接下來(lái),我將要描述每個(gè)類(lèi)或接口的內(nèi)容以及如何使用它。
3.1 RecyclerView.ViewHolder
ViewHolder的基本用法是用來(lái)存放View對(duì)象。Android團(tuán)隊(duì)很早之前就推薦使用“ViewHolder設(shè)計(jì)模式”,但是沒(méi)有要求開(kāi)發(fā)者在Adapter中必須使用ViewHolder模式。那么現(xiàn)在對(duì)于這種新型的RecyclerView.Adapter,我們必須實(shí)現(xiàn)并使用這種模式。
Google官方等了這么長(zhǎng)時(shí)間才強(qiáng)制使用ViewHolder模式,這有點(diǎn)奇怪,但遲做總比不做好。如果您不了解ViewHolder模式,請(qǐng)查看一下Android training Hold View Objects in a View Holder。另外網(wǎng)上有大量關(guān)于ListView優(yōu)化的文章。面試重點(diǎn)。
有一件事是專(zhuān)門(mén)針對(duì)RecyclerView的。ViewHolder子類(lèi)可以通過(guò)訪問(wèn)公共成員itemView來(lái)訪問(wèn)ViewHolder的根視圖。所以不需要在ViewHolder子類(lèi)中存儲(chǔ)。
下面是示例的ViewHolder的代碼,ViewHolder是示例Adapter的內(nèi)部類(lèi):
- public static class MyViewHolder extends RecyclerView.ViewHolder{
- TextView tv;
- public MyViewHolder(View itemView) {
- super(itemView);
- tv = (TextView) itemView.findViewById(R.id.tv);
- }
- }
3.2 Adapter
Adapter扮演著兩個(gè)角色。一是,根據(jù)不同ViewType創(chuàng)建與之相應(yīng)的的Item-Layout,二是,訪問(wèn)數(shù)據(jù)集合并將數(shù)據(jù)綁定到正確的View上。這就需要我們重寫(xiě)以下3個(gè)方法:
- public VH onCreateViewHolder(ViewGroup parent, int viewType) 創(chuàng)建Item視圖,并返回相應(yīng)的ViewHolder
- public void onBindViewHolder(VH holder, int position) 綁定數(shù)據(jù)到正確的Item視圖上。
- public int getItemCount() 返回該Adapter所持有的item數(shù)量
示例代碼如下所示:
- @Override
- public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- MyViewHolder holder = new MyViewHolder(LayoutInflater.from(context)
- .inflate(R.layout.item_recycler_view,parent,false));
- return holder;
- }
- @Override
- public void onBindViewHolder(MyViewHolder holder, int position) {
- holder.tv.setText(mDatas.get(position));
- }
- @Override
- public int getItemCount() {
- return mDatas.size();
- }
因此,一個(gè)基本的RecyclerView.Adapter如下:
- public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.MyViewHolder> {
- private Context context;
- private List<String> mDatas;
- public RecyclerAdapter(Context context, List<String> mDatas) {
- this.context = context;
- this.mDatas = mDatas;
- }
- @Override
- public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- MyViewHolder holder = new MyViewHolder(LayoutInflater.from(context)
- .inflate(R.layout.item_recycler_view, parent, false));
- return holder;
- }
- @Override
- public void onBindViewHolder(MyViewHolder holder, int position) {
- holder.tv.setText(mDatas.get(position));
- }
- @Override
- public int getItemCount() {
- return mDatas == null ? 0 : mDatas.size();
- }
- public static class MyViewHolder extends RecyclerView.ViewHolder {
- TextView tv;
- public MyViewHolder(View itemView) {
- super(itemView);
- tv = (TextView) itemView.findViewById(R.id.tv);
- }
- }
- }
3.3 RecyclerView.LayoutManager
LayoutManager的職責(zé)是擺放Item的位置,并且負(fù)責(zé)決定何時(shí)回收和重用Item。它有一個(gè)默認(rèn)的實(shí)現(xiàn):LinearLayoutManager,它可以用于垂直和水平列表。
RecyclerView.LayoutManager是一個(gè)抽象類(lèi),RecyclerView為我們提供3個(gè)實(shí)現(xiàn)類(lèi):
- LinearLayoutManager 現(xiàn)行管理器,支持橫向、縱向。
- GridLayoutManager 網(wǎng)格布局管理器
- StaggeredGridLayoutManager 瀑布流式布局管理器
3.3.1 LinearlayoutManager
LinearlayoutManager是LayoutManager的默認(rèn)實(shí)現(xiàn)。你可以使用這個(gè)類(lèi)來(lái)創(chuàng)建垂直或水平列表。
- // 設(shè)置RecyclerView布局方式為縱向布局
- LinearLayoutManager layoutManager = new LinearLayoutManager(context);
- layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
- recyclerView.setLayoutManager(layoutManager);
- // 設(shè)置RecyclerView布局方式為橫向布局
- LinearLayoutManager layoutManager= new LinearLayoutManager(this);
- layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
- recyclerView.setLayoutManager(layoutManager);
運(yùn)行程序,我們看到如下效果:
我們發(fā)現(xiàn)和ListView有一些不同,沒(méi)有分割線讓這個(gè)列表看起來(lái)很不美觀,給RecyclerView設(shè)置分割線這個(gè)我們?cè)谙乱恍」?jié)說(shuō)明。
當(dāng)然LinearlayoutManager中還有一些很實(shí)用的API:
- findFirstVisibleItemPosition() 返回當(dāng)前***個(gè)可見(jiàn)Item的position
- findFirstCompletelyVisibleItemPosition() 返回當(dāng)前***個(gè)完全可見(jiàn)Item的position
- findLastVisibleItemPosition() 返回當(dāng)前***一個(gè)可見(jiàn)Item的position
- findLastCompletelyVisibleItemPosition() 返回當(dāng)前***一個(gè)完全可見(jiàn)Item的position
3.3.2 GridLayoutManager
有兩個(gè)構(gòu)造方法:
- /**
- * Creates a vertical GridLayoutManager
- *
- * @param context Current context, will be used to access resources.
- * @param spanCount 設(shè)置行數(shù),因?yàn)槟J(rèn)是縱向的
- */public GridLayoutManager(Context context, int spanCount) {
- super(context);
- setSpanCount(spanCount);
- }/**
- * @param context Current context, will be used to access resources.
- * @param spanCount 設(shè)置行數(shù)或者列數(shù),根據(jù)方向
- * @param orientation 布局方向 HORIZONTAL or VERTICAL.
- * @param reverseLayout 是否反向顯示 When set to true, layouts from end to start.
- */public GridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) {
- super(context, orientation, reverseLayout);
- setSpanCount(spanCount);
- }
我們按如下代碼設(shè)置:
- // 設(shè)置縱向的4列的表格布局GridLayoutManager layoutManager = new GridLayoutManager(this,4,GridLayoutManager.VERTICAL,false);
- recyclerView.setLayoutManager(layoutManager);
- // 設(shè)置橫向的3行的表格布局GridLayoutManager layoutManager = new GridLayoutManager(this,3,GridLayoutManager.HORIZONTAL,false);
- recyclerView.setLayoutManager(layoutManager);
3.3.3 StaggeredGridLayoutManager
我們來(lái)看StaggeredGridLayoutManager的構(gòu)造方法,只有一個(gè)方法
- /**
- * Creates a StaggeredGridLayoutManager with given parameters.
- *
- * @param spanCount 設(shè)置行數(shù)或者列數(shù),根據(jù)方向,縱向就是列數(shù),橫向就是行數(shù)
- * @param orientation 方向
- */
- public StaggeredGridLayoutManager(int spanCount, int orientation) {}
因此我們這樣設(shè)置:
- // 設(shè)置縱向的瀑布流布局
- StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(4,StaggeredGridLayoutManager.VERTICAL);
- recyclerView.setLayoutManager(layoutManager);
我們看效果和GridLayoutManager沒(méi)有什么區(qū)別呢,那我們來(lái)小小的修改一下,你就可以看到它的強(qiáng)大。
我們給每個(gè)item的布局加入margin:
- <?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical" android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_margin="5dp"
- android:background="@color/colorAccent">
- <TextView
- android:id="@+id/tv"
- android:layout_width="100dp"
- android:layout_height="100dp"
- android:layout_gravity="center"
- android:gravity="center"
- android:textColor="#fff"
- /></LinearLayout>
然后我們?cè)谶m配器的onBindViewHolder方法中為我們的item設(shè)置個(gè)隨機(jī)的高度:
- //貼上部分代碼,其余的請(qǐng)看附件
- private List<Integer> mHeights;
- public RecyclerAdapter(Context context, List<String> mDatas) {
- this.context = context;
- this.mDatas = mDatas;
- mHeights = new ArrayList<Integer>();
- for (int i = 0; i < mDatas.size(); i++) {
- mHeights.add((int) (100 + Math.random() * 300));
- }
- }
- @Override
- public void onBindViewHolder(MyViewHolder holder,
- final int position) {
- ViewGroup.LayoutParams lp = holder.tv.getLayoutParams();
- lp.height = mHeights.get(position);
- holder.tv.setLayoutParams(lp);
- }
運(yùn)行,我們可以看到效果,是不是很炫!
3.4 RecyclerView.ItemDecoration
通過(guò)設(shè)置
- recyclerView.addItemDecoration(new DividerDecoration(Context context, int orientation));
來(lái)改變Item之間的偏移量或者對(duì)Item進(jìn)行裝飾。
例如,在上面的設(shè)置LinearlayoutManager之后加入以下代碼,那么列表的效果就會(huì)發(fā)生改變:
- // 給縱向顯示RecyclerView設(shè)置分割線
- recyclerView.addItemDecoration(new DividerItemDecoration(activity,DividerItemDecoration.VERTICAL));
- // 給橫向顯示RecyclerView設(shè)置分割線
- recyclerView.addItemDecoration(new DividerItemDecoration(activity,DividerItemDecoration.HORIZONTAL));
當(dāng)然,你也可以對(duì)RecyclerView設(shè)置多個(gè)ItemDecoration,列表展示的時(shí)候會(huì)遍歷所有的ItemDecoration并調(diào)用里面的繪制方法,對(duì)Item進(jìn)行裝飾。
RecyclerView.ItemDecoration是一個(gè)抽象類(lèi),可以通過(guò)重寫(xiě)以下三個(gè)方法,來(lái)實(shí)現(xiàn)Item之間的偏移量或者裝飾效果:
- public void onDraw(Canvas c, RecyclerView parent) 裝飾的繪制在Item條目繪制之前調(diào)用,所以這有可能被Item的內(nèi)容所遮擋
- public void onDrawOver(Canvas c, RecyclerView parent) 裝飾的繪制在Item條目繪制之后調(diào)用,因此裝飾將浮于Item之上
- public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) 與padding或margin類(lèi)似,LayoutManager在測(cè)量階段會(huì)調(diào)用該方法,計(jì)算出每一個(gè)Item的正確尺寸并設(shè)置偏移量。
當(dāng)然了,如果我們使用GridLayoutManager后,對(duì)于分割線,前面的DividerItemDecoration就不適用了,主要是因?yàn)樗诶L制的時(shí)候,比如水平線,針對(duì)每個(gè)child的取值為:
- final int left = parent.getPaddingLeft();
- final int right = parent.getWidth() - parent.getPaddingRight();
因?yàn)槊總€(gè)Item一行,這樣是沒(méi)問(wèn)題的。而GridLayoutManager時(shí),一行有多個(gè)childItem,這樣就多次繪制了,并且GridLayoutManager時(shí),Item如果為***一列(則右邊無(wú)間隔線)或者為***一行(底部無(wú)分割線)。
按照上述的方法,我們可以自定義一個(gè)DividerGridItemDecoration。由于代碼篇幅過(guò)長(zhǎng),我們?cè)诟郊薪o出。我們給GridLayoutManager設(shè)置DividerGridItemDecoration,運(yùn)行效果如下:
3.5 RecyclerView.ItemAnimator
ItemAnimator能夠幫助Item實(shí)現(xiàn)獨(dú)立的動(dòng)畫(huà)。
ItemAnimator作觸發(fā)于以下三種事件:
- 某條數(shù)據(jù)被插入到數(shù)據(jù)集合中
- 從數(shù)據(jù)集合中移除某條數(shù)據(jù)
- 更改數(shù)據(jù)集合中的某條數(shù)據(jù)
幸運(yùn)的是,在Android中默認(rèn)實(shí)現(xiàn)了一個(gè)DefaultItemAnimator,我們可以通過(guò)以下代碼為Item增加動(dòng)畫(huà)效果:
- recyclerView.setItemAnimator(new DefaultItemAnimator());
在之前的版本中,當(dāng)時(shí)據(jù)集合發(fā)生改變時(shí),我們通過(guò)調(diào)用.notifyDataSetChanged(),來(lái)刷新列表,因?yàn)檫@樣做會(huì)觸發(fā)列表的重繪,所以并不會(huì)出現(xiàn)任何動(dòng)畫(huà)效果,因此需要調(diào)用一些以notifyItem*()作為前綴的特殊方法,比如:
- public final void notifyItemInserted(int position) 向指定位置插入Item
- public final void notifyItemRemoved(int position) 移除指定位置Item
- public final void notifyItemChanged(int position) 更新指定位置Item
下面我們使用DefaultItemAnimator來(lái)展示一下動(dòng)畫(huà)效果,修改代碼如下:
在Activity中添加兩個(gè)按鈕add和remove負(fù)責(zé)動(dòng)態(tài)插入和移除item
- btnAdd.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- adapter.addData(1);
- }
- });
- btnRemove.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- adapter.removeData(2);
- }
- });
在Adapter中添加兩個(gè)方法:
- public void addData(int position) {
- mDatas.add(position, "Insert");
- mHeights.add( (int) (100 + Math.random() * 300));
- notifyItemInserted(position);
- }public void removeData(int position) {
- mDatas.remove(position);
- notifyItemRemoved(position);
- }
運(yùn)行結(jié)果如下:
3.6 Listeners
很遺憾,RecyclerView并沒(méi)有像ListView那樣提供以下兩個(gè)Item的點(diǎn)擊監(jiān)聽(tīng)事件
- public void setOnItemClickListener(@Nullable OnItemClickListener listener) Item點(diǎn)擊事件監(jiān)聽(tīng)
- public void setOnItemLongClickListener(OnItemLongClickListener listener) Item長(zhǎng)按事件監(jiān)聽(tīng)
但是這并不能阻攔我們的腳步,我可以定義這樣的兩個(gè)方法。
- public interface OnItemClickLitener {
- void onItemClick(View view, int position);
- void onItemLongClick(View view, int position);
- }
- private OnItemClickLitener litener;
- public void setLitener(OnItemClickLitener litener) {
- this.litener = litener;
- }
在onBindViewHolder方法中給需要響應(yīng)點(diǎn)擊事件的控件設(shè)置監(jiān)聽(tīng)器:
- // 如果設(shè)置了回調(diào),則響應(yīng)點(diǎn)擊事件holder.itemView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if (litener != null) {
- litener.onItemClick(v, position);
- }
- }
- });
- holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
- @Override
- public boolean onLongClick(View v) {
- if (litener != null) {
- litener.onItemLongClick(v, position);
- } return false;
- }
- });
在Activity中設(shè)置這個(gè)方法:
- adapter.setLitener(new RecyclerAdapter.OnItemClickLitener() {
- @Override
- public void onItemClick(View view, int position) {
- Toast.makeText(activity,"第"+position+"項(xiàng)被點(diǎn)擊了",Toast.LENGTH_SHORT).show();
- }
- @Override
- public void onItemLongClick(View view, int position) {
- Toast.makeText(activity,"第"+position+"項(xiàng)被長(zhǎng)按了",Toast.LENGTH_SHORT).show();
- }
- });
效果如圖: