Android做下拉刷新的时候,在做些什么

转载注明出处:http://www.jianshu.com/p/4607129c9efa

1. 简介

好长时间没有写博客了,一来是工作忙,抽不出空,二来是迷上了王者荣耀。现在正好赶上项目空闲期,写一篇关于下拉刷新的文章,个人觉得上来加载更多功能使用场景非常少,而且没有必要做的那么麻烦,文章最后会提一下加载更多的实现。

最近项目中遇见了下拉刷新的需求,正好研究了一下,分享一下自己的心得。

主要参考文章或工程:

郭霖大神—Android下拉刷新完全解析,教你如何一分钟实现下拉刷新功能

自个儿写Android的下拉刷新/上拉加载控件

XListView

这三篇文章各自提供了实现下拉刷新的思路,文章会分别介绍这三种实现方式的优劣。文章中会涉及到点击事件分发知识,大家可以查看这篇文章Android事件分发机制详解。自己写对三种实现做了部分优化,写了demo,地址链接

2. 分析

下拉刷新主要分为两部分,一部分是刷新头部Header,一部分是内容展示区域,一般是列表。通过某些方法,来控制刷新头部Header的展示范围,达到下拉刷新的效果,如下图。

图-1 下拉刷新原理图

做下拉刷新之前,分析一下下拉刷新场景以及达到的效果,常见的下拉刷新最少有四种状态

  • 正常状态,下拉刷新头部不展示,用户可以正常操作列表
  • 下拉状态,用户下拉列表,但是没有到达刷新时机,松开手后,刷新头部会自动隐藏
  • 松开刷新状态,到达这个状态时候,刷新头部是完全展示的,用户松开手,即可刷新,如果下拉距离过大,列表会自动上移,完整的露出刷新头部,头部显示刷新中文案。
  • 刷新中状态,请求数据的刷新态,在这种状态下,根据交互需求有不同的实现。
    • 刷新中状态,用户不能操作列表
    • 刷新中状态,可以滑动和操作列表,但刷新头部一直置顶
    • 刷新中状态,可以滑动和操作列表,刷新头部会随着列表的滑动而一起滑动,参考欣慰微博下拉刷新。(大部分下拉刷新的交互效果)

前三种状态会根据用户手势的移动相互切换,大部分下拉刷新中状态交互是第三种,以新浪微博为参考蓝本,本文最终实现的效果也是以这个效果为目标。

3. 第一个例子链接

郭霖大神文章篇幅写的比较多,很多可以不用关心,关于下拉刷新的核心代码在ListView的OnTouchListener中,是通过修改Header的MarginTop值控制Header显示可见范围,到达下拉刷新的效果。缺点就是,每次更改Header的MarginTop值时候,会触发父布局重新onMeasure()/onLayout()方法,如果ListView中Item内容比较复杂,有卡顿现象,同时没有处理刷新中状态点击事件,如果要处理,需要额外添加复杂的逻辑。

3.1 第一个例子实现过程

3.1.1 初始化Header

父布局中包含下拉刷新的Header和ListView,在父布局的构造方法中实例化Header,并放入父布中。

public PtrFirstRefreshableView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
}

/**
 * 实例化刷新头部并将刷新头部添加的父布局
 */
private void init() {
    mHeader = new PtrFirstRefreshHeader(getContext());
    touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    addView(mHeader, 0);
}

3.1.2 隐藏Header

在父布局中onLayout()方法中,设置Header的topMarigin,隐藏Header,设置ListView的点击监听器,记录一个标签isLayouted保证只设置一次。

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    
    // 如果是第一次Layout, 做一些设置
    if (changed && !isLayouted) {
        isLayouted = true;
        // 设置刷新头部MarginTop, 隐藏刷新头部
        mHeaderHeight = mHeader.getHeight();
        mHeaderLayoutParams = (LayoutParams) mHeader.getLayoutParams();
        mHeader.setTopMargin(-mHeaderHeight);

        // 设置ListView的事件监听
        mListView = (ListView) getChildAt(1);
        mListView.setOnTouchListener(this);
    }
}

