前言
最近開始了實習,熟悉項目時,發現一些RecyclerView的Adapter類都繼承了一個名爲BaseQuickAdapter的類,百度得知這是由BRVAH(官方網站)提供的萬用適配器,相比原始的適配器,能減少70%的代碼。遂學習之,學習後發現針不戳,只要熟悉RecyclerView的基礎使用即可快速上手,並且框架高度封裝,能極大提高開發效率。另外,不熟悉RecyclerView使用的同學可以看看我之前的文章:Android——RecyclerView使用匯總
框架引入
首先在build.gradle(Project: YourProjectName)中添加:
allprojects {
repositories {
...
maven {
url 'https://jitpack.io' }
}
}
注意: 此文件下的repositories有兩個,分別在buildscript和allprojects下,配置在allprojects下才是正確的,不要配置錯。
然後在build.gradle(Module: app)中添加依賴:
dependencies {
...
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4'
}
注意: 由於BRAVH從2.x升級到3.x爲不完全兼容升級(官方原話),因此不同版本之間的代碼差異可能較大,本文以3.0.4版本爲準。(PS:網絡上大部分教程似乎都仍是2.x版本,我之前的demo也是用的是2.x版本,後來換成3.x版本後報紅了一大片…)
基礎使用
假設我們的需求是在頁面顯示多張圖片以及它們的名稱,那麼我們首先需要一個存儲圖片路徑和圖片名稱的Bean類以及用於展示它們的佈局文件,佈局文件十分簡單,避免篇幅過長就不展示了。
public class ItemBean {
private String text;
private int imgResId;
/* 省略構造方法和Get/Set方法 */
}
然後需要一個Adapter將數據和佈局與RecyclerView綁定。創建MyAdapter並繼承BaseQuickAdapter<T, VH>,第一個泛型對應數據類型,就是ItemBean;第二個泛型對應ViewHolder,一般直接填寫BaseViewHolder即可。接着實現相關方法,此時我們的適配器大概是這個模樣:
public class MyAdapter extends BaseQuickAdapter<ItemBean, BaseViewHolder> {
public MyAdapter(int layoutResId, @Nullable List<ItemBean> data) {
super(layoutResId, data);
}
public MyAdapter(int layoutResId) {
super(layoutResId);
}
@Override
protected void convert(final BaseViewHolder helper, final ItemBean item) {
}
}
- 兩個構造方法比較好理解,參數分別是佈局資源ID和初始數據。
- convert方法類似原始適配器中的onBindViewHolder方法,用於綁定數據、設置事件等。
最後,完成convert方法,再爲RecyclerView設置此適配器即完成了最基本的使用。
protected void convert(final BaseViewHolder helper, final ItemBean item) {
//爲指定ID的組件設置屬性
helper.setText(R.id.item_tv,item.getText());
helper.setImageResource(R.id.item_iv,item.getImgResId());
//上下兩種方法都行
//先獲取指定組件,再設置屬性
//TextView textView = helper.getView(R.id.item_tv);
//textView.setText(item.getText());
//ImageView imageView = helper.getView(R.id.item_iv);
//imageView.setImageResource(item.getImgResId());
}
//初始化RecyclerView
RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new GridLayoutManager(this,3));
//初始化數據
List<ItemBean> dataList = initData();
//初始化適配器
MyAdapter myAdapter = new MyAdapter(R.layout.layout_item);
myAdapter.setNewInstance(dataList);
//設置適配器
recyclerView.setAdapter(myAdapter);
添加空佈局
myAdapter.setRecyclerView(recyclerView); //一定要有這行代碼,否則無效
myAdapter.setEmptyView(R.layout.layout_empty);
添加頭部和尾部
添加頭部和尾部並沒有像添加空佈局一樣有接收佈局ID的方法,需要手動填充。
//設置頭部和尾部
View footView = LayoutInflater.from(this).inflate(R.layout.layout_foot, recyclerView, false);
myAdapter.setFooterView(footView);
View headView = LayoutInflater.from(this).inflate(R.layout.layout_head, recyclerView, false);
myAdapter.setHeaderView(headView);
添加事件
事件的對象可以是Item,也可以是Item中的子控件,事件可以是點擊或者長按。下面分別介紹如何添加Item和Item子控件的點擊事件,長按事件類似則不過多贅述。
Item事件
//設置Item點擊事件
myAdapter.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(@NonNull BaseQuickAdapter<?, ?> adapter, @NonNull View view, int position) {
Toast.makeText(MainActivity.this,"你點擊了Item" + (position + 1), Toast.LENGTH_SHORT).show();
}
});
Item子控件事件
添加Item子控件事件的方式有兩種,一種是在Adapter的convert方法中獲取控件,然後設置;另一種方式則與設置Item事件類似,不過需要先註冊對應的控件。
- 在convert方法中設置
protected void convert(final BaseViewHolder helper, final ItemBean item) {
TextView textView = helper.getView(R.id.item_tv);
textView.setText(item.getText());
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getContext(),"你點擊了TextView" + (helper.getAdapterPosition() + 1), Toast.LENGTH_SHORT).show();
}
});
ImageView imageView = helper.getView(R.id.item_iv);
imageView.setImageResource(item.getImgResId());
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getContext(),"你點擊了ImageView" + (helper.getAdapterPosition() + 1), Toast.LENGTH_SHORT).show();
}
});
}
- 使用adapter設置
myAdapter.addChildClickViewIds(R.id.item_iv,R.id.item_tv); //註冊對應的控件
myAdapter.setOnItemChildClickListener(new OnItemChildClickListener() {
@Override
public void onItemChildClick(@NonNull BaseQuickAdapter adapter, @NonNull View view, int position) {
if (view instanceof TextView) {
Toast.makeText(MainActivity.this,"你點擊了TextView" + (position + 1), Toast.LENGTH_SHORT).show();
} else if (view instanceof ImageView) {
Toast.makeText(MainActivity.this,"你點擊了ImageView" + (position + 1), Toast.LENGTH_SHORT).show();
}
}
});
添加加載動畫
myAdapter.setAnimationWithDefault(AnimationType type);
默認提供五種動畫類型:漸顯、縮放、從下方滑入、從左側滑入、從右側滑入。
/**
* 內置默認動畫類型
*/
enum class AnimationType {
AlphaIn, ScaleIn, SlideInBottom, SlideInLeft, SlideInRight
}
實現效果:在主界面新增一個按鈕,當點擊按鈕時爲列表新增一個數據,並使其縮放顯示。
//添加加載動畫
myAdapter.setAnimationWithDefault(BaseQuickAdapter.AnimationType.ScaleIn);
//增加數據
Button button = findViewById(R.id.add_btn);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
myAdapter.addData(new ItemBean("新增的圖片",R.drawable.pic11));
}
});
上拉加載
簡單使用
使用此功能首先需要讓適配器實現LoadMoreModule接口:
public class MyAdapter extends BaseQuickAdapter<ItemBean, BaseViewHolder> implements LoadMoreModule
然後從適配器獲取到這個LoadMoreModule並設置監聽事件即可:
//上拉加載
myAdapter.getLoadMoreModule().setOnLoadMoreListener(new OnLoadMoreListener() {
@Override
public void onLoadMore() {
//模擬加載數據
List<ItemBean> newData = new ArrayList<>();
newData.add(new ItemBean("爺",R.drawable.pic17));
newData.add(new ItemBean("驚了",R.drawable.pic16));
newData.add(new ItemBean("爺",R.drawable.pic17));
myAdapter.addData(newData);
//加載結束
Toast.makeText(MainActivity.this,"加載數據成功",Toast.LENGTH_SHORT).show();
myAdapter.getLoadMoreModule().loadMoreComplete();
}
});
相關功能
關閉自動加載
當需求希望用戶手動點擊加載數據時,可以開啓此功能。
setAutoLoadMore(false);
//handler
handler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
if (msg.what == 200) {
List<ItemBean> newData = new ArrayList<>();
newData.add(new ItemBean("針", R.drawable.pic18));
newData.add(new ItemBean("牛", R.drawable.pic19));
newData.add(new ItemBean("啤", R.drawable.pic20));
myAdapter.addData(newData);
//加載完成
myAdapter.getLoadMoreModule().loadMoreComplete();
}
}
};
//上拉加載
myAdapter.getLoadMoreModule().setAutoLoadMore(false);//關閉自動加載
myAdapter.getLoadMoreModule().setOnLoadMoreListener(new OnLoadMoreListener() {
@Override
public void onLoadMore() {
//模擬加載數據
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Message msg = new Message();
msg.what = 200;
handler.sendMessage(msg);
}
}).start();
}
});
加載結束
當沒有更多數據時,開啓此功能。
loadMoreEnd();
//handler
handler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
if (msg.what == 200) {
//省略部分代碼
//加載結束
myAdapter.getLoadMoreModule().loadMoreEnd();
}
}
};
加載成功/失敗
與加載結束類似,根據後臺獲取數據的回調分別調用即可。
loadMoreComplete();//加載成功
loadMoreFail();//加載失敗
自定義提示佈局
需求總是花裏胡哨的,自定義佈局總是不可避免的。加載更多數據時,一般有五種狀態——未加載(等待加載)、加載中、加載失敗、加載成功、加載結束(沒有數據了)。其中,加載成功一般會伴隨着新數據的顯示,所以不需要特別提示用戶。那麼,首先爲其餘四種狀態編寫不同的提示佈局:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_40">
<LinearLayout
android:id="@+id/load_more_loading_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/loading_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleSmall"
android:layout_marginRight="@dimen/dp_4"
android:indeterminateDrawable="@drawable/brvah_sample_footer_loading_progress"/>
<TextView
android:id="@+id/loading_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/dp_4"
android:text="正在加載中..."
android:textColor="#0dddb8"
android:textSize="@dimen/sp_14"/>
</LinearLayout>
<FrameLayout
android:id="@+id/load_more_load_fail_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<TextView
android:id="@+id/tv_prompt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="#0dddb8"
android:text="加載失敗,點👴重試"/>
</FrameLayout>
<FrameLayout
android:id="@+id/load_more_load_complete_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="👇點擊查看更多👇"
android:textColor="@android:color/darker_gray"/>
</FrameLayout>
<FrameLayout
android:id="@+id/load_more_load_end_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="沒有數據了,👴很生氣"
android:textColor="@android:color/darker_gray"/>
</FrameLayout>
</FrameLayout>
然後,我們只需根據規則,創建一個類繼承BaseLoadMoreView,並重寫相關方法,在對應的方法下返回對應的容器ID即可。
public class CustomLoadMoreView extends BaseLoadMoreView {
@NotNull
@Override
public View getRootView(@NotNull ViewGroup parent) {
return LayoutInflater.from(parent.getContext()).inflate(R.layout.view_load_more, parent, false);
}
@NotNull
@Override
public View getLoadingView(@NotNull BaseViewHolder holder) {
return holder.findView(R.id.load_more_loading_view);
}
@NotNull
@Override
public View getLoadComplete(@NotNull BaseViewHolder holder) {
return holder.findView(R.id.load_more_load_complete_view);
}
@NotNull
@Override
public View getLoadEndView(@NotNull BaseViewHolder holder) {
return holder.findView(R.id.load_more_load_end_view);
}
@NotNull
@Override
public View getLoadFailView(@NotNull BaseViewHolder holder) {
return holder.findView(R.id.load_more_load_fail_view);
}
}
最後,將編寫好的CustomLoadMoreView設置到適配器中,並對代碼做一些修改來模擬不同的加載狀態。
//handler
handler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what) {
case 200:
List<ItemBean> newData = new ArrayList<>();
newData.add(new ItemBean("針", R.drawable.pic18));
newData.add(new ItemBean("牛", R.drawable.pic19));
newData.add(new ItemBean("啤", R.drawable.pic20));
myAdapter.addData(newData);
//加載完成
myAdapter.getLoadMoreModule().loadMoreComplete();
break;
case 201:
myAdapter.getLoadMoreModule().loadMoreFail();
break;
case 202:
myAdapter.getLoadMoreModule().loadMoreEnd();
break;
}
}
};
//上拉加載
myAdapter.getLoadMoreModule().setLoadMoreView(new CustomLoadMoreView());//👈👈👈
myAdapter.getLoadMoreModule().setAutoLoadMore(false);
myAdapter.getLoadMoreModule().setOnLoadMoreListener(new OnLoadMoreListener() {
@Override
public void onLoadMore() {
//模擬加載數據
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Message msg = new Message();
switch (count) {
case 0:
//模擬加載成功
msg.what = 200;
break;
case 1:
//模擬加載失敗
msg.what = 201;
break;
default:
//模擬沒有更多數據
msg.what = 202;
break;
}
count++;
handler.sendMessage(msg);
}
}).start();
}
});
側滑和拖拽
此框架中,加載更多(LoadMore)和拖拽(Drag)都是單獨的模塊,需要根據需求集成。類似LoadMoreModule,使用拖拽和側滑功能之前,需要讓適配器實現相應接口。
public class MyAdapter extends BaseQuickAdapter<ItemBean, BaseViewHolder> implements DraggableModule
並且大部分功能邏輯已經集成在框架中,接下來只需要讓適配器允許側滑和拖拽即可使用側滑刪除和拖拽移動功能。
myAdapter.getDraggableModule().setSwipeEnabled(true);//允許側滑
myAdapter.getDraggableModule().setDragEnabled(true); //允許拖拽
如果想要實現更多的功能,就對事件進行監聽處理吧,我大概對每個方法都試驗了一下,結論在註釋裏。
myAdapter.getDraggableModule().setOnItemDragListener(new OnItemDragListener() {
//拖拽開始
@Override
public void onItemDragStart(RecyclerView.ViewHolder viewHolder, int pos) {
}
/**
* 因拖拽而發生位置互換時回調
* @param source 非用戶拖拽的那個改變位置的Item
* @param from 用戶拖拽的Item換位之前的position
* @param target 用戶拖拽的Item
* @param to 用戶拖拽的Item換位之後的position
*/
@Override
public void onItemDragMoving(RecyclerView.ViewHolder source, int from, RecyclerView.ViewHolder target, int to) {
}
//拖拽結束
@Override
public void onItemDragEnd(RecyclerView.ViewHolder viewHolder, int pos) {
}
});
myAdapter.getDraggableModule().setOnItemSwipeListener(new OnItemSwipeListener() {
//側滑開始
@Override
public void onItemSwipeStart(RecyclerView.ViewHolder viewHolder, int pos) {
}
//側滑結束時回調,如果item被移除,pos將爲負
@Override
public void clearView(RecyclerView.ViewHolder viewHolder, int pos) {
}
//Item被移出時回調
@Override
public void onItemSwiped(RecyclerView.ViewHolder viewHolder, int pos) {
}
//側滑中,可以利用canvas在滑動的空白區域繪製,dX和dY分別是用戶在不同方向上滑動的偏移量
@Override
public void onItemSwipeMoving(Canvas canvas, RecyclerView.ViewHolder viewHolder, float dX, float dY, boolean isCurrentlyActive) {
}
});
使用側滑的繪製功能,實現一個側滑刪除表情的時候在背後生成一張“可憐”表情的效果。
@Override
public void onItemSwipeMoving(Canvas canvas, RecyclerView.ViewHolder viewHolder, float dX, float dY, boolean isCurrentlyActive) {
canvas.drawBitmap(bitmap, 0, 0, new Paint());
}
最後,有一個小問題:之前使用ItemTouchHelper實現側滑和拖拽功能時,功能邏輯都是由編寫者自行完成的,而此框架已經將其功能封裝得比較完全,例如側滑刪除已經寫死。倘若想要實現類似QQ那樣的置頂之類的功能,可能要對源碼進行修改,或者還有其他的方法我還沒有發現?