上一章介绍了RecyclerView的下拉刷新功能的实现和源码分析。在一个RecyclerView完整的功能中,下拉刷新和上拉加载是必须包含的,所以本节就介绍上拉加载更多的实现和源码分析。
需求分析
上拉加载更多也就是拉到RecyclerView底部,再上拉就会显示一个正在加载更多信息,如下图所示:
当上拉加载没有更多数据时,就显示没有更多的提示信息,如下所示:
上拉加载更多 比 下拉刷新更简单,因为下拉刷新需要实时动态改变刷新头部的高度(可以参考我上一篇博文Android RecyclerView下拉刷新的实现和源码分析),而上拉加载不用,其实上拉加载可以作为RecyclerView中的最后一个item, 这样上拉到底部的时候就会显示加载更多,然后再加一些判断逻辑来调用用户设置的监听回调接口 就可以实现上拉刷新功能。本文实现后的效果如下图所示:
加载更多底部UI布局
本文加载更多底部UI布局挺简单,这里仅作原理分析,不做复杂的UI结构。UI布局中包含一个提示文字TextView和一个进度提示控件ProgressBar。UI布局代码如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center">
<RelativeLayout android:layout_width="match_parent" android:layout_height="30dp" android:layout_margin="10dp">
<TextView android:id="@+id/LoadingMoreFooter_HintTextView" android:text="上拉刷新" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:layout_centerInParent="true"/>
<ProgressBar android:id="@+id/LoadingMoreFooter_ProgressBar" android:layout_width="20dp" android:layout_height="20dp" android:layout_centerVertical="true" android:layout_toLeftOf="@id/LoadingMoreFooter_HintTextView"/>
</RelativeLayout>
</LinearLayout>
LoadingMoreFooter 上拉加载更多源码分析
上拉加载跟下拉刷新一样,同样需要是作为RecyclerView中的一个item,所以需要继承LinearLayout,里面包含一个container容器来显示上述的UI布局。
/** * 加载更多底部 */
public class LoadingMoreFooter extends LinearLayout {
private LinearLayout container;//内部容器
private TextView hintTextView;//加载更多提示文字
private ProgressBar progressBar;//加载更多进度条
public static final int STATE_LOADING=0;//标志正在加载中
public static final int STATE_COMPLETE=1;//标志加载完成
public static final int STATE_NOMORE=2;//标志没有更多内容
private int measureHeight;//表示底部UI布局高度
public LoadingMoreFooter(Context context) {
this(context,null);
}
public LoadingMoreFooter(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public LoadingMoreFooter(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
container= (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.loadingmore_footer,null);//初始化容器视图
setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
addView(container,
new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
hintTextView= (TextView) container.findViewById(R.id.LoadingMoreFooter_HintTextView);
progressBar= (ProgressBar) container.findViewById(R.id.LoadingMoreFooter_ProgressBar);
measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);//调用measure测试底部视图
measureHeight=getMeasuredHeight();//获取底部视图高度
}
public void onStateChange(int state){
switch (state){
case STATE_LOADING:
progressBar.setVisibility(View.VISIBLE);
hintTextView.setText(R.string.LoadingMore_Footer_Hint_Loading);
break;
case STATE_COMPLETE:
progressBar.setVisibility(View.GONE);
hintTextView.setText(R.string.LoadingMore_Footer_Hint_Complete);
break;
case STATE_NOMORE:
progressBar.setVisibility(View.GONE);
hintTextView.setText(R.string.LoadingMore_Footer_Hint_NoMore);
break;
}
}
public int getMeasureHeight() {
return measureHeight;
}
}
这里需要注意的是,使用LayoutInflater解析完布局后,调用measure方法测量布局的高度和宽度等信息,然后调用getMeasureHeight获取测量后的高度。这个高度主要用于当加载更多 网络超时等情况发生时,RecyclerView可以上滑并将加载更多底部隐藏起来。
上刷加载更多包含3种不同的状态,分别是STATE_LOADING正在加载更多,STATE_COMPLETE加载更多完成,STATE_NOMORE暂无加载更多信息,根据这些状态去修改LoadingMoreFooter 布局文件中相应控件的显示和隐藏,文字设置等信息。
PullToRefreshRecyclerView上拉逻辑处理
上拉加载更多 主要的逻辑还是包含在自定义RecyclerView中,本文的上拉加载 有处理超时情况的方式,当超过一定时间间隔后刷新仍没完成就上滑隐藏LoadingMoreFooter,并提示刷新超时。
public class PullToRefreshRecyclerView extends RecyclerView{
private boolean loadingMoreEnabled=true;//上拉刷新 开关
private boolean isLoadingMore=false;//是否正在加载更多
private boolean isNoMore=false;//是否有更多内容
private static final int TYPE_REFRESH_HEADER=10000;//刷新头部 类型标号
private static final int TYPE_LOADINGMORE_FOOTER=10001;//加载更多底部 类型标号
private int TIMEOUT=30000;//刷新超时时间 如果超过这个时间还没刷新完成就会关闭刷新并提示刷新过程中出现问题
private int DEFAULT_DURATION = 500;//底部超时上滑动画时间
private LoadingMoreFooter loadingMoreFooter;//加载更多底部
private void init(){
dataObserver=new DataObserver();
handler=new Handler();
if(pullToRefreshEnabled){
refreshHeader=new RefreshHeader(getContext());//获取刷新头部
}
if(loadingMoreEnabled){
loadingMoreFooter=new LoadingMoreFooter(getContext());//获取加载更多底部
}
}
public void loadingMoreComplete(){
handler.removeCallbacksAndMessages(null);
isLoadingMore=false;
loadingMoreFooter.onStateChange(LoadingMoreFooter.STATE_COMPLETE);
}
public void setNoMore(boolean noMore){
handler.removeCallbacksAndMessages(null);
isLoadingMore=false;
isNoMore=noMore;
loadingMoreFooter.onStateChange(noMore?LoadingMoreFooter.STATE_NOMORE:LoadingMoreFooter.STATE_LOADING);
}
上拉加载更多的难点在于如何判断是否拉到最后一个item,在以前ListView是通过监听滚动即onScrollListener来判断最后一个item是否可见,在RecyclerView中同样是通过监听滚动的方式来实现的,但不同的是由于RecyclerView中有3种不同布局管理器(GridLayoutManager,StaggeredGridLayoutManager,LinearLayoutManager),所以要分3种情况来获取列表中最后一个可见项item的位置。
@Override
public void onScrollStateChanged(int state){//列表滚动监听 上拉加载更多底部就是通过监听滚动来判断是否触发事件的
super.onScrollStateChanged(state);
if(state==RecyclerView.SCROLL_STATE_IDLE&&//当列表滚动停止时
onRefreshListener!=null&&//有设置监听回调接口
!isLoadingMore&&//当前不是正在加载更多
loadingMoreEnabled){//加载更多功能开启
LayoutManager layoutManager=getLayoutManager();//获取布局管理器
int lastVisibleItemPosition;//当前列表最后一项可见项 需要根据不同的布局管理器来获取
if(layoutManager instanceof GridLayoutManager){
lastVisibleItemPosition=((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
}else if(layoutManager instanceof StaggeredGridLayoutManager){
int[] into=new int[((StaggeredGridLayoutManager)layoutManager).getSpanCount()];//span是一行内被分成几列 into是一个数组 因为StaggeredGrid每一行的列数都是不一样的
((StaggeredGridLayoutManager)layoutManager).findLastVisibleItemPositions(into);
lastVisibleItemPosition=findMaxSpan(into);
}else{
lastVisibleItemPosition=((LinearLayoutManager)layoutManager).findLastVisibleItemPosition();
}
if(layoutManager.getChildCount()>0&&//childView是列表中的可见项item
lastVisibleItemPosition>=layoutManager.getItemCount()-1&&
layoutManager.getItemCount()>=layoutManager.getChildCount()&&
!isNoMore&&//上拉可以加载内容
refreshHeader.getState()<RefreshHeader.STATE_REFRESHING){//刷新头部当前不是正在刷新
isLoadingMore=true;
loadingMoreFooter.onStateChange(LoadingMoreFooter.STATE_LOADING);
onRefreshListener.onLoadMore();
handler.postDelayed(new Runnable() {
@Override
public void run() {//超时事件处理
smoothScrollBy(0,-loadingMoreFooter.getMeasureHeight());
Toast.makeText(getContext(),"加载更多超时",Toast.LENGTH_SHORT).show();
onRefreshListener.onLoadMoreTimeOut();
handler.postDelayed(new Runnable() {//防止在滚动完成后 触发loadMore事件 必须等滚动动画结束后才设置isLoadingMore
@Override
public void run() {
isLoadingMore=false;
}
},DEFAULT_DURATION);
}
},TIMEOUT);
}
}
}
private int findMaxSpan(int[] lastPositions){
int max=lastPositions[0];
for(int value:lastPositions){
if(value>max){
max=value;
}
}
return max;
}
在onScrollStateChanged方法中主要是一些逻辑的判断,判断是否滚动到最后一个item,即最后一个item是否可见,根据是否可见来触发回调监听器中的方法onRefreshListener.onLoadMore()。这里获取最后一个可见item位置lastVisibleItemPosition的方式中,GridLayoutManager和LinearLayoutManager可以直接调用findLastVisibleItemPosition来获取,但是StaggeredGridLayoutManager就没那么简单,因为StaggeredGridLayoutManager是类似瀑布流的布局,即每一行的item数量是不一定的,span这里表示将RecyclerView一行划分为几个区域(不同的item),调用findLastVisibleItemPositions(into)获取到可见项位置数组into后,还需要进行比较获取最大位置即最后一个item位置。
还需要注意的layoutManager.getChildCount()获取的是当前RecyclerView可见项的数量,而layoutManager.getItemCount()是RecyclerView中所有的item数量,包含可见和不可见的。
handler.postDelayed(new Runnable() {
@Override
public void run() {//超时事件处理
smoothScrollBy(0,-loadingMoreFooter.getMeasureHeight());
Toast.makeText(getContext(),"加载更多超时",Toast.LENGTH_SHORT).show();
onRefreshListener.onLoadMoreTimeOut();
handler.postDelayed(new Runnable() {//防止在滚动完成后 触发loadMore事件 必须等滚动动画结束后才设置isLoadingMore
@Override
public void run() {
isLoadingMore=false;
}
},DEFAULT_DURATION);
}
},TIMEOUT);
这里是为了处理超时事件的发生而采用handler发送一个延迟消息,当上拉加载更多成功后在loadingMoreComplete调用handler.removeCallbacksAndMessages(null)清除这个延迟消息,当上拉加载更多超过设定的时间后,延迟消息中的Runnable就会执行,使用了smoothScrollBy(0,-loadingMoreFooter.getMeasureHeight())方法来上滑移动隐藏LoadingMoreFooter ,注意
handler.postDelayed(new Runnable() {//防止在滚动完成后 触发loadMore事件 必须等滚动动画结束后才设置isLoadingMore
@Override
public void run() {
isLoadingMore=false;
}
},DEFAULT_DURATION);
这里必须延迟设置isLoadingMore为false,因为没有延迟的话当上滑隐藏LoadingMoreFooter 至列表停止滚动时,又会再次出发上拉加载更多的操作。
自定义监听回调接口OnRefreshListener如下所示:
public interface OnRefreshListener {
void onRefresh();
void onRefreshTimeOut();
void onLoadMore();
void onLoadMoreTimeOut();
}
当然在上一篇博文中的自定义Adapter类中WrapAdapter还需要进行一些修改,主要是onCreateViewHolder和getItemCount,getItemViewType的修改,如下所示:
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(viewType==TYPE_REFRESH_HEADER){//刷新头部类型
return new SimpleViewHolder(refreshHeader);
}else if(viewType==TYPE_LOADINGMORE_FOOTER){
return new SimpleViewHolder(loadingMoreFooter);
}
return adapter.onCreateViewHolder(parent,viewType);//其它为自定义Adapter里面的Item类型
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
if(isRefreshHeader(position)||isLoadingMoreFooter(position)){
return;
}
int adjPosition=position-1;//减去刷新头部Item的数量1
if(adapter!=null){
if(adjPosition<adapter.getItemCount()){
adapter.onBindViewHolder(holder,adjPosition);
}
}
}
@Override
public void onBindViewHolder(ViewHolder holder, int position, List<Object> payloads){
if(isRefreshHeader(position)||isLoadingMoreFooter(position)){
return;
}
int adjPosition=position-1;//减去刷新头部Item的数量1
if(adapter!=null){
if(adjPosition<adapter.getItemCount()){
adapter.onBindViewHolder(holder,adjPosition,payloads);
}
}
}
@Override
public int getItemCount() {
if(loadingMoreEnabled){
if(adapter!=null){
return adapter.getItemCount()+2;
}else{
return 2;
}
}else{
if(adapter!=null){
return adapter.getItemCount()+1;
}else{
return 1;
}
}
}
@Override
public int getItemViewType(int position){
int adjPosition=position-1;//减去刷新头部Item的数量1
if(isRefreshHeader(position)){
return TYPE_REFRESH_HEADER;
}
if(isLoadingMoreFooter(position)){
return TYPE_LOADINGMORE_FOOTER;
}
if(adapter!=null){
if(adjPosition<adapter.getItemCount()){
return adapter.getItemViewType(adjPosition);
}
}
return 0;
}
public boolean isLoadingMoreFooter(int position){
if(loadingMoreEnabled){
return position==getItemCount()-1;
}else{
return false;
}
}
总结
总体来说,上拉加载更多比上拉刷新更容易实现,因为上拉加载的原理就是在RecyclerView中的最后一个item中显示加载更多文字和提示,你只需要滑动到最后一个item自然就能看到正在加载更多信息的布局了,难点在于如何判断是否滑动到最后一个item。
源代码:https://github.com/QQ402164452/PullToRefreshRecyclerView
参考:XRecyclerView