android 仿SwipeRefreshLayout实现下拉刷新上拉加载控件(PullRefreshLayout)

一、我为什么要自己实现下拉刷新上拉加载控件

       公司的项目之前一直用的是PullToRefresh,先不说PullToRefresh已经停止维护了,PullToRefresh扩展起来很麻烦,但并不是说它的扩展性不好,就程序本身的设计思想和设计模式而言是非常好的,非常值得学习,只是PullToRefresh难以适应各种各样的需求罢了。如果项目中使用了像StickyHeaderListView,SwipeListview这样的开源项目,而且需要下拉刷新和上拉加载的功能,PullToRefresh就显得格外尴尬。官方的SwipeRefreshLayout就很好,继承自ViewGroup,基本上能为任何一个view提供下拉刷新功能,但是它并没有实现上拉加载,而且它的下拉样式有时间也不是产品想要的。现在已经有很多开源项目都是以SwipeRefreshLayout这种形式实现的,但我还是决定自己写一个,原因就是为了满足产品的需求,并且自己写的这个没有多样的刷新样式,代码不复杂便于维护,只是为了适应自己的项目。好了扯了这么多没用的,还是让我们开始正题吧!

二、PullRefreshLayout的实现原理

       像SwipeRefreshLayout一样,我们的PullRefreshLayout也继承ViewGroup。PullRefreshLayout应该包含三个部分:refreshHeader(刷新头部视图),refreshView(需要刷新和加载功能的view),refreshFooter(加载尾部视图)。refreshHeader和refreshFooter在初始布局时应该是隐藏的,我们先通过重写onMeasure方法确定这三个部分的大小,首先refreshView应该填充整个PullRefreshLayout可视区域,再通过重写onLayout方法对着三个部分进行排版,将refreshHeaderView和refreshFooterView的位置放置在PullRefreshLayout可视区域外就达到了隐藏的效果,排版要达到的效果如下图所示


下面贴出onMeasure和onLayout方法的代码

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (mRefreshView == null) {
            ensureRefreshView();
        }
        if (mRefreshView == null) {
            return;
        }
        mRefreshView.measure(MeasureSpec.makeMeasureSpec(
                getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
                getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));

        measureView(mRefreshHeaderView);
        measureView(mLoadingFooterView);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (0 == getChildCount()) {
            return;
        }

        int childLeft = getPaddingLeft();
        int childTop = getPaddingTop();
        int childRight = getPaddingRight();
        int childBottom = getPaddingBottom();

        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        int refreshViewWidth = width - childLeft - childRight;
        int refreshViewHeight = height - childTop - childBottom;
        mRefreshView.layout(childLeft, childTop, childLeft + refreshViewWidth, childTop + refreshViewHeight);

        int headerHeight = mRefreshHeaderView.getMeasuredHeight();
        int headerWidth = mRefreshHeaderView.getMeasuredWidth() - childLeft - childRight;
        mRefreshHeaderView.layout(childLeft, - headerHeight, childLeft + headerWidth, 0);

        int footerHeight = mLoadingFooterView.getMeasuredHeight();
        int footerWidth = mLoadingFooterView.getMeasuredWidth() - childLeft - childRight;
        int footerTop = refreshViewHeight + childBottom;
        mLoadingFooterView.layout(childLeft, footerTop, childLeft + footerWidth, footerTop + footerHeight);
    }
    private void measureView(View targetView) {
        if (targetView == null) {
            return;
        }

        LayoutParams p = targetView.getLayoutParams();
        if (p == null) {
            p = new LayoutParams(
                    LayoutParams.MATCH_PARENT,
                    LayoutParams.WRAP_CONTENT);
        }

        int lpHeight = p.height;
        int childHeightSpec;
        if (lpHeight > 0) {
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        }
        int childWidthSpec = MeasureSpec.makeMeasureSpec(
                getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                MeasureSpec.EXACTLY);
        targetView.measure(childWidthSpec, childHeightSpec);
    }
