PullToRefreshListView 源码学习

在很多项目中,都使用过PullToRefreshListView,终于在不忙的时候,看了一下PullToRefreshListView的源码。

PullToRefreshBase

这个类是PullToRefreshListView的一个核心的类,该类是一个泛型抽象类,ListView,GridView,ScrollView等都需要继承它。PullToRefreshBase本质上是一个LinearLayout。

public abstract class PullToRefreshBase<T extends View> extends LinearLayout implements IPullToRefresh<T>

PullToRefreshBase实际上包含了三个View,一个是头部要下拉刷新时候的mHeaderLayout,一个是要加载更多的mFooterLayout,还有一个就是mRefreshableView,这个是泛型,具体是ListView,还是GridView,获取其他View,需要看泛型T。

private int mTouchSlop;
    private float mLastMotionX, mLastMotionY;
    private float mInitialMotionX, mInitialMotionY;

    private boolean mIsBeingDragged = false;
    private State mState = State.RESET;
    private Mode mMode = Mode.getDefault();

    private Mode mCurrentMode;
    T mRefreshableView;
    private FrameLayout mRefreshableViewWrapper;

    private boolean mShowViewWhileRefreshing = true;
    private boolean mScrollingWhileRefreshingEnabled = false;
    private boolean mFilterTouchEvents = true;
    private boolean mOverScrollEnabled = true;
    private boolean mLayoutVisibilityChangesEnabled = true;

    private Interpolator mScrollAnimationInterpolator;
    private AnimationStyle mLoadingAnimationStyle = AnimationStyle.getDefault();

    private LoadingLayout mHeaderLayout;
    private LoadingLayout mFooterLayout;

    private OnRefreshListener<T> mOnRefreshListener;
    private OnRefreshListener2<T> mOnRefreshListener2;
    private OnPullEventListener<T> mOnPullEventListener;

    private SmoothScrollRunnable mCurrentSmoothScrollRunnable;

代码中,其实就是下面这三个View

T mRefreshableView;
...
private LoadingLayout mHeaderLayout;
private LoadingLayout mFooterLayout;

按着正常的流程,我们看源码一般先要看它的构造方法,这个也不例外。PullToRefreshBase的构造方法是这样的:

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

    public PullToRefreshBase(Context context, Mode mode) {
        super(context);
        mMode = mode;
        init(context, null);
    }

    public PullToRefreshBase(Context context, Mode mode, AnimationStyle animStyle) {
        super(context);
        mMode = mode;
        mLoadingAnimationStyle = animStyle;
        init(context, null);
    }

到此,我们知道,init就是构造方法的核心代码。

分析init()方法

进入init方法之后,会发现,它首先会判断,是上下拉刷新,还是左右拉刷新,左右拉的实现是PullToRefreshHorizontalScrollView,我们这里仅分析上下拉,它们的原理都是一样的。
init的完整代码:

