PullToRefreshListView 源码学习
2016-03-01 15:12
281 查看
在很多项目中,都使用过PullToRefreshListView,终于在不忙的时候,看了一下PullToRefreshListView的源码。
PullToRefreshBase实际上包含了三个View,一个是头部要下拉刷新时候的mHeaderLayout,一个是要加载更多的mFooterLayout,还有一个就是mRefreshableView,这个是泛型,具体是ListView,还是GridView,获取其他View,需要看泛型T。
代码中,其实就是下面这三个View
按着正常的流程,我们看源码一般先要看它的构造方法,这个也不例外。PullToRefreshBase的构造方法是这样的:
到此,我们知道,init就是构造方法的核心代码。
init的完整代码:
进入init方法的时候可以发现,它会先设置一下布局的方向,是上下布局,还是左右布局,通过getPullToRefreshScrollDirection()来判断
然后,就开始给三个View,创建对象并添加到PullToRefreshBase里面
createRefreshableView是一个抽象方法,它需要实现它的子类来提供T这个对象,比如ListView实现了PullToRefreshBase,他们就由ListView来提供对象。这里,ListView就会返回一个ListView对象,于是,这个泛型T,就是ListView。
createLoadingLayout负责创建mHeaderLayout和mFooterLayout
在init方法的最后面,有一个方法叫updateUIForMode(),这里面是隐藏mHeaderLayout和mFooterLayout进入方法updateUIForMode(),代码如下,不重要的省略一下。
其中,最核心的是refreshLoadingViewsSize()于是,我们进入refreshLoadingViewsSize,看一下它是怎么隐藏头部和尾部的。
原来,它会根据方向,首先给头部和尾部设置宽高。然后调用setPadding(pLeft, pTop, pRight, pBottom)来隐藏头部和尾部。
接下来,查看PullToRefreshBase在手放到手机屏幕滑动的时候,它做了什么事情?我们知道,滑动事件,会在onTouchEvent里面去做处理,但是,要知道,PullToRefreshBase是一个LinearLayout,而该布局里面的三个View中,有一个是ListView(以下泛型T,都以ListView为例子),而ListView它也有onTouchEvent事件,那么PullToRefreshBase事件与ListView的事件,就发生了冲突。
在代码中有一个方法,叫isReadyForPull(),在MotionEvent.ACTION_MOVE的时候,如果这个方法返回true,mIsBeingDragged也就返回true,于是,就拦截了事件,不把事件分发到ListView里面去,从而使PullToRefreshBase的onTouch来处理改事件,PullToRefreshBase的onTouch就来对下拉或者上拉滑动事件进行处理。
发现,isReadyForPull会根据Mode来判断是上拉,还是下拉,如果是下拉。就调用isReadyForPullStart()来判断,如果是上拉就调用isReadyForPullEnd(),如果Mode是BOTH就两者都调用。
抽象方法的实现,肯定就是PullToRefreshBase中三个View的那个泛型T,也就是ListView来实现它。而这个ListView就是PullToRefreshListView,也就是我们经常使用的这个View,但是这个ListView还继承了PullToRefreshAdapterViewBase,真正的实现在PullToRefreshAdapterViewBase
从isFirstItemVisible() 可以看到,它通过if (mRefreshableView.getFirstVisiblePosition() <= 1)来判断,当前是不是显示ListView的第一个Item。
通过isLastItemVisible()通过if (lastVisiblePosition >= lastItemPosition - 1)来判断,当前显示的是不是ListView的最后一个Item
由此,我们知道了,如果是当前显示的第一个Item是ListView的第一个item,那么,就拦截ListView的往下拉的事件,使下拉事件让PullToRefreshBase来处理。而ListView,只做上拉的滚动。
如果当前显示的是ListView的最后一个Item,那么,就拦截ListView的上拉事件,让上拉事件给PullToRefreshBase来处理。而ListView只做下拉的滚动。
此时,我们知道了ListView也就是PullToRefreshListView和PullToRefreshBase对事件拦截和分发的处理过程。那么,剩下的就是分析PullToRefreshBase的onTouch事件是怎么处理下拉和上拉的?
在代码中,当手指按下的时候,代码处理MotionEvent.ACTION_DOWN事件,并用mInitialMotionY记录当前按下的位置坐标。
当手机滑动的时候,代码走到MotionEvent.ACTION_MOVE
MotionEvent.ACTION_MOVE中调用了,pullEvent();,进去分析pullEvent();:
然后,发现,它通过Math.round计算滑动的偏移值。并最终把这个值传递到setHeaderScroll(newScrollValue);修改mHeaderLayout或者mFooterLayout的位置。
代码中,它最终是通过调用scrollTo();这个方法来改变位置,当手机滑动的时候,scrollTo();一直被调用着。
smoothScrollTo(0);是把头部mHeaderLayout,和尾部的mFooterLayout进行隐藏。
这里可以看到,使用了插值器DecelerateInterpolator,这个插值器 在动画开始的地方快然后慢,于是就得到一种弹簧的效果。
并且调用postDelayed(mCurrentSmoothScrollRunnable, delayMillis);像个定时器一样,直到mHeaderLayout和mFooterLayout隐藏完毕。
SmoothScrollRunnable内部代码:
最终它是调用setHeaderScroll(),而setHeaderScroll内部还是调用scrollTo来进行隐藏。
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负责创建mHeaderLayout和mFooterLayout
在init方法的最后面,有一个方法叫updateUIForMode(),这里面是隐藏mHeaderLayout和mFooterLayout进入方法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里面去,从而使PullToRefreshBase的onTouch来处理改事件,PullToRefreshBase的onTouch就来对下拉或者上拉滑动事件进行处理。
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也就是PullToRefreshListView和PullToRefreshBase对事件拦截和分发的处理过程。那么,剩下的就是分析PullToRefreshBase的onTouch事件是怎么处理下拉和上拉的?
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);像个定时器一样,直到mHeaderLayout和mFooterLayout隐藏完毕。
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 分析完毕!
相关文章推荐
- AngularJS 数据绑定
- 面向对象设计原则
- [嵌入式开发板]iTOP-4412开发板linux 系统存储空间的修改
- Memcached 工作原理
- iOS 播放音效
- 整数因子分解
- MATLAB 删除元胞数组中空元素
- 命令行获取本机保存的无线密码
- (五)外观模式-代码实现
- ASP.NET MVC URL重写与优化(1)-使用Global路由表定制URL
- mvn多模块开发消除重复依赖造成的打包失败
- AngularJS Select(选择框)
- SQL Server多表多列更新
- Android客户端与Eclipse服务器端的Socket通信
- BP神经网络求解异或问题(Python实现)
- js 获取页面窗口的大小
- 如何决定要使用多少点来做FFT?(转载) (2011
- 傅里叶级数的几何意义 – 巧妙记忆公式的方法
- spring jdbctemplate 用法
- virtualbox安装最新的mirantis openstack8.0