RecyclerView 和 ListView 使用對比分析

原文地址:http://blog.coderclock.com/2016/08/08/android/RecyclerView%20%E5%92%8C%20ListView%20%E4%BD%BF%E7%94%A8%E5%AF%B9%E6%AF%94%E5%88%86%E6%9E%90/html

今天這篇文章主要是向你們介紹 RecyclerView 和 ListView 的使用對比,文章主要包括如下幾點的內容:android

  • RecyclerView 和 ListView 佈局效果的對比
  • RecyclerView 和 ListView 一些經常使用的功能 和 API 的對比
  • RecyclerView 和 ListView 在 Android L 引入嵌套滾動機制以後的對比

有一點須要強調下,文中全部的效果在真機上都是很流暢的,由於錄製 GIF 圖很容易掉幀,因此特意放慢了操做,千萬不要誤會成卡頓了啊!git

佈局效果對比

做爲一枚控件,要引發開發者使用的慾望天然先是從顯示效果看起(看臉的世界),ListView 你們對效果已經很熟悉了,這裏直接跳過,而做爲 RecyclerView,它能帶給效果要比 ListView 強大得多,以下圖github

Android 默認提供的 RecyclerView 就能支持 線性佈局網格佈局瀑布流佈局 三種(這裏咱們暫且不提代碼細節,後文再說),並且同時還可以控制橫向仍是縱向滾動。怎樣,從效果上足以碾壓 ListView 有木有。數組

  • 橫向滾動的ListView開源控件是否是能夠不用再找了?對,你沒看錯!
  • 瀑布流效果的開源控件是否是能夠不用再找了?對,你沒看錯!
  • 連橫向滾動的GridView都不用找了!對,你沒看錯!

到此,展現效果上的差距一目瞭然。ide

API 使用對比

固然,一個控件咱們不能徹底只看效果,關鍵仍是要看實用性,看看有沒有方便咱們調用的 API提升咱們的開發效率。因此,接下來咱們就從各個方面來看看 RecyclerView 和 ListView 在提供的API調用上的一些實踐比較。函數

基礎使用

ListView 的基礎使用你們再熟悉不過,其使用的關鍵點主要以下:工具

  • 繼承重寫 BaseAdapter 類
  • 自定義 ViewHolder 和 convertView 一塊兒完成複用優化工做

因爲 ListView 已經老生常談,因此此處就不去寫示例代碼了。 RecyclerView 基礎使用關鍵點一樣有兩點:佈局

  • 繼承重寫 RecyclerView.Adapter 和 RecyclerView.ViewHolder
  • 設置佈局管理器,控制佈局效果

示例代碼大體以下:學習

// 第一步:繼承重寫 RecyclerView.Adapter 和 RecyclerView.ViewHolder
public class AuthorRecyclerAdapter extends RecyclerView.Adapter<AuthorRecyclerAdapter.AuthorViewHolder> { ... @Override public AuthorViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { ... return viewHolder; } @Override public void onBindViewHolder(AuthorViewHolder holder, int position) { ... } @Override public int getItemCount() { if (mData == null) { return 0; } return mData.size(); } class AuthorViewHolder extends RecyclerView.ViewHolder { ... public AuthorViewHolder(View itemView) { super(itemView); ... } } } mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view); mRecyclerAdapter = new AuthorRecyclerAdapter(mData); // 第二步:設置佈局管理器,控制佈局效果
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(RecyclerDemoActivity.this); linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); mRecyclerView.setLayoutManager(linearLayoutManager); mRecyclerView.setAdapter(mRecyclerAdapter);

ViewHolder 的編寫規範化了從基礎使用上看,咱們明顯能夠看出,RecyclerView 相比 ListView 在基礎使用上的區別主要有以下幾點:

  • RecyclerView 複用 Item 的工做 Google 全幫你搞定,再也不須要像 ListView 那樣本身調用 setTag
  • RecyclerView 須要多出一步 LayoutManager 的設置工做

佈局效果

在最開始就提到,RecyclerView 可以支持各類各樣的佈局效果,這是 ListView 所不具備的功能,那麼這個功能如何實現的呢?其核心關鍵在於 RecyclerView.LayoutManager 類中。從前面的基礎使用能夠看到,RecyclerView 在使用過程當中要比 ListView 多一個 setLayoutManager 步驟,這個 LayoutManager 就是用於控制咱們 RecyclerView 最終的展現效果的。

