XRecyclerview的XListView式的下拉刷新和上拉加载(改自XListView,修改比率低于30%,效果很好)

本文介绍如何在RecyclerView中实现下拉刷新和上拉加载功能,通过对XListView原理的分析,重新构建了适配器并在RecyclerView中加入了头部和尾部布局,实现了与XListView相同的效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

       自从RecyclerView出现之后,由于其灵活性受到越来越多人的青睐,其主要的作用就是显示类型一致的大量数据,这跟之前的ListView、GridView的作用是一致的。当数据量很大时,此时需要实现分页显示。针对ListView,有人开发了XListView来实现下拉刷新和上拉加载框架,效果不错,很受人欢迎。那么,RecyclerView是如何实现刷新功能呢? RecyclerView与SwipeRefreshLayout组合,可以实现下拉刷新,这是谷歌支持的方法,但是上拉加载谷歌就没有对应的控件支持。通过对XListView的原理分析之后,我觉得可以完全按照XListView的原理来实现RecyclerView的下拉刷新和上拉加载,而且效果还是完全一致的。
       我们先来看一下效果图。

                  下拉刷新                                              上拉加载更多                                         点击加载更多
       首先,我们要实现上图的效果,前提要先理解XListView的原理。在这里我们感谢赵凯强博主给的原理分析: http://blog.youkuaiyun.com/zhaokaiqiang1992/article/details/42392731 。在这里我们只对XListView的原理进行简单的概括。
XListView的主要原理实现:
1、XListView主要包含了三部分:XListView、XListViewFooter、XListViewHeader。
2、XListViewHeader包含了正常、准备刷新、正在加载三种状态。手在屏幕上滑动时根据手滑动的距离通过setVisiableHeight()方法设置Header的布局高度属性来达到拉伸和收缩的效果。
3、XListViewFooter跟XListView的差不多,只不过它是设置BottomMargin来实现而已。
4、XListViewHeader添加到头部,XlistViewFooter添加到尾部,总的item数目为内容主体的数目加上2(例如:原来item为20条,加上Header和Footer之后就是22)。整个过程的动画都是在这两个布局上改变的。
5、手在屏幕上滑动时,根据手指的移动距离,设置setVisiableHeight()改变Header的下拉变化,设置BottomMargin改变Footer的上拉效果。当手机离开屏幕时,调用Scroller.startScroll()来实现布局的回弹,同时调用更新或者加载更多的方法。

      为了使RecyclerView具有XListView的一样的效果,同时减少工作量,我们在XListView的基础上修改,修改后的RecyclerView命名为XRecyclerView。
首先我们来确定要修改的部分:
1、不修改XListViewHeader和XListViewFooter的代码(源码100%原用),只将文件名修改成XRecyclerViewHeader和XRecyclerViewFooter。
2、重组RecyclerView的适配器,添加头尾布局到主体布局上。
3、根据需求处理滑动事件,使之更适合开发的需要。
4、在XRecyclerView中,唯独只有WrapAdapter是新增的,其他的部分大部分是保留XListView代码,同时只是修改个别逻辑。

添加XRecyclerViewHeader和XRecyclerViewFooter到主体布局
       我们创建XRecyclerView类,继承RecyclerView。由于ListView与RecyclerView的适配机制不一样,我们不能按照XListView的方法添加headerView和footerView,所以我们通过重组适配器Adapter来实现XRecyclerView的头尾布局的添加。
       同XListView一样,在XRecyclerView中的initView(),方法中实例化XRecyclerViewHeader和XRecyclerViewFooter,并获取Header的高度。代码如下:
private void initView(Context context) {
    scroller = new Scroller(context, new DecelerateInterpolator());
    //实例化Header和Footer
    headerView = new XRecyclerViewHeader(context);
    footerView = new XRecyclerViewFooter(context);
    //获取Header的高度
    headerViewContent = (RelativeLayout) headerView.findViewById(R.id.xlistview_header_content);
    headerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @SuppressWarnings("deprecation")
        @Override
        public void onGlobalLayout() {
            headerHeight = headerViewContent.getHeight();
            getViewTreeObserver().removeGlobalOnLayoutListener(this);
        }
    });
}
       重写XRecyclerView中的setAdapter(Adapter adapter)方法,并在方法中重构适配器,重构适配器的类名为WrapAdapter,同时把实例化的headerView和footerView传入适配器WrapAdapter。代码如下:
@Override
public void setAdapter(Adapter adapter) {
    mWrapAdapter = new WrapAdapter(this,headerView, footerView, adapter);
    super.setAdapter(mWrapAdapter);
}
        先看一下WraptAdapter类。
public class WrapAdapter extends RecyclerView.Adapter
   
     {
    private static final int TYPE_REFRESH_HEADER = -5; //添加刷新头
    private static final int TYPE_NORMAL = 0;
    private static final int TYPE_FOOTER = -3;//添加上拉加载布局

    private RecyclerView.Adapter adapter;

    private XRecyclerViewHeader mHeaderViews;

    private XRecyclerViewFooter mFootView;

    private XRecyclerView recyclerView;

    public WrapAdapter(XRecyclerView recyclerView,XRecyclerViewHeader headerViews, XRecyclerViewFooter footView, RecyclerView.Adapter adapter) {
        this.adapter = adapter;
        this.mHeaderViews = headerViews;
        this.mFootView = footView;
        this.recyclerView = recyclerView;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_REFRESH_HEADER) {
            return new SimpleViewHolder(mHeaderViews);
        } else if (viewType == TYPE_FOOTER) {
            return new SimpleViewHolder(mFootView);
        }
        return adapter.onCreateViewHolder(parent, viewType);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (isHeader(position)) {
            return;
        }

        int adjPosition = position - 1;
        int adapterCount;
        if (adapter != null) {
            adapterCount = adapter.getItemCount();
            if (adjPosition < adapterCount) {
                adapter.onBindViewHolder(holder, adjPosition);
                return;
            }
        }
    }

    @Override
    public int getItemCount() {
        int count = 2;
        if (adapter != null) {
            count = 2 + adapter.getItemCount();
        }
        //判断是都为列表数为零.则隐藏头部和尾部的item
        if(count == 2){
            mHeaderViews.setVisibility(View.GONE);
            mFootView.setVisibility(View.GONE);
        } else {
            mHeaderViews.setVisibility(View.VISIBLE);
            //如果设置了加载更多为不可为时,隐藏尾部
            if(recyclerView.isEnableLoadMore()){
                mFootView.setVisibility(View.VISIBLE);
            } else {
                //如果设置了不能下拉刷新,则itemCount减少1
                count--;
                mFootView.setVisibility(View.GONE);
            }
        }
        return count;
    }

    @Override
    public int getItemViewType(int position) {
        //如果position是0,返回刷新的标志位
        if (position == 0) {
            return TYPE_REFRESH_HEADER;
        }
        //如果position是footerView的位置时,返回加载更多的标志位
        if (isFooter(position)) {
            return TYPE_FOOTER;
        }
        //否则返回正常的标志位
        int adjPosition = position - 1;
        int adapterCount;
        if (adapter != null) {
            adapterCount = adapter.getItemCount();
            if (adjPosition < adapterCount) {
                return adapter.getItemViewType(adjPosition);
            }
        }
        return TYPE_NORMAL;
    }

    @Override
    public long getItemId(int position) {
        if (adapter != null && position >= 1) {
            int adjPosition = position - 1;
            int adapterCount = adapter.getItemCount();
            if (adjPosition < adapterCount) {
                return adapter.getItemId(adjPosition);
            }
        }
        return -1;
    }

    private class SimpleViewHolder extends RecyclerView.ViewHolder {
        public SimpleViewHolder(View itemView) {
            super(itemView);
        }
    }

    public boolean isHeader(int position) {
        return position >= 0 && position < 1;
    }

    public boolean isFooter(int position) {
        return position < getItemCount() && position >= getItemCount() - 1 && recyclerView.isEnableLoadMore();
    }
}
   
       接下来我们对WraptAdapter进行分析。
