前言:雖然在日常開發(fā)中已經(jīng)多次接觸過RecycleView扔役,但也只是用到其最基本的功能,并沒有深入研究其他內(nèi)容警医。接下來將抽出時間去了解RecycleView的相關(guān)內(nèi)容亿胸,同時在博客中進(jìn)行記錄,以此加深印象预皇。這篇文章主要是介紹RecycleView的使用方法侈玄。
一、RecycleView是什么
? ? ? ?RecycleView是Android5.0后谷歌推出的一個用于在有限的窗口中展示大量數(shù)據(jù)集的控件吟温,位于support-v7包中序仙。它可以實(shí)現(xiàn)與ListView和GridView一樣的效果,提供了一種插拔式的體驗(yàn)鲁豪,高度的解耦潘悼,異常的靈活,只需設(shè)置其提供的不同的LayoutManager爬橡,ItemAnimator和ItemDecoration治唤,就能實(shí)現(xiàn)不同的效果。
二糙申、RecycleView的優(yōu)點(diǎn)
? ?1宾添、支持局部刷新。
? ?2、可以自定義item增刪時的動畫辞槐。
? ?3掷漱、能夠?qū)崿F(xiàn)item拖拽和側(cè)滑刪除等功能。
? ?4榄檬、默認(rèn)已實(shí)現(xiàn)View的復(fù)用卜范,而且回收機(jī)制更加完善。
三鹿榜、RecycleView的使用方法
首先要在build.gradle文件中添加引用
compile 'com.android.support:recyclerview-v7:26.1.0'
主頁面布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayou>
item布局:
<?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="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:text="數(shù)據(jù)" />
</LinearLayout>
adapter代碼:
public class MyRecycleViewAdapter extends RecyclerView.Adapter<MyRecycleViewAdapter.MyHolder> {
private List mList;//數(shù)據(jù)源
MyRecycleViewAdapter(List list) {
mList = list;
}
//創(chuàng)建ViewHolder并返回海雪,后續(xù)item布局里控件都是從ViewHolder中取出
@Override
public MyHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//將我們自定義的item布局R.layout.item_one轉(zhuǎn)換為View
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_one, parent, false);
//將view傳遞給我們自定義的ViewHolder
MyHolder holder = new MyHolder(view);
//返回這個MyHolder實(shí)體
return holder;
}
//通過方法提供的ViewHolder,將數(shù)據(jù)綁定到ViewHolder中
@Override
public void onBindViewHolder(MyHolder holder, int position) {
holder.textView.setText(mList.get(position).toString());
}
//獲取數(shù)據(jù)源總的條數(shù)
@Override
public int getItemCount() {
return mList.size();
}
/**
* 自定義的ViewHolder
*/
class MyHolder extends RecyclerView.ViewHolder {
TextView textView;
public MyHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.tv_content);
}
}
}
MainActivity代碼:
public class MainActivity extends AppCompatActivity {
private RecyclerView mRecycleView;
private MyRecycleViewAdapter mAdapter;//適配器
private LinearLayoutManager mLinearLayoutManager;//布局管理器
private List mList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mList = new ArrayList();
mRecycleView = findViewById(R.id.rv_list);
//初始化數(shù)據(jù)
initData(mList);
//創(chuàng)建布局管理器舱殿,垂直設(shè)置LinearLayoutManager.VERTICAL奥裸,水平設(shè)置LinearLayoutManager.HORIZONTAL
mLinearLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
//創(chuàng)建適配器,將數(shù)據(jù)傳遞給適配器
mAdapter = new MyRecycleViewAdapter(mList);
//設(shè)置布局管理器
mRecycleView.setLayoutManager(mLinearLayoutManager);
//設(shè)置適配器adapter
mRecycleView.setAdapter(mAdapter);
}
public void initData(List list) {
for (int i = 1; i <= 40; i++) {
list.add("第" + i + "條數(shù)據(jù)");
}
}
}
Adapter
? ?? ?使用時需要創(chuàng)建adapter(適配器)類沪袭,該類繼承于RecyclerView.Adapter<VH>
湾宙,其中VH是我們adapter類中創(chuàng)建的一個繼承于RecyclerView.ViewHolder
的靜態(tài)內(nèi)部類。
可以看到該適配器類主要有3個方法和1個自定義ViewHolder組成:
- onCreateViewHolder: 創(chuàng)建ViewHolder并返回冈绊,后續(xù)item布局里控件都是從ViewHolder中取出侠鳄。
- onBindViewHolder:通過方法提供的ViewHolder,將數(shù)據(jù)綁定到ViewHolder中死宣。
- getItemCount:獲取數(shù)據(jù)源總的條數(shù)伟恶。
- MyHolder :這是RecyclerView.ViewHolder的實(shí)現(xiàn)類,用于初始化item布局中的子控件毅该。需要注意的是博秫,在這個類的構(gòu)造方法中需要傳遞item布局的View給父類 。
使用方法:
//設(shè)置適配器adapter
mRecycleView.setAdapter(mAdapter);
LayoutManager
? ?? ?布局管理器眶掌,通過不同的布局管理器來控制item的排列順序挡育,負(fù)責(zé)item元素的布局和復(fù)用。RecycleView提供了三種布局管理器:
- LinearLayoutManager:線性布局朴爬,以垂直或水平滾動列表方式顯示項(xiàng)目即寒。
- GridLayoutManager:網(wǎng)格布局,在網(wǎng)格中顯示項(xiàng)目寝殴。
- StaggeredGridLayoutManager:瀑布流布局蒿叠,在分散對齊網(wǎng)格中顯示項(xiàng)目。
使用方法:
mRecycleView.setLayoutManager(new LinearLayoutManager(this,
LinearLayoutManager.HORIZONTAL,false));
運(yùn)行效果:
? ?? ?以上是LinearLayoutManager布局呈現(xiàn)的效果蚣常,假如遇到特殊需求市咽,也可以通過繼承
RecyclerView.LayoutManager
來自定義LayoutManager,重寫它的方法來實(shí)現(xiàn)所需要的效果抵蚊。
ItemDecoration
? ?? ?RecyclerView可以通過addItemDecoration()
設(shè)置分割線施绎。Android并沒有提供實(shí)現(xiàn)好的分割線溯革,所以任何的分割線樣式都需要用戶自己實(shí)現(xiàn)」茸恚可以通過繼承RecyclerView.ItemDecoration
類來實(shí)現(xiàn)致稀。
ItemDecoration源碼:
public abstract static class ItemDecoration {
public void onDraw(Canvas c, RecyclerView parent, State state) {
onDraw(c, parent);
}
@Deprecated
public void onDraw(Canvas c, RecyclerView parent) {
}
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
@Deprecated
public void onDrawOver(Canvas c, RecyclerView parent) {
}
@Deprecated
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}
該抽象類主要由三個方法組成:
- onDraw(Canvas c, RecyclerView parent, State state):在Item繪制之前被調(diào)用(先于drawChildren),主要用于繪制分割線樣式俱尼。
- onDrawOver(Canvas c, RecyclerView parent, State state):在Item繪制之后被調(diào)用(慢于drawChildren)抖单,主要用于繪制分割線樣式。
- getItemOffsets(Rect outRect, View view, RecyclerView parent, State state):通過outRect.set()為每個Item設(shè)置一定的偏移量遇八。
? ?? ?onDraw
和onDrawOver
這兩個方法都是用于繪制分割線矛绘,我們在使用時只需要按需求選擇一個進(jìn)行實(shí)現(xiàn)就可以。它們的區(qū)別在于執(zhí)行時間的不同刃永,我們可以通過源碼找到它們的區(qū)別货矮。
RecyclerView源碼:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
@Override
public void draw(Canvas c) {
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
}
View源碼:
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
public void draw(Canvas canvas) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
}
}
? ?? ?首先執(zhí)行的是RecyclerView中重寫的draw()
方法,然后會去執(zhí)行super.draw()
斯够,即View的draw()
方法囚玫。在View的draw()
方法中,會先去執(zhí)行onDraw()
读规,再去執(zhí)行dispatchDraw()
方法抓督,由于RecyclerView重寫了onDraw()
方法,所以是先執(zhí)行了RecyclerView中的onDraw()
方法掖桦。因此本昏,它們的執(zhí)行順序?yàn)椋?code>onDraw()->dispatchDraw()
->onDrawOver()
供汛。不理解的話可以參照上面的圖多看兩遍枪汪。
Google給我們提供了一個實(shí)現(xiàn)類:DividerItemDecoration
,我們可以參照它去實(shí)現(xiàn)自定義的Item Decoration怔昨。
DividerItemDecoration源碼:
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
public static final int VERTICAL = LinearLayout.VERTICAL;
private static final String TAG = "DividerItem";
private static final int[] ATTRS = new int[]{ android.R.attr.listDivider };
private Drawable mDivider;
private int mOrientation;
private final Rect mBounds = new Rect();
public DividerItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
if (mDivider == null) {
Log.w(TAG, "@android:attr/listDivider was not set in the theme used for this "
+ "DividerItemDecoration. Please set that attribute all call setDrawable()");
}
a.recycle();
setOrientation(orientation);
}
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL && orientation != VERTICAL) {
throw new IllegalArgumentException(
"Invalid orientation. It should be either HORIZONTAL or VERTICAL");
}
mOrientation = orientation;
}
public void setDrawable(@NonNull Drawable drawable) {
if (drawable == null) {
throw new IllegalArgumentException("Drawable cannot be null.");
}
mDivider = drawable;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() == null || mDivider == null) {
return;
}
if (mOrientation == VERTICAL) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
private void drawVertical(Canvas canvas, RecyclerView parent) {
canvas.save();
final int left;
final int right;
//noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
if (parent.getClipToPadding()) {
left = parent.getPaddingLeft();
right = parent.getWidth() - parent.getPaddingRight();
canvas.clipRect(left, parent.getPaddingTop(), right,
parent.getHeight() - parent.getPaddingBottom());
} else {
left = 0;
right = parent.getWidth();
}
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
final int top = bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}
private void drawHorizontal(Canvas canvas, RecyclerView parent) {
canvas.save();
final int top;
final int bottom;
//noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
if (parent.getClipToPadding()) {
top = parent.getPaddingTop();
bottom = parent.getHeight() - parent.getPaddingBottom();
canvas.clipRect(parent.getPaddingLeft(), top,
parent.getWidth() - parent.getPaddingRight(), bottom);
} else {
top = 0;
bottom = parent.getHeight();
}
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
final int right = mBounds.right + Math.round(child.getTranslationX());
final int left = right - mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (mDivider == null) {
outRect.set(0, 0, 0, 0);
return;
}
if (mOrientation == VERTICAL) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
}
? ?? ?從源碼中可以看到該類是用系統(tǒng)中的android.R.attr.listDivider
來作為分割線雀久,通過DividerItemDecoration構(gòu)造方法中的setOrientation(orientation)
來設(shè)置分割線的方向。在getItemOffsets()
中利用outRect.set()
去設(shè)置了繪制的范圍趁舀,再在onDraw()
中進(jìn)行真正的繪制赖捌。
使用方法:
//設(shè)置分割線
mRecycleView.addItemDecoration(new DividerItemDecoration(this,
DividerItemDecoration.VERTICAL));
運(yùn)行效果:
事件監(jiān)聽
????RecyclerView并沒有給我們提供現(xiàn)成的點(diǎn)擊事件監(jiān)聽,需要我們自己去實(shí)現(xiàn)矮烹。我們可以在RecyclerView的Adapter中自定義一個接口越庇,并創(chuàng)建一個供其他類設(shè)置監(jiān)聽的方法。
public class MyRecycleViewAdapter extends RecyclerView.Adapter<MyRecycleViewAdapter.MyHolder> {
private List mList;//數(shù)據(jù)源
private OnItemClickListener onItemClickListener;
/**
* 供外部調(diào)用設(shè)置監(jiān)聽
* @param onItemClickListener
*/
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
/**
* 自定義的接口
*/
public interface OnItemClickListener {
void onItemClick(View view, int position);
}
//通過方法提供的ViewHolder奉狈,將數(shù)據(jù)綁定到ViewHolder中
@Override
public void onBindViewHolder(final MyHolder holder, int position) {
holder.textView.setText(mList.get(position).toString());
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (onItemClickListener != null) {
onItemClickListener.onItemClick(v, holder.getAdapterPosition() + 1);
}
}
});
}
}
? ?? ?以上省略了部分與該內(nèi)容無關(guān)的代碼卤唉。當(dāng)我們定義好接口后,我們在onBindViewHolder()
方法中為holder.itemView
(itemView是列表中的每一個item項(xiàng))設(shè)置了點(diǎn)擊事件監(jiān)聽仁期,然后在onClick()
中判斷是否有用戶傳遞過onItemClickListener
實(shí)例進(jìn)來桑驱,有的話會調(diào)用他的onItemClick()
竭恬,將點(diǎn)擊事件轉(zhuǎn)移到我們的自定義接口上,傳給外面的調(diào)用者熬的。調(diào)用者代碼如下:
mAdapter.setOnItemClickListener(new MyRecycleViewAdapter.OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
Toast.makeText(getApplicationContext(), "第" + position + "條數(shù)據(jù)", Toast.LENGTH_SHORT).show();
}
});
? ?? ?到這里點(diǎn)擊事件就完成了痊硕。如果你想實(shí)現(xiàn)長按也是同樣的方法,在自定義的接口中多加一個長按的方法押框,然后holder.itemView
調(diào)用setOnLongClickListener()
去將長按事件轉(zhuǎn)移到自定義的接口上岔绸。
ItemAnimator 動畫
? ?? ?RecyclerView可以通過mRecyclerView.setItemAnimator(ItemAnimator animator)
來設(shè)置添加和移除時的動畫效果。ItemAnimator
是一個抽象類橡伞,RecyclerView為我們提供了一個ItemAnimator
的實(shí)現(xiàn)類DefaultItemAnimator
亭螟。
使用方法:
//設(shè)置動畫效果
mRecycleView.setItemAnimator(new DefaultItemAnimator());
? ?? ?在adapter中添加兩個方法,用于添加和移除Item骑歹。這里要注意的是预烙,更新數(shù)據(jù)集要用notifyItemInserted(position)
與notifyItemRemoved(position)
,而不是notifyDataSetChanged()
道媚,否則沒有動畫效果扁掸。
/**
* 添加數(shù)據(jù)
*/
public void addItem() {
mList.add(0, "new ");
notifyItemInserted(0);
}
/**
* 移除數(shù)據(jù)
* @param position
*/
public void removeItem(int position) {
mList.remove(position);
notifyItemRemoved(position);
}
效果是按下底部“添加”按鈕會在頂部插入數(shù)據(jù),點(diǎn)擊列表中的Item則刪除該條數(shù)據(jù)最域。
? ?? ?如果我們對這種動畫效果不滿意谴分,也可以去自定義各種動畫效果。目前github上有許多開源的項(xiàng)目镀脂,例如RecyclerViewItemAnimators牺蹄,我們可以直接去引用或?qū)W習(xí)它的動畫效果。
結(jié)論:以上就是RecyclerView的基本用法薄翅,看到這里可能很多人會覺得它比ListView復(fù)雜得多沙兰,很多東西都需要自己去定義,但正是這種定制性使得它具有良好的擴(kuò)展性翘魄,我們可以根據(jù)具體需求去自定義自己想要實(shí)現(xiàn)的效果鼎天,相信你也會喜歡上這個控件!