PullToRefresh源码阅读

简介

看看github上著名的下拉刷新的源码,先跑一下demo,截几张图,看看效果







代码分析

从最简单的PullToRefreshListActivity看起,里面最主要的是PullToRefreshListView,PullToRefreshListView继承自PullToRefreshListView,而PullToRefreshAdapterViewBase继承自PullToRefreshBase,PullToRefreshBase继承自LinearLayout

PullToRefreshListView

PullToRefreshAdapterViewBase<ListView>

PullToRefreshBase<T>

LinearLayout 

主要就看PullToRefreshBase这个类。实际上下拉头一直放在listview的上边,只不过一开始的时候设置了一个paddingTop=-height,也就是说把整个上拉头,放在了listview 的上边,在下拉过程2中,调用scrollto的方法,把整个view下移,这样就导致了,下拉头一点点出来。

PullToRefreshBase类内部主要看onInterceptTouchEvent和onTouchEvent这个方法。

先看onInterceptTouchEvent,

	public final boolean onInterceptTouchEvent(MotionEvent event) {
        LogUtil.d("onInterceptTouchEvent: "+mHeaderLayout.getContentSize()+"event:"+event.getAction());


//        LogUtil.d("onInterceptTouchEvent"+event.getAction());
        //mScrollingWhileRefreshingEnabled一般为true
        //isPullToRefreshEnabled一般都true
		if (!isPullToRefreshEnabled()) {
			return false;
		}

		final int action = event.getAction();

		if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            //手放开,就变为false
			mIsBeingDragged = false;
			return false;
		}

		if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) {
            LogUtil.d("直接返回true,阻止touch事件传递给listview");
			return true;
		}

		switch (action) {
			case MotionEvent.ACTION_MOVE: {
				// If we're refreshing, and the flag is set. Eat all MOVE events
				if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
                    //TODO 发生时机
                    LogUtil.d("中途返回true,很少发生");
					return true;
				}

                //比如,第一项显示出来了isFirstItemVisible,要拉 下拉头了
				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);
                    LogUtil.d("move event"+" mIsBeingDragged:"+mIsBeingDragged);
					if (absDiff > mTouchSlop && (!mFilterTouchEvents || absDiff > Math.abs(oppositeDiff))) {
						if (mMode.showHeaderLoadingLayout() && diff >= 1f && isReadyForPullStart()) {
                            LogUtil.d("y:"+y+" x:"+x+" mtouchslop:"+mTouchSlop);
                            //下拉,多次move事件会导致进入此处
							mLastMotionY = y;
							mLastMotionX = x;
							mIsBeingDragged = true;
                            LogUtil.d("mIsBeingDragged变为true"+" mLastMotionX:"+mLastMotionX+" mLastMotionY"+mLastMotionY);
							if (mMode == Mode.BOTH) {
								mCurrentMode = Mode.PULL_FROM_START;
							}
						} else if (mMode.showFooterLoadingLayout() && diff <= -1f && isReadyForPullEnd()) {

                            LogUtil.d("2");
							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;
			}
		}
//        LogUtil.d("返回mIsBeingDragged"+mIsBeingDragged);
		return mIsBeingDragged;
	}

首先L8 isPullToRefreshEnabled一般为true不进去,isPullToRefreshEnabled表示是否允许下拉刷新。鼠标按下产生down事件,此时mIsBeingDragged为false,进入L80,而isReadyForPull调用PullToRefreshAdapterViewBase::isFirstItemVisible,意思是如果listview第一项显示出来,那此次事件就有可能导致下拉头一点点出来。如果listview的第一项都没显示出来,那肯定拉listview,很好理解。如果listview首项已经显示出来了,就记下down事件的位置,记录在mLastMotionY和mLastMotionX内,onInterceptTouchEvent返回false。下一次事件就是move事件,走到L28,if一般不进去,暂时不看。isReadyForPull前面分析过了,此时为true,进去,走到L54,absDiff表示纵向移动的距离,如果移动的距离超过了mTouchSlop这个阈值,我们就认为这是一次有效的下拉。会走到L58,更新mLastMotionY和mLastMotionX。一般多次move事件才会触发一次有效下拉。然后把mIsBeingDragged置为true,表示正在被下拉,onInterceptTouchEvent返回true。此时onInterceptTouchEvent返回true,会有什么意义呢?首先此次move事件被截获,不在下传,进入本类的onTouchEvent,其次,后续的move和up事件不再传入onInterceptTouchEvent,而是直接传入onTouchEvent。

   public final boolean onTouchEvent(MotionEvent event) {

       //LogUtil.d("onTouchEvent");

        //isPullToRefreshEnabled一般返回true,只有在DISABLED和 MANUAL_REFRESH_ONLY的时候返回false
        if (!isPullToRefreshEnabled()) {
            return false;
        }

        //TODO mScrollingWhileRefreshingEnabled和isRefreshing
        // If we're refreshing, and the flag is set. Eat the event
        if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
            return true;
        }

        //LogUtil.d(""+event.getAction()+" onTouchEvent");
        if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0) {
            return false;
        }

        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE: {
                if (mIsBeingDragged) {
                    LogUtil.d("ACTION_MOVE的ontouchevent" + " mLastMotionX:" + mLastMotionX + " mLastMotionY" + mLastMotionY);
                    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;
            }
        }

        return false;
    }

 
 