好了,布局已经搞定了,接下来无可厚非要根据用户的touchEvent事件来控制刷新头尾视图的展示与隐藏了,先不急,让我们先来看两张效果图

          

很明显,第一张图刚开始是向上拉,事件是交由RefreshView处理的,接下来向下拉,直到满足需要展示RefreshHeaderView的条件时,事件转交由PullRefreshLayout处理。第二张图也一样,事件由RefreshView和RefreshHeaderView交替处理。出于这种场景,我重写了onInterceptTouchEvent方法,将Touch事件直接拦截,在需要RefreshView处理时,才将事件分发。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mDownX = ev.getX();
                mDownY = ev.getY();
                mLastY = mDownY;
                break;
            case MotionEvent.ACTION_MOVE:
                float dx = ev.getX() - mDownX;
                float dy = ev.getY() - mDownY;
                RefreshHeaderView.PullDownRefreshStatus refreshStatus = mRefreshHeaderView.getRefreshStatus();

                if ((refreshStatus == RefreshHeaderView.PullDownRefreshStatus.REFRESHING || mIsLoadingMore)) {
                    if (Math.abs(dy) > 5) {
                        return true;
                    }
                } else {
                    if (Math.abs(dy) > Math.abs(dx)) {
                        return true;
                    }
                }
        }
        return false;
    }

可以看到并不是所有的事件都拦截了,当页面正在刷新时,并认为是有效的move时拦截事件,反之只有垂直方向的位移大于水平方向的位移时拦截(这样做是为了兼容有侧滑功能的ListView,但当正在刷新时如果不拦截水平方向的Touch事件会出现问题,因为这种事件普通listview也是处理的,会出现刷新视图不能隐藏的情况。这里有点瑕疵安静)。只要onInterceptTouchEvent方法返回true,onTouchEvent方法会立即调用

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mIsHeaderCollapseAnimating || mIsFooterExpandAnimating || mIsFooterCollapseAnimating) {
            return true;
        }

        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_MOVE:
                if (!mIsFooterCollapseAnimating && !mIsFooterExpandAnimating && !mIsHeaderCollapseAnimating) {
                    scrollRefreshLayout(event);
                }
                mLastY = event.getY();
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                RefreshHeaderView.PullDownRefreshStatus refreshStatus = mRefreshHeaderView.getRefreshStatus();
                // 根据下拉状态,判断是隐藏header,还是触发下拉刷新
                if (refreshStatus == RefreshHeaderView.PullDownRefreshStatus.RELEASE_REFRESH) {
                    collapseRefreshHeaderView(RefreshHeaderView.PullDownRefreshStatus.REFRESHING);
                } else if (refreshStatus == RefreshHeaderView.PullDownRefreshStatus.PULL_DOWN_REFRESH && getScrollY() < 0) {
                    collapseRefreshHeaderView(refreshStatus);
                }
                mPullDownDistance = 0;

                if (mIsDispatchDown) {
                    mRefreshView.dispatchTouchEvent(event);
                    mIsDispatchDown = false;
                }
                mFlag = false;
                break;
        }
        return true;