@SuppressWarnings("deprecation")
    private void init(Context context, AttributeSet attrs) {
        switch (getPullToRefreshScrollDirection()) {
            case HORIZONTAL:
                setOrientation(LinearLayout.HORIZONTAL);
                break;
            case VERTICAL:
            default:
                setOrientation(LinearLayout.VERTICAL);
                break;
        }

        setGravity(Gravity.CENTER);

        ViewConfiguration config = ViewConfiguration.get(context);
        mTouchSlop = config.getScaledTouchSlop();

        // Styleables from XML
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PullToRefresh);

        if (a.hasValue(R.styleable.PullToRefresh_ptrMode)) {
            mMode = Mode.mapIntToValue(a.getInteger(R.styleable.PullToRefresh_ptrMode, 0));
        }

        if (a.hasValue(R.styleable.PullToRefresh_ptrAnimationStyle)) {
            mLoadingAnimationStyle = AnimationStyle.mapIntToValue(a.getInteger(
                    R.styleable.PullToRefresh_ptrAnimationStyle, 0));
        }

        // Refreshable View
        // By passing the attrs, we can add ListView/GridView params via XML
        mRefreshableView = createRefreshableView(context, attrs);
        addRefreshableView(context, mRefreshableView);

        // We need to create now layouts now
        mHeaderLayout = createLoadingLayout(context, Mode.PULL_FROM_START, a);
        mFooterLayout = createLoadingLayout(context, Mode.PULL_FROM_END, a);

        /** * Styleables from XML */
        if (a.hasValue(R.styleable.PullToRefresh_ptrRefreshableViewBackground)) {
            Drawable background = a.getDrawable(R.styleable.PullToRefresh_ptrRefreshableViewBackground);
            if (null != background) {
                mRefreshableView.setBackgroundDrawable(background);
            }
        } else if (a.hasValue(R.styleable.PullToRefresh_ptrAdapterViewBackground)) {
            Utils.warnDeprecation("ptrAdapterViewBackground", "ptrRefreshableViewBackground");
            Drawable background = a.getDrawable(R.styleable.PullToRefresh_ptrAdapterViewBackground);
            if (null != background) {
                mRefreshableView.setBackgroundDrawable(background);
            }
        }

        if (a.hasValue(R.styleable.PullToRefresh_ptrOverScroll)) {
            mOverScrollEnabled = a.getBoolean(R.styleable.PullToRefresh_ptrOverScroll, true);
        }

        if (a.hasValue(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled)) {
            mScrollingWhileRefreshingEnabled = a.getBoolean(
                    R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled, false);
        }

        // Let the derivative classes have a go at handling attributes, then
        // recycle them...
        handleStyledAttributes(a);
        a.recycle();

        // Finally update the UI for the modes
        updateUIForMode();
    }

进入init方法的时候可以发现,它会先设置一下布局的方向,是上下布局,还是左右布局,通过getPullToRefreshScrollDirection()来判断

switch (getPullToRefreshScrollDirection()) {
            case HORIZONTAL:
                setOrientation(LinearLayout.HORIZONTAL);
                break;
            case VERTICAL:
            default:
                setOrientation(LinearLayout.VERTICAL);
                break;
        }

然后,就开始给三个View,创建对象并添加到PullToRefreshBase里面

// Refreshable View
        // By passing the attrs, we can add ListView/GridView params via XML
        mRefreshableView = createRefreshableView(context, attrs);
        addRefreshableView(context, mRefreshableView);

        // We need to create now layouts now
        mHeaderLayout = createLoadingLayout(context, Mode.PULL_FROM_START, a);
        mFooterLayout = createLoadingLayout(context, Mode.PULL_FROM_END, a);

createRefreshableView是一个抽象方法,它需要实现它的子类来提供T这个对象,比如ListView实现了PullToRefreshBase,他们就由ListView来提供对象。这里,ListView就会返回一个ListView对象,于是,这个泛型T,就是ListView。

createLoadingLayout负责创建mHeaderLayoutmFooterLayout

在init方法的最后面,有一个方法叫updateUIForMode(),这里面是隐藏mHeaderLayoutmFooterLayout进入方法updateUIForMode(),代码如下,不重要的省略一下。

...
// Hide Loading Views
refreshLoadingViewsSize();
...

其中,最核心的是refreshLoadingViewsSize()于是,我们进入refreshLoadingViewsSize,看一下它是怎么隐藏头部和尾部的。

/** * Re-measure the Loading Views height, and adjust internal padding as * necessary */
    protected final void refreshLoadingViewsSize() {
        final int maximumPullScroll = (int) (getMaximumPullScroll() * 1.2f);

        int pLeft = getPaddingLeft();
        int pTop = getPaddingTop();
        int pRight = getPaddingRight();
        int pBottom = getPaddingBottom();

        switch (getPullToRefreshScrollDirection()) {
            case HORIZONTAL:
                if (mMode.showHeaderLoadingLayout()) {
                    mHeaderLayout.setWidth(maximumPullScroll);
                    pLeft = -maximumPullScroll;
                } else {
                    pLeft = 0;
                }

                if (mMode.showFooterLoadingLayout()) {
                    mFooterLayout.setWidth(maximumPullScroll);
                    pRight = -maximumPullScroll;
                } else {
                    pRight = 0;
                }
                break;

            case VERTICAL:
                if (mMode.showHeaderLoadingLayout()) {
                    mHeaderLayout.setHeight(maximumPullScroll);
                    pTop = -maximumPullScroll;
                } else {
                    pTop = 0;
                }

                if (mMode.showFooterLoadingLayout()) {
                    mFooterLayout.setHeight(maximumPullScroll);
                    pBottom = -maximumPullScroll;
                } else {
                    pBottom = 0;
                }
                break;
        }

        if (DEBUG) {
            Log.d(LOG_TAG, String.format("Setting Padding. L: %d, T: %d, R: %d, B: %d", pLeft, pTop, pRight, pBottom));
        }
        setPadding(pLeft, pTop, pRight, pBottom);
    }

