RecyclerView学习(2) 上拉加载 下拉刷新 高仿京东下拉刷新 支持扩展

转载请注明出处 http://blog.csdn.net/u011453163/article/details/62896816

本篇内容在我的另一篇博客
RecyclerView学习(1) 添加头部和尾部

的基础上改进的,文末会附上源码。

RecyclerView 的上拉加载和下拉刷新 已经是一个老话题了,网上也有很多实现方式,本着学习的态度就当做一个新的课题来研究以及一路上的坑。

RecyclerView的上拉加载下拉刷新 实现的方式目前有这么几种

1 使用官方组件 SwipeRefreshLayout 包裹RecyclerView 实现下拉接口 ,上拉加载则是监听滚动到底部来处理一些UI变化

2 加载和刷新 都是借用RecyclerView 原始头部和尾部来实现。

3 自定义一个布局包裹RecyclerView 来实现。

本篇内容使用的是第二种方式。效果图

这里写图片描述

实现刷新和加载的核心思想

1.添加可刷新头部和尾部,如何添加头部和尾部参照我的上一篇博客RecyclerView学习(1) 添加头部和尾部

2 上拉和下拉效果,通过改变头部和尾部的高度实现,弹性效果使用的是属性动画还实现的

接下来分析如何实现

关于如何添加头部和尾部就不过多解释了,参照第一篇,只是做了一些小调整使得头部和尾部始终在两端。

本篇主要分析上拉下拉的实现。

下拉刷新

 1 判断滑动到顶部
 2 解决手势和RecyclerView自身可滚动的冲突
 3 回弹效果

判断滑动到顶部的方法我知道的有这么几种

1 通过此方法

canScrollVertically(int direction)

但是在头部或尾部隐藏的情况下无效

2 通过监听滚动计算

3 通过获取position==0 的Item 的top==0来判断

refreshView.getTop()==0

本篇用的是第三种

核心部分

onTouchEvent(MotionEvent e)

  @Override
    public boolean onTouchEvent(MotionEvent e) {
        if (rdownY == -1) {
            rdownY = e.getRawY();
        }
        if(ldownY==-1){
            ldownY=e.getRawY();
        }
        switch (e.getAction()) {
            case MotionEvent.ACTION_MOVE:
                float rdy = e.getRawY() - rdownY;
                float ldy =ldownY- e.getRawY();
                if (refreshView == null) {
                    refreshView = getLayoutManager().findViewByPosition(0);
                    if (refreshView instanceof BaseRefreshHeader) {
                        ((BaseRefreshHeader) refreshView).setOnRefreshListener(onRefreshListener);
                    }
                }
                if(loadView==null){
                    loadView = getLayoutManager().findViewByPosition(getLayoutManager().getItemCount()-1);
                    if(loadView instanceof BaseLoadFooter){
                        ((BaseLoadFooter) loadView).setOnLoadListener(onLoadListener);
                    }
                }
                if (allowPulldown()&&!isLoading()&&supportPullRefresh) {
                    ((BaseRefreshHeader) refreshView).pulldown((int) (rdy / 2));
                    if (refreshView.getHeight() >= 0 && rdy > 0) {
                        return false;
                    }
                }else{
                    rdownY = e.getRawY();
                }
                if (allowPullup()&&!isRefreshing()&&supportPullLoad){
                    ((BaseLoadFooter) loadView).pullup((int) (ldy/ 2));
                    offsetChildrenVertical(1);//消除上拉时抖动  抖动的原因猜测是 view的高度是向下延伸的
                    if(loadView.getHeight()>0&&ldy>0){
                        scrollToPosition(getLayoutManager().getItemCount()-1);
                        return false;
                    }
                }else {
                    ldownY=e.getRawY();
                }
                break;
            case MotionEvent.ACTION_UP:
                if (refreshView instanceof BaseRefreshHeader&& refreshView.getParent() != null&&supportPullRefresh) {
                    ((BaseRefreshHeader) refreshView).loosen();
                }else if(loadView instanceof BaseLoadFooter && loadView.getParent() != null&&supportPullLoad){
                     ((BaseLoadFooter) loadView).loosen();
                }
                rdownY = -1;
                ldownY=-1;
                break;
        }
        return super.onTouchEvent(e);
    }

onTouchEvent事件逻辑包括了上拉和下拉。

1 初始化 按下坐标 默认-1 只要小于0都可以

  if (rdownY == -1) {
            rdownY = e.getRawY();
        }
        if(ldownY==-1){
            ldownY=e.getRawY();
        }

