深入解析Android中的RecyclerView組件
前些日子,組里為了在目前的Android程序里實(shí)現(xiàn)基于ListView子項(xiàng)的動畫效果,希望將最新的RecyclerView引入到程序中,于是我便花了一些時間研究了一下RecyclerView的基本情況。本文算是對這些日子里了解的內(nèi)容做一些匯總。
在網(wǎng)上關(guān)于RecyclerView的基本使用方式已經(jīng)有了比較詳細(xì)介紹,而且其設(shè)計結(jié)構(gòu)也類似于ListView,所以本文將不重點(diǎn)介紹如何使用,在文末的引用中都可以相關(guān)內(nèi)容。這里主要是介紹RecyclerView的基本功能、設(shè)計理念,以及系統(tǒng)提供API的情況。
什么是RecyclerView
RecyclerView是在Android L(也就是后來的Lollipop)中新加入的一種ViewGroup。但因?yàn)樗挂詓upport-v7庫的形式加入到Android系統(tǒng)中,所以不僅僅是Lollipop版本以后的Android系統(tǒng)可以使用,只要系開發(fā)項(xiàng)目中引入這個庫就在任意API級別中使用。不過查閱其文檔,可以感受到RecyclerView是如此的強(qiáng)烈的“還在完善中”的感覺,因?yàn)閷λ慕榻B只有短短的一句話:
A flexible view for providing a limited window into a large data set.
單看這句話,其實(shí)已經(jīng)被經(jīng)常使用的ListView和GridView基本也是滿足這句描述的。而且從單呈現(xiàn)效果來看RecyclerView和ListView、GridView并沒有大多的差別。
不過它的“flexible”并不單單指它可以在ListView和GridView之間隨意互相切換,更在于它可以創(chuàng)造出更多的復(fù)雜的可滾動的視圖,比如水平方向的ListView,或者是Web上很流行的瀑布流式布局(Masonry)。只是大部分布局系統(tǒng)都沒有提供,必須由開發(fā)者自己實(shí)現(xiàn)。
所以RecyclerView的“flexible”:什么都可以做,但什么都要自己做。
RecyclerView和ListView的主要區(qū)別
只要用Android提供的LieanerLayoutManager并配以VERTICAL模式,RecyclerView就可以完美達(dá)到ListView的基本效果。兩者的設(shè)計結(jié)構(gòu)也都是數(shù)據(jù)(Dataset)與視圖(View)分離,然后通過適配器(Adapter)來連接的方式。
但RecyclerView相對ListView來說有以下幾大提升:
強(qiáng)制使用ViewHolder
在ListView性能優(yōu)化方面,Android就推薦使用ViewHolder來減少findViewById()的使用以提高效率。不過對于ListView上的ViewHolder,Android只是建議而非強(qiáng)制使用。不過因?yàn)槭褂肰iewHolder模式太有意義了,所以在RecyclerView中ViewHolder就變成了必須使用的模式,Adapter要求返回的也從普通的View變成了ViewHolder。不過如果實(shí)現(xiàn)時沒有自定義的一些View實(shí)際變量,ViewHolder也依然失去其意義。
沒有OnItemClickListener
ListView從它的父類AdapterView直接繼承了對子項(xiàng)目點(diǎn)擊的響應(yīng),開發(fā)者可以定義自己的OnItemClickListner來接受點(diǎn)擊事件。但這個設(shè)計也造成了一些問題,比如子項(xiàng)內(nèi)部視圖如果設(shè)置了OnClickListener,那么子項(xiàng)目視圖本身并不會知道,從而可能會導(dǎo)致視圖點(diǎn)擊狀態(tài)沒有同步等問題。ReyclerView沒有提供簡便的響應(yīng)子項(xiàng)目被點(diǎn)擊的監(jiān)聽器,雖然它有一個OnItemTouchListener,但在這個接口方法中沒有任何關(guān)于那個子項(xiàng)目被點(diǎn)擊的信息,該接口只是幫助開發(fā)者截獲觸摸事件,對于如何處理,檢測被觸摸目標(biāo)對象都留給了開發(fā)者去完成
視圖與布局分離
ListView做到了數(shù)據(jù)和視圖的分離,RecyclerView在視圖和布局之間再進(jìn)一步分離,于是便有了LayoutManager。RecyclerView負(fù)責(zé)管理視圖的重復(fù)利用,然后將布局方式全權(quán)交給了LayoutManager,通過配置或者切換LayoutManager就可以獲得不同的布局效果。不像ListView被限制在垂直滾動布局。同時RecyclerView還提供了ItemDecoration,在已有的子視圖基礎(chǔ)上還可以添加額外的視圖。比如做一條分割線,在ListView就需要額外占用一個ViewType來提供視圖,現(xiàn)在則不需要在Adapter中加入這些與實(shí)際邏輯業(yè)務(wù)無關(guān)的輔助內(nèi)容。
支持子項(xiàng)目層次的動畫效果
ListView也可以支持子項(xiàng)目層次的動畫效果,在Android Developers的DevBytes頻道里有很多關(guān)于這方面的介紹,不過在看過其實(shí)現(xiàn)之后就會發(fā)現(xiàn)其解決方案是多么的丑陋而冗長。很多時候都是在計算和分析子視圖的位置狀態(tài)。RecyclerView則帶來了非常簡潔的ItemAnimator接口。當(dāng)Adapter中的數(shù)據(jù)發(fā)生“增刪改移”變化,通過調(diào)用Adapter相應(yīng)的方法就可以激活動畫的產(chǎn)生。當(dāng)然開發(fā)者還需要自己實(shí)現(xiàn)具體的ItemAnimator對象來完成所需的動畫效果,但是其清晰的結(jié)構(gòu)和接口已比ListView有極大的進(jìn)步。
RecyclerView的現(xiàn)狀
上邊提到RecyclerView的套件已經(jīng)加入了豪華Support Library v7,而且是以單獨(dú)的庫放入,所以只要在Android Studio項(xiàng)目的Gradle編譯文件的dependencies下加入下邊的這句就可以開始使用RecyclerView了:
compile 'com.android.support:recyclerview-v7:21.0.+'
不過看過庫中提供的那些自帶對象實(shí)現(xiàn),體現(xiàn)基本操作流程,就會發(fā)現(xiàn)RecyclerView能做的看起來很多,但是已經(jīng)做到的實(shí)在太少。
RecyclerView.Adapter
RecyclerView提供了一個抽象Adapter類,然后就沒有了。沒有任何可以直接使用的子類,像ListAdapter那樣的ArrayAdapter、SimpleCursorAdapter現(xiàn)成的類都沒有。一切都留給開發(fā)者自己去實(shí)現(xiàn)定義。
仔細(xì)想想這也很挺正常,相信應(yīng)該很少在實(shí)際產(chǎn)品中有使用ArrayAdapter的,因?yàn)榇蟛糠至斜矶疾粫呛唵蔚囊恍形淖?。對于CursorAdapter使用也往往會實(shí)現(xiàn)不同的繼承類來提供子視圖。再者RecyclerView的Adapter和ListAdapter在理念上還是一樣的,所以想實(shí)現(xiàn)個RecyclerViewCursorAdapter,直接從CursorAdapter取材便可。
RecyclerView的Adapter相對ListAdapter在接口上有幾處變化也值得注意。
首先其將getView()方法分拆成了createViewHolder()和bindViewHolder()兩個。不過這個沒有什么好緊張的,在CursorAdapter里就已經(jīng)有見到過這個更加合理的設(shè)計。另外返回對象也從View變成了ViewHolder只需提一下。
最關(guān)鍵的注意點(diǎn)在于createViewHolder(ViewGroup parent, int viewType)第二參數(shù)雖然是整形,但是它并不是以往的當(dāng)前子項(xiàng)的位置(position),而是調(diào)用getItemViewType()獲得的的子項(xiàng)的類別。似乎在創(chuàng)建ViewHolder時,RecyclerView故意在隱藏子項(xiàng)的詳細(xì)信息,希望開發(fā)者完全只依賴其類別來創(chuàng)建相應(yīng)的View以及ViewHolder。
其次,RecyclerView的Adapter除了和ListAdapter一樣有notifyDataSetChanged()方法外,還有一堆會觸發(fā)動畫效果的通知數(shù)據(jù)改變的方法:
- final void notifyItemChanged(int position);
- final void notifyItemInserted(int position);
- final void notifyItemMoved(int fromPosition, int toPosition);
- final void notifyItemRangeChanged(int positionStart, int itemCount);
- final void notifyItemRangeInserted(int positionStart, int itemCount);
- final void notifyItemRangeRemoved(int positionStart, int itemCount);
- final void notifyItemRemoved(int position);
調(diào)用這些方法就會激發(fā)ItemAnimator上對應(yīng)的用于產(chǎn)生動畫的方法。
RecyclerView.ViewHolder
在ListView的年代里,其實(shí)已經(jīng)在使用ViewHolder,只是那時的方法看起來比較討巧,要隱藏在View的tag里?,F(xiàn)在RecyclerView強(qiáng)制使用ViewHolder,并且ViewHolder除了有對子視圖的引用,還有諸如ItemViewType和Position這些信息。
RecyclerView.LayoutManager
LayoutManager相對于ListView來說是一個新部分,通過繼承該類來實(shí)現(xiàn)自定義的布局方式,而不像ListView只有固定的布局方式。Support庫提供了兩個現(xiàn)成的子類:LinearLayoutManager和StaggeredGridLayoutManager。前者可以獲得和ListView一樣的布局,還可以是水平方向的;后者則提供了形如GridView的布局。所以應(yīng)用程序中的基本日常所以都可以被滿足。
如果需要實(shí)現(xiàn)自定義的LayoutManager,就比較麻煩了,需要理解Recycle、Scrap、Dirty這些關(guān)于子項(xiàng)目視圖狀態(tài)的概念。本人還沒有嘗試過創(chuàng)建一個自定義的LayoutManager,但在文末的引用文檔中有部分介紹實(shí)現(xiàn)方式的。
RecyclerView.ItemAnimator
通過實(shí)現(xiàn)繼承實(shí)現(xiàn)ItemAnimator,然后創(chuàng)建對象設(shè)置到RecyclerView上就可以得到基于子項(xiàng)目的動畫效果。不過如何正確合理地創(chuàng)建一個ItemAnimator子類,卻沒有詳細(xì)的描述指南。
窺探庫中提供的唯一一個可用的子類DefaultItemAnimator,可以看出它的動畫效果是簡單的alpha漸變。同時也會發(fā)現(xiàn)其實(shí)現(xiàn)是如此的復(fù)雜,有很多對于動畫步驟的操作,還得注意動畫在中途被打斷的處理,在結(jié)束時也要重置視圖狀態(tài)以便重用。這也反過來說明ItemAnimator基本沒有提供任何關(guān)于如何實(shí)現(xiàn)和管理動畫的信息。另一方面因?yàn)镈efaultItemAnimator的實(shí)現(xiàn)過于具體,因此它并不是合適作為自定義ItemAnimator的父類。
相信當(dāng)RecyclerView越來越多的被應(yīng)用到程序中時,更多關(guān)于這方面的合理設(shè)計會被提出來。目前在Github上也有不少參考了DefaultItemAnimator的實(shí)現(xiàn)方式,比如這個,還有這個。
關(guān)于ItemAnimator的使用,有幾點(diǎn)值得提醒的是:如果沒有提供ItemAnimator,RecyclerView默認(rèn)會創(chuàng)建一個DefaultItemAnimator用于動畫,所以不需要顯示地設(shè)置DefaultItemAnimator對象到RecyclerView上;添加(add)和刪除(remove)是默認(rèn)起效的,但是修改(change)的效果需要調(diào)用setSupportsChangeAnimations(boolean)來指定是否啟用,其默認(rèn)是沒有修改的動畫。
總體而言,RecyclerView的功能非常強(qiáng)大,其結(jié)構(gòu)設(shè)計也十分開放,這也造成它的上手使用相對比較困難。隨著越來越多的人開始嘗試使用這個部件,也會有越來越深刻的理解和設(shè)計實(shí)現(xiàn)。另外,閱讀RecyclerView的源碼可以幫助對其設(shè)計思想的了解,在以后設(shè)計其它的復(fù)用視圖時可以有更好的參照。
DEMO
說了這么多,我們來看一個RecyclerVIew實(shí)現(xiàn)圖片文字按鈕的混排的demo:
首先還是看我的工程結(jié)構(gòu)吧。
首先還是貼出我的main_acitivy.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" android:orientation="vertical" > <!-- A RecyclerView with some commonly used attributes --> <android.support.v7.widget.RecyclerView android:id="@+id/my_recycler_view" android:scrollbars="vertical" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout>
其他幾個xml就不用貼了,很簡單的放了寫TextVIew,ImgeView之類。
然后我們就來看看代碼,首先是Bean里面的代碼。
package com.androidl.bob; public class Bean { public static final int Y_TYPE = 0; //view類型0 public static final int X_TYPE = 1; //view類型2 public static final int Z_TYPE = 2;//view 類型3 private int type; private String text; public Bean(int type, String text) { super(); this.type = type; this.text = text; } public int getType() { return type; } public void setType(int type) { this.type = type; } public String getText() { return text; } public void setText(String text) { this.text = text; } }
然后是Adapter里面的代碼:
package com.androidl.bob; import java.util.List; import com.example.androidl.R; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.ViewHolder; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; /** * Date : 2014/7/15 * * @author edsheng * */ public class RecycleAdapter extends RecyclerView.Adapter<ViewHolder> { private List<Bean> beans; public RecycleAdapter(List<Bean> beans) { super(); this.beans = beans; } /** * 內(nèi)部TextHoler * * @author edsheng * */ public class TextHoler extends RecyclerView.ViewHolder { public TextView textView; public TextHoler(View textview) { super(textview); this.textView = (TextView) textview.findViewById(R.id.mytext); } } /** * iamgeHolder * * @author edsheng * */ public class ImageHoler extends RecyclerView.ViewHolder { public ImageView Imageview; public ImageHoler(View textview) { super(textview); this.Imageview = (ImageView) textview.findViewById(R.id.myiamge); } } /** * 按鈕的holder * * @author edsheng * */ public class ButtonHolder extends RecyclerView.ViewHolder { public Button button; public ButtonHolder(View textview) { super(textview); this.button = (Button) textview.findViewById(R.id.mybutton); } } @Override public int getItemCount() { // TODO Auto-generated method stub return beans.size(); } /** * 獲取消息的類型 */ @Override public int getItemViewType(int position) { // TODO Auto-generated method stub return beans.get(position).getType(); } /** * 創(chuàng)建VIewHolder */ @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewtype) { // TODO Auto-generated method stub View v = null; ViewHolder holer = null; switch (viewtype) { case Bean.X_TYPE: v = LayoutInflater.from(parent.getContext()).inflate( R.layout.recylce_item_x, null); holer = new TextHoler(v); break; case Bean.Y_TYPE: v = LayoutInflater.from(parent.getContext()).inflate( R.layout.recylce_item_y, null); holer = new ButtonHolder(v); break; case Bean.Z_TYPE: v = LayoutInflater.from(parent.getContext()).inflate( R.layout.recylce_item_z, null); holer = new ImageHoler(v); break; } return holer; } /** * 綁定viewholder */ @Override public void onBindViewHolder(ViewHolder holder, int position) { // TODO Auto-generated method stub switch (getItemViewType(position)) { case Bean.X_TYPE: TextHoler textholer = (TextHoler) holder; textholer.textView.setText(beans.get(position).getText()); break; case Bean.Y_TYPE: ButtonHolder buttonHolder = (ButtonHolder) holder; buttonHolder.button.setText(beans.get(position).getText()); break; case Bean.Z_TYPE: ImageHoler imageHoler = (ImageHoler) holder; // imageHoler.Imageview.setImageResource(android.R.drawable.checkbox_on_background); break; } } }
最后是activity的代碼。
package com.androidl.bob; import java.util.ArrayList; import java.util.List; import android.app.Activity; import android.os.Bundle; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import com.example.androidl.R; public class Mainactivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setContentView(R.layout.main_activity); RecyclerView mRecyclerView = (RecyclerView) findViewById(R.id.my_recycler_view); // // improve performance if you know that changes in content // // do not change the size of the RecyclerView // mRecyclerView.setHasFixedSize(true); //創(chuàng)建布局管理器 LinearLayoutManager mLayoutManager = new LinearLayoutManager(this); mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); mRecyclerView.setLayoutManager(mLayoutManager); //初始化數(shù)據(jù) List<Bean> myDataset = new ArrayList<Bean>(); myDataset.add(new Bean(Bean.Z_TYPE, "圖片")); myDataset.add(new Bean(Bean.X_TYPE, "文字")); myDataset.add(new Bean(Bean.Y_TYPE, "按鈕")); myDataset.add(new Bean(Bean.Z_TYPE, "圖片")); myDataset.add(new Bean(Bean.X_TYPE, "shit")); myDataset.add(new Bean(Bean.X_TYPE, "我擦")); myDataset.add(new Bean(Bean.Z_TYPE, "圖片")); myDataset.add(new Bean(Bean.Y_TYPE, "按鈕")); myDataset.add(new Bean(Bean.Y_TYPE, "按鈕")); myDataset.add(new Bean(Bean.X_TYPE, "文字")); //創(chuàng)建Adapter RecycleAdapter mAdapter = new RecycleAdapter(myDataset); mRecyclerView.setAdapter(mAdapter); } }
相關(guān)文章
Material Design系列之自定義Behavior支持所有View
這篇文章主要為大家詳細(xì)介紹了Material Design系列之自定義Behavior支持所有View,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-09-09詳細(xì)解讀Android系統(tǒng)中的application標(biāo)簽
這篇文章主要介紹了Android系統(tǒng)中的application標(biāo)簽,以application來聲明App是Android入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下2016-04-04屏蔽RecyclerView單邊滑動到頭陰影(fadingEdge)的方法
這篇文章主要給大家介紹了如何屏蔽RecyclerView單邊滑動到頭陰影(fadingEdge)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-04-04Android實(shí)現(xiàn)無標(biāo)題欄全屏的方法
這篇文章主要介紹了Android實(shí)現(xiàn)無標(biāo)題欄全屏的三種方法,感興趣的小伙伴們可以參考一下2016-07-07RecyclerView實(shí)現(xiàn)抖音縱向滾動ViewPager效果
這篇文章主要為大家詳細(xì)介紹了RecyclerView實(shí)現(xiàn)抖音縱向滾動ViewPager效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-07-07Android系統(tǒng)的五種數(shù)據(jù)存儲形式實(shí)例(二)
Android系統(tǒng)有五種數(shù)據(jù)存儲形式,分別是文件存儲、SP存儲、數(shù)據(jù)庫存儲、contentprovider 內(nèi)容提供者、網(wǎng)絡(luò)存儲。本文介紹了Android系統(tǒng)的五種數(shù)據(jù)存儲形式,有興趣的可以了解一下。2016-12-12