而 LayoutManager 只是一個抽象類而已,系統已經爲咱們提供了三個相關的實現類 LinearLayoutManager(線性佈局效果)GridLayoutManager(網格佈局效果)StaggeredGridLayoutManager(瀑布流佈局效果)。若是你想用 RecyclerView 來實現本身 YY 出來的一種效果,則應該去繼承實現本身的 LayoutManager,並重寫相應的方法,而不該該想着去改寫 RecyclerView。關於 LayoutManager 的使用有下面一些常見的 API(有些在 LayoutManager 實現的子類中)

canScrollHorizontally();//可否橫向滾動
canScrollVertically();//可否縱向滾動
scrollToPosition(int position);//滾動到指定位置
 setOrientation(int orientation);//設置滾動的方向
getOrientation();//獲取滾動方向
 findViewByPosition(int position);//獲取指定位置的Item View
findFirstCompletelyVisibleItemPosition();//獲取第一個徹底可見的Item位置
findFirstVisibleItemPosition();//獲取第一個可見Item的位置
findLastCompletelyVisibleItemPosition();//獲取最後一個徹底可見的Item位置
findLastVisibleItemPosition();//獲取最後一個可見Item的位置

上面僅僅是列出一些經常使用的 API 而已,更多的 API 能夠查看官方文檔,一般你想用 RecyclerView 實現某種效果,例如指定滾動到某個 Item 位置,可是你在 RecyclerView 中又找不到能夠調用的 API 時,就能夠跑到 LayoutManager 的文檔去看看,基本都在那裏。另外還有一點關於瀑布流佈局效果 StaggeredGridLayoutManager 想說的,看到網上有些文章寫的示例代碼,在設置了 StaggeredGridLayoutManager 後仍要去 Adapter 中動態設置 View 的高度,才能實現瀑布流,這種作法是徹底錯誤的,之因此 StaggeredGridLayoutManager 的瀑布流效果出不來,基本是 item 佈局的 xml 問題以及數據問題致使。若是要在 Adapter 中設置 View 的高度,則徹底違背了 LayoutManager 的設計理念了。

空數據處理

ListView 提供了 setEmptyView 這個 API 來讓咱們處理 Adapter 中數據爲空的狀況,只需輕輕一 set 就能搞定一切。代碼設置和效果以下

mListView = (ListView) findViewById(R.id.listview); mListView.setEmptyView(findViewById(R.id.empty_layout));//設置內容爲空時顯示的視圖

而 RecyclerView 並無提供此類 API,因此,這些工做須要本身來幹。雖然說這類邏輯並不複雜,可是做爲一個有追求的程序猿,能偷懶仍是要想着偷懶的嘛…

HeaderView 和 FooterView

在 ListView 的設計中,存在着 HeaderView 和 FooterView 兩種類型的視圖,而且系統也提供了相應的 API 來讓咱們設置

使用 HeaderView 和 FooterView 的好處在於,當咱們指向在 ListView 的頭部或者底部添加一個 View 的時候(例如:添加一個下拉刷新視圖,底部加載更多視圖),咱們能夠不用影響到 Adapter 的編寫,使用起來至關方便。而到了 RecyclerView 中,翻來翻去你都不會看到相似 addFooterView 、 addFooterView 這種 API,是的,沒錯,壓根就沒有…這也是 RecyclerView 讓我以爲很雞肋的地方,按道理說應該是使用頻率很高的 API,竟然都不給我(一臉懵逼)。那有木有解決方法呢,確定有,系統不給就本身動手豐衣足食唄。我想到的方法比較笨,就是在 Adapter 中提供三種類型(Header,Footer以及普通Item)的 Type 和 View,可是這種方法寫起來很麻煩,對 Adapter 的影響很大,改動的代碼量多而且也容易產生BUG。這裏須要吹一下鴻洋老師的解決方案了,你們能夠看他的文章:優雅的爲RecyclerView添加HeaderView和FooterView 。他的實現思路是經過裝飾者模式來擴充 Adapter 的功能,從而實現添加 HeaderView 和 FooterView,而且不影響 Adapter 的編寫工做,牛逼的是還能支持多個 HeaderView 和 FooterView (雖然我暫時想不到有什麼應用場景,哈哈,不過先記着,之後說不定有用)。這是我目前看到的最同意的方案了,若是你有更 nice 的方案,也歡迎給我留言。