2 MotionEvent.ACTION_MOVE 计算滑动差量以及获取第一个和最后一个Item

 float rdy = e.getRawY() - rdownY;
                float ldy =ldownY- e.getRawY();
                if (refreshView == null) {
                    refreshView = getLayoutManager().findViewByPosition(0);
                    if (refreshView instanceof BaseRefreshHeader) {
                        ((BaseRefreshHeader) refreshView).setOnRefreshListener(onRefreshListener);
                    }
                }
                if(loadView==null){
                    loadView = getLayoutManager().findViewByPosition(getLayoutManager().getItemCount()-1);
                    if(loadView instanceof BaseLoadFooter){
                        ((BaseLoadFooter) loadView).setOnLoadListener(onLoadListener);
                    }
                }

3 可下拉上拉判断 (可支持下拉和上拉的逻辑是 到达顶部,当前状态不是正在刷新或者加载 以及是否支持上拉加载下拉刷新)

 /**是否正在刷新*/
    private boolean isRefreshing(){
        if(refreshView instanceof BaseRefreshHeader){
           return  ((BaseRefreshHeader) refreshView).getStatus()==BaseRefreshHeader.STATUS_REFRESHING;
        }
        return false;
    }
    /**是否正在加载*/
    private boolean isLoading(){
        if(loadView instanceof BaseLoadFooter){
            return  ((BaseLoadFooter) loadView).getStatus()==BaseLoadFooter.STATUS_LOADING;
        }
        return false;
    }

    /**允许下拉操作*/
    private boolean allowPulldown(){
       return refreshView instanceof BaseRefreshHeader
               && refreshView.getParent() != null
               && refreshView.getTop()==0;
    }

    /**允许上拉操作*/
    private boolean allowPullup(){
        return loadView instanceof BaseLoadFooter
                && loadView.getParent()!=null
                && loadView.getBottom()==getLayoutManager().getHeight();
    }

4 解决改变高度的同时和自身滑动冲突(在下拉或上拉之后往反方向滑动时 在减少高度的同时RecyclerView也在滚动 这样高度计算就会出问题 而且体验极差)处理方式 在下拉或上拉之后 屏蔽RecyclerView自身事件处理

 if (allowPulldown()&&!isLoading()&&supportPullRefresh) {
                    ((BaseRefreshHeader) refreshView).pulldown((int) (rdy / 2));
                    if (refreshView.getHeight() >= 0 && rdy > 0) {
                        return false;
                    }
                }else{
                    rdownY = e.getRawY();
                }
                if (allowPullup()&&!isRefreshing()&&supportPullLoad){
                    ((BaseLoadFooter) loadView).pullup((int) (ldy/ 2));
                    offsetChildrenVertical(1);//消除上拉时抖动  抖动的原因猜测是 view的高度是向下延伸的
                    if(loadView.getHeight()>0&&ldy>0){
                        scrollToPosition(getLayoutManager().getItemCount()-1);
                        return false;
                    }
                }else {
                    ldownY=e.getRawY();
                }

5 上拉和下拉之后 当高度大于0以后 只改变高度屏蔽滚动

 if (refreshView.getHeight() >= 0 && rdy > 0) {
                        return false;
                    }


if(loadView.getHeight()>0&&ldy>0){
                     scrollToPosition(getLayoutManager().getItemCount()-1);                    return false;
                    }

这里下拉和上拉有点点区别 因为View的高度是向下延伸的 所以在上拉的时候要调用以下方法来实现拉伸效果 ,scrollToPosition是将使item完整的出现在界面上 如果已在界面上则无效

 scrollToPosition(getLayoutManager().getItemCount()-1);

(另外刚开始上拉时会出现微微抖动 猜测是和View高度向下延伸的原因 此方法可消除抖动 )

offsetChildrenVertical(1);

5 处理了下拉的操作之后就是手指抬起之后刷新的逻辑了,这里使用抽象类基础头部和尾部,方便头部和尾部的扩展 BaseRefreshHeader,BaseLoadFooter。实现逻辑基本是一样的。

public abstract class BaseRefreshHeader extends RelativeLayout {


    public static final int STATUS_NORMAL = -1;//无刷新状态

    public static final int STATUS_REFRESHING = 1;//刷新中


    private  int status = STATUS_NORMAL;

    private OnRefreshListener onRefreshListener;

    public BaseRefreshHeader(Context context) {
        this(context, null);
    }