3.1.3 处理点击事件

这个地方逻辑复杂一些,获取用户点击事件后,调用checkTopShow()方法检查当前是否需要处理点击事件,如果ListView的第一个Item展示,且顶部距离父布局为0,则可以下拉刷新。

DOWN事件中记录用户起始位置,注意一定要通过getRawY()获取手指相对屏幕的位置,而不是通过getY()获取手指相对ListView的位置,因为ListView会随着手指滑动而滑动,如果用getY()获取位置会有偏差。

MOVE事件中,如果用户手指向上滑动,且刷新头部是完全隐藏的,不做处理;如果当时非刷新中状态,根据头部MarginTop的值更改当前刷新状态,同时更改刷新头部MarginTop。

UP事件中,用户松开手,如果当前状态是下拉状态,则隐藏刷新头部;如果当前状态是松开刷新状态,则更改状态为刷新中状态,同是隐藏多余margin,仅显示完整的刷新头部,同时调用回调监听(在RefreshingTask类中)。

在整个过程中,如果当前状态处于下拉状态或者松开刷新状态,设置ListView属性,让ListView失去焦点,否则那点击Item会一直处于点击状态。

public boolean onTouch(View v, MotionEvent event) {
    checkTopShow(event);
    if (ableToPull) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownY = event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                float yMove = event.getRawY();
                int distance = (int) (yMove - mDownY);
                // 如果手指向上滑动,并且下拉头是完全隐藏的,不处理
                if (distance <= 0 && mHeader.getTopMargin() <= -mHeaderHeight) {
                    return false;
                }
                if (distance < touchSlop) {
                    return false;
                }
                if (mStatus != STATUS_REFRESHING) {
                    if (mHeader.getTopMargin()  > 0) {
                        mStatus = STATUS_RELEASE_TO_REFRESH;
                    } else {
                        mStatus = STATUS_PULL_TO_REFRESH;
                    }
                    // 通过偏移下拉头的topMargin值,来实现下拉效果
                    int topMargin = (distance / 2) - mHeaderHeight;
                    mHeader.setTopMargin(topMargin);

                    // 更新刷新头部圆环动画
                    mHeader.updateCircle(Math.abs(distance * 1f / 2f / mHeaderHeight));
                }
                break;
            case MotionEvent.ACTION_UP:
            default:
                if (mStatus == STATUS_RELEASE_TO_REFRESH) {
                    // 松手时如果是释放立即刷新状态,就去调用正在刷新的任务
                    mStatus = STATUS_REFRESHING;
                    updateHeaderView();
                    new RefreshingTask().execute();
                    mHeader.startLoading();
                } else if (mStatus == STATUS_PULL_TO_REFRESH) {
                    // 松手时如果是下拉状态,就去调用隐藏下拉头的任务
                    mStatus = STATUS_NORMAL;
                    updateHeaderView();
                    new HideHeaderTask().execute();
                }
                break;
        }

        if (mStatus == STATUS_PULL_TO_REFRESH ||
                mStatus == STATUS_RELEASE_TO_REFRESH) {
            mListView.setPressed(false);
            mListView.setFocusable(false);
            mListView.setFocusableInTouchMode(false);
            updateHeaderView();
            return true;
        }
    }
    return false;
}


private void checkTopShow(MotionEvent event) {
    View firstChild = mListView.getChildAt(0);
    if (firstChild != null) {
        // 如果列表第一个item可见且距离ListView顶部为0,则说明ListView已经到最顶部,此时可以下拉刷新
        int firstVisiblePos = mListView.getFirstVisiblePosition();
        if (firstVisiblePos == 0 && firstChild.getTop() == 0) {
            ableToPull = true;
        } else {
            if (mHeader.getTopMargin() != -mHeaderHeight) {
                mHeader.setTopMargin(-mHeaderHeight);
            }
            ableToPull = false;
        }
    } else {
        ableToPull = true;
    }
}

3.1.4 隐藏头部

