RecyclerView是support v7中提供的一個控件,可以說是listView和GridView的增強版,提供了一種插拔式的體驗。提供了三個設置方法來供我們快速做出一些我們想要的效果效果。
- LayoutManager:控制的是RecyclerView的顯示方式。
- ItemDecoration:繪制Item之間的間隔,并在特定時間繪制一些我們想要的東西。
- ItemAnimator:控制Item增刪的動畫。
今天我們來學習一下RecyclerView的基本使用方法。我們想要使用它,首先我們要在app/build.gradle文件中添加依賴。
implementation 'com.android.support:recyclerview-v7:27.1.1'
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/rcl_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
添加好布局之后,和我們的listView一樣,要創建Adapter和ViewHolder。和listView不同Recycler提供了內部抽象類為我們來繼承,RecyclerView.Adapter<ViewHolder> 和 RecyclerView.ViewHolder。我們先來看一個例子。
class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder>{
private List<User> userList;
public MyAdapter(List<User> userList) {
this.userList = userList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_main,parent,false);
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.tvName.setText("Name:"+userList.get(position).getName()); holder.tvAge.setText("Age:"+userList.get(position).getAge());
}
@Override
public int getItemCount() {
return userList.size();
}
class ViewHolder extends RecyclerView.ViewHolder{
private TextView tvName ;
private TextView tvAge;
public ViewHolder(View itemView) {
super(itemView);
tvName = itemView.findViewById(R.id.tv_name);
tvAge = itemView.findViewById(R.id.tv_age);
}
}
}
我們可以看到 我們的MyAdapter繼承自RecyclerView.Adapter,重寫了三個方法。
- onCreateViewHolder():用來創建ViewHolder實例,我們在這個方法中將item布局加載進來,并創建一個ViewHolder實例,將我們的item布局傳入到ViewHolder的構造方法中,并將ViewHolder返回。
- onBindViewHolder():通過ViewHolder拿到的item布局中的View進行賦值,寫過listView的你對這一步一定不會陌生。
- getItemCount():;返回數據集的個數,也就是我們RecyclerView的item的個數。
ViewHolder的實現,我們繼承RecyclerView.ViewHolder,重寫了構造方法。
- 在構造方法中我們拿到Item的View。進行findViewById查找到我們索要用的控件。方便我們進行使用。
接下來我們在Activity中找到我們到RecyclerView進行使用。
//創建一個存儲User數據的集合
List<User> userList = new ArrayList<>();
//在布局中找到我們的RecyclerView
rclMain = findViewById(R.id.rcl_main);
//增加一些User假數據,放在我們的集合當中
for (int i=0 ;i<=30 ;i++){
userList.add(new User("張"+i,20+i));
}
//為我們的RecyclerView設置Adapter
rclMain.setAdapter(new MyAdapter(userList));
//設置我們的LayoutManager為線性布局豎直方向的
rclMain.setLayoutManager(new LinearLayoutManager(this));
//設置我們的Item之間的間隔為DividerItemDecoration為豎直方向。如需修改其默認樣式,
//可以修改 <!-- Application theme. -->中名字為android:listDivider的屬性。
rclMain.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
以上的效果和listView完全相同,并且Item之間的間隔是一個白色的一條線。大家可以看一下1-1的圖。
LayoutManager
RecyclerView 中設置LayoutManager方法的參數是RecyclerView.LayoutManager吧,這是一個抽象類,好在系統提供了3個實現類:
- LinearLayoutManager 現行管理器,支持橫向、縱向。(默認)
- GridLayoutManager 網格布局管理器。
- StaggeredGridLayoutManager 瀑布就式布局管理器。
如果我們想做一個橫向listView的效果也非常簡單,稍微修改一下Item布局,將我們設置的LayoutManager設置成橫向的LinearLayoutManager就可以了
rclMain.setLayoutManager(new LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false));
GridLayoutManager 實現縱向的網格布局。
//設置列數為3的縱向網格布局,注意 一下網格中間的間隔我這里設置的是item的margin實現的。我們上邊Demo中使用的DividerItemDecoration,在這里是無法使用的,因為網格布局的間隔是縱向與橫向都要的,而DividerItemDecoration只能設置縱向或者橫向。無法同時設置
rclMain.setLayoutManager(new GridLayoutManager(this,3,GridLayoutManager.VERTICAL,false));
GridLayoutManager 實現橫向的網格布局。
//設置列數為3的縱向網格布局,注意 一下網格中間的間隔我這里設置的是item的margin實現的。我們上邊Demo中使用的DividerItemDecoration,在這里是無法使用的,因為網格布局的間隔是縱向與橫向都要的,而DividerItemDecoration只能設置縱向或者橫向。無法同時設置
rclMain.setLayoutManager(new GridLayoutManager(this,3,GridLayoutManager.HORIZONTAL,false));
StaggeredGridLayoutManager實現瀑布流,已經看過上面的實現網格布局和線性list布局,我相信實現網格布局對你來說已經非常之簡單了。如何隨機設置item的高度保證item每個都不一樣高。這里就不貼出來了。相信一定難不倒你的。
//設置數量和方向,我們設置的是3列縱向布局。
rclMain.setLayoutManager(new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL));
只要修改一行代碼就能實現三種不同的布局方式,Recycler的強大你是不是已經體會到了。反正我早已經被他震撼到了。
ItemDecoration
我們可以使用addItemDecoration(Recycler.ItemDecoration)添加item之間要繪制的東西。RecyclerView.ItemDecoration是抽象類,Android只提供了DividerItemDecoration一個實現類用來給使用LinearLayoutManager的RecyclerView繪制Item之間的間隙。我們可以修改APPTheme 中的android:listDivider來修改我們想要的間隙樣式
values/styles.xml
<!-- Base application theme. -->
<style name="AppBaseTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme" parent="AppBaseTheme">
<item name="android:listDivider">@drawable/divider_bg</item>
</style>
divider_bg.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="line" >
<stroke android:color="@android:color/white"/>
<size android:height="4dp" />
</shape>
而Android提供的這個不能滿足我們所有的需求,比如我們想給我們的網格布局添加item之間的間隔就不能使用DividerItemDecoration,我們只能自己來實現了。繼承RecyclerView.ItemDecoration。首先我們來了解一下RecyclerView.ItemDecoration中的幾個方法的含義
- getItemOffests:可以通過outRect.set(l,t,r,b)設置指定itemview的paddingLeft,paddingTop, paddingRight, paddingBottom
- onDraw:可以通過一系列c.drawXXX()方法在繪制itemView之前繪制我們需要的內容。可以理解為在Item下層 像BackGround
- onDrawOver:與onDraw類似,只不過是在繪制itemView之后繪制,具體表現形式,就是繪制的內容在itemview上層。
我們只設置item間隔的話 是不需要進行onDraw和onDrawOver操作的,除非你想在你要設置的這個間隔中繪制一些想要的東西。而想要設置Item之間的間隔,只要通過計算我們就可以通過getItemOffests設置item的padding就能夠實現我們的效果。首先我們來看一下我們具體的實現思路。
當orientation為vertical時,我們需要在getItemOffsets方法中計算每個Item的PaddingLeft,以及PaddingRight,保證每個Item的paddingLeft+paddingRight相等,這樣才能達到均分的目的。
這里我們可以用數字的帶入來計算我們每個item的padding規律,從而推導出我們要的公式。這里我也是查看的大神的博客,如果想知道具體規則可在本章底部找到答案。
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
RecyclerView.LayoutManager manager = parent.getLayoutManager();
int childPosition = parent.getChildAdapterPosition(view);
int itemCount = parent.getAdapter().getItemCount();
if (manager != null) {
if (manager instanceof GridLayoutManager) {
// manager為GridLayoutManager時
setGridOffset(((GridLayoutManager) manager).getOrientation(), ((GridLayoutManager) manager).getSpanCount(), outRect, childPosition, itemCount);
}
}
}
/**
* 設置GridLayoutManager 類型的 offest
*
* @param orientation 方向
* @param spanCount 個數
* @param outRect padding
* @param childPosition 在 list 中的 postion
* @param itemCount list size
*/
private void setGridOffset(int orientation, int spanCount, Rect outRect, int childPosition, int itemCount) {
float totalSpace = mSpace * (spanCount - 1) + mEdgeSpace * 2; // 總共的padding值
float eachSpace = totalSpace / spanCount; // 分配給每個item的padding值
int column = childPosition % spanCount; // 列數
int row = childPosition / spanCount;// 行數
float left;
float right;
float top;
float bottom;
if (orientation == GridLayoutManager.VERTICAL) {
top = 0; // 默認 top為0
bottom = mSpace; // 默認bottom為間距值
if (mEdgeSpace == 0) {
left = column * eachSpace / (spanCount - 1);
right = eachSpace - left;
// 無邊距的話 只有最后一行bottom為0
if (itemCount / spanCount == row) {
bottom = 0;
}
} else {
if (childPosition < spanCount) {
// 有邊距的話 第一行top為邊距值
top = mEdgeSpace;
} else if (itemCount / spanCount == row) {
// 有邊距的話 最后一行bottom為邊距值
bottom = mEdgeSpace;
}
left = column * (eachSpace - mEdgeSpace - mEdgeSpace) / (spanCount - 1) + mEdgeSpace;
right = eachSpace - left;
}
} else {
// orientation == GridLayoutManager.HORIZONTAL 跟上面的大同小異, 將top,bottom替換為left,right即可
left = 0;
right = mSpace;
if (mEdgeSpace == 0) {
top = column * eachSpace / (spanCount - 1);
bottom = eachSpace - top;
if (itemCount / spanCount == row) {
right = 0;
}
} else {
if (childPosition < spanCount) {
left = mEdgeSpace;
} else if (itemCount / spanCount == row) {
right = mEdgeSpace;
}
top = column * (eachSpace - mEdgeSpace - mEdgeSpace) / (spanCount - 1) + mEdgeSpace;
bottom = eachSpace - top;
}
}
outRect.set((int) left, (int) top, (int) right, (int) bottom);
}
接下來是onDraw和onDrawOver的用法。這里我制作簡單的介紹,如果你想要做出一些不一樣的內容。文章末尾我會給出幾個比較好的博客,供大家欣賞。
首先我們要知道onDraw和onDrawOver的執行時間,一下是RecyclerView繪制流程的源碼
/**
* RecyclerView的draw方法
* @param c
*/
@Override
public void draw(Canvas c) {
// 調用父類也就是View的draw方法
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
// 執行ItemDecorations的onDrawOver方法
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
}
/**
* View的draw方法
* @param canvas
*/
@CallSuper
public void draw(Canvas canvas) {
....
// View會繼續調用onDraw
if (!dirtyOpaque) onDraw(canvas);
....
}
/**
* RecyclerView的onDraw方法
* @param c
*/
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
// 執行ItemDecorations的onDraw方法
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
View先會調用draw方法,在draw中又會調用onDraw方法。 而在RecyclerView的draw方法中會先通過super.draw() 調用父類也就是View的draw方法,進而繼續調用RecyclerView的OnDraw方法,ItemDecorations的onDraw方法就在此時會被調用,RecyclerView執行完super.draw()之后,ItemDecorations的onDrawOver方法也被調用,這也就解釋了為什么說onDraw會繪制在itemview之前,表現形式是在最底層(抽象的說法,最底層應該是background),onDrawOver是在itemview繪制之后,表現形式在最上層。知道了onDraw和onDrawOver的具體執行時間與含義。我們這里再來看Android提供給我們的DividerItemDecoration實現我們listView的Divider效果是如何做到的
@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();
}
@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);
}
}
原理很簡單,總結起來就是通過getItemOffsets方法來設置我們item的paddingBottom或paddingRight,然后在我們onDraw方法中遍歷我們的item并在我們需要的區域內繪制我們想要的繪制的內容。
setItemAnimator
設置我們的Item動畫效果。Android提供了一個默認的實現。DefaultItemAnimator,添加以下代碼我們就可以了。
rclMain.setItemAnimator(new DefaultItemAnimator());
添加了這行代碼我們再運行我們的代碼發現,完全木有效果啊,什么鬼。
這里我忘記和大家說了。如果我們想要看到效果,就在我們的數據集合中添加刪除數據。并執行notifyItemInserted(position)或notifyItemRemoved(position) ,就可以看到效果啦。當然這一種效果怎么可能滿足的了我們產品的需求呢,幸好我們有大神。大神們已經開源了很多有關的代碼。這就要大家自己去挖掘了。
點擊事件
因為RecyclerView沒有給我們提供onItemClickListener的方法。所以這個只能我們自己來實現,具體實現比較簡單,和我們ListView,Item中設置點擊按鈕的實現方式是一樣的。我們只需要自己在RecyclerView中寫一個onclickListener監聽器,并提供出去,在BindViewHolder的時候設置ItemView的點擊事件,調用我們監聽器的方法回調給我們的使用者就可以了。
class HomeAdapter extends RecyclerView.Adapter<HomeAdapter.MyViewHolder>
{
//...
public interface OnItemClickLitener
{
void onItemClick(View view, int position);
void onItemLongClick(View view , int position);
}
private OnItemClickLitener mOnItemClickLitener;
public void setOnItemClickLitener(OnItemClickLitener mOnItemClickLitener)
{
this.mOnItemClickLitener = mOnItemClickLitener;
}
@Override
public void onBindViewHolder(final MyViewHolder holder, final int position)
{
holder.tv.setText(mDatas.get(position));
// 如果設置了回調,則設置點擊事件
if (mOnItemClickLitener != null)
{
holder.itemView.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v)
{
int pos = holder.getLayoutPosition();
mOnItemClickLitener.onItemClick(holder.itemView, pos);
}
});
holder.itemView.setOnLongClickListener(new OnLongClickListener()
{
@Override
public boolean onLongClick(View v)
{
int pos = holder.getLayoutPosition();
mOnItemClickLitener.onItemLongClick(holder.itemView, pos);
return false;
}
});
}
}
//...
}
知道了如何設置Item的點擊事件,我相信設置item中單獨View的點擊事件也一定難不住你了。
不同類型的Item
我們可以根據RecyclerView.Adapter提供的getItemViewType();方法根據Position位置返回我們不同的Type類型。
在onCreateViewHolder中根據不同的type創建不同的布局和ViewHolder并返回。在onBindViewHolder中根據ViewHodler instanceof方法獲取他數據哪個ViewHolder來進行賦值操作,例子如下:
lass HomeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_HEADER = 1;
private static final int TYPE_ITEM = 2;
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_HEADER) {
//...
return new HeaderViewHolder(v);
} else {
//...
return new MyViewHolder(v);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof HeaderViewHolder) {
//...
} else if (holder instanceof MyViewHolder) {
//...
}
}
@Override
public int getItemCount() {
return mDatas.size() + 1; //增加頭部ItemView
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return TYPE_HEADER;
} else {
return TYPE_ITEM;
}
}
class HeaderViewHolder extends RecyclerView.ViewHolder {
//...
}
class MyViewHolder extends RecyclerView.ViewHolder {
//...
}
}
參考文章:http://www.lxweimin.com/p/bb09d3ddc64f
https://blog.csdn.net/lmj623565791/article/details/45059587
http://www.lxweimin.com/p/e742df6f59e2
http://www.lxweimin.com/p/6a093bcc6b83
http://www.lxweimin.com/p/3221b5c8fc38