原来,它会根据方向,首先给头部和尾部设置宽高。然后调用setPadding(pLeft, pTop, pRight, pBottom)来隐藏头部和尾部。

接下来,查看PullToRefreshBase在手放到手机屏幕滑动的时候,它做了什么事情?我们知道,滑动事件,会在onTouchEvent里面去做处理,但是,要知道,PullToRefreshBase是一个LinearLayout,而该布局里面的三个View中,有一个是ListView(以下泛型T,都以ListView为例子),而ListView它也有onTouchEvent事件,那么PullToRefreshBase事件与ListView的事件,就发生了冲突。

PullToRefreshBase的事件分发

View的事件分发会从最外层的ViewGroup往最内层的View来分发,会走dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent,如果ViewGroup在onInterceptTouchEvent 返回true,则表示要拦截事件,那么被拦截的时间就由当前的ViewGroup的onTouchEvent进行处理。PullToRefreshBase 是一个LinearLayout,LinearLayout也是一个ViewGroup。PullToRefreshBase是在onInterceptTouchEvent进行事件拦截

@Override
    public final boolean onInterceptTouchEvent(MotionEvent event) {

        if (!isPullToRefreshEnabled()) {
            return false;
        }

        final int action = event.getAction();

        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            mIsBeingDragged = false;
            return false;
        }

        if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) {
            return true;
        }

        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                // If we're refreshing, and the flag is set. Eat all MOVE events
                if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
                    return true;
                }

                if (isReadyForPull()) {
                    final float y = event.getY(), x = event.getX();
                    final float diff, oppositeDiff, absDiff;

                    // We need to use the correct values, based on scroll
                    // direction
                    switch (getPullToRefreshScrollDirection()) {
                        case HORIZONTAL:
                            diff = x - mLastMotionX;
                            oppositeDiff = y - mLastMotionY;
                            break;
                        case VERTICAL:
                        default:
                            diff = y - mLastMotionY;
                            oppositeDiff = x - mLastMotionX;
                            break;
                    }
                    absDiff = Math.abs(diff);

                    if (absDiff > mTouchSlop && (!mFilterTouchEvents || absDiff > Math.abs(oppositeDiff))) {
                        if (mMode.showHeaderLoadingLayout() && diff >= 1f && isReadyForPullStart()) {
                            mLastMotionY = y;
                            mLastMotionX = x;
                            mIsBeingDragged = true;
                            if (mMode == Mode.BOTH) {
                                mCurrentMode = Mode.PULL_FROM_START;
                            }
                        } else if (mMode.showFooterLoadingLayout() && diff <= -1f && isReadyForPullEnd()) {
                            mLastMotionY = y;
                            mLastMotionX = x;
                            mIsBeingDragged = true;
                            if (mMode == Mode.BOTH) {
                                mCurrentMode = Mode.PULL_FROM_END;
                            }
                        }
                    }
                }
                break;
            }
            case MotionEvent.ACTION_DOWN: {
                if (isReadyForPull()) {
                    mLastMotionY = mInitialMotionY = event.getY();
                    mLastMotionX = mInitialMotionX = event.getX();
                    mIsBeingDragged = false;
                }
                break;
            }
        }

        return mIsBeingDragged;
    }

在代码中有一个方法,叫isReadyForPull(),在MotionEvent.ACTION_MOVE的时候,如果这个方法返回true,mIsBeingDragged也就返回true,于是,就拦截了事件,不把事件分发到ListView里面去,从而使PullToRefreshBaseonTouch来处理改事件,PullToRefreshBaseonTouch就来对下拉或者上拉滑动事件进行处理。

isReadyForPull()如何判断是要准备下拉或者上拉?

点进去查看isReadyForPull()做了什么?,