getItemCount()方法:
@Override
public int getItemCount() {
    int count = 2;
    if (adapter != null) {
        count = 2 + adapter.getItemCount();
    }
    //判断是都为列表数为零.则隐藏头部和尾部的item
    if(count == 2){
        mHeaderViews.setVisibility(View.GONE);
        mFootView.setVisibility(View.GONE);
    } else {
        mHeaderViews.setVisibility(View.VISIBLE);
        //如果设置了加载更多为不可为时,隐藏尾部
        if(recyclerView.isEnableLoadMore()){
            mFootView.setVisibility(View.VISIBLE);
        } else {
            //如果设置了不能下拉刷新,则itemCount减少1
            count--;
            mFootView.setVisibility(View.GONE);
        }
    }
    return count;
}
       通过代码我们发现,item的总数目是itemCount加上2(即count+2)。当count为2的时候,此时只是包含了头部和尾部的两个item,是没有主体数据显示,此时我们隐藏掉头部和尾部。如果数据count大于2时,是有主体数据显示,此时我们要判断我们的XRecyclerView是否设置上拉加载更多,如果没有设置,则隐藏footerView,count也要在count+2的基础上减去(即count + 1),此设置是为了适用于不需要上拉刷新的使用。
getItemViewType()方法:
@Override
public int getItemViewType(int position) {
    //如果position是0,返回刷新的标志位
    if (position == 0) {
        return TYPE_REFRESH_HEADER;
    }
    //如果position是footerView的位置时,返回加载更多的标志位
    if (isFooter(position)) {
        return TYPE_FOOTER;
    }
    //否则返回正常的标志位
    int adjPosition = position - 1;
    int adapterCount;
    if (adapter != null) {
        adapterCount = adapter.getItemCount();
        if (adjPosition < adapterCount) {
            return adapter.getItemViewType(adjPosition);
        }
    }
    return TYPE_NORMAL;
}
       在这个方法,在header和footer位置处返回对应的标识,用于在onCreateViewHolder(ViewGroun parent,int viewType)识别,然后加载对应的布局,至此就将头尾布局添加到XRecyclerView中
       isHeader()和isFooter()两个方法是判断当前的item是否属于头部或者尾部的位置,尾部还特别判断了是否有开启加载更多的条件。

XRecyclerView上下滑动的动画实现
       根据赵凯强博主的讲述中,我们知道XListView的上下滑动的动画的效果是根据手势的滑动距离来设定header布局的高度或者footer布局是与底部的距离来实现的,所以XRecyclerView的实现方法也是一样。在这里我们直接重写XRecyclerView的onTouchEvent(MotionEvent ev)方法。先贴上代码。