局部刷新

在 ListView 中,說到刷新不少童鞋會記得 notifyDataSetChanged() ,可是說到局部刷新估計有不少童鞋就知道得比較少了。咱們知道在更新了 ListView 的數據源後,須要經過 Adapter 的 notifyDataSetChanged 來通知視圖更新變化,這樣作比較的好處就是調用簡單,壞處就是它會重繪每一個 Item,但實際上並非每一個 Item 都須要重繪。最多見的,例如:朋友圈點贊,點贊只是更新當前點讚的Item,並不須要每一個 Item 都更新。然而 ListView 並無提供局部刷新刷新某個 Item 的 API 給咱們,一樣本身自足,套路大體以下方的 updateItemView:

 

public class AuthorListAdapter extends BaseAdapter { ... @Override public View getView(int position, View convertView, ViewGroup parent) { ... return convertView; } /** * 更新Item視圖,減小沒必要要的重繪 * * @param listView * @param position */
public void updateItemView(ListView listView, int position) { //換算成 Item View 在 ViewGroup 中的 index
int index = position - listView.getFirstVisiblePosition(); if (index >= 0 && index < listView.getChildCount()) { //更新數據
AuthorInfo authorInfo = mAuthorInfoList.get(position); authorInfo.setNickName("Google Android"); authorInfo.setMotto("My name is Android ."); authorInfo.setPortrait(R.mipmap.ic_launcher); //更新單個Item
View itemView = listView.getChildAt(index); getView(position, itemView, listView); } } }

便可實現刷新單個 Item 的效果

RecyclerView.Adapter 則咱們提供了 notifyItemChanged 用於更新單個 Item View 的刷新,咱們能夠省去本身寫局部更新的工做。

實現效果以下

動畫效果

若是你細心觀察上面 ListView 和 RecyclerView 局部更新 Item 的效果,你會發現相比 ListView 而言, RecyclerView 在作局部刷新的時候有一個漸變的動畫效果。這也是 RecyclerView 相對很是值得一提的地方,做爲 ListView 自身並無爲咱們提供封裝好的 API 來實現動畫效果切換。因此,若是要給 ListView 的 Item 加動畫,咱們只能本身經過屬性動畫來操做 Item 的視圖。 Github 也有不少封裝得好好的開源庫給咱們用,如:ListViewAnimations 就封裝了大量的效果供咱們玩耍,童鞋們能夠自行學習一下

ListViewAnimations 主要大體實現方式是經過裝飾者模式來擴充 Adapter ,並結合屬性動畫 Animator 來添加動畫效果。相比之下,RecyclerView 則爲咱們提供了不少基本的動畫 API ,以下方的增刪移改

簡單的調用便可實現相應的效果,用起來方便不少,視覺交互上也會更好些

若是你對動畫效果有追求,以爲系統提供的並不能知足你的需求,也能夠經過相應接口實現本身的動畫效果,方式也很是簡單,繼承 RecyclerView.ItemAnimator 類,並實現相應的方法,再調用 RecyclerView 的 setItemAnimator(RecyclerView.ItemAnimator animator) 方法設置完便可實現自定義的動畫效果。

系統也爲咱們提供了兩個默認的動畫實現:SimpleItemAnimator 和 DefaultItemAnimator。而 RecyclerView 在不手動調用 setItemAnimator 的狀況下,則默認用了內置的 DefaultItemAnimator 。

固然編寫自定義的 ItemAnimator 也是須要必定工做量的,這裏一樣爲你們介紹一個針對 RecyclerView 開源的動畫庫:recyclerview-animators。其內部封裝了大量的動畫效果給供咱們調用。

若是想要學習怎麼寫一個自定義 ItemAnimator ,上面介紹的開源庫的代碼一樣不容錯過。哦,對了,若是談到動畫效果,還有一個很關鍵的類不得不提,那就是 ItemTouchHelper 。