private boolean isReadyForPull() {
        switch (mMode) {
            case PULL_FROM_START:
                return isReadyForPullStart();
            case PULL_FROM_END:
                return isReadyForPullEnd();
            case BOTH:
                return isReadyForPullEnd() || isReadyForPullStart();
            default:
                return false;
        }
    }

发现,isReadyForPull会根据Mode来判断是上拉,还是下拉,如果是下拉。就调用isReadyForPullStart()来判断,如果是上拉就调用isReadyForPullEnd(),如果Mode是BOTH就两者都调用。

寻根问底,isReadyForPullStart()和isReadyForPullEnd()又是什么?

点进去代码可以发现,它们都是抽象方法:

/**
     * Implemented by derived class to return whether the View is in a state
     * where the user can Pull to Refresh by scrolling from the end.
     * 
     * @return true if the View is currently in the correct state (for example,
     *         bottom of a ListView)
     */
    protected abstract boolean isReadyForPullEnd();

    /**
     * Implemented by derived class to return whether the View is in a state
     * where the user can Pull to Refresh by scrolling from the start.
     * 
     * @return true if the View is currently the correct state (for example, top
     *         of a ListView)
     */
    protected abstract boolean isReadyForPullStart();

抽象方法的实现,肯定就是PullToRefreshBase中三个View的那个泛型T,也就是ListView来实现它。而这个ListView就是PullToRefreshListView,也就是我们经常使用的这个View,但是这个ListView还继承了PullToRefreshAdapterViewBase,真正的实现在PullToRefreshAdapterViewBase

PullToRefreshAdapterViewBase实现的抽象方法

protected boolean isReadyForPullStart() {
        return isFirstItemVisible();
    }

    protected boolean isReadyForPullEnd() {
        return isLastItemVisible();
    }
private boolean isFirstItemVisible() {
        final Adapter adapter = mRefreshableView.getAdapter();

        if (null == adapter || adapter.isEmpty()) {
            if (DEBUG) {
                Log.d(LOG_TAG, "isFirstItemVisible. Empty View.");
            }
            return true;

        } else {

            /** * This check should really just be: * mRefreshableView.getFirstVisiblePosition() == 0, but PtRListView * internally use a HeaderView which messes the positions up. For * now we'll just add one to account for it and rely on the inner * condition which checks getTop(). */
            if (mRefreshableView.getFirstVisiblePosition() <= 1) {
                final View firstVisibleChild = mRefreshableView.getChildAt(0);
                if (firstVisibleChild != null) {
                    return firstVisibleChild.getTop() >= mRefreshableView.getTop();
                }
            }
        }

        return false;
    }

isFirstItemVisible() 可以看到,它通过if (mRefreshableView.getFirstVisiblePosition() <= 1)来判断,当前是不是显示ListView的第一个Item。

private boolean isLastItemVisible() {
        final Adapter adapter = mRefreshableView.getAdapter();

        if (null == adapter || adapter.isEmpty()) {
            if (DEBUG) {
                Log.d(LOG_TAG, "isLastItemVisible. Empty View.");
            }
            return true;
        } else {
            final int lastItemPosition = mRefreshableView.getCount() - 1;
            final int lastVisiblePosition = mRefreshableView.getLastVisiblePosition();

            if (DEBUG) {
                Log.d(LOG_TAG, "isLastItemVisible. Last Item Position: " + lastItemPosition + " Last Visible Pos: "
                        + lastVisiblePosition);
            }

            /** * This check should really just be: lastVisiblePosition == * lastItemPosition, but PtRListView internally uses a FooterView * which messes the positions up. For me we'll just subtract one to * account for it and rely on the inner condition which checks * getBottom(). */
            if (lastVisiblePosition >= lastItemPosition - 1) {
                final int childIndex = lastVisiblePosition - mRefreshableView.getFirstVisiblePosition();
                final View lastVisibleChild = mRefreshableView.getChildAt(childIndex);
                if (lastVisibleChild != null) {
                    return lastVisibleChild.getBottom() <= mRefreshableView.getBottom();
                }
            }
        }

        return false;
    }

通过isLastItemVisible()通过if (lastVisiblePosition >= lastItemPosition - 1)来判断,当前显示的是不是ListView的最后一个Item