也就是说触发“有效下拉”的这次move事件会进入onTouchEvent(1),后续的所有move事件都进入onTouchEvent(2),up事件也进入onTouchEvent(3)。

先看(1),有效下拉的move事件进入onTouchEvent之后,进入L25,记录mLastMotionY和mLastMotionX,并触发pullEvent,并且返回true。这里记录的mLastMotionY和mLastMotionX值,实际上和onInterceptTouchEvent的L58完全一样。这里的核心是pullEvent,后面看看。

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;


                LogUtil.d("initialMotionValue="+initialMotionValue+"  lastMotionValue"+lastMotionValue);
				break;
		}


		switch (mCurrentMode) {
			case PULL_FROM_END:
				newScrollValue = Math.round(Math.max(initialMotionValue - lastMotionValue, 0) / FRICTION);
				itemDimension = getFooterSize();
				break;
			case PULL_FROM_START:
			default:
                //TODO FRICTION
				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);
			}
		}
	}

走到L14,初始化initialMotionValue和lastMotionValue,分别表示鼠标按下时候的位置和有效下拉move的位置,2个一减就是下拉的距离,把下拉的距离除以2,作为我们要下拉的距离,这样会显得比较有摩擦力,有拉的感觉,当然你也可以设置为除以3,除以4,都无所谓。L29算出newScrollValue,就是我们想要下拉的距离,注意是负的。后面进入setHeaderScroll,最后调用onPull和setState,onPull是用来实现下拉的时候旋转的,setState是状态改变,都无关紧要,关键是setHeaderScroll,后面再看看setHeaderScroll

代码

value = Math.min(maximumPullScroll,Math.max(-maximumPullScroll, value));

这句话根据最大可下拉距离和当前下拉距离中挑一个大的(他们都是负的,大的反而是下拉的短的),再从这个值和maximumPullScroll挑一个小的,得到的值就是真正要下拉的距离。使用scrollTo(0, value);方法来实现整个布局下移,这样本来隐藏在上面的下拉头就一点点出来了。我记了一组  setHeaderScroll的开始时的value信息如下

setHeaderScroll: -28

setHeaderScroll: -60

setHeaderScroll: -71

setHeaderScroll: -82

setHeaderScroll: -92

setHeaderScroll: -101

 

什么时候,文案开始变化,由“下拉刷新”变成“放开以刷新”,看L49,如果newScrollValue小于头的高度的话就是PULL_TO_REFRESH状态,即显示“下拉刷新”,如果newScrollValue大于头的高度的话就是RELEASE_TO_REFRESH状态,即显示“放开以刷新”

其他

1、addViewInternal和addview

PullToRefreshBase内有2个函数addViewInternal和addview,分别表示什么?

   

protected final void addViewInternal(Viewchild, int index, ViewGroup.LayoutParams params) {

       super.addView(child, index, params);

}

addViewInternal内的super是LinearLayout来addview,而addview是T的addview,对我们这里就是ListView的addview

 

2、PullToRefreshBase内LoadingLayoutProxy用处

LoadingLayoutProxy实现了ILoadingLayout,定义了load 过程中一些列约定,比如什么时候显示什么图案,显示什么文字,相当于一种协议

 

3、init的时候,调用refreshLoadingViewsSize把头的高度设置为0

onSizeChanged的时候,又调用了refreshLoadingViewsSize,把头高度设置为934


4、上拉的时候怎么显示“End oflist”

首先要知道,下拉到底了,怎么判断拉到底了呢

mLastItemVisible = (totalItemCount > 0)&& (firstVisibleItem + visibleItemCount >= totalItemCount - 1);

滚动的时候如果滚到了末尾会调PullToRefreshAdapterViewBase内的

mOnLastItemVisibleListener.onLastItemVisible();

 

5、load代码在哪里?什么时候开始真正load

手放开的时候,调setState(State.REFRESHING, true);,在这里刷新,会调到PullToRefreshListActivity里的newGetDataTask().execute();在这里执行异步任务。看GetDataTask的onPostExecute,可以看到mListItems.addFirst("Added after refresh...");和实际效果一致。

 

6、菜单里有个demo,点击一下会出现下拉展示的效果,怎么实现的?

看mPullRefreshListView.demo();会调smoothScrollToAndBack,在里面post(mCurrentSmoothScrollRunnable);,看SmoothScrollRunnable内的run,主要掉setHeaderScroll(mCurrentY);然后调scrollTo(0,value);来实现

7、内部有个listview,他不是普通的listview是InternalListView,实现了EmptyViewMethodAccessor接口,

看PullToRefreshAdapterViewBase的setEmptyView,我们把newEmptyView从他的parent手上夺过来,放到refreshableViewWrapper里面去,本来listview没数据的时候显示newEmptyView,现在我们把这个newEmptyView放到了一个FrameLayout里,让他支持下拉刷新。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值