簡單說明
為什么有了ListView還需要RecyclerView?
主要有這幾個(gè)原因:
- 只支持豎直方向上的列表形狀排列,不支持橫向、網(wǎng)格(GridView)、瀑布流等其它排列方式,不靈活,適用性不廣。
- 在緩存機(jī)制不是很好,還有一些優(yōu)化的空間。
RecyclerView相比于ListView的優(yōu)缺點(diǎn):
- 更靈活,適用性更廣。
- 更方便添加Item的動(dòng)畫,分割線等
- 支持局部刷新和定向刷新
- 使用起來沒有ListView簡單
- 不支持Item的點(diǎn)擊事件,需要自己處理。
一般使用
RecyclerView的一般使用和ListView在總體上差不多。區(qū)別主要有以下幾點(diǎn):
- 必須添加一個(gè)布局管理器來聲明列表中的Item的排列方式
- 如果需要分割線,可以單獨(dú)添加,且名字叫Item裝飾:ItemDecoration。
- Adapter的創(chuàng)建,相對(duì)于ListView更規(guī)范化,且稍微復(fù)雜一些。
RecyclerView rv = findViewById(R.id.rv_second);
// 設(shè)置布局管理器,這里是最簡單的豎直線性排列的布局
rv.setLayoutManager(new LinearLayoutManager(this));
// 設(shè)置Adapter
rv.setAdapter(adapter);
// 設(shè)置分割線
rv.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
Adapter的使用
1. 整體結(jié)構(gòu)
Adatper需要繼承RecyclerView.Adapter類,且需要額外增加一個(gè)ViewHolder類繼承RecyclerView.ViewHolder。然后將自定義的Holder作為Adapter的泛型類型。關(guān)于Holder的處理,放到后面再說
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.Holder> {
static class Holder extends RecyclerView.ViewHolder {
}
}
2. 復(fù)寫方法
Adapter需要復(fù)寫父類的三個(gè)抽象方法,分別如下。
// 直接返回?cái)?shù)量,固定式寫法
@Override
public int getItemCount() {
return data == null ? 0 : data.size();
}
// 創(chuàng)建ItemView,將ItemView和Holder綁定,當(dāng)然也要綁定itemView中的控件
onCreateViewHolder()
// 在這里處理數(shù)據(jù),將position對(duì)應(yīng)的JavaBean對(duì)象中的數(shù)據(jù)設(shè)置進(jìn)holder.xx控件中
onBindViewHolder()
其實(shí)寫法和ListView 的Adapter在優(yōu)化之后的寫法是一樣的。只是將ListView.Adapter中的getView()方法中的代碼分開放到onCreateViewHolder(),Holder類,和onBindViewHolder()方法三部分中去。
ListView.Adapter的getView方法和RecyclerView.Adapter的onCreateViewHolder、onBindViewHolder方法的比較:
ListView的Adapter
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 對(duì)convertView和view中的控件的復(fù)用
ViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_fruit_first, parent, false);
holder = new ViewHolder();
holder.iv = convertView.findViewById(R.id.iv_fruit);
holder.tv = convertView.findViewById(R.id.tv_fruit);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
// 下面是將數(shù)據(jù)設(shè)置進(jìn)具體的convertView的控件中
Fruit fruit = data.get(position);
holder.iv.setImageResource(fruit.getDrawableResId());
holder.tv.setText(fruit.getName());
return convertView;
}
RecyclerView的Adapter
/**
* 創(chuàng)建ItemView,將ItemView和Holder綁定,當(dāng)然也要綁定itemView中的控件
*
* @param parent 就是RecyclerView對(duì)象本身
* @param viewType 如果有多種布局,根據(jù)這個(gè)viewType的值不同,要加載不同的布局
* @return 在onBindViewHolder方法中使用的Holder對(duì)象
*/
@NonNull
@Override
public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_fruit_first, parent, false);
// 這里將view和holder的綁定方式與ListViewAdapter不同,不再是使用Tag的方式了,而是將view作為holder的成員變量
Holder holder = new Holder(view);
holder.iv = view.findViewById(R.id.iv_fruit);
holder.tv = view.findViewById(R.id.tv_fruit);
return holder;
}
/**
* 在這里處理數(shù)據(jù),將position對(duì)應(yīng)的JavaBean對(duì)象中的數(shù)據(jù)設(shè)置進(jìn)holder.xx控件中
*
* @param holder 就是或新建,或復(fù)用的Holder對(duì)象
* @param position item對(duì)應(yīng)的索引
*/
@Override
public void onBindViewHolder(@NonNull Holder holder, int position) {
Fruit fruit = data.get(position);
holder.iv.setImageResource(fruit.getDrawableResId());
holder.tv.setText(fruit.getName());
}
3. 不要忘記將數(shù)據(jù)設(shè)置進(jìn)Adapter中
private ArrayList<Fruit> data;
public FruitAdapter(ArrayList<Fruit> data) {
this.data = data;
}
4. ViewHolder
與ListView中的Holder不同,RecyclerViewAdapter中的Holder需要繼承RecyclerView.ViewHolder,且因?yàn)镽ecyclerView.ViewHolder只有一個(gè)要itemView作為參數(shù)的構(gòu)造方法,所以我們自定義的ViewHolder也要必須添加構(gòu)造方法。
/**
* 因?yàn)楦割愔挥幸粋€(gè)需要view參數(shù)的構(gòu)造方法,所以Holder類必須添加一個(gè)構(gòu)造方法,能夠調(diào)用父類的這個(gè)構(gòu)造方法
*/
static class Holder extends RecyclerView.ViewHolder {
ImageView iv;
TextView tv;
/**
* 構(gòu)造方法,將itemView與holder對(duì)象綁定,并調(diào)用父類的有itemView作為參數(shù)的構(gòu)造方法
*
* @param itemView 就是Adapter的onCreateViewHolder方法創(chuàng)建的View
*/
public Holder(@NonNull View itemView) {
super(itemView);
}
}
5. 完整代碼如下
/**
* 1. 一般情況下我們同時(shí)需要自定義Holder類繼承Rv中的ViewHolder,然后將Holder類設(shè)置為Adapter中的泛型
* 2. 繼承Rv.Adapter類之后,需要復(fù)寫3個(gè)抽象方法
*/
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.Holder> {
private ArrayList<Fruit> data;
public FruitAdapter(ArrayList<Fruit> data) {
this.data = data;
}
/**
* 和ListView一樣,也是獲取列表需要渲染加載的數(shù)據(jù)的數(shù)量
*/
@Override
public int getItemCount() {
return data == null ? 0 : data.size();
}
/**
* 創(chuàng)建ItemView,將ItemView和Holder綁定,當(dāng)然也要綁定itemView中的控件
*
* @param parent 就是RecyclerView對(duì)象本身
* @param viewType 如果有多種布局,根據(jù)這個(gè)viewType的值不同,要加載不同的布局
* @return 在onBindViewHolder方法中使用的Holder對(duì)象
*/
@NonNull
@Override
public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_fruit_first, parent, false);
Holder holder = new Holder(view);
holder.iv = view.findViewById(R.id.iv_fruit_first);
holder.tv = view.findViewById(R.id.tv_fruit_first);
return holder;
}
/**
* 在這里處理數(shù)據(jù),將position對(duì)應(yīng)的JavaBean對(duì)象中的數(shù)據(jù)設(shè)置進(jìn)holder.xx控件中
*
* @param holder 就是或新建,或復(fù)用的Holder對(duì)象
* @param position item對(duì)應(yīng)的索引
*/
@Override
public void onBindViewHolder(@NonNull Holder holder, int position) {
Fruit fruit = data.get(position);
holder.iv.setImageResource(fruit.getDrawableResId());
holder.tv.setText(fruit.getName());
}
/**
* 因?yàn)楦割愔挥幸粋€(gè)需要view參數(shù)的構(gòu)造方法,所以Holder類必須添加一個(gè)構(gòu)造方法,能夠調(diào)用父類的這個(gè)構(gòu)造方法
*/
static class Holder extends RecyclerView.ViewHolder {
ImageView iv;
TextView tv;
/**
* 構(gòu)造方法,將itemView與holder對(duì)象綁定,并調(diào)用父類的有itemView作為參數(shù)的構(gòu)造方法
*
* @param itemView 就是Adapter的onCreateViewHolder方法創(chuàng)建的View
*/
public Holder(@NonNull View itemView) {
super(itemView);
}
}
}
其它布局:LayoutManager
常用的布局管理器有2種,分別是線性和網(wǎng)格。可以達(dá)到普通的列表、橫向列表、網(wǎng)格狀、瀑布流布局的效果。
LinearLayoutManager
最常用的就是LinearLayoutManager。
下面就是創(chuàng)建一個(gè)最普通的類似ListView的布局管理器。
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
rv.setLayoutManager(linearLayoutManager);
LinearLayoutManager還有一個(gè)常用的構(gòu)造方法。
- 參數(shù)orientation表示排列方向
- 參數(shù)reverseLayout表示是否倒序展示數(shù)據(jù)
/**
* @param context Current context, will be used to access resources.
* @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link
* #VERTICAL}.
* @param reverseLayout When set to true, layouts from end to start.
*/
public LinearLayoutManager(Context context, @RecyclerView.Orientation int orientation,
boolean reverseLayout) {
setOrientation(orientation);
setReverseLayout(reverseLayout);
}
因此我們?nèi)绻胍@示水平方向列表,直接使用這個(gè)構(gòu)造方法即可。下面的代碼,再將itemView的寬度不設(shè)置為match_parent,就可以實(shí)現(xiàn)水平方向的排列。當(dāng)然就算itemView的寬度是match_parent,也是水平排列的列表,但是每個(gè)item的寬度就會(huì)都占用屏幕的寬度了。
LinearLayoutManager linearLayoutManager =
new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
rv.setLayoutManager(linearLayoutManager);
StaggeredGridLayoutManager
如果想要網(wǎng)格狀的布局,就可以使用StaggeredGridLayoutManager來完成。
和LinearLayoutManager不同,我們常用的StaggeredGridLayoutManager構(gòu)造方法是不傳Context,而必須指定方向和行列數(shù)的構(gòu)造方法。
/**
* Creates a StaggeredGridLayoutManager with given parameters.
*
* @param spanCount If orientation is vertical, spanCount is number of columns. If
* orientation is horizontal, spanCount is number of rows.
* @param orientation {@link #VERTICAL} or {@link #HORIZONTAL}
*/
public StaggeredGridLayoutManager(int spanCount, int orientation) {
mOrientation = orientation;
setSpanCount(spanCount);
mLayoutState = new LayoutState();
createOrientationHelpers();
}
StaggeredGridLayoutManager的使用如下,不過要注意itemView的寬高的設(shè)置。
// 創(chuàng)建一個(gè)豎直方向排列,一共只有2列的網(wǎng)格狀布局管理器
StaggeredGridLayoutManager staggeredGridLayoutManager
= new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
rv.setLayoutManager(staggeredGridLayoutManager);
// 創(chuàng)建一個(gè)水平方向排列,一共只有2行的網(wǎng)格狀布局管理器
StaggeredGridLayoutManager staggeredGridLayoutManager
= new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.HORIZONTAL);
rv.setLayoutManager(staggeredGridLayoutManager);
瀑布流
瀑布流就是控件的寬度或者高度不等的網(wǎng)格型布局。
瀑布流的實(shí)現(xiàn)很簡單,就是在StaggeredGridLayoutManager的基礎(chǔ)之上,更改ItemView的高度或者寬度即可。只是這里注意,要使用控件的LayoutParams來修改寬高等尺寸屬性。
/**
* 將數(shù)據(jù)設(shè)置進(jìn)itemView中的控件,也就是ViewHolder中的成員變量。
*/
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
// 不能通過view直接設(shè)置它的寬高,需要通過一個(gè)LayoutParams的成員變量來修改寬高。
ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
// 想辦法讓itemView的高度在某個(gè)范圍內(nèi)變化
layoutParams.height = DensityUtil.dip2px(holder.itemView.getContext(), 60) +
DensityUtil.dip2px(holder.itemView.getContext(), new Random().nextInt(60));
// 不要忘記設(shè)置數(shù)據(jù)
Fruit fruit = data.get(position);
holder.iv.setImageResource(fruit.getDrawableRes());
holder.tv.setText(fruit.getName());
}
dp轉(zhuǎn)px的工具方法也很簡單。獲取系統(tǒng)的屏幕像素密度,乘以要dp數(shù)值,就是像素值。
public class DensityUtil {
/**
* 根據(jù)手機(jī)的分辨率從 dp 的單位 轉(zhuǎn)成為 px(像素)
*/
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* 根據(jù)手機(jī)的分辨率從 px(像素) 的單位 轉(zhuǎn)成為 dp
*/
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
}
分割線/裝飾:ItemDecoration
分割線
一般情況下,我們?cè)赗ecyclerView中添加分割線的方式是將線畫在itemView中,然后根據(jù)條件決定線是否顯示。
如果想要itemView之間有間距,我們一般也是用在itemView中添加margin的方式完成。
但是如果有比較復(fù)雜的的對(duì)于itemView分割線、背景樣式等的處理的時(shí)候,我們就需要使用ItemDecoration來完成了。
ItemDecoration
案例完整代碼:
public class FruitDecoration extends RecyclerView.ItemDecoration {
private final Paint mPaint;
public FruitDecoration(Context context) {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(context.getResources().getColor(R.color.purple_200));
}
/**
* 可以實(shí)現(xiàn)類似于Padding的效果。就是控制各個(gè)Item之間的間距等。
*
* @param outRect 就是ItemView的四周的邊距。就是系統(tǒng)會(huì)根據(jù)outRect的值來擴(kuò)展item的區(qū)域。
* @param view 當(dāng)前ItemView
* @param parent 就是RecyclerView
* @param state 存儲(chǔ)一些RecyclerView的狀態(tài)等,用的不多
*/
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
/**
* 這里有兩個(gè)獲取position的方法。他們一般情況下沒有區(qū)別。
* 僅在RecyclerView刷新布局后,layoutPosition會(huì)晚adapterPosition大約16ms。
* 因?yàn)槔L制完成之后,layoutPosition才有正確的值,所以我們一般用adapterPosition就好。
*
* 只有在使用findViewHolderForLayoutPosition獲取當(dāng)前item的ViewHolder時(shí),用layoutPosition才更好,因?yàn)榇藭r(shí)layoutPosition 和用戶在屏幕上看到的一定是一樣的
*/
int position = parent.getChildAdapterPosition(view);
int layoutPosition = parent.getChildLayoutPosition(view);
/**
* 這里注意,下面兩個(gè)count的值的不同。
* viewCount是當(dāng)前RecyclerView中的itemView的數(shù)量。并不是數(shù)據(jù)的數(shù)量,Adapter中的getItemCount才是
* 因?yàn)镽ecyclerView不會(huì)一次性創(chuàng)建所以的itemView,而是會(huì)進(jìn)行view的復(fù)用。
*/
int viewCount = parent.getChildCount();
int childCount = parent.getAdapter().getItemCount();
if (position == 0) {
outRect.bottom = 20;
} else if (position == childCount - 1) {
outRect.top = 20;
} else {
outRect.top = 20;
outRect.bottom = 20;
}
}
/**
* 繪制ItemView的背景。意思就是這里畫出來的圖像,會(huì)顯示在itemView的下面。這個(gè)方法會(huì)在繪制itemView之前調(diào)用.
* 這里的繪制區(qū)域是根據(jù)上面的getItemOffsets決定的。
* 注意這里的canvas指的是RecyclerView的布局部分,而不是itemView的界面。如果想在每一個(gè)ItemView的相同位置畫圖案,需要計(jì)算對(duì)應(yīng)的坐標(biāo)。
*
* @param c 繪畫的布
* @param parent 就是RecyclerView對(duì)象本身
* @param state state本身
*/
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
}
/**
* 繪制ItemView的前景。意思就是這里畫出來的圖像,會(huì)顯示在itemView的上面
*
* @param c 繪畫的布
* @param parent 就是RecyclerView對(duì)象本身
* @param state state本身
*/
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
int radius = child.getHeight() / 2;
int centerX = child.getRight() - 20 - radius;
int centerY = child.getTop() + child.getHeight() / 2;
c.drawCircle(centerX, centerY, radius, mPaint);
}
}
}
動(dòng)畫:ItemAnimator
點(diǎn)擊事件
主要的實(shí)現(xiàn)方式還是在Adapter的onBindViewHolder中設(shè)置點(diǎn)擊事件。
多種布局
對(duì)應(yīng)的Layout布局文件
getItemViewType
onCreateViewHolder
Holder類
onBindViewHolder
添加頭尾
下拉刷新上拉更多
好用的第三方框架
BRVAH(BaseRecyclerViewAdapterHelper)(RecyclerView使用框架):http://www.recyclerview.org/
SmartRefrshLayout(下拉刷新框架):https://gitee.com/scwang90/SmartRefreshLayout
參考資料
Android RecyclerView 使用完全解析 體驗(yàn)藝術(shù)般的控件
Android 優(yōu)雅的為RecyclerView添加HeaderView和FooterView
【騰訊Bugly干貨分享】Android ListView 與 RecyclerView 對(duì)比淺析—緩存機(jī)制