由此,我们知道了,如果是当前显示的第一个Item是ListView的第一个item,那么,就拦截ListView的往下拉的事件,使下拉事件让PullToRefreshBase来处理。而ListView,只做上拉的滚动。

如果当前显示的是ListView的最后一个Item,那么,就拦截ListView的上拉事件,让上拉事件给PullToRefreshBase来处理。而ListView只做下拉的滚动。

此时,我们知道了ListView也就是PullToRefreshListViewPullToRefreshBase对事件拦截和分发的处理过程。那么,剩下的就是分析PullToRefreshBaseonTouch事件是怎么处理下拉和上拉的?

PullToRefreshBase的onTouch事件

返回PullToRefreshBase的onTouch事件,看一下它是如何进行滑动操作?

switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE: {
                if (mIsBeingDragged) {
                    mLastMotionY = event.getY();
                    mLastMotionX = event.getX();
                    pullEvent();
                    return true;
                }
                break;
            }

            case MotionEvent.ACTION_DOWN: {
                if (isReadyForPull()) {
                    mLastMotionY = mInitialMotionY = event.getY();
                    mLastMotionX = mInitialMotionX = event.getX();
                    return true;
                }
                break;
            }

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP: {
                if (mIsBeingDragged) {
                    mIsBeingDragged = false;

                    if (mState == State.RELEASE_TO_REFRESH
                            && (null != mOnRefreshListener || null != mOnRefreshListener2)) {
                        setState(State.REFRESHING, true);
                        return true;
                    }

                    // If we're already refreshing, just scroll back to the top
                    if (isRefreshing()) {
                        smoothScrollTo(0);
                        return true;
                    }

                    // If we haven't returned by here, then we're not in a state
                    // to pull, so just reset
                    setState(State.RESET);

                    return true;
                }
                break;
            }
        }

在代码中,当手指按下的时候,代码处理MotionEvent.ACTION_DOWN事件,并用mInitialMotionY记录当前按下的位置坐标。

当手机滑动的时候,代码走到MotionEvent.ACTION_MOVE
MotionEvent.ACTION_MOVE中调用了,pullEvent();,进去分析pullEvent();:

/** * Actions a Pull Event * * @return true if the Event has been handled, false if there has been no * change */
    private void pullEvent() {
        final int newScrollValue;
        final int itemDimension;
        final float initialMotionValue, lastMotionValue;

        switch (getPullToRefreshScrollDirection()) {
            case HORIZONTAL:
                initialMotionValue = mInitialMotionX;
                lastMotionValue = mLastMotionX;
                break;
            case VERTICAL:
            default:
                initialMotionValue = mInitialMotionY;
                lastMotionValue = mLastMotionY;
                break;
        }

        switch (mCurrentMode) {
            case PULL_FROM_END:
                newScrollValue = Math.round(Math.max(initialMotionValue - lastMotionValue, 0) / FRICTION);
                itemDimension = getFooterSize();
                break;
            case PULL_FROM_START:
            default:
                newScrollValue = Math.round(Math.min(initialMotionValue - lastMotionValue, 0) / FRICTION);
                itemDimension = getHeaderSize();
                break;
        }
        setHeaderScroll(newScrollValue);

        if (newScrollValue != 0 && !isRefreshing()) {
            float scale = Math.abs(newScrollValue) / (float) itemDimension;
            switch (mCurrentMode) {
                case PULL_FROM_END:
                    mFooterLayout.onPull(scale);
                    break;
                case PULL_FROM_START:
                default:
                    mHeaderLayout.onPull(scale);
                    break;
            }

            if (mState != State.PULL_TO_REFRESH && itemDimension >= Math.abs(newScrollValue)) {
                setState(State.PULL_TO_REFRESH);
            } else if (mState == State.PULL_TO_REFRESH && itemDimension < Math.abs(newScrollValue)) {
                setState(State.RELEASE_TO_REFRESH);
            }
        }
    }

然后,发现,它通过Math.round计算滑动的偏移值。并最终把这个值传递到setHeaderScroll(newScrollValue);修改mHeaderLayout或者mFooterLayout的位置。