ItemTouchHelper 是系統爲咱們提供的一個用於滑動和刪除 RecyclerView 條目的工具類,用起來也是很是簡單的,大體兩步:

  • 建立 ItemTouchHelper 實例,同時實現 ItemTouchHelper.SimpleCallback 中的抽象方法,用於初始化 ItemTouchHelper
  • 調用 ItemTouchHelper 的 attachToRecyclerView 方法關聯上 RecyclerView 便可

示例代碼大體以下:

//ItemTouchHelper 用於實現 RecyclerView Item 拖曳效果的類
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() { @Override public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { //actionState : action狀態類型,有三類 ACTION_STATE_DRAG (拖曳),ACTION_STATE_SWIPE(滑動),ACTION_STATE_IDLE(靜止)
int dragFlags = makeFlag(ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);//支持上下左右的拖曳
int swipeFlags = makeMovementFlags(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);//表示支持左右的滑動
return makeMovementFlags(dragFlags, swipeFlags);//直接返回0表示不支持拖曳和滑動
} /** * @param recyclerView attach的RecyclerView * @param viewHolder 拖動的Item * @param target 放置Item的目標位置 * @return
*/ @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { int fromPosition = viewHolder.getAdapterPosition();//要拖曳的位置
int toPosition = target.getAdapterPosition();//要放置的目標位置
Collections.swap(mData, fromPosition, toPosition);//作數據的交換
notifyItemMoved(fromPosition, toPosition); return true; } /** * @param viewHolder 滑動移除的Item * @param direction */ @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { int position = viewHolder.getAdapterPosition();//獲取要滑動刪除的Item位置
mData.remove(position);//刪除數據
notifyItemRemoved(position); } }); itemTouchHelper.attachToRecyclerView(mRecyclerView);

雖然代碼中有註釋,但仍是稍稍解釋一下,主要重寫的是 getMovementFlags 、 onMove 、 onSwiped 三個抽象方法,getMovementFlags 用於告訴系統,咱們的 RecyclerView 究竟是支持滑動仍是拖曳。如上面的示例代碼,就是表示着同時支持上下左右四個方向的拖曳和左右兩個方向的滑動效果。若是時滑動,則 onSwiped 會被回調,若是是拖曳 onMove 會被回調。咱們再到其中實現相應的業務操做便可。最終效果以下

想一想咱們之前用 ListView 的時候要怎麼作,RecyclerView 真的爽多了。

監聽 Item 的事件

ListView 爲咱們準備了幾個專門用於監聽 Item 的回調接口,如單擊、長按、選中某個 Item 等

說實話,其實我並不大喜歡這樣的設計,如 setOnItemClickListener ,在咱們不添加 HeaderView 和 FooterView 的時候,咱們能夠經過回調參數中的 position 去拿到數據源列表中對應 Item 的數據。

可是,添加了 HeaderView 和 FooterView 以後就不同了,ListView 會把 HeaderView 和 FooterView 算入 position 內。假設你原先在 onItemClick 回調方法中寫了 mDataList.get(position) 這樣的業務代碼而且這段代碼運行良好許久,但在某天你忽然加了個 HeaderView 後,這段代碼就開始變的有問題了,此時由於 HeaderView 佔用的位置算入了 position 以內,因此 position 的最大值其實是大於 mDataList 包含元素的個數值的,所以代碼會報數組越界的錯誤。固然,咱們能夠去避免這種問題的發生,就是不經過 position 來獲取數據,二是經過回調方法中的 id 。

這樣就不會受到添加 HeaderView 和 FooterView 的影響了,這個 id 的值就是來自咱們編寫好的 Adapter 中的 getItemId 函數中返回的 id,使用 IDE 生成此函數時,默認是返回0,須要將 position 做爲 Item 的 id 返回。

並同時在 onItemClick 中判斷 id 是否值爲 -1,由於 HeaderView 和 FooterView 的返回值就是 -1。前面講到我並不大喜歡 setOnItemClickListener 這種設計,除了由這些因素的影響外,更關鍵的是我的認爲針對 Item 的事件實際上寫在 getView 方法中會更加合適,如 setOnItemClickListener 我更喜歡用在 getView 中爲每一個 convertView 設置 setOnClickListener 的方式去取代它。

而再來看看 RecyclerView ,它並無像 ListView 提供太多關於 Item 的某種事件監聽,惟一的就是 addOnItemTouchListener

