android 打造真正的下拉刷新上拉加载recyclerview(三):下拉刷新上拉加载

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/anyfive/article/details/53036125

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

前言

之前,我们介绍了下拉刷新上拉加载RecyclerView的使用,那么现在,我们就来说一下这个下拉刷新是怎么实现的。

在开发过程中,我想了两种方案。一是使用LinearLayout嵌套头部、recyclerview、尾部的方式,如下图:

第一种方案

  • 当recyclerview滑动到顶部时,移动LinearLayout露出头部;
  • 当recyclerview滑动到底部时,移动LinearLayout露出尾部;

著名的PullToRefreshListView采用的就是这种方式。

但后来,我放弃了这个方案,为什么呢?

因为多次尝试对recyclerview内部的fling事件进行处理,总是达不到自己想要的效果,我想要的是:
比如当前正在刷新,我向下fling RecyclerView,这时候RecyclerView向上滚动到顶部后,剩余速度继续露出RefreshHeader,而且我不喜欢每次都全露出来,而是要该露多少就露多少。简单地说,就是我想要给人一种刷新头部就是隶属于RecyclerView的、不存在断层的感觉。

恩,懂我意思吗?(刚刚怕表达不清楚,特地把同事叫来看他懂不懂)

总之,这种方案处理的效果我不满意!那怎么办呢?重来吧,删代码(心在滴血)。

于是有了第二种方案:给RecyclerView添加两个头部,分别是:用于造成下拉效果的辅助头部、刷新头部;添加两个尾部,分别是:加载尾部,用于造成上拉效果的辅助尾部。当滑动到顶部时,改变辅助头部的高度,把其他item往下推,造成下拉的感觉;上拉同理。

我还是再画个图吧:

第二种方案

  • 在onLayout中,通过设置RecyclerView的margin,将头部和尾部偏移出屏幕;
  • 辅助头部:初始高度为1px;当RecyclerView滑动到顶部时,通过改变高度,造成下拉效果;
  • 辅助尾部:初始高度为1px;当RecyclerView滑动到底部时,通过改变高度,造成上拉的效果

思路就是这样,但在实际的开发过程中,下拉还好,而上拉会遇到各种各样的问题,不过好在解决了这些问题后,实际的效果完美符合我的要求,所以PTLRecyclerView采用了这个方案进行实现。

接下来我们来依次介绍下拉和上拉,以及开发过程中遇到的问题。

下拉刷新

其实下拉刷新是比较简单的,PullToRefreshRecyclerView继承于HeaderAndFooterRecyclerView,我们按顺序来一一介绍PullToRefreshRecyclerView中的几个主要方法:

  1. 首先介绍下全局变量,免得看代码的时候吃力:
//    当前状态
private int mState = STATE_DEFAULT;
//    初始
public final static int STATE_DEFAULT = 0;
//    正在下拉
public final static int STATE_PULLING = 1;
//    松手刷新
public final static int STATE_RELEASE_TO_REFRESH = 2;
//    刷新中
public final static int STATE_REFRESHING = 3;

//    下拉阻尼系数
private float mPullRatio = 0.5f;

//    辅助头部
private View topView;

//    刷新头部
private View mRefreshView;
//    刷新头部的高度
private int mRefreshViewHeight = 0;

//    触摸事件辅助,当RecyclerView滑动到顶部时,记录触摸事件的y轴坐标
private float mFirstY = 0;
//    当前是否正在下拉
private boolean mPulling = false;

//    是否可以下拉刷新
private boolean mRefreshEnable = true;

//    回弹动画
private ValueAnimator valueAnimator;

//    刷新监听
private OnRefreshListener mOnRefreshListener;

//    刷新头部构造器
private RefreshHeaderCreator mRefreshHeaderCreator;
  1. 在构造函数中初始化,获得默认的刷新头部:
private void init(Context context) {
    if (topView == null) {
        topView = new View(context);
//        该view的高度不能为0,否则将无法判断是否已滑动到顶部
        topView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 1));
//        设置默认LayoutManager
        setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));
//        初始化默认的刷新头部
        mRefreshHeaderCreator = new DefaultRefreshHeaderCreator();
        mRefreshView = mRefreshHeaderCreator.getRefreshView(context,this);
    }
}
  1. 在onLayout方法中,获得刷新头部的高度,并偏移RecyclerView:
/**
 * 在measure的时候,隐藏刷新头部
 */
@Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mRefreshView != null && mRefreshViewHeight == 0) {
            mRefreshView.measure(0,0);
            mRefreshViewHeight = mRefreshView.getLayoutParams().height;
            ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
            marginLayoutParams.setMargins(marginLayoutParams.leftMargin, marginLayoutParams.topMargin-mRefreshViewHeight-1, marginLayoutParams.rightMargin, marginLayoutParams.bottomMargin);
            setLayoutParams(marginLayoutParams);
        }
        super.onMeasure(widthSpec, heightSpec);
    }
  1. 触摸事件:
@Override
public boolean onTouchEvent(MotionEvent e) {
//    若是不可以下拉
    if (!mRefreshEnable) return super.onTouchEvent(e);
//    若刷新头部为空,不处理
    if (mRefreshView == null)
        return super.onTouchEvent(e);
//    若回弹动画正在进行,不处理
    if (valueAnimator != null && valueAnimator.isRunning())
        return super.onTouchEvent(e);

    switch (e.getAction()) {
        case MotionEvent.ACTION_MOVE:
            if (!mPulling) {
                if (isTop()) {
//                    当listview滑动到最顶部时,记录当前y坐标
                    mFirstY = e.getRawY();
                }
//                若listview没有滑动到最顶部,不处理
                else
                    break;
            }
            float distance = (int) ((e.getRawY() - mFirstY)*mPullRatio);
//            若向上滑动(此时刷新头部已隐藏),不处理
            if (distance < 0) break;
            mPulling = true;
//            若刷新中,距离需加上头部的高度
            if (mState == STATE_REFRESHING) {
                distance += mRefreshViewHeight;
            }
//            下拉
            setState(distance);
            return true;
        case MotionEvent.ACTION_UP:
//            回弹
            replyPull();
            break;
    }
    return super.onTouchEvent(e);
}
  1. 判断是否滑动到了顶部:
private boolean isTop() {
    return !ViewCompat.canScrollVertically(this, -1);
}
  1. 设置当前下拉状态:
private void setState(float distance) {
//    刷新中,状态不变
    if (mState == STATE_REFRESHING) {
    }
    else if (distance == 0) {
        mState = STATE_DEFAULT;
    }
//    松手刷新
    else if (distance >= mRefreshViewHeight) {
        int lastState = mState;
        mState = STATE_RELEASE_TO_REFRESH;
        if (mRefreshHeaderCreator != null)
            if (!mRefreshHeaderCreator.onReleaseToRefresh(distance,lastState))
                return;
    }
//    正在拖动
    else if (distance < mRefreshViewHeight) {
        int lastState = mState;
        mState = STATE_PULLING;
        if (mRefreshHeaderCreator != null)
            if (!mRefreshHeaderCreator.onStartPull(distance,lastState))
                return;
    }
//    开始下拉
    startPull(distance);
}

这里可以看到,当头部构造器的onStartPull和onReleaseToRefresh返回false时,便不再下拉,其实这里也是为了应对类似“超过多少就不再下拉了”这种需求。

  1. 改变辅助头部的高度,造成下拉的效果:
private void startPull(float distance) {
//        辅助头部的高度不能为0,否则将无法判断是否已滑动到顶部
    if (distance < 1)
        distance = 1;
    if (topView != null) {
        LayoutParams layoutParams = (LayoutParams) topView.getLayoutParams();
        layoutParams.height = (int) distance;
        topView.setLayoutParams(layoutParams);
    }
}
  1. 松手回弹,在这个方法中,我们需要判断是直接刷新,还是直接回弹到原来位置:
private void replyPull() {
    mPulling = false;
//    回弹位置
    float destinationY = 0;
//    判断当前状态
//    若是刷新中,回弹
    if (mState == STATE_REFRESHING) {
        destinationY = mRefreshViewHeight;
    }
//    若是松手刷新,刷新,回弹
    else if (mState == STATE_RELEASE_TO_REFRESH) {
//        改变状态
        mState = STATE_REFRESHING;
//        刷新
        if (mRefreshHeaderCreator != null)
            mRefreshHeaderCreator.onStartRefreshing();
        if (mOnRefreshListener != null)
            mOnRefreshListener.onStartRefreshing();
//        若在onStartRefreshing中调用了completeRefresh方法,将不会滚回初始位置,因此这里需加个判断
        if (mState != STATE_REFRESHING) return;
        destinationY = mRefreshViewHeight;
    } else if (mState == STATE_DEFAULT || mState == STATE_PULLING) {
        mState = STATE_DEFAULT;
    }

    LayoutParams layoutParams = (RecyclerView.LayoutParams) topView.getLayoutParams();
    float distance = layoutParams.height;
    if (distance <= 0) return;

    valueAnimator = ObjectAnimator.ofFloat(distance, destinationY).setDuration((long) (distance * 0.5));
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float nowDistance = (float) animation.getAnimatedValue();
            startPull(nowDistance);
        }
    });
    valueAnimator.start();
}
  1. 完成刷新:
public void completeRefresh() {
    if (mRefreshHeaderCreator != null)
        mRefreshHeaderCreator.onStopRefresh();
    mState = STATE_DEFAULT;
    replyPull();
    mRealAdapter.notifyDataSetChanged();
}
  1. 在设置适配器的时候,添加辅助头部和刷新头部:
@Override
public void setAdapter(Adapter adapter) {
    super.setAdapter(adapter);
    if (mRefreshView != null) {
        addHeaderView(topView);
        addHeaderView(mRefreshView);
    }
}
  1. 设置自定义的头部:
public void setRefreshViewCreator(RefreshHeaderCreator refreshHeaderCreator) {
    this.mRefreshHeaderCreator = refreshHeaderCreator;
    mRefreshView = refreshHeaderCreator.getRefreshView(getContext(),this);
//    若有适配器,添加到头部
    if (mAdapter != null) {
        addHeaderView(topView);
        addHeaderView(mRefreshView);
    }
    mRealAdapter.notifyDataSetChanged();
}

以上就是PullToRefreshRecyclerView主要的几个方法了,介绍得算比较清楚吧,再加上代码中已经有注释了,就不再累赘了。核心就一句话:拦截触摸事件,改变辅助头部的高度。 就是这么easy~~~~

上拉加载

本来上拉加载我想单独用一篇文章来介绍的,但其实上拉加载的处理和下拉刷新的处理逻辑是一致的,因此在这里便一起介绍了吧,双飞更开心呦客官~~

咳咳,说正经的,上面我们说过上拉加载会遇到各种问题,具体有哪些呢?

  1. 滑动到底部时,继续上拉,改变辅助底部的高度造成上拉的效果,然后现实很骨感,你会发现(通过调试或打印)辅助底部的高度是在改变,但RecyclerView中的item并没有挤上去啊,根本就没有上拉的效果出现。
  2. 当你添加FooterView的时候,发现你添加的FooterView居然跑到刷新底部的下面去了,坑了个爹…..
  3. 哎,怎么好像没了,我记得碰到了很多问题呀…….

以下是我的解决方法:
1. 这个问题我实在没想到什么好办法,因此用了最粗暴的方式:在改变高度后直接调用scrollToPosition滚动到最底部。这样做有什么后果呢?效率肯定是不高的,但为了效果,我可以忍….经过测试,StaggredLayoutManager不会有任何影响,效果溜溜哒。但是但是,LinearLayoutManager上拉时会出现卡顿的现象,这个怎么忍!当然GridLayoutManager也会卡顿,毕竟他是LinearLayoutManager的儿子啊,遗传病。为什么呢?因为LinearLayoutManager对item的layout和StaggredLayoutManager的是不一样的,既然StaggredLayoutManager没问题,那么我们用只有一列的StaggredLayoutManager替代LinearLayoutManager就是最粗暴的方法。当然,更好的方式是直接继承LayoutManager写一个自己的LinearLayoutManager,但由于时间和水平的限制,就……采用StaggredLayoutManager吧。这就是为什么我之前说使用PullToLoadRecyclerView的时候,要用PTLLinearLayout和PTLGridLayoutManager。
2. 这个问题其实最好解决,继承HeaderAndFooterAdapter写一个PullToLoadAdapter就可以啦。

虽然解决方法比较坑爹,但不管黑猫还是白猫,能抓老鼠的就是好猫。当然,这么说有点过分了,所以在这里,希望有大牛有更好的方法,欢迎到github上提交您的代码,共同构建这个项目。

PullToLoadRecyclerView和PullToRefreshRecyclerView的代码逻辑其实基本一致,而PullToLoadAdapter的代码和HeaderAndFooterAdapter也比较像,因此这里就不再展开了,有兴趣的同学可以去github上把项目clone下来看看。