    public BaseRefreshHeader(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BaseRefreshHeader(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,0));
    }

    public void setHeaderHeight(int height) {
        if (height > 0) {
            RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) getLayoutParams();
            layoutParams.height = height;
            setLayoutParams(layoutParams);
        } else {
            RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) getLayoutParams();
            layoutParams.height = 0;
            setLayoutParams(layoutParams);
            status = STATUS_NORMAL;
            reset();
        }
    }


    public void pulldown(int height) {
        if(height<0){
            return;
        }
        if(status==STATUS_REFRESHING){
            setHeaderHeight(refreshHeight()+height);
            move(refreshHeight()+height);
        }else {
            setHeaderHeight(height);
            move(height);
        }
        if(status!=STATUS_REFRESHING) {
            if (height > refreshHeight()) {
                refresh(true);
            } else {
                refresh(false);
            }
        }
    }

    public int getStatus() {
        return status;
    }

    public void loosen() {
        if (getHeight()>=refreshHeight()) {
            animMove(100, getHeight(), refreshHeight());
            if(status!=STATUS_REFRESHING){
                status = STATUS_REFRESHING;
                if (onRefreshListener!=null){
                    onRefreshListener.onRefresh();
                    loosenAndRefresh();
                }
            }
        } else {
            animMove(50, getHeight(), 0);
        }
    }

    /**拉伸过程*/
    protected abstract void move(int height);

    /**可刷新的高度*/
    protected abstract int refreshHeight();

    /**是否已经达到可刷新状态*/
    protected abstract void refresh(boolean canRefresh);

    /**松开并刷新*/
    protected abstract void loosenAndRefresh();

    /**复位*/
    protected abstract void reset();

    private void animMove(long duratuon, int... values) {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(values);
        valueAnimator.setDuration(duratuon);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int dy = (int) animation.getAnimatedValue();
                setHeaderHeight(dy);
            }
        });
        valueAnimator.start();
    }

    public void complete(){
        status = STATUS_NORMAL;
        animMove(200, getHeight(), 0);
    }

    public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
        this.onRefreshListener = onRefreshListener;
    }
}

头部抽象类 几个抽象方法主要用于扩展 有几个关键点

1 初始高度 0 隐藏头部

 public BaseRefreshHeader(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,0));
    }

2 弹性效果使用的是属性动画实现的

 private void animMove(long duratuon, int... values) {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(values);
        valueAnimator.setDuration(duratuon);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int dy = (int) animation.getAnimatedValue();
                setHeaderHeight(dy);
            }
        });
        valueAnimator.start();
    }

3 一些满足大部分需求的抽象方法

/**拉伸过程*/
    protected abstract void move(int height);

    /**可刷新的高度*/
    protected abstract int refreshHeight();

    /**是否已经达到可刷新状态*/
    protected abstract void refresh(boolean canRefresh);

    /**松开并刷新*/
    protected abstract void loosenAndRefresh();

    /**复位*/
    protected abstract void reset();

4 刷新过程中下拉不会重复刷新

public void pulldown(int height) {
        if(height<0){
            return;
        }
        if(status==STATUS_REFRESHING){
            setHeaderHeight(refreshHeight()+height);
            move(refreshHeight()+height);
        }else {
            setHeaderHeight(height);
            move(height);
        }
        if(status!=STATUS_REFRESHING) {
            if (height > refreshHeight()) {
                refresh(true);
            } else {
                refresh(false);
            }
        }
    }

头部的逻辑很简单 主要是通过高度高确定刷新的时机。

到这里核心的逻辑就分析完了,接下来看看如何来使用,使用也是及其简单的

 rvConetents= (ExpandRecyclerView) findViewById(R.id.recyclerview);

        layoutManager=new LinearLayoutManager(this);
        rvConetents.setLayoutManager(layoutManager);
        rvConetents.setSupportPullRefresh(true);
        rvConetents.setSupportPullLoad(true);
        for (int i=0;i<8;i++){
            ss.add("字符初始"+i);
        }
        testAdapter=new TestAdapter(this,ss);
        rvConetents.setAdapter(testAdapter);
        rvConetents.setOnRefreshListener(new OnRefreshListener() {
            @Override
            public void onRefresh() {
                Log.d("RecyclerViewActivity", "刷新");
                rvConetents.postDelayed(new Runnable() {
                    @Override
                    public void run() {

                        ss.clear();
                        for (int i=0;i<2;i++){
                            ss.add("字符新加"+i);
                        }
                        testAdapter.notifyDataSetChanged();

                        rvConetents.refreshComplete();
                    }
                },3000);
            }
        });
        rvConetents.setOnLoadListener(new OnLoadListener() {
            @Override
            public void onLoad() {
                Log.d("RecyclerViewActivity", "加载");
                rvConetents.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        rvConetents.loadComplete();
                        for (int i=0;i<10;i++){
                            ss.add("字符新加"+i);
                        }
                        testAdapter.notifyDataSetChanged();
                    }
                },3000);
            }
        });