@Override
public boolean onTouchEvent(MotionEvent ev) {
    LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
    int totalItemCount = getLayoutManager().getItemCount();
    //如果列表条数为零(放弃头尾两个item),则不处理
    if(totalItemCount == 2){
        return false;
    }
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            isEvent = true;
            // 记录按下的坐标
            lastY = ev.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:
            // 计算移动距离
            float deltaY = ev.getRawY() - lastY;
            //这里的处理是为了防止与item的点击事件其冲突
            if(!isEvent){
                isEvent = true;
                deltaY = 0;
            }
            lastY = ev.getRawY();
            //有一个在加载数据的时候,另外一个不加载数据
            if(!isRefreashing && !isLoadingMore) {
                // 是第一项并且标题已经显示或者是在下拉
                if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0 && (headerView.getVisiableHeight() > 0 || deltaY > 0) && enableRefresh) {
                    updateHeaderHeight(deltaY / OFFSET_RADIO);
                } else if (layoutManager.findLastCompletelyVisibleItemPosition() == totalItemCount - 1 && (footerView.getBottomMargin() > 0 || deltaY < 0) && enableLoadMore) {
                    updateFooterHeight(-deltaY / OFFSET_RADIO);
                }
            }
            break;
        case MotionEvent.ACTION_UP:
            isEvent = false;
            if (layoutManager.findFirstVisibleItemPosition() == 0) {
                if (enableRefresh && headerView.getVisiableHeight() > headerHeight) {
                    isRefreashing = true;
                    headerView.setState(XRecyclerViewHeader.STATE_REFRESHING);
                    if (mXRecyclerViewListener != null) {
                        mXRecyclerViewListener.onRefresh();
                    }
                }
                resetHeaderHeight();
            } else if (layoutManager.findLastVisibleItemPosition() == totalItemCount - 1) {
                if (enableLoadMore && footerView.getBottomMargin() > PULL_LOAD_MORE_DELTA) {
                    startLoadMore();
                }
                resetFooterHeight();
            }
            break;
    }
    return super.onTouchEvent(ev);
}
       首先我们判断整个列表是否有数据,有数据才会对列表数据处理。当我们的手指点击到屏幕的时候,先记下当前点击的Y坐标,手指滑动的时候,记下滑动的距离,此距离是当前滑动触发的位置减去上一次触发的位置的距离差。if(isRefreshing && !isLoadingMore)是处理如果正在刷新操作或者加载更多数据的请求时,禁止触发加载数据或者刷新数据事件。
       在满足无触发刷新或者加载数据的情况下,根据layoutManager.findFirstCompletelyVisibleItemPosition(),判断第一个完全可见的item是不是Position为0(即为头部布局),同时开启下拉刷新的功能时,调用updateHeaderHeight(deltay/OFFSET_RADIO)方法改变头部的得高度到达动态下来的效果。
private void updateHeaderHeight(float delta) {
    headerView.setVisiableHeight((int) delta + headerView.getVisiableHeight());
    // 未处于刷新状态,更新箭头
    if (enableRefresh && !isRefreashing) {
        if (headerView.getVisiableHeight() > headerHeight) {
            headerView.setState(XRecyclerViewHeader.STATE_READY);
        } else {
            headerView.setState(XRecyclerViewHeader.STATE_NORMAL);
        }
    }
}
       从该方法我们可以看到,如果下拉滑动到footerView高度的时候,会改变headerView的状态,变成刷新状态。
       相同的,上拉的原理是类似的。但是有个注意点是我们这边用是findFirstCompletelyVisibleItemPosition()方法而不是findFirstVisibleItemPosition()方法,如果用的是后者的方法,会出现上拉或者下拉的时候,出现布局悬停的情况。
       当手指放开的时候,先从headerView分析。根据滑动的距离和触发刷新条件来判断,如果条件不满足,则直接调用resetHeaderHeight()方法,实现headerView的回弹,当满足刷新的条件时候,则回弹到headerView触顶,同时触发刷新数据方法,数据刷新结束则回弹隐藏。
回弹代码:
private void resetHeaderHeight() {
    // 当前的可见高度
    int height = headerView.getVisiableHeight();
    // 如果正在刷新并且高度没有完全展示
    if ((isRefreashing && height <= headerHeight) || (height == 0)) {
        return;
    }
    // 默认会回滚到header的位置
    int finalHeight = 0;
    // 如果是正在刷新状态,则回滚到header的高度
    if (isRefreashing && height > headerHeight) {
        finalHeight = headerHeight;
    }
    mScrollBack = SCROLLBACK_HEADER;
    // 回滚到指定位置
    scroller.startScroll(0, height, 0, finalHeight - height,
            SCROLL_DURATION);
    invalidate();
}
       从代码上可以看出:当手指放开时,会根据刷新状态赋值给finalHeight,如果如果条件满足,则finalHeight设置为headerView的高度,否则设置为0,然后设置scroller的参数,并在computeScroll获取的currY()位置,然后设置headerView的高度,动态实现回弹。
在整个的手势滑动中,我们这边有一个标志位 isEvent,该表位为是为了防止在手点击到屏幕上时,item的点击事件与滑动事件冲突。
       footerView的实现方式也是如出一辙。至此,我们主要原理已经分析了,更详细的请看源码。
       本来想免费分享的,但是这边的资源分没有0的,只能设置最低的1分,如果没有积分的,请留下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值