自定义的刷新头部和加载尾部

有没有遇到过这种情况,当你辛辛苦苦找到一个需要的库时,却发现他的UI居然不支持自定义!摔!在实际开发中,产品和设计怎么会允许你使用那个库默认的UI设计,这是基本不可能的事。因此,支持自定义的刷新头部和加载尾部是非常非常重要的事!!

之前在介绍使用方法时,我们就已经介绍了如何使用自定义的刷新头部和加载尾部,而通过上面的代码,你应该也已经理解了RefreshHeaderCreator和LoadFooterCreator的工作方式。

其实就是使用这两个抽象类,把刷新头部和加载尾部的UI与RecyclerView进行解耦,交给用户自己去实现,项目中的默认刷新头部和加载尾部就是很好的例子,相信你看完应该就知道怎么去构造自己的刷新头部和加载尾部了。

直接上DefaultRefreshHeaderCreator的代码:

public class DefaultRefreshHeaderCreator extends RefreshHeaderCreator {

    private View mRefreshView;
    private ImageView iv;
    private TextView tv;

    private int rotationDuration = 200;

    private int loadingDuration = 1000;
    private ValueAnimator ivAnim;

    @Override
    public boolean onStartPull(float distance,int lastState) {
        if (lastState == PullToRefreshRecyclerView.STATE_DEFAULT ) {
            iv.setImageResource(R.drawable.arrow_down);
            iv.setRotation(0f);
            tv.setText("下拉刷新");
        } else if (lastState == PullToRefreshRecyclerView.STATE_RELEASE_TO_REFRESH) {
            startArrowAnim(0);
            tv.setText("下拉刷新");
        }
        return true;
    }

    @Override
    public void onStopRefresh() {
        if (ivAnim != null) {
            ivAnim.cancel();
        }
    }

    @Override
    public boolean onReleaseToRefresh(float distance,int lastState) {
        if (lastState == PullToRefreshRecyclerView.STATE_DEFAULT ) {
            iv.setImageResource(R.drawable.arrow_down);
            iv.setRotation(-180f);
            tv.setText("松手立即刷新");
        } else if (lastState == PullToRefreshRecyclerView.STATE_PULLING) {
            startArrowAnim(-180f);
            tv.setText("松手立即刷新");
        }
        return true;
    }

    @Override
    public void onStartRefreshing() {
        iv.setImageResource(R.drawable.loading);
        startLoadingAnim();
        tv.setText("正在刷新...");
    }

    @Override
    public View getRefreshView(Context context, RecyclerView recyclerView) {
        mRefreshView = LayoutInflater.from(context).inflate(R.layout.layout_ptr_ptl,recyclerView,false);
        iv = (ImageView) mRefreshView.findViewById(R.id.iv);
        tv = (TextView) mRefreshView.findViewById(R.id.tv);
        return mRefreshView;
    }

    private void startArrowAnim(float roration) {
        if (ivAnim != null) {
            ivAnim.cancel();
        }
        float startRotation = iv.getRotation();
        ivAnim = ObjectAnimator.ofFloat(startRotation,roration).setDuration(rotationDuration);
        ivAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                iv.setRotation((Float) animation.getAnimatedValue());
            }
        });
        ivAnim.start();
    }

    private void startLoadingAnim() {
        if (ivAnim != null) {
            ivAnim.cancel();
        }
        ivAnim = ObjectAnimator.ofFloat(0,360).setDuration(loadingDuration);
        ivAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                iv.setRotation((Float) animation.getAnimatedValue());
            }
        });
        ivAnim.setRepeatMode(ObjectAnimator.RESTART);
        ivAnim.setRepeatCount(ObjectAnimator.INFINITE);
        ivAnim.setInterpolator(new LinearInterpolator());
        ivAnim.start();
    }

}

系不系很简单?

照例上两张用烂了的效果图:

刷新加载

Grid刷新加载

Staggred刷新加载

源码地址:https://github.com/whichname/PTLRecyclerView

有意见或建议或疑问等等,欢迎提出~~

传送门:

android 打造真正的下拉刷新上拉加载recyclerview(一):使用

android 打造真正的下拉刷新上拉加载recyclerview(二):添加删除头尾部

android 打造真正的下拉刷新上拉加载recyclerview(四):自动加载和其他封装

猜你喜欢

转载自blog.csdn.net/anyfive/article/details/53036125