手把手教你實現Android RecyclerView上拉加載功能
摘要
一直在用到RecyclerView時都會微微一顫,因為一直都沒去了解怎么實現上拉加載,受夠了每次去Github找開源引入,因為感覺就為了一個上拉加載功能而去引入一大堆你不知道有多少BUG的代碼,不僅增加了項目的冗余程度,而且出現BUG的時候,你卻發現很難去改,正因為這樣,我就下定決心去了解如何來實現RecyclerView的上拉加載功能,相信大家和我有過同樣的情況,但是我相信,只要你給自己幾分鐘看完這篇文章,你就會發現實現一個上拉加載是非常的簡單。
什么是上拉加載
上拉加載和下拉刷新相對應,在Android API LEVEL 19(即4.4)之后,Google官方推出了SwipeRefreshLayout和RecyclerView的共同使用,為我們提供了更加便捷的列表下拉刷新功能,但是,并沒有給我們提供上拉加載功能,但是在RecyclerView強大的可擴展之下,Github上面有了很多開源項目實現了上拉加載功能,即我們不會一次性將所有數據加載到列表中,當用戶滑動到底部時,再向服務器請求數據,再填充數據到列表中,這樣不僅可以有更好的人機交互,同時在減少了服務器的壓力的同時也對客戶端的性能有了更好的提升。本篇文章主要通過介紹實現以下簡單的上拉加載功能,同學們可以在掌握了最基本的實現功能之后,再通過擴展和優化,甚至可以封裝成比較通用的代碼,開源到Github上面。
Demo
實現思路
一、XML的實現
布局很簡單,只有一個SwipeRefreshLayout包裹了一個RecyclerView,相信用過RecyclerView的都很容易看懂。如下為activity_main.xml:
<LinearLayout xmlns:android="
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout></code></pre>
然后,我們RecyclerView的Item布局也是非常簡單,只有一個TextView。如下為item.xml:
<LinearLayout xmlns:android="
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="120dp"
android:background="@android:color/holo_blue_dark"
android:gravity="center"
android:textSize="30sp"
android:textColor="#ffffff"
android:text="11"
android:layout_marginBottom="1dp"/>
</LinearLayout></code></pre>
看到我們效果圖都知道,在我們上拉時,還有一個提示的條目,我定義為 footview.xml:
<LinearLayout xmlns:android="
<TextView
android:id="@+id/tips"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="30dp"
android:textSize="15sp"
android:layout_marginBottom="1dp"/>
</LinearLayout></code></pre>
二、初始化SwipeRefreshLayout
在準備好了布局文件之后,我們就把目光轉到Activity中去,首先我們需要初始化SwipeRefreshLayout,初始化也是很簡單,這里省去了findView操作,所以就只有設置轉動的顏色,還有設置刷新的監聽事件:
private void initRefreshLayout() {
refreshLayout.setColorSchemeResources(android.R.color.holo_blue_light, android.R.color.holo_red_light,
android.R.color.holo_orange_light, android.R.color.holo_green_light);
refreshLayout.setOnRefreshListener(this);
}
@Override
public void onRefresh() {
// 設置可見
refreshLayout.setRefreshing(true);
// 重置adapter的數據源為空
adapter.resetDatas();
// 獲取第第0條到第PAGE_COUNT(值為10)條的數據
updateRecyclerView(0, PAGE_COUNT);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
// 模擬網絡加載時間,設置不可見
refreshLayout.setRefreshing(false);
}
}, 1000);
}</code></pre>
三、定義RecyclerView的Adapter
public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<String> datas; // 數據源
private Context context; // 上下文Context
private int normalType = 0; // 第一種ViewType,正常的item
private int footType = 1; // 第二種ViewType,底部的提示View
private boolean hasMore = true; // 變量,是否有更多數據
private boolean fadeTips = false; // 變量,是否隱藏了底部的提示
private Handler mHandler = new Handler(Looper.getMainLooper()); //獲取主線程的Handler
public MyAdapter(List<String> datas, Context context, boolean hasMore) {
// 初始化變量
this.datas = datas;
this.context = context;
this.hasMore = hasMore;
}
// 獲取條目數量,之所以要加1是因為增加了一條footView
@Override
public int getItemCount() {
return datas.size() + 1;
}
// 自定義方法,獲取列表中數據源的最后一個位置,比getItemCount少1,因為不計上footView
public int getRealLastPosition() {
return datas.size();
}
// 根據條目位置返回ViewType,以供onCreateViewHolder方法內獲取不同的Holder
@Override
public int getItemViewType(int position) {
if (position == getItemCount() - 1) {
return footType;
} else {
return normalType;
}
}
// 正常item的ViewHolder,用以緩存findView操作
class NormalHolder extends RecyclerView.ViewHolder {
private TextView textView;
public NormalHolder(View itemView) {
super(itemView);
textView = (TextView) itemView.findViewById(R.id.tv);
}
}
// // 底部footView的ViewHolder,用以緩存findView操作
class FootHolder extends RecyclerView.ViewHolder {
private TextView tips;
public FootHolder(View itemView) {
super(itemView);
tips = (TextView) itemView.findViewById(R.id.tips);
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 根據返回的ViewType,綁定不同的布局文件,這里只有兩種
if (viewType == normalType) {
return new NormalHolder(LayoutInflater.from(context).inflate(R.layout.item, null));
} else {
return new FootHolder(LayoutInflater.from(context).inflate(R.layout.footview, null));
}
}
@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
// 如果是正常的imte,直接設置TextView的值
if (holder instanceof NormalHolder) {
((NormalHolder) holder).textView.setText(datas.get(position));
} else {
// 之所以要設置可見,是因為我在沒有更多數據時會隱藏了這個footView
((FootHolder) holder).tips.setVisibility(View.VISIBLE);
// 只有獲取數據為空時,hasMore為false,所以當我們拉到底部時基本都會首先顯示“正在加載更多...”
if (hasMore == true) {
// 不隱藏footView提示
fadeTips = false;
if (datas.size() > 0) {
// 如果查詢數據發現增加之后,就顯示正在加載更多
((FootHolder) holder).tips.setText("正在加載更多...");
}
} else {
if (datas.size() > 0) {
// 如果查詢數據發現并沒有增加時,就顯示沒有更多數據了
((FootHolder) holder).tips.setText("沒有更多數據了");
// 然后通過延時加載模擬網絡請求的時間,在500ms后執行
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
// 隱藏提示條
((FootHolder) holder).tips.setVisibility(View.GONE);
// 將fadeTips設置true
fadeTips = true;
// hasMore設為true是為了讓再次拉到底時,會先顯示正在加載更多
hasMore = true;
}
}, 500);
}
}
}
}
// 暴露接口,改變fadeTips的方法
public boolean isFadeTips() {
return fadeTips;
}
// 暴露接口,下拉刷新時,通過暴露方法將數據源置為空
public void resetDatas() {
datas = new ArrayList<>();
}
// 暴露接口,更新數據源,并修改hasMore的值,如果有增加數據,hasMore為true,否則為false
public void updateList(List<String> newDatas, boolean hasMore) {
// 在原有的數據之上增加新數據
if (newDatas != null) {
datas.addAll(newDatas);
}
this.hasMore = hasMore;
notifyDataSetChanged();
}
}</code></pre>
四、初始化RecyclerView
private void initRecyclerView() {
// 初始化RecyclerView的Adapter
// 第一個參數為數據,上拉加載的原理就是分頁,所以我設置常量PAGE_COUNT=10,即每次加載10個數據
// 第二個參數為Context
// 第三個參數為hasMore,是否有新數據
adapter = new MyAdapter(getDatas(0, PAGE_COUNT), this, getDatas(0, PAGE_COUNT).size() > 0 ? true : false);
mLayoutManager = new GridLayoutManager(this, 1);
recyclerView.setLayoutManager(mLayoutManager);
recyclerView.setAdapter(adapter);
recyclerView.setItemAnimator(new DefaultItemAnimator());
// 實現上拉加載重要步驟,設置滑動監聽器,RecyclerView自帶的ScrollListener
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
// 在newState為滑到底部時
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
// 如果沒有隱藏footView,那么最后一個條目的位置就比我們的getItemCount少1,自己可以算一下
if (adapter.isFadeTips() == false && lastVisibleItem + 1 == adapter.getItemCount()) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
// 然后調用updateRecyclerview方法更新RecyclerView
updateRecyclerView(adapter.getRealLastPosition(), adapter.getRealLastPosition() + PAGE_COUNT);
}
}, 500);
}
// 如果隱藏了提示條,我們又上拉加載時,那么最后一個條目就要比getItemCount要少2
if (adapter.isFadeTips() == true && lastVisibleItem + 2 == adapter.getItemCount()) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
// 然后調用updateRecyclerview方法更新RecyclerView
updateRecyclerView(adapter.getRealLastPosition(), adapter.getRealLastPosition() + PAGE_COUNT);
}
}, 500);
}
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
// 在滑動完成后,拿到最后一個可見的item的位置
lastVisibleItem = mLayoutManager.findLastVisibleItemPosition();
}
});
}
// 上拉加載時調用的更新RecyclerView的方法
private void updateRecyclerView(int fromIndex, int toIndex) {
// 獲取從fromIndex到toIndex的數據
List<String> newDatas = getDatas(fromIndex, toIndex);
if (newDatas.size() > 0) {
// 然后傳給Adapter,并設置hasMore為true
adapter.updateList(newDatas, true);
} else {
adapter.updateList(null, false);
}
}</code></pre>
所以,Activity的完整代碼如下:
public class MainActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener {
private SwipeRefreshLayout refreshLayout;
private RecyclerView recyclerView;
private List<String> list;
private int lastVisibleItem = 0;
private final int PAGE_COUNT = 10;
private GridLayoutManager mLayoutManager;
private MyAdapter adapter;
private Handler mHandler = new Handler(Looper.getMainLooper());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
findView();
initRefreshLayout();
initRecyclerView();
}
private void initData() {
list = new ArrayList<>();
for (int i = 1; i <= 40; i++) {
list.add("條目" + i);
}
}
private void findView() {
refreshLayout = (SwipeRefreshLayout) findViewById(R.id.refreshLayout);
recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
}
private void initRefreshLayout() {
refreshLayout.setColorSchemeResources(android.R.color.holo_blue_light, android.R.color.holo_red_light,
android.R.color.holo_orange_light, android.R.color.holo_green_light);
refreshLayout.setOnRefreshListener(this);
}
private void initRecyclerView() {
adapter = new MyAdapter(getDatas(0, PAGE_COUNT), this, getDatas(0, PAGE_COUNT).size() > 0 ? true : false);
mLayoutManager = new GridLayoutManager(this, 1);
recyclerView.setLayoutManager(mLayoutManager);
recyclerView.setAdapter(adapter);
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
if (adapter.isFadeTips() == false && lastVisibleItem + 1 == adapter.getItemCount()) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
updateRecyclerView(adapter.getRealLastPosition(), adapter.getRealLastPosition() + PAGE_COUNT);
}
}, 500);
}
if (adapter.isFadeTips() == true && lastVisibleItem + 2 == adapter.getItemCount()) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
updateRecyclerView(adapter.getRealLastPosition(), adapter.getRealLastPosition() + PAGE_COUNT);
}
}, 500);
}
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
lastVisibleItem = mLayoutManager.findLastVisibleItemPosition();
}
});
}
private List<String> getDatas(final int firstIndex, final int lastIndex) {
List<String> resList = new ArrayList<>();
for (int i = firstIndex; i < lastIndex; i++) {
if (i < list.size()) {
resList.add(list.get(i));
}
}
return resList;
}
private void updateRecyclerView(int fromIndex, int toIndex) {
List<String> newDatas = getDatas(fromIndex, toIndex);
if (newDatas.size() > 0) {
adapter.updateList(newDatas, true);
} else {
adapter.updateList(null, false);
}
}
@Override
public void onRefresh() {
refreshLayout.setRefreshing(true);
adapter.resetDatas();
updateRecyclerView(0, PAGE_COUNT);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
refreshLayout.setRefreshing(false);
}
}, 1000);
}
}</code></pre>
后話
以上代碼我是考慮到了更多的邊界條件,所以在代碼上會稍微多了一點,但是也不影響觀看。大家也可以通過改變數據源的數量和PAGE_COUNT等來測試,每個人在具體使用上都會有不同的要求,所以基本代碼我擺了出來,眾口難調,更多的細節需要大家來優化,例如footView可以設置一個動畫條,下拉刷新用其他樣式替換原生的樣式等,我想,這些對于學習完這篇文章的你來說,都會是簡單的問題了。
來自:https://juejin.im/post/58ef5588ac502e006c169a08