RecyclerView是android v7包下的一個新的控件,用來顯示列表型的數據。在Recyclerview之前,我們現實縱向的列表數據時一般都是使用ListView,而用GridView來顯示網格型的列表數據。而在RecyclerView中,官方把視圖、適配器和數據集合到了一起,Adapter和ViewHolder成爲了Recyclerview的內部類。Recyclerview只負責數據的回收,不關心數據的顯示方法,把數據的顯示方法交給了LayoutManager去管理,強制使用ViewHolder。因此使用Recyclerview時寫法規範了很多。
SwipeRefreshLayout是android v4包下一個刷新的控件,不過該控件只提供了下拉刷新的功能,今天,爲大家帶來使用SwipeRefreshLayout+Recyclerview來實現Recyclerview的下拉刷新和自動加載更多。
首先看一下例子的最終效果圖:
我們要做的是在下拉時刷新數據集(DataSet)並且把新的數據加在Recyclerview的最上面,當往上拖動Recyclerview時,當到達數據集的最後一個item時,自動加載更多的數據(而不是像下拉刷新要拖動一段距離來觸發)。
實現的思路:使用SwipeRefreshLayout來實現下拉刷新功能,Recyclerview本身實現自動加載更多的功能。
首先是主佈局文件:activity_main.xml
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical"
tools:context=".MainActivity">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swiperefresh"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>
主佈局非常簡單,只有一個toolbar和一個SwipeRefreshLayout,而Recyclerview嵌套在SwipeRefreshLayout當中。
到這裏,我們先想一下Recyclerview所需要的幾樣東西:Recyclerview、DataSet、Adapter。
現在Recyclerview已經寫好了,DataSet是在調用者中獲得,並且傳入到Adapter中的,這一點和ListView是一樣。
接着就是我們的Adapter:MyAdapter.java
等等,在寫適配器之前,我們需要一個實體,這個實體是和Recyclerview中的一個item是相對應的,在該例子中,實體很簡單,只有一張圖片和一段文字,新建一個Item.java:
public class Item {
public int image;
public String text;
}
這裏爲了方便所以直接使用圖片資源的ID了,實際項目中,很可能是從本地讀取獲取網絡讀取的,那麼這個圖片應該用URI或者Bitmap,這個要根據具體來設定。然後就是我們的適配器了,誒等等!!示例圖中最後那個loading是什麼鬼?好像和item不一樣?沒錯,這個是Recyclerview的尾部,主要是用來提示用戶我們的應該正在爲您加載數據,您先等一下,尾部一般會用一個正在加載的圈圈或者自定義的動畫來實現,這裏爲了方便我直接使用了一段文字,所以我們在res/layout下創建一個footer.xml:
<?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="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="loading..."
android:gravity="center_horizontal"
android:textSize="20sp"
android:textStyle="bold"
/>
</LinearLayout>
這個很簡單,就不多說了。
接着,真的到了我們最核心的Adapter了:創建一個MyAdapter.java並且繼承RecyclerView.Adapter,支持泛型(用來傳遞我們的實體)
public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static int TYPE_NORMAL = 0;
private static int TYPE_FOOTER = 1;
private LayoutInflater mInflater;
private Context mContext;
private List<Item> mDatas;
public MyAdapter(Context context, List<Item> data) {
this.mContext = context;
this.mDatas = data;
mInflater = LayoutInflater.from(context);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(TYPE_NORMAL == viewType) {
View view = mInflater.inflate(R.layout.item, parent, false);
MyViewHolder holder = new MyViewHolder(view);
return holder;
} else if(TYPE_FOOTER == viewType) {
View view = mInflater.inflate(R.layout.footer, parent, false);
FooterViewHolder holder = new FooterViewHolder(view);
return holder;
} else {
return null;
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(holder instanceof MyViewHolder) {
((MyViewHolder)holder).imageView.setImageResource(mDatas.get(position).image);
((MyViewHolder)holder).textView.setText(mDatas.get(position).text);
}
}
@Override
public int getItemViewType(int position) {
if(position + 1 == getItemCount()) {
return TYPE_FOOTER;
} else {
return TYPE_NORMAL;
}
}
@Override
public int getItemCount() {
return mDatas.size() + 1;
}
}
class MyViewHolder extends RecyclerView.ViewHolder {
public ImageView imageView;
public TextView textView;
public MyViewHolder(View itemView) {
super(itemView);
imageView = (ImageView) itemView.findViewById(R.id.imageView);
textView = (TextView) itemView.findViewById(R.id.text);
}
}
class FooterViewHolder extends RecyclerView.ViewHolder {
public TextView footer;
public FooterViewHolder(View itemView) {
super(itemView);
footer = (TextView) itemView.findViewById(R.id.footer);
}
}
這裏有3個類,第一個就是我們定義的適配器,後兩個是ViewHolder。
自定義適配器裏聲明瞭幾個成員,重寫了一些方法,下面簡單地說明一下這些成員和方法的作用:
TYPE_NORMAL和TYPE_FOOTER:其實就是用來區分Recyclerview中的不同item的,因爲我們的Recyclerview除了顯示真實的數據item之外,還有一個用來表示加載中的footer。
LayoutManager、Context和List<Item>和我們在使用ListView是一樣的,這裏不再多說了。
然後是幾個重寫的方法:
onCreateViewHolder(ViewGroup parent, intviewType):創建View。因爲設備在繪製Recyclerview時是一個item一個item來畫的,所以第一次繪製時需要調用這個方法。這裏我們注意到第二個參數viewType,因爲Recyclerview是可以混合不同類型的view的,本例子中有帶數據是item和一個footer,所以我們在方法中進行了判斷。在該方法中主要的工作是把item的佈局文件轉換成一個view。
onBindViewHolder(RecyclerView.ViewHolderholder, int position):綁定ViewHolder。在該方法中主要的工作是給item中具體的控件設置值。
getItemViewType(int position):獲取item的類型。本例子有兩種類型的View,帶數據的item和一個footer。如果我們的Recyclerview中只用一種View,可以不重寫該方法。
getItemCount():獲取Recyclerview中item的個數,和ListView一致。本例子中有一個footer,所以返回數據集的個數加1。
下面的兩個ViewHolder就是給Item中的具體控件賦值。
最後回到我們的MainActivity:
public class MainActivity extends AppCompatActivity {
private List<Item> data = new ArrayList<>();
private int lastVisibleItem;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
initData();
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
final MyAdapter adapter = new MyAdapter(this, data);
final LinearLayoutManager manager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
final SwipeRefreshLayout srl = (SwipeRefreshLayout) findViewById(R.id.swiperefresh);
srl.setColorSchemeColors(Color.BLUE);
srl.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
Item item = new Item();
item.image = R.drawable.user;
item.text = "新增一個item";
data.add(0, item);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
adapter.notifyDataSetChanged();
srl.setRefreshing(false);
}
}, 2000);
}
});
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView,
int newState) {
super.onScrollStateChanged(recyclerView, newState);
//RecyclerView沒有拖動而且已經到達了最後一個item,執行自動加載
if (newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem + 1 == adapter.getItemCount()) {
for(int i = 0; i < 3; i++) {
Item item = new Item();
item.image = R.drawable.user;
item.text = String.valueOf(Integer.parseInt(data.get(lastVisibleItem - 1).text) + 1);
data.add(item);
}
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
adapter.notifyDataSetChanged();
}
}, 1500);
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
lastVisibleItem = manager.findLastVisibleItemPosition();
}
});
recyclerView.setHasFixedSize(true);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(manager);
}
private void initData() {
for(int i = 1; i <= 6; i++) {
Item item = new Item();
item.image = R.drawable.user;
item.text = String.valueOf(i);
data.add(item);
}
}
}
在activity中,主要做的工作有三個:給我們的SwipeRefreshLayout添加刷新事件、給Recyclerview添加觸摸事件、給Recyclerview綁定適配器。
1、 setOnRefreshListener方法接收一個SwipeRefreshLayout.OnRefreshListener的接口,重寫它的onRefresh方法,在此我們在數據集的前面添加“新增一個item”的Item,並且調用Recyclerview.Adapter.notifyDataSetChanged()方法讓通知適配器我們的數據集發生了變化。最後記得調用SwipeRefreshLayout.setRefreshing(false)來讓刷新動作停止,否則SwipeRefreshLayout會一直在刷新,即使我們的數據已經刷新完畢了,它還會像嗑了炫邁一樣根本停不下來!!!另外值得一提的是SwipeRefreshLayout有一個setColorSchemeResources(int... colorResIds)方法用來設置刷新時的圈圈的顏色變化,就是圈圈每轉一圈就換一種顏色,最多可傳入4中顏色,可以設置資源文件的顏色(R.color.xx),也可以像例子中一樣直接用android.graphics.Color下的預定義的顏色,甚至可以傳入顏色的16進制整數(比如0FFF0000FF)。本例子只設置了一種顏色。
2、 使用Recyclerview.addOnScrollListener(newRecyclerView.OnScrollListener())方法給Recyclerview添加滾動事件來實現自動加載更多的功能。我們在onScrolled方法中通過Recyclerview的佈局管理器LinearLayoutManager(本例子使用線性佈局)獲取最後一個可見item的位置值(如無意外就是我們的footer的位置),然後在onScrollStateChanged方法中進行判斷——當在不滾動(newState == RecyclerView.SCROLL_STATE_IDLE)時且可見item爲數據集中最後一個(lastVisibleItem+ 1 == adapter.getItemCount())時,就加載新的數據。最後再通知適配器我們的數據集發生了變化。
3、 最後是綁定適配器並設置佈局管理器到Recyclerview上。
在本文的最後總結一下實現的步驟:
1、 準備好佈局:SwipeRefreshLayout、Recyclerview、footer等
2、 準備好需要顯示的item的實體類
3、 實現我們的Adapter
4、 在調用者中初始化各個控件,並且給SwipeRefreshLayout設置刷新動作,給Recyclerview添加滾動事件、綁定適配器和佈局管理器
最後值得一提的是知乎和知乎日報都是使用SwipeRefreshLayout來實現的,只是它們的自動加載更多沒有footer而已。