API 的名字言簡意賅,就是監聽 Item 的觸摸事件。若是你想要擁有 ListView 那樣監聽某個 Item 的某個操做方法,能夠看看這篇文章 RecyclerView沒法添加onItemClickListener最佳的高效解決方案 ,做者的實現思路就是經過 addOnItemTouchListener 和系統提供的 GestureDetector 手勢判斷結合實現的。不過,我仍是更喜歡原先本身用慣的方式,雖然會被人吐槽 new 出了大量的監聽器,但我的以爲這樣封裝會更好(哈哈,也換你們吐槽這種方式的其餘劣處,看看我是否是須要改改了)。

OK,關於 RecyclerView 和 ListView 一些經常使用的功能和 API 的對比,就大體到此。最後再來談談 Android L 開始以後,對 RecyclerView 和 ListView 的使用存在什麼影響。

嵌套滾動機制

熟悉 Android 觸摸事件分發機制的童鞋確定知道,Touch 事件在進行分發的時候,由父 View 向它的子 View 傳遞,一旦某個子 View 開始接收進行處理,那麼接下來全部事件都將由這個 View 來進行處理,它的 ViewGroup 將不會再接收到這些事件,直到下一次手指按下。而嵌套滾動機制(NestedScrolling)就是爲了彌補這一機制的不足,爲了讓子 View 能和父 View 同時處理一個 Touch 事件。關於嵌套滾動機制(NestedScrolling),實現上相對是比較複雜的,此處就不去拓展說明,其關鍵在於 NestedScrollingChild 和 NestedScrollingParent 兩個接口,以及系統對這兩個接口的實現類NestedScrollingChildHelper 和 NestedScrollingParentHelper 你們能夠查閱相關的資料。可能提及來太抽象了,這裏拿一個簡單的示例效果來講明好了,以下方是用 CollapsingToolbarLayout 和 RecyclerView 搭配的效果:

一開始上面一大塊區域就是 CollapsingToolbarLayout ,下方的列表是 RecyclerView ,固然 RecyclerView 向上滑動時,CollapsingToolbarLayout 可以同時網上收縮,直到只剩下頂部的 Toolbar。之因此可以實現這種效果,就是徹底依賴於嵌套滾動機制,若是沒有這套機制,按照原有的觸摸事件分發邏輯, RecyclerView 內部已經把 Touch 事件消耗掉了,徹底沒法引發頂部的 CollapsingToolbarLayout 產生聯動收縮的效果。咱們能夠查看 RecyclerView 的代碼實現,發現它已經實現了 NestedScrollingChild 接口

若是在其餘代碼佈局都不變的狀況下,咱們把 RecyclerView 替換成 ListView ,則沒法產生上面圖中的動態效果,由於 ListView 並不支持嵌套滾動機制,事件在 ListView 內部已經被消耗且沒法傳遞出來,你們能夠自行嘗試驗證一下。對下方 AppBarLayout 的使用也是同理。

關於 AppBarLayout 和 CollapsingToolbarLayout,它們並非什麼第三方控件,而是 Android 官方提供的 MaterialDesign 設計風格的控件,你們能夠在官方文檔中搜索到它們的資料,若是你用過 Android 原生系統,你能夠在通信錄等系統內置應用看到它們的身影。若是你想使用相似 AppBarLayout 、 CollapsingToolbarLayout 這種須要嵌套滾動的機制才能達到效果的控件,那麼 RecyclerView 將是你的不二之選,由於 ListView 在此根本沒法發揮做用。一樣的,ScrollView 也是不支持嵌套滾動機制,可是你可使用 NestedScrollView 。

總結

這裏只是客觀的去分析一些使用上的差別,並非想突出哪一個控件好哪一個控件很差,你們能夠根據本身的使用場景來選擇是要用 RecyclerView 仍是 ListView,畢竟,合適的纔是最好的。洋洋曬曬寫了一大堆,相信你已經看到很多 RecyclerView 和 ListView 使用上的區別了,不知道有沒有我在文中沒提到的呢,歡迎下方留言。文中全部的項目實踐代碼都在這裏:https://github.com/D-clock/AndroidSystemUiTraining ,須要的同窗能夠自行下載查看。

相關文章
相關標籤/搜索