这里我们主要看一下scrollRefreshLayout方法,它控制着事件的分发和刷新视图的隐藏和显示

    private void scrollRefreshLayout(MotionEvent event) {
        int scrollY = getScrollY();
        float dy = mLastY - event.getY();
        RefreshHeaderView.PullDownRefreshStatus refreshStatus = mRefreshHeaderView.getRefreshStatus();

        if (refreshStatus == RefreshHeaderView.PullDownRefreshStatus.REFRESHING) {
            if (scrollY == 0) {
                // scrollY == 0时,下拉刷新view已经不可见,此时需要判断事件由谁来处理
                if (dy < 0 && !canChildScrollUp()) {
                    //方向为下拉,并且refreshView不能继续往下滚时自己处理事件,否则事件分发给refreshView处理
                    scrollRefreshLayoutWithRefreshing((int) dy, scrollY);
                } else {
                    dispatchTouchEventToRefreshView(event);
                }
            } else {
                scrollRefreshLayoutWithRefreshing((int) dy, scrollY);
            }
        } else if (mIsLoadingMore) {
            if (scrollY == 0) {
                // scrollY == 0时,上拉加载view已经不可见,此时需要判断事件由谁来处理
                if (dy > 0 && !canChildScrollDown()) {
                    // 方向为上拉,并且refreshView不能继续往上滚时自己处理事件,否则事件分发给refreshView处理
                    scrollRefreshLayoutWithLoading((int) dy, scrollY);
                } else {
                    dispatchTouchEventToRefreshView(event);
                }
            } else {
                scrollRefreshLayoutWithLoading((int) dy, scrollY);
            }
        } else {
            if (dy < 0 && !canChildScrollUp() || getScrollY() < 0) {
                // 下拉刷新
                if (mMode == PULL_FROM_START || mMode == BOTH) {
                    mPullDownDistance += -dy;

                    if (mPullDownDistance > 0) {
                        // 阻尼
                        float damping = (float) (mPullDownDistance / (Math.log(mPullDownDistance) / Math.log(FACTOR)));
                        damping = Math.max(0, damping);
                        scrollTo(0, (int) -damping);

                        if (damping >= mRefreshHeaderView.getHeight()) {
                            // 滚动距离超过下拉刷新视图的高,将状态变为释放
                            mRefreshHeaderView.setRefreshStatus(RefreshHeaderView.PullDownRefreshStatus.RELEASE_REFRESH);
                        } else {
                            mRefreshHeaderView.setRefreshStatus(RefreshHeaderView.PullDownRefreshStatus.PULL_DOWN_REFRESH);
                        }
                    } else {
                        scrollTo(0, 0);
                        mPullDownDistance = 0;
                    }
                }
            } else if (dy > 0 && !canChildScrollDown() && getScrollY() == 0 && !mIsFooterCollapseAnimating) {
                if (!mFlag && (mMode == PULL_FROM_END || mMode == BOTH) && !mIsFooterExpandAnimating) {
                    mFlag = true;
                    // 上拉加载
                    expandLoadingFooterView();
                }
            } else {
                if (!(!canChildScrollDown() && dy > 0)) {
                    if (Math.abs(dy) > 5) {
                        dispatchTouchEventToRefreshView(event);
                    }
                }
            }
        }
    }
刷新视图的滚动主要借助了ViewGroup的getScrollY和scrollBy方法,这里先对这两个方法做下简单的说明

1.scrollBy

以现在的位置为基础,根据传入的dx或dy在水平或垂直方向滚动一段距离,dy为负向下滚,dy为正向上滚。

2.getScrollY

这个主要说下它的参照原点和正负方向,它的y轴正方向和数学中的坐标系一样,向上为正(这个坐标系官方自己搞的真心乱),参照原点是layout之后,top或lef值最小的子view的坐定点,这样说很难理解,直接看下图


好了,开始讲我们的逻辑。我们这里就先撇开上拉加载,我们只讲下拉刷新,上拉加载与下拉刷新类似。首先分出两种状态

1.下拉刷新正在执行

此时,当getScrollY == 0时,下拉刷新view已经完全不可见,此时需要判断事件由谁来处理。方向为下拉,并且refreshView不能继续往下滚时PullRefreshLayout自己处理事件,否则事件分发给refreshView处理。当getScrollY != 0时,下拉刷新view还可见,这时要根据move的距离,移动PullRefreshLayout。

2.下拉刷新没有执行

此时,当方向为下拉并且RefreshView不能再往上滚动时(dy < 0 && !canChildScrollUp()),触发下拉刷新,刷新头拉下来一段距离后,再往上拉(dy > 0),此时要慢慢隐藏刷新头,所以又加了一个getScrollY < 0的判断。当下移的距离大于等于刷新头的高度时,将刷新头的状态改为释放状态,否则为下拉状态。在action up 是判断当前是否处于释放状态,处于释放状态则触发刷新。

