心灵鸡汤:知之者不如好之者,好之者不如乐之者。android
一直在用到RecyclerView时都会微微一颤,由于一直都没去了解怎么实现上拉加载,受够了每次去Github找开源引入,由于感受就为了一个上拉加载功能而去引入一大堆你不知道有多少BUG的代码,不只增长了项目的冗余程度,并且出现BUG的时候,你却发现很难去改,正由于这样,我就下定决心去了解如何来实现RecyclerView的上拉加载功能,相信你们和我有过一样的状况,可是我相信,只要你给本身几分钟看完这篇文章,你就会发现实现一个上拉加载是很是的简单。git
上拉加载和下拉刷新相对应,在Android API LEVEL 19(即4.4)以后,Google官方推出了SwipeRefreshLayout和RecyclerView的共同使用,为咱们提供了更加便捷的列表下拉刷新功能,可是,并无给咱们提供上拉加载功能,可是在RecyclerView强大的可扩展之下,Github上面有了不少开源项目实现了上拉加载功能,即咱们不会一次性将全部数据加载到列表中,当用户滑动到底部时,再向服务器请求数据,再填充数据到列表中,这样不只能够有更好的人机交互,同时在减小了服务器的压力的同时也对客户端的性能有了更好的提高。本篇文章主要经过介绍实现如下简单的上拉加载功能,同窗们能够在掌握了最基本的实现功能以后,再经过扩展和优化,甚至能够封装成比较通用的代码,开源到Github上面。github
布局很简单,只有一个SwipeRefreshLayout包裹了一个RecyclerView,相信用过RecyclerView的都很容易看懂。以下为activity_main.xml:缓存
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <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>
而后,咱们RecyclerView的Item布局也是很是简单,只有一个TextView。以下为item.xml:服务器
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <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>
看到咱们效果图都知道,在咱们上拉时,还有一个提示的条目,我定义为 footview.xml:网络
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <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>
在准备好了布局文件以后,咱们就把目光转到Activity中去,首先咱们须要初始化SwipeRefreshLayout,初始化也是很简单,这里省去了findView操做,因此就只有设置转动的颜色,还有设置刷新的监听事件:ide
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); }
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(); } }
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); } }
因此,Activity的完整代码以下:oop
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); } }
以上代码我是考虑到了更多的边界条件,因此在代码上会稍微多了一点,可是也不影响观看。你们也能够经过改变数据源的数量和PAGE_COUNT等来测试,每一个人在具体使用上都会有不一样的要求,因此基本代码我摆了出来,众口难调,更多的细节须要你们来优化,例如footView能够设置一个动画条,下拉刷新用其余样式替换原生的样式等,我想,这些对于学习完这篇文章的你来讲,都会是简单的问题了。布局
Github下载:PullToLoadData-RecyclerViewpost
CSDN资源:PullToLoadData-RecyclerView