使用和普通的RecyclerView基本没什么区别 只是多了下拉刷新 上拉加载的一些监听回调。这里就不细讲了

**

遇到的坑

**


 java.lang.IllegalArgumentException: called detach on an already detached child ViewHolder{22a52cd8 position=7 id=-1, oldPos=-1, pLpos:-1 scrap [attachedScrap] tmpDetached no parent}
                                                                                   at android.support.v7.widget.RecyclerView$5.detachViewFromParent(RecyclerView.java:737)
                                                                                   at android.support.v7.widget.ChildHelper.detachViewFromParent(ChildHelper.java:284)
                                                                                   at android.support.v7.widget.RecyclerView$LayoutManager.detachViewInternal(RecyclerView.java:7593)
                                                                                   at android.support.v7.widget.RecyclerView$LayoutManager.detachViewAt(RecyclerView.java:7586)

猜测在刷新的时候RecyclerView复用Item的时候 出现了复用到不存在的item 出现奔溃

 if (vh != null) {
                        if (vh.isTmpDetached() && !vh.shouldIgnore()) {
                            throw new IllegalArgumentException("called detach on an already"
                                    + " detached child " + vh);
                        }
                        if (DEBUG) {
                            Log.d(TAG, "tmpDetach " + vh);
                        }
                        vh.addFlags(ViewHolder.FLAG_TMP_DETACHED);
                    }

出现问题时 google了好久没有找到好的处理方式 猜测既然是复用出了问题干脆就把childview都清除好了

private final RecyclerView.AdapterDataObserver dataObserver = new RecyclerView.AdapterDataObserver() {
        @Override
        public void onChanged() {
            if(getChildCount()>1) {
                removeViews(1, getChildCount()-1);
            }
            expandAdapter.notifyDataSetChanged();
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            expandAdapter.notifyItemRangeInserted(positionStart, itemCount);
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount) {
            expandAdapter.notifyItemRangeChanged(positionStart, itemCount);
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
            expandAdapter.notifyItemRangeChanged(positionStart, itemCount);
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            expandAdapter.notifyItemRangeRemoved(positionStart, itemCount);
        }

        @Override
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
            expandAdapter.notifyItemMoved(fromPosition, toPosition);
        }
    };

在数据改变的时候清除所有的childview。

**

高度扩展

**
不支持扩展的都是耍流氓,高仿一把京东的下拉刷新效果。看效果(录屏软件有点问题 掉帧了)
这里写图片描述

直接看代码 主要利用了基类的几个抽象方法

public class JDRefreshHeader extends BaseRefreshHeader {
    ImageView ivPeople,ivGoods,ivGoodsAnim;
    TextView tv;

    public JDRefreshHeader(Context context) {
        this(context,null);
    }

    public JDRefreshHeader(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public JDRefreshHeader(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        LayoutInflater.from(context).inflate(R.layout.jd_refresh_header,this);
        ivPeople= (ImageView) findViewById(R.id.iv_people);
        ivGoods= (ImageView) findViewById(R.id.iv_goods);
        ivGoodsAnim= (ImageView) findViewById(R.id.iv_goods_anim);
        tv= (TextView) findViewById(R.id.tv);
        ivGoodsAnim.setVisibility(GONE);
    }

    @Override
    protected void move(int height) {

    }

    @Override
    protected int refreshHeight() {
        return 140;
    }

    @Override
    protected void refresh(boolean canRefresh) {
         if(canRefresh){
             tv.setText("松开刷新");
         }else {
             tv.setText("下拉刷新");
         }
    }

    @Override
    protected void loosenAndRefresh() {
        tv.setText("更新中...");
        ivGoods.setVisibility(GONE);
        ivPeople.setVisibility(GONE);
        ivGoodsAnim.setVisibility(VISIBLE);
        ivGoodsAnim.setBackgroundResource(R.drawable.jd_anim);
        AnimationDrawable animationDrawable;
        animationDrawable= (AnimationDrawable) ivGoodsAnim.getBackground();
        animationDrawable.start();
    }

    @Override
    protected void reset() {
        tv.setText("");
        ivGoods.setVisibility(VISIBLE);
        ivPeople.setVisibility(VISIBLE);
        ivGoodsAnim.setVisibility(GONE);
    }
}

继承基类 实现几个抽象方法 提供一个刷新高度(很关键) 刷新头部 设置非常简单。

        rvConetents.setAdapter(testAdapter);
        rvConetents.addHeadView(new JDRefreshHeader(this));

到此RecyclerView的上拉加载下拉刷新就解析完成,有什么不对的地方欢迎指正。

源码地址

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值