好了,就讲到这里,大家可以去下载完整的代码https://github.com/weijia1991/PullRefreshLayout

欢迎大家来拍砖!


  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
首先吐槽一下现在流行的刷新库,一个字大,包涵个人很多集成到项目中不需要的类,也很难找到很满意的效果,所以自己自己动手丰衣足食,撸一个。1.概述对所有基础控件(包括,嵌套滑动例如RecyclerView、NestedScrollView,普通的TextView、ListView、ScrollerView、LinearLayout等)提供下拉刷新上拉加载的支持,处理了横向滑动冲突(例如:顶部banner的情况) ,且实现无痕过度。gradle (改用bintray-release,2017-7-8 16:00上传,以下暂时不会生效)compile 'com.yan:pullrefreshlayout:1.1.2'2.说明支持所有基础控件 loading 出现效果默认(STATE_FOLLOW、STATE_PLACEHOLDER_FOLLOW、STATE_CENTER、STATE_PLACEHOLDER_CENTER、STATE_FOLLOW_CENTER、STATE_CENTER_FOLLOW)  //-控件设置-     refreshLayout.autoRefresh();// 自动刷新     refreshLayout.setOverScrollDampingRatio(0.2f);//  值越大overscroll越短 default 0.2     refreshLayout.setAdjustTwinkDuring(3);// 值越大overscroll越慢 default 3     refreshLayout.setScrollInterpolator(interpolator);// 设置scroller的插值器     refreshLayout.setLoadMoreEnable(true);// 上拉加载是否可用 default false     refreshLayout.setDuringAdjustValue(10f);// 动画执行时间调节,越大动画执行越慢 default 10f     // 刷新或加载完成后回复动画执行时间,为-1时,根据setDuringAdjustValue()方法实现 default 300     refreshLayout.setRefreshBackTime(300);     refreshLayout.setDragDampingRatio(0.6f);// 阻尼系数 default 0.6     refreshLayout.setPullFlowHeight(400);// 拖拽最大范围,为-1时拖拽范围不受限制 default -1     refreshLayout.setRefreshEnable(false);// 下拉刷新是否可用 default false     refreshLayout.setPullTwinkEnable(true);// 回弹是否可用 default true      refreshLayout.setAutoLoadingEnable(true);// 自动加载是否可用 default false          // headerView和footerView需实现PullRefreshLayout.OnPullListener接口调整状态     refreshLayout.setHeaderView(headerView);// 设置headerView     refreshLayout.setFooterView(footerView);// 设置footerView          /**     * 设置header或者footer的的出现方式,默认7种方式     * STATE_FOLLOW, STATE_PLACEHOLDER_FOLLOW, STATE_PLACEHOLDER_CENTER     * , STATE_CENTER, STATE_CENTER_FOLLOW, STATE_FOLLOW_CENTER     * ,STATE_PLACEHOLDER     */     refreshLayout.setRefreshShowGravity(RefreshShowHelper.STATE_CENTER,RefreshShowHelper.STATE_CENTER);     refreshLayout.setHeaderShowGravity(RefreshShowHelper.STATE_CENTER)// header出现动画     refreshLayout.setFooterShowGravity(RefreshShowHelper.STATE_CENTER)// footer出现动画     // PullRefreshLayout.OnPullListener         public interface OnPullListener {             // 刷新或加载过程中位置相刷新或加载触发位置的百分比,时刻调用             void onPullChange(float percent);             void onPullReset();// 数据重置调用             void onPullHoldTrigger();// 拖拽超过触发位置调用             void onPullHoldUnTrigger();// 拖拽回到触发位置之前调用             void onPullHolding(); // 正在刷新             void onPullFinish();// 刷新完成         }3.demo用到的库loading 动画 AVLoadingIndicatorView(https://github.com/81813780/AVLoadingIndicatorView)

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值