Android 5.X新特性之RecyclerView基本解析及無限複用
說到RecyclerView,相信大家都不陌生,它是我們經典級ListView的升級版,升級後的RecyclerView展現了極大的靈活性。同時內部直接封裝了ViewHolder,不用我們自己定義ViewHolder就能實現item的回收和複用功能。當然它肯定不止這些好處,比如我們可以自定義分割線,可以更加方便的實現列表的佈局方式等等。雖說我們自己在第一次使用時,會比使用listView和gridView稍微的複雜一些,需要自定義的也多了一點,但是它卻更好的體現了靈活性,可以隨自己的喜好來隨便的定義,當然最主要的是能更好的複用,只需一次的定義,卻可隨處的複用。
下面,我們來好好的學習下它的使用。
首先,我們要是用RecyclerView必須引入support-V7包,拿android studio來舉例:
先開啟File->選擇Project Structure,之後在左邊Modules選擇你的專案,然後在點選右邊的Dependencies,然後點選綠色的+號選擇新增Library,然後找到recyclerview-v7雙擊加入到依賴庫中。然後可以在build.gradle中檢視:
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.3.0'
compile 'com.android.support:recyclerview-v7:23.3.0'
}
可以找dependencies 中找到對recyclerview的支援,則說明新增成功,如果還沒有的,可以clean下自己的工程。
接下來,我們就進入Recyclerview的學習。RecyclerView的學習目標就是以下四個方法,把以下四個方法完全的掌握了,也就真正的掌握了RecyclerView。
RecyclerView.setAdapter:用來設定adapter,顯示資料
RecyclerView.setLayoutManager :用來設定顯示佈局的,目前系統給出三種佈局,分別是垂直,水平和瀑布流式佈局
RecyclerView.setItemAnimator :用來設定顯示動畫的
RecyclerView.addItemDecoration :用來設定列表分割線的
接下來我們就學習怎麼使用以上四個方法來真正掌握Recyclerview的使用。要使用Recyclerview,我們必須先定義一個類(CustomRecyclerAdapter)並繼承Recyclerview.Adapter,且實現它裡面的方法,程式碼如下:
public class CustomRecyclerAdapter extends RecyclerView.Adapter<CustomRecyclerAdapter.ViewHolderHelper>{
@Override
public ViewHolderHelper onCreateViewHolder(ViewGroup parent, int viewType) {
return null;
}
@Override
public void onBindViewHolder(ViewHolderHelper holder, int position) {
}
@Override
public int getItemCount() {
return 0;
}
public class ViewHolderHelper extends RecyclerView.ViewHolder{
public ViewHolderHelper(View itemView) {
super(itemView);
}
}
}
在我們還沒正式開始使用之前,先大體上了解下上面三個方法是做什麼的:
A. onCreateViewHolder()方法:該方法就是將佈局檔案轉化為View並傳遞給RecyclerView封裝好的ViewHolder。
B. onBindViewHolder()方法:該方法將會在固定的位置上把ViewHolder裡的itemView資料對映在item中。
C. getItemCount()方法:該方法和listView中的getCount()一樣,都是返回Item的個數。
瞭解了這三個方法,我們來先實現最簡單的應用,把我們的資料顯示在app中。
首先,我們建立一個佈局檔案recycler_view.xml,如下
<?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="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
</LinearLayout>
建立RecycerActivity用來載入佈局檔案:
public class RecycerActivity extends Activity {
private RecyclerView mRecyclerView;
private List<String> mData;
private CustomRecyclerAdapter mCustomRecyclerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.recycer_view);
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
initData();
//線性佈局管理器
recyclerViewLayoutManager = new LinearLayoutManager(this);
//設定佈局管理器
mRecyclerView.setLayoutManager(recyclerViewLayoutManager);
//設定顯示動畫
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
//設定adapter
mCustomRecyclerAdapter = new CustomRecyclerAdapter(mData);
mRecyclerView.setAdapter(mCustomRecyclerAdapter);
}
private void initData() {
mData = new ArrayList<String>();
for(int i = 0; i < 10; i++){
mData.add("第"+i+"item");
}
}
}
經過修改的CustomRecyclerAdapter 如下,
public class CustomRecyclerAdapter extends RecyclerView.Adapter<CustomRecyclerAdapter.ViewHolderHelper>{
private List<String> mData;
public CustomRecyclerAdapter(List<String> data) {
mData = data;
}
@Override
public ViewHolderHelper onCreateViewHolder(ViewGroup parent, int viewType) {
//onCreateViewHolder方法就是將佈局檔案轉化為View並傳遞給RecyclerView封裝好的ViewHolder
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_view, parent, false);
return new ViewHolderHelper(view);
}
@Override
public void onBindViewHolder(ViewHolderHelper holder, int position) {
holder.textView.setText(mData.get(position));
}
@Override
public int getItemCount() {
return mData.size();
}
}
item_view.xml佈局檔案
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_item"
android:layout_toRightOf="@+id/iv_item"
android:layout_width="match_parent"
android:layout_height="30dp"
android:text="I am item view"/>
</RelativeLayout>
好,基礎工作已做足,我們先來看看效果吧
已經顯示出來了,不過,大家看著是不是很彆扭呢,連個分割線也沒有,還不如listView呢,別急,我們在上面也提到過,RecyclerView給了我們最大的發揮自由度,它本身並沒有給定列表的分割線,這是需要我們自己定義的。由此我們來定義自己的分割線。自定義分割線是需要我們繼承RecyclerView.ItemDecoration類,並實現它的onDraw()方法。請看程式碼:
public class DividerItemDecoration extends RecyclerView.ItemDecoration{
public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
private int mOrientation;
private Context mContext;
private TextPaint mTextPaint;
private float listDividerSize = 2;
private int listDividerColor;
public DividerItemDecoration(Context context,int orientation){
mContext = context;
mTextPaint = new TextPaint();
mTextPaint.setColor(Color.RED);
setOrientation(orientation);
}
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
throw new IllegalArgumentException("invalid orientation");
}
mOrientation = orientation;
}
@Override
public void onDraw(Canvas c, RecyclerView parent) {
super.onDraw(c, parent);
if(mOrientation == HORIZONTAL_LIST){
drawHorizontal(c, parent);
}else{
drawVertical(c, parent);
}
}
/**
* 繪製垂直分割線
* @param c
* @param parent
*/
private void drawVertical(Canvas c, RecyclerView parent) {
//分割線的左邊界 = 子View的左padding值
int rectLeft = parent.getPaddingLeft();
//分割線的右邊界 = 子View的寬度 - 子View的右padding值
int rectRight = parent.getWidth() - parent.getPaddingRight();
int childCount = parent.getChildCount();
for(int i = 0; i < childCount; i ++){
View child = parent.getChildAt(i);
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
// 分割線的top = 子View的底部 + 子View的margin值
int rectTop = child.getBottom() + layoutParams.bottomMargin;
// 分割線的bottom = 分割線的top + 分割線的高度
float rectBottom = rectTop + listDividerSize;
c.drawRect(rectLeft,rectTop,rectRight,rectBottom,mTextPaint);
}
}
/**
* 繪製水平分割線
* @param c
* @param parent
*/
private void drawHorizontal(Canvas c, RecyclerView parent) {
//分割線的上邊界 = 子View的上padding值
int rectTop = parent.getPaddingTop();
//分割線的下邊界 = 子View的高度 - 子View的底部padding值
int rectBottom = parent.getHeight() - parent.getPaddingBottom();
int childCount = parent.getChildCount();
for(int i = 0; i < childCount; i ++){
View child = parent.getChildAt(i);
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
//分割線的Left = 子View的右邊界 + 子View的左margin值
int rectLeft = child.getRight() + layoutParams.rightMargin;
//分割線的right = 分割線的Left + 分割線的寬度
float rectRight = rectLeft + listDividerSize;
c.drawRect(rectLeft,rectTop,rectRight,rectBottom,mTextPaint);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
if(mOrientation == VERTICAL_LIST){
outRect.set(0,0,0,(int)listDividerSize);
} else{
outRect.set(0,0,(int)listDividerSize,0);
}
}
}
程式碼很好理解,這裡考慮了兩個情況,分別是垂直和水平的佈局,然後再ondraw()裡面計算出四角邊值,最後直接繪製一個矩形即可。
在RecycerActivity 中的onCreate中新增上一句
mRecyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL_LIST));
現在再來看看效果。
現在已顯示出來了,分割線也出來了,在這裡只列出了垂直方向的佈局,就不再列出其他樣式的佈局程式碼了,夥伴們可自行寫寫看。
或許有經驗的小夥伴們已經知道我們的RecyclerView自己是沒有實現點選事件的,這裡需要我們來根據業務的需求自己來實現。這裡我們利用事件回撥機制來完成事件的觸發。
首先我們需要在CustomRecyclerAdapter中定義一個介面,並在其中定義兩個可用的事件方法,如下:
public interface OnItemClickListener{
void onItemClickListener();
void onLongItemClickListener();
}
這裡提供了用於點選和長按的事件方法,接下來我們需要對外暴露該介面用於被呼叫
public void setOnClickItemListener(OnItemClickListener onItemClickListener){
mOnItemClickListener = onItemClickListener;
}
然後我們可以在ViewHolderHelper 做如下的修改:
public class ViewHolderHelper extends RecyclerView.ViewHolder{
private TextView textView;
public ViewHolderHelper(View itemView) {
super(itemView);
textView = (TextView)itemView.findViewById(R.id.tv_item);
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mOnItemClickListener.onItemClickListener();
}
});
textView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
mOnItemClickListener.onLongItemClickListener();
return false;
}
});
}
}
首先得到我們itemView中的textView對應的Id,然後為textView新增事件,但是隻是新增卻並不實現,它的用意是誰呼叫誰實現。
最後在我們的RecycerActivity中新增如下程式碼,請看:
mCustomRecyclerAdapter.setOnClickItemListener(new CustomRecyclerAdapter.OnItemClickListener() {
@Override
public void onItemClickListener() {
Toast.makeText(getContext(),"單擊",Toast.LENGTH_SHORT).show();
}
@Override
public void onLongItemClickListener() {
Toast.makeText(getContext(),"長單擊",Toast.LENGTH_SHORT).show();
}
});
下面在來看看是否可以實現事件的觸發了呢?
ok,學到這裡大家至少對RecylerView有了一個初步的認識,但是我們一想,這樣寫的話肯定達不到我們標題所說的無限複用,甚至連複用也遙不可及,是的,這樣寫是不可能完成複用的,接下來我們一步一步的慢慢調整,讓它可以支援一次編寫N次複用,達到極大多數的重複使用,即使不符合需求,我們也只需要修改丁點即可滿足需求,這是我們的目標,接下來一步一步的實現。
首先,我們先整理下,看看可以調整哪些目標能逐步的實現重複使用的目標:
1. 資料型別:我們在使用List集合時,是無法固定型別的,有可能是String,int等等型別,所以我們不應該固定為哪一種型別。
2. 在onCreateViewHolder方法中,它需要對映一個佈局檔案並轉化為View或是一個自定義View傳遞給RecyclerView封裝好的ViewHolder,為了可以達到複用,所以我們就不可以在此直接對映佈局檔案。
3. 在onBindViewHolder方法中也不應該直接為itemView設定屬性,如上面的:holder.textView.setText(mData.get(position));
4. 我們不應該在ViewHolder的構造方法中直接獲取我們的itemView,並給它新增觸發事件
以上幾個是我們能很直觀的得到的能重構的問題所在,至於其他的不容易想到的我們再重構的時候慢慢講解。現在我們逐一的解決上面的問題,使我們能更達到重複使用的目的。
1,針對資料型別的不一致,我們可以根據具體的使用場景利用泛型進行傳遞到Adapter中,比如:我們再定義CustomRecyclerAdapter時使用泛型,讓呼叫者傳遞過來它所擁有的型別,這樣我們就可以不用考慮型別的不一致了。請看下面片段程式碼
public class CustomRecyclerAdapter<T> extends RecyclerView.Adapter<CustomViewHolderHelper>{
private Context mContext;
private List<T> mData;
private CustomOnItemClickListener mOnItemClickListener;
public CustomRecyclerAdapter(Context context, List<T> data) {
mContext = context;
mData = data;
}
}
...
RecycerActivity中在呼叫時可以這樣使用:
mCustomRecyclerAdapter = new CustomRecyclerAdapter(this,mData);
這樣就可以把型別給確定下來了,同時也解決了問題1的複用。
2 , 在onCreateViewHolder方法中,和問題一的解決方案是一樣的,我們把需要的itemView給傳遞過去而不是固定寫死在方法中
public class CustomRecyclerAdapter<T> extends RecyclerView.Adapter<CustomViewHolderHelper>{
private Context mContext;
private List<T> mData;
protected int mLayoutResId;
private CustomOnItemClickListener mOnItemClickListener;
public CustomRecyclerAdapter(Context context, int layoutResId, List<T> data) {
mContext = context;
mLayoutResId = layoutResId;
mData = data;
}
/**
* Called when RecyclerView needs a new ViewHolder of the given type to represent
* an item.
* 當 RecyclerView 依據給出的型別需要一個新的 ViewHolder 去展示一個 item 時,該方法將會被呼叫
*
* 這個給出的型別是在 getItemViewType返回的,預設返回 0 。
*
* @param parent
* @param viewType
* @return
*/
@Override
public CustomViewHolderHelper onCreateViewHolder(ViewGroup parent, int viewType) {
//onCreateViewHolder方法就是將佈局檔案轉化為View並傳遞給RecyclerView封裝好的ViewHolder
View view = LayoutInflater.from(parent.getContext()).inflate(mLayoutResId, parent, false);
return new CustomViewHolderHelper(view);
}
}
RecycerActivity中在呼叫時可以這樣使用:
mCustomRecyclerAdapter = new CustomRecyclerAdapter(this,R.layout.item_view,mData);
3 , 在onBindViewHolder方法中也不應該直接為itemView設定屬性,我們可以在這裡記錄itemView的position,並給它設定監聽事件,更重要的我們這這裡可以建立一個抽象方法,讓呼叫者自己去實現業務邏輯。請看程式碼:
public abstract class CustomRecyclerAdapter<T> extends RecyclerView.Adapter<CustomViewHolderHelper>
implements View.OnClickListener,View.OnLongClickListener{
private Context mContext;
private List<T> mData;
protected int mLayoutResId;
private CustomOnItemClickListener mOnItemClickListener;
public CustomRecyclerAdapter(Context context, int layoutResId, List<T> data) {
mContext = context;
mLayoutResId = layoutResId;
mData = data;
}
/**
* Called when RecyclerView needs a new ViewHolder of the given type to represent
* an item.
* 當 RecyclerView 依據給出的型別需要一個新的 ViewHolder 去展示一個 item 時,該方法將會被呼叫
*
* 這個給出的型別是在 getItemViewType返回的,預設返回 0 。
*
* @param parent
* @param viewType
* @return
*/
@Override
public CustomViewHolderHelper onCreateViewHolder(ViewGroup parent, int viewType) {
//onCreateViewHolder方法就是將佈局檔案轉化為View並傳遞給RecyclerView封裝好的ViewHolder
View view = LayoutInflater.from(parent.getContext()).inflate(mLayoutResId, parent, false);
return new CustomViewHolderHelper(view);
}
/**
* Called by RecyclerView to display the data at the specified position. This method should
* update the contents of the ViewHolder#itemView to reflect the item at the given position.
*
* RecyclerView 將要在特殊的位置上顯示資料時,該方法將被呼叫。該方法將會在固定的位置上
* 把ViewHolder裡的itemView資料對映在item中。
* @param holder
* @param position
*/
@Override
public void onBindViewHolder(CustomViewHolderHelper holder, int position) {
//把每一個itemView設定一個標籤,方便以後根據標籤獲取到該itemView以便做其他事項,比如點選事件
holder.itemView.setTag(position);
holder.itemView.setOnClickListener(this);
holder.itemView.setOnLongClickListener(this);
T itemData = mData.get(position);
displayContents(holder,itemData);
}
/**
*用來在holder中設定每個ItemView的顯示資料
* 設定為抽象方法,是為:自己本身並不實現,誰使用誰設定
* @param holder
* @param itemData
*/
protected abstract void displayContents(CustomViewHolderHelper holder, T itemData);
}
上面註解也寫的很清楚,相信大家一看就明白,至於為什麼可以直接使用holder.itemView來獲取每個itemView是因為在onCreateViewHolder()方法中,我們返回了一個新的物件引用,這個物件的構造方法中使用super(itemView);把我們的itemView傳遞到了ViewHolder中,請看它的原始碼構造方法:
public ViewHolder(View itemView) {
if (itemView == null) {
throw new IllegalArgumentException("itemView may not be null");
}
this.itemView = itemView;
}
因此我們可以在onBindViewHolder()方法中,直接使用holder.itemView來獲取itemView。那麼接下來,在RecycerActivity中,我們這樣使用:
mCustomRecyclerAdapter = new CustomRecyclerAdapter<String>(this, R.layout.item_view, mData) {
@Override
protected void displayContents(CustomViewHolderHelper holder, String itemData) {
holder.setText(R.id.tv_item,itemData);
}
};
當然你必須也得在ViewHolder中定義相應的方法,如:
public class CustomViewHolderHelper extends RecyclerView.ViewHolder{
private SparseArray<View> views;
public CustomViewHolderHelper(View itemView) {
super(itemView);
views = new SparseArray<View>();
}
private <T extends View> T converToViewFromId(int resId) {
View view = views.get(resId);
if(view == null){
view = itemView.findViewById(resId);
}
views.put(resId,view);
return (T)view;
}
public CustomViewHolderHelper setText(int resId, String value){
TextView itemView = converToViewFromId(resId);
if (TextUtils.isEmpty(value)) {
itemView.setText("");
} else {
itemView.setText(value);
}
return this;
}
}
ok,這樣我們連第四個問題也一併解決了,看下效果吧,完全的一樣,這樣我們就實現的重複使用,但是有人會有疑問,這裡也就只能使用TextView啊,其實已經在ViewHolder中給出了答案,大家只需要在新增相對應的方法即可,比如
public CustomViewHolderHelper setImageResource(int viewId, int imageResId) {
ImageView view = converToViewFromId(viewId);
view.setImageResource(imageResId);
return this;
}
public CustomViewHolderHelper setOnClickListener(int viewId, View.OnClickListener listener) {
View view = converToViewFromId(viewId);
view.setOnClickListener(listener);
return this;
}
上面我又添加了兩個方法,用於點選事件和載入圖片的ImageView,下面我們再把itemView佈局檔案修改下:
item_view.xml佈局檔案:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/iv_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tv_item"
android:layout_toRightOf="@+id/iv_item"
android:layout_width="match_parent"
android:layout_height="30dp"
android:text="I am item view"/>
<Button
android:id="@+id/btn_item"
android:layout_toRightOf="@+id/iv_item"
android:layout_below="@+id/tv_item"
android:layout_width="wrap_content"
android:text="點我"
android:layout_height="wrap_content" />
</RelativeLayout>
RecycerActivity中在呼叫時可以這樣使用:
mCustomRecyclerAdapter = new CustomRecyclerAdapter<String>(this, R.layout.item_view, mData) {
@Override
protected void displayContents(CustomViewHolderHelper holder, String itemData) {
holder.setText(R.id.tv_item,itemData)
.setImageResource(R.id.iv_item,R.mipmap.ic_launcher)
.setOnClickListener(R.id.btn_item, new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(RecycerActivity.this,"您單擊了按鈕",Toast.LENGTH_SHORT).show();
}
});
}
};
ok,來看下效果吧
是不是可以了呢,這樣我們就可以完全的一次定義N次複用了,每次使用只需要更換不同的佈局檔案即可而不需要再次編寫程式碼,學會了吧。
其實我們這節課主要講解的RecyclerView.setAdapter的內容,其他的三個我們並沒有詳細的介入,我們會再以後的博文中陸續的講解。
好了,今天就講到這裡吧,祝大家學習愉快。
更多資訊請關注微信平臺,有部落格更新會及時通知。愛學習愛技術。