在用户手指离开屏幕时候,会根据当前状态选择是隐藏头部还是仅展示头部,仅以隐藏头部为例,代码如下。关于AsyncTask的使用可以查看AsyncTask 第一篇使用篇

class HideHeaderTask extends AsyncTask<Void, Integer, Integer> {

    @Override
    protected Integer doInBackground(Void... params) {
        int topMargin = mHeaderLayoutParams.topMargin;
        while (true) {
            topMargin = topMargin + SCROLL_SPEED;
            if (topMargin <= -mHeaderHeight) {
                topMargin = -mHeaderHeight;
                break;
            }
            publishProgress(topMargin);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return topMargin;
    }

    @Override
    protected void onProgressUpdate(Integer... topMargin) {
        mHeader.setTopMargin(topMargin[0]);
    }

    @Override
    protected void onPostExecute(Integer topMargin) {
        mHeader.setTopMargin(topMargin);
    }
}

3.1.5 请求完毕恢复原状态

网络请求完成后,要隐藏刷新头部,同时恢复原状态。

public void finishRefreshing() {
    mStatus = STATUS_NORMAL;
    new HideHeaderTask().execute();
    mHeader.stopLoading();
}

3.2 第一个例子实现效果

最终的实现效果如下:

图-2 第一个例子实现效果图

3.3 第一个例子总结

该方案思路清晰,不需要对ListView进行拓展。缺点也比较明显,如果ListView中Item过于复杂,会有卡顿现象,而且代码中并没有对刷新中状态的点击事件进行处理,如果在刷新中状态中,滑动布局,会将刷新头部隐藏,在完成请求之前,无法将头部下拉展出,要对此进行修复,需要添加额外的逻辑。不推荐。

4. 第二个例子链接

原文章是使用scrollTo()/scrollBy()方法实现下拉刷新,默认控件向上位移一段距离,正好将刷新头部隐藏。然后根据用户的手势通过scrollBy()方法将刷新头部逐渐展示出来。因为使用scrollTo()/scrollBy()来移动控件,是移动父布局中所有的子控件,如果逻辑处理不当会出现子控件部分移出父布局的情况,子控件显示出现问题。原文章实现很简单,下面实例的代码是做过优化后的代码实现。

4.1 第二个例子实现过程

4.1.1 初始化Header

实例化Header,并将其添加至父布局。

public PtrSecondRefreshableView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
}

private void init(Context context) {
    mHeader = new PtrSecondRefreshHeader(context);
    addView(mHeader, 0);

    mScroller = new Scroller(getContext());
}

4.1.2 隐藏Header

在父布局的onMeasure()方法中测量子View的大小,在onLayout()方法中将刷新头部向上偏移,达到隐藏Header效果。

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

    // 测量子View大小
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
    }
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);

    mLayoutContentHeight = 0;
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        if (child == mHeader) { // 如果是刷新头部,向上偏移
            child.layout(0, 0 - child.getMeasuredHeight(), child.getMeasuredWidth(), 0);
        } else {
            child.layout(0, mLayoutContentHeight, child.getMeasuredWidth(), mLayoutContentHeight + child.getMeasuredHeight());
            mLayoutContentHeight += child.getMeasuredHeight();
        }
    }
}

4.1.3 拦截事件

在父布局的onTouchEvent中设置Header的可见范围,所以用户手势在操作屏幕时候,在某些情况下父布局需要拦截点击事件。

public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercept = false;

    if(mStatus == STATUS_REFRESHING) {
        return false;
    }

    // 记录此次触摸事件的y坐标
    int y = (int) event.getY();

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            intercept = false;
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            if (y > mLastMoveY) { // 下滑操作
                View child = getChildAt(1);
                if (child instanceof AdapterView) {
                    AdapterView adapterChild = (AdapterView) child;
                    // 判断AbsListView是否已经到达内容最顶部(如果已经到达最顶部,就拦截事件,自己处理滑动)
                    if (adapterChild.getFirstVisiblePosition() == 0
                            || adapterChild.getChildAt(0).getTop() == 0) {
                        intercept = true;
                    }
                }
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercept = false;
            break;
        }
    }

    mLastMoveY = y;

    return intercept;
}

