简介
看看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里,让他支持下拉刷新。