/** * Helper method which just calls scrollTo() in the correct scrolling * direction. * * @param value - New Scroll value */
    protected final void setHeaderScroll(int value) {
        if (DEBUG) {
            Log.d(LOG_TAG, "setHeaderScroll: " + value);
        }
        // Clamp value to with pull scroll range
        final int maximumPullScroll = getMaximumPullScroll();
        value = Math.min(maximumPullScroll, Math.max(-maximumPullScroll, value));

        if (mLayoutVisibilityChangesEnabled) {
            if (value < 0) {
                mHeaderLayout.setVisibility(View.VISIBLE);
            } else if (value > 0) {
                mFooterLayout.setVisibility(View.VISIBLE);
            } else {
                mHeaderLayout.setVisibility(View.INVISIBLE);
                mFooterLayout.setVisibility(View.INVISIBLE);
            }
        }

        if (USE_HW_LAYERS) {
            /** * Use a Hardware Layer on the Refreshable View if we've scrolled at * all. We don't use them on the Header/Footer Views as they change * often, which would negate any HW layer performance boost. */
            ViewCompat.setLayerType(mRefreshableViewWrapper, value != 0 ? View.LAYER_TYPE_HARDWARE
                    : View.LAYER_TYPE_NONE);
        }

        switch (getPullToRefreshScrollDirection()) {
            case VERTICAL:
                scrollTo(0, value);
                break;
            case HORIZONTAL:
                scrollTo(value, 0);
                break;
        }
    }

代码中,它最终是通过调用scrollTo();这个方法来改变位置,当手机滑动的时候,scrollTo();一直被调用着。

刷新或者加载完成之后的隐藏

当松开手,就到了onTouch事件的* MotionEvent.ACTION_UP*事件,改事件处理,是调用了如下代码:

// If we're already refreshing, just scroll back to the top
                    if (isRefreshing()) {
                        smoothScrollTo(0);
                        return true;
                    }

smoothScrollTo(0);是把头部mHeaderLayout,和尾部的mFooterLayout进行隐藏。

if (oldScrollValue != newScrollValue) {
            if (null == mScrollAnimationInterpolator) {
                // Default interpolator is a Decelerate Interpolator
                mScrollAnimationInterpolator = new DecelerateInterpolator();
            }
            mCurrentSmoothScrollRunnable = new SmoothScrollRunnable(oldScrollValue, newScrollValue, duration, listener);

            if (delayMillis > 0) {
                postDelayed(mCurrentSmoothScrollRunnable, delayMillis);
            } else {
                post(mCurrentSmoothScrollRunnable);
            }
        }

这里可以看到,使用了插值器DecelerateInterpolator,这个插值器 在动画开始的地方快然后慢,于是就得到一种弹簧的效果。
并且调用postDelayed(mCurrentSmoothScrollRunnable, delayMillis);像个定时器一样,直到mHeaderLayoutmFooterLayout隐藏完毕。

SmoothScrollRunnable内部代码:

    @Override
        public void run() {

            /** * Only set mStartTime if this is the first time we're starting, * else actually calculate the Y delta */
            if (mStartTime == -1) {
                mStartTime = System.currentTimeMillis();
            } else {

                /** * We do do all calculations in long to reduce software float * calculations. We use 1000 as it gives us good accuracy and * small rounding errors */
                long normalizedTime = (1000 * (System.currentTimeMillis() - mStartTime)) / mDuration;
                normalizedTime = Math.max(Math.min(normalizedTime, 1000), 0);

                final int deltaY = Math.round((mScrollFromY - mScrollToY)
                        * mInterpolator.getInterpolation(normalizedTime / 1000f));
                mCurrentY = mScrollFromY - deltaY;
                setHeaderScroll(mCurrentY);
            }

            // If we're not at the target Y, keep going...
            if (mContinueRunning && mScrollToY != mCurrentY) {
                ViewCompat.postOnAnimation(PullToRefreshBase.this, this);
            } else {
                if (null != mListener) {
                    mListener.onSmoothScrollFinished();
                }
            }
        }

最终它是调用setHeaderScroll(),而setHeaderScroll内部还是调用scrollTo来进行隐藏。

PullToRefreshListView 分析完毕!

你可能感兴趣的:(PullToRefreshListView 源码学习)