4.1.4 处理点击事件

重写父布局的onTouchEvent(),根据用户手势做出相应展示效果。

public boolean onTouchEvent(MotionEvent event) {
    float nowY = event.getY();

    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            mLastMoveY = nowY;
            break;
        case MotionEvent.ACTION_MOVE:
            float distance = mLastMoveY - nowY;
            if(distance < 0) { // 如果是向下滑动,移动子View
                // 头部没有完全展示
                if(Math.abs(getScrollY()) <= mHeader.getMeasuredHeight()) {
                    mStatus = STATUS_PULL_TO_REFRESH;
                    scrollBy(0, (int) (distance * DRAG_COEFFICIENT_NORMAL));
                } else { // 头部已经完全展示
                    scrollBy(0, (int) (distance * DRAG_COEFFICIENT_LIMIT));
                    mHeader.updateText(R.string.ptr_refresh_release);
                    mStatus = STATUS_RELEASE_TO_REFRESH;
                }
            } else { // 如果是向上滑动,移动子View
                if(getScrollY() < 0) {
                    scrollBy(0, (int) distance);
                }
                if(Math.abs(getScrollY()) <= mHeader.getMeasuredHeight()) {
                    mStatus = STATUS_PULL_TO_REFRESH;
                    mHeader.updateText(R.string.ptr_refresh_normal);
                }
            }

            // 更新刷新头部动画
            mHeader.updateCircle(Math.abs(getScrollY() * 1f/ mHeader.getMeasuredHeight()));

            break;
        case MotionEvent.ACTION_UP:
        default:
            if(mStatus == STATUS_RELEASE_TO_REFRESH) { // 用户松开手后,如果是松开刷新状态,则回弹显示完整Header,并刷新数据
                mHeader.updateText(R.string.ptr_refresh_refreshing);
                mHeader.startLoading();
                mStatus = STATUS_REFRESHING;
                    
                // 刷新状态回调
                if(mListener != null) {
                    mListener.onRefresh();
                }

                mScroller.startScroll(0, getScrollY(), 0, -(getScrollY() + mHeader.getMeasuredHeight()), 200);
                invalidate();
            } else if(mStatus == STATUS_PULL_TO_REFRESH){ // 用户松开手后,如果是下拉刷新状态,则隐藏Header
                mScroller.startScroll(0, getScrollY(), 0, -getScrollY(), 200);
                invalidate();
                mHeader.updateText(R.string.ptr_refresh_normal);
                mStatus = STATUS_NORMAL;
            }
            break;
    }

    mLastMoveY = nowY;
    return true;
}

4.1.5 处理刷新中状态

onInterceptTouchEvent()方法中,如果是刷新中状态,拦截事件,会导致用户无法操作ListView;如果不拦截事件,则事件会传递到ListView,这样当用户滚动列表ListView时候,刷新头部会一直悬浮在顶部。所以需要在dispatchTouchEvent()方法中处理刷新中状态。

public boolean dispatchTouchEvent(MotionEvent event) {
    int nowY = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mLastDownY = event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            if(mStatus == STATUS_REFRESHING) {
                float distance = mLastDownY - nowY;
                // 如果手势向下滑动且列表中第一个Item可见,向下移动全部子View
                if(distance < 0
                        && mListView.getFirstVisiblePosition() == 0
                        && mListView.getChildAt(0).getTop()==0)  {
                    scrollBy(0, (int) (distance * DRAG_COEFFICIENT_LIMIT));
                    isListViewMove = true;
                    mLastDownY = nowY;

                    return true;
                } else { // 如果手势向上滑动
                    if(getScrollY() < 0) { // 当Header没有完全隐藏,移动全部子View;当Header完全隐藏,将事件传递给ListView
                        if(getScrollY() + distance > 0) {
                            scrollBy(0, 0);
                        } else {
                            scrollBy(0, (int) distance);
                        }
                        mLastDownY = nowY;
                        isListViewMove = true;
                        return true;
                    }
                }
            }
            break;
        case MotionEvent.ACTION_UP:
        default:
            // 用户抬起手,如果子View通过scrollBy移动过
            if(isListViewMove) {
                isListViewMove = false;
                // 如果子View向下移动,向下移动距离大于Header高度,则自动回弹,显示完整Header
                if(getScrollY() < 0 && Math.abs(getScrollY()) > mHeader.getMeasuredHeight()) {
                    mScroller.startScroll(0, getScrollY(), 0, -(getScrollY() + mHeader.getMeasuredHeight()), 200);
                    invalidate();
                }
                return true;
            }
            isListViewMove = false;
            break;
    }

    return super.dispatchTouchEvent(event);
}

4.1.6 请求完毕恢复原状态

请求完成后,隐藏Header,恢复原状态。

public void finishRefresh() {
    if(!mScroller.isFinished()) {
        mScroller.abortAnimation();
    }
    scrollTo(0, 0);

    mHeader.updateText(R.string.ptr_refresh_normal);
    mHeader.stopLoading();
    mStatus = STATUS_NORMAL;
}

4.2 第二个例子最终实现效果

最终效果如下图:

图-3 第二个例子效果图

4.3 第二个例子总结

涉及点击事件的三个方法dispatchTouchEvent()onInterceptTouchEvent()onTouchEvent()都有对点击事件不同的处理逻辑。虽然能勉强到达文章开头提到的效果,但是在零界点,特别是刷新头部Header刚好隐藏的零界点,会有卡顿现象,加上处理逻辑比较复杂。不推荐。

5. 第三个例子链接

第三个例子是很久的开源项目,不同于前两种实现方式,前面两种都是自定义一个父布局,然后将刷新头部和列表放入其中,第三个例子是直接将刷新头部放在列表ListView的头部,然后动态的设置刷新头部的高度,达到下拉刷新的效果。

5.1 实现过程

5.1.1 初始化Header

在ListView的构造函数中,初始化Heade作为ListView的HeaderView,这里有两点要注意,一是Header的布局文件,因为要动态设置Header的高度,所以布局文件需要嵌套一层,外面一层动态设置高度,里面一层包容所有的Header布局,高度不变;二是因为初始Header是不显示的,想要获取Header的真正高度,要在所有的View初始化以后才能获取。

public PtrThirdRefreshableView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
}

private void init(Context context) {
    mScroller = new Scroller(context, new DecelerateInterpolator());
    
    // 初始化Header,在初始化时候设置高度为0
    mHeader = new PtrThirdRefreshHeader(context);
    mHeaderContainer = mHeader.findViewById(R.id.dgp_header_container);

    addHeaderView(mHeader);

    mHeader.getViewTreeObserver().addOnGlobalLayoutListener(
            new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    // 获取Header的完全展示时候的高度
                    getViewTreeObserver().removeGlobalOnLayoutListener(this);
                    mHeaderViewHeight = mHeaderContainer.getHeight();
                    mHeader.setContentHeight(mHeaderViewHeight);
                }
            });
}

5.1.2 处理点击事件

MOVE事件中,根据当前状态,动态更新刷新Header的高度;在UP事件中根据当前Header展示高度,来做相应处理。

public boolean onTouchEvent(MotionEvent ev) {
    if (mLastY == -1) {
        mLastY = ev.getRawY();
    }

    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mLastY = ev.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:
            final float deltaY = ev.getRawY() - mLastY;
            mLastY = ev.getRawY();
            // 如果列表中第一个Item是可见的, 且Header的部分可见或者向下滑动,则动态设置Header高度
            if (getFirstVisiblePosition() == 0
                    && (mHeader.getShowHeight() > 0 || deltaY > 0)) {
                updateHeaderHeight(deltaY * OFFSET_RADIO);
                return true;
            }
            break;
        default:
            mLastY = -1;
            // 用户松开手时候,如果列表第一个Item可以见
            if (getFirstVisiblePosition() == 0) {
                // 如果Header展示的高度大于Header的真正高度,则可刷新
                if (mHeader.getShowHeight()  > mHeaderViewHeight) {
                    mPullRefreshing = true;
                    mHeader.updateText(R.string.ptr_refresh_refreshing);
                    mHeader.startLoading();
                    if (mRefreshListener != null) {
                        mRefreshListener.onRefresh();
                    }
                }
                // 根据当前情况重置Header高度
                resetHeaderHeight();
                return true;
            }
            break;
    }
    return super.onTouchEvent(ev);
}

/**
 * 动态更新Header高度
 * 
 * @param delta
 */
private void updateHeaderHeight(float delta) {
    mHeader.setShowHeight((int) (delta + mHeader.getShowHeight()));
    if (!mPullRefreshing) {
        if (mHeader.getShowHeight() > mHeaderViewHeight) {
            mHeader.updateText(R.string.ptr_refresh_release);
        } else {
            mHeader.updateText(R.string.ptr_refresh_normal);
        }
    }
    setSelection(0);
}

5.1.3 请求完毕恢复原状态

请求完毕后,恢复原状态,这里没有使用设置Header的高度来隐藏Header,为了移动平滑通过Scroller将Header移动到屏幕外,不显示在屏幕中,达到隐藏的目的。使用Scroller需要复写computeScroll()方法,才能移动。

public void finishRefresh() {
    if (mPullRefreshing == true) {
        mPullRefreshing = false;
        resetHeaderHeight();
        mHeader.stopLoading();
    }
}

private void resetHeaderHeight() {
    int height = mHeader.getShowHeight();
    if (height == 0)
        return;
    if (mPullRefreshing && height <= mHeaderViewHeight) {
        return;
    }
    int finalHeight = 0;
    // 如果当前是刷新中状态,且Header的展示高度要大于Header的真实高度,则滑动列表,完整展示Header,否则隐藏Header
    if (mPullRefreshing && height > mHeaderViewHeight) {
        finalHeight = mHeaderViewHeight;
    }
    mScrollBack = SCROLL_BACK_HEADER;
    mScroller.startScroll(0, height, 0, finalHeight - height,
            SCROLL_DURATION);
    invalidate();
}

/**
 * 使用了Scroller, 需要复写该方法
 */
@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
        if (mScrollBack == SCROLL_BACK_HEADER) {
            mHeader.setShowHeight(mScroller.getCurrY());
        }
        postInvalidate();
    }
    super.computeScroll();
}

5.2 第三个例子最终实现效果

最终效果如下图:

图-4 第三个例子效果图

5.3 第三个例子总结

相比前两种实现方法,第三种是最简单最方便的实现方式,而且完全不用考虑刷新中状态的点击事件处理,唯一的缺点可能要对某些手机做一些适配,个人比较推荐。

6. 上拉加载

这三个例子中,后面两个例子都实现了上拉加载更多,而在市面上大部分应用没有上拉加载,看得出在实际场景中,上拉加载更多的使用频率不高。以大量使用列表的应用新浪微博为例子,滑动到列表最下方,继续向上拉时候不会像下拉刷新一样,有一定拉伸的弹簧效果,而是直接在加载了。我猜测这种实现是在Adapter中,当滑动到最后一个Item时候,直接返回一个加载中的View,同时请求数据,当用户看见这个View时候,其实请求已经发出去了(部分应用是设置一个按钮,然用户手动点击请求数据)。

7. 总结

下拉刷新开源库很多,上面列举出的几种实现可能不是最优的,个人认为最好的下拉刷新库是这个下拉刷新库,它基本支持所有的布局。但是在选择使用哪一个开源库的时候,并不是实现的最全最好的那个,而是最贴合实际业务的库。

在做下拉刷新时候,因为没有去对这个功能做出具体分析,走了很多弯路,浪费很多时间,以此为戒。

最后附上三个例子工程地址:https://github.com/Kyogirante/PtrDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容