PullToRefreshListview下拉刷新的原理分析
内容区域如何创建和添加
以PullToRefreshListView为例子,这是PullToRefreshBase类中内容区域mRefreshableView的创建部分:
private void init(Context context, AttributeSet attrs) {
...
mRefreshableView = createRefreshableView(context, attrs);
addRefreshableView(context, mRefreshableView);
...
}
createRefreshableView方法本身是抽象的:
/**
* This is implemented by derived classes to return the created View. If you
* need to use a custom View (such as a custom ListView), override this
* method and return an instance of your custom class.
* <p/>
* Be sure to set the ID of the view in this method, especially if you're
* using a ListActivity or ListFragment.
*
* @param context Context to create view with
* @param attrs AttributeSet from wrapped class. Means that anything you
* include in the XML layout declaration will be routed to the
* created View
* @return New instance of the Refreshable View
*/
protected abstract T createRefreshableView(Context context, AttributeSet attrs);
这个抽象方法在PullToRefreshListView的实现如下:
@Override
protected ListView createRefreshableView(Context context, AttributeSet attrs) {
ListView lv = createListView(context, attrs);
// Set it to this so it can be used in ListActivity/ListFragment
lv.setId(android.R.id.list);
return lv;
}
创建完内容区域后,再看添加的代码:
private void addRefreshableView(Context context, T refreshableView) {
mRefreshableViewWrapper = new FrameLayout(context);
mRefreshableViewWrapper.addView(refreshableView, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
addViewInternal(mRefreshableViewWrapper, new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
}
首先是实例化mRefreshableViewWrapper为一个FrameLayout,然后将refreshableView作为子View添加,接着看addViewInternal:
protected final void addViewInternal(View child, ViewGroup.LayoutParams params) {
super.addView(child, -1, params);
}
PullToRefreshBase类是LinearLayout的子类,因此该方法是将mRefreshableViewWrapper添加到线性布局上。
到这一步内容区域创建并添加完成。
Header和Footer布局的创建和添加
回到init方法:
mHeaderLayout = createLoadingLayout(context, Mode.PULL_FROM_START, a);
mFooterLayout = createLoadingLayout(context, Mode.PULL_FROM_END, a);
mHeaderLayout 和mHeaderLayout 分别是头部和底部布局。再看mHeaderLayout :
protected LoadingLayout createLoadingLayout(Context context, Mode mode, TypedArray attrs) {
LoadingLayout layout = mLoadingAnimationStyle.createLoadingLayout(context, mode,
getPullToRefreshScrollDirection(), attrs);
layout.setVisibility(View.INVISIBLE);
return layout;
}
发现布局的类型是LoadingLayout ,由mLoadingAnimationStyle.createLoadingLayout创建:
LoadingLayout createLoadingLayout(Context context, Mode mode, Orientation scrollDirection, TypedArray attrs) {
switch (this) {
case ROTATE:
default:
return new RotateLoadingLayout(context, mode, scrollDirection, attrs);
case FLIP:
return new FlipLoadingLayout(context, mode, scrollDirection, attrs);
}
}
根据AnimationStyle的类型,可以创建翻转和旋转两种布局:
public class RotateLoadingLayout extends LoadingLayout {}
public class FlipLoadingLayout extends LoadingLayout {}
public abstract class LoadingLayout extends FrameLayout implements ILoadingLayout {}
可以看到,它们最终的父类是FrameLayout,由LoadingLayout根据方向选择加载的布局:
switch (scrollDirection) {
case HORIZONTAL:
LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header_horizontal, this);
break;
case VERTICAL:
default:
LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header_vertical, this);
break;
}
在updateUIForMode方法中,Footer和Header分别被添加到垂直线性布局的上部和下部:
protected void updateUIForMode() {
// We need to use the correct LayoutParam values, based on scroll
// direction
final LayoutParams lp = getLoadingLayoutLayoutParams();
// Remove Header, and then add Header Loading View again if needed
if (this == mHeaderLayout.getParent()) {
removeView(mHeaderLayout);
}
if (mMode.showHeaderLoadingLayout()) {
addViewInternal(mHeaderLayout, 0, lp);
}
// Remove Footer, and then add Footer Loading View again if needed
if (this == mFooterLayout.getParent()) {
removeView(mFooterLayout);
}
if (mMode.showFooterLoadingLayout()) {
addViewInternal(mFooterLayout, lp);
}
// Hide Loading Views
refreshLoadingViewsSize();
// If we're not using Mode.BOTH, set mCurrentMode to mMode, otherwise
// set it to pull down
mCurrentMode = (mMode != Mode.BOTH) ? mMode : Mode.PULL_FROM_START;
}
可以看出mMode.showHeaderLoadingLayout()和mMode.showFooterLoadingLayout()是添加首尾的关键。
public boolean showHeaderLoadingLayout() {
return this == PULL_FROM_START || this == BOTH;
}
public boolean showFooterLoadingLayout() {
return this == PULL_FROM_END || this == BOTH || this == MANUAL_REFRESH_ONLY;
}
当Mode这个枚举为PULL_FROM_START将添加首部,PULL_FROM_END添加尾部,BOTH两者都添加。
下面的代码将通过设置padding隐藏Header和Footer
// Hide Loading Views
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);
}
以垂直为例,maximumPullScroll是Header的高度,负的maximumPullScroll是布局的top padding,这样初始状态就正好隐藏了Header。
下拉展示出HEADER
当我们下拉列表,Header布局将被逐渐展示,下面Touch事件分析:
@Override
public final boolean onInterceptTouchEvent(MotionEvent event) {
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;
}
}
}
LogHelper.E("onInterceptTouchEvent ACTION_MOVE mIsBeingDragged "+mIsBeingDragged);
}
break;
}
case MotionEvent.ACTION_DOWN: {
if (isReadyForPull()) {
mLastMotionY = mInitialMotionY = event.getY();
mLastMotionX = mInitialMotionX = event.getX();
mIsBeingDragged = false;
LogHelper.E("onInterceptTouchEvent ACTION_DOWN mIsBeingDragged "+mIsBeingDragged);
}
break;
}
}
return mIsBeingDragged;
}
首先分析onInterceptTouchEvent,该方法最先被执行,当发生ACTION_DOWN,mLastMotionY 、mInitialMotionY 被设置为按下的位置,mIsBeingDragged设置为false。
接着垂直滑动,观察ACTION_MOVE事件,首先diff会被算出,以垂直滑动分析,该值为垂直滑动的距离,oppositeDiff为水平滑动的距离。
absDiff为diff的绝对值,mTouchSlop为判断手指滑动的最小距离,isReadyForPullStart()是判断是否展示Header的关键:
/**
* 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它是一个抽象方法,以PullToRefreshAdapterViewBase的实现为例:
protected boolean isReadyForPullStart() {
return isFirstItemVisible();
}
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;
}
只有当第一项的顶部到达父布局的顶部,isReadyForPullStart才会返回true,mIsBeingDragged被设置为true,mCurrentMode设置为Mode.PULL_FROM_START,mLastMotionY 和mLastMotionX被设置为最新坐标。
onInterceptTouchEvent返回true后,事件不再向下传递,而是交给onTouchEvent处理:
@Override
public final boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE: {
LogHelper.E("onTouchEvent ACTION_MOVE mIsBeingDragged "+mIsBeingDragged);
if (mIsBeingDragged) {
mLastMotionY = event.getY();
mLastMotionX = event.getX();
pullEvent();
return true;
}
break;
}
case MotionEvent.ACTION_DOWN: {
LogHelper.E("onTouchEvent ACTION_DOWN mIsBeingDragged "+mIsBeingDragged);
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;
}
}
}
观察到ACTION_MOVE事件执行了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;
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);
}
}
}
依然以垂直滑动下拉分析,newScrollValue为滚动距离,itemDimension为Header的高度。
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);
}
LogHelper.E("setHeaderScroll: " + value);
switch (getPullToRefreshScrollDirection()) {
case VERTICAL:
scrollTo(0, value);
break;
case HORIZONTAL:
scrollTo(value, 0);
break;
}
}
setHeaderScroll方法通过scrollTo方法将滑动的距离转换为线性布局向上滚动的距离,这样就能够将隐藏的Header逐渐展示出来。
下拉刷新中的状态变化
再回到pullEvent:
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);
}
}
scale是滚动距离的绝对值与Header高度之比,RotateLoadingLayout根据该值旋转图标。
当滑动的距离的绝对值小于Header高度,mState设置为PULL_TO_REFRESH,反之设置为RELEASE_TO_REFRESH:
final void setState(State state, final boolean... params) {
mState = state;
if (DEBUG) {
Log.d(LOG_TAG, "State: " + mState.name());
}
switch (mState) {
case RESET:
onReset();
break;
case PULL_TO_REFRESH:
onPullToRefresh();
break;
case RELEASE_TO_REFRESH:
onReleaseToRefresh();
break;
case REFRESHING:
case MANUAL_REFRESHING:
onRefreshing(params[0]);
break;
case OVERSCROLLING:
// NO-OP
break;
}
// Call OnPullEventListener
if (null != mOnPullEventListener) {
mOnPullEventListener.onPullEvent(this, mState, mCurrentMode);
}
}
protected void onPullToRefresh() {
switch (mCurrentMode) {
case PULL_FROM_END:
mFooterLayout.pullToRefresh();
break;
case PULL_FROM_START:
mHeaderLayout.pullToRefresh();
break;
default:
// NO-OP
break;
}
}
protected void onReleaseToRefresh() {
switch (mCurrentMode) {
case PULL_FROM_END:
mFooterLayout.releaseToRefresh();
break;
case PULL_FROM_START:
mHeaderLayout.releaseToRefresh();
break;
default:
// NO-OP
break;
}
}
若是PULL_TO_REFRESH状态,执行onPullToRefresh,调用Header的pullToRefresh或releaseToRefresh方法,FlipLoadingLayout会执行图标的动画。
若在展示Header中抬起手指,对应ACTION_UP事件:
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;
}
若抬起手指时状态为RELEASE_TO_REFRESH,状态被设置为REFRESHING,否则设置为RESET。
同时调用onRefreshing(true):
protected void onRefreshing(final boolean doScroll) {
if (mMode.showHeaderLoadingLayout()) {
mHeaderLayout.refreshing();
}
if (mMode.showFooterLoadingLayout()) {
mFooterLayout.refreshing();
}
if (doScroll) {
if (mShowViewWhileRefreshing) {
// Call Refresh Listener when the Scroll has finished
OnSmoothScrollFinishedListener listener = new OnSmoothScrollFinishedListener() {
@Override
public void onSmoothScrollFinished() {
callRefreshListener();
}
};
switch (mCurrentMode) {
case MANUAL_REFRESH_ONLY:
case PULL_FROM_END:
smoothScrollTo(getFooterSize(), listener);
break;
default:
case PULL_FROM_START:
smoothScrollTo(-getHeaderSize(), listener);
break;
}
} else {
smoothScrollTo(0);
}
} else {
// We're not scrolling, so just call Refresh Listener now
callRefreshListener();
}
}
当切换到REFRESHING状态下,Header执行refreshing方法,以RotateLoadingLayout为例,将会执行图标的旋转动画。
同时smoothScrollTo(-getHeaderSize(), listener)方法将布局滚动到刚好展示Header,listener对象回调callRefreshListener 方法:
private void callRefreshListener() {
if (null != mOnRefreshListener) {
mOnRefreshListener.onRefresh(this);
} else if (null != mOnRefreshListener2) {
if (mCurrentMode == Mode.PULL_FROM_START) {
mOnRefreshListener2.onPullDownToRefresh(this);
} else if (mCurrentMode == Mode.PULL_FROM_END) {
mOnRefreshListener2.onPullUpToRefresh(this);
}
}
}
该方法调用mOnRefreshListener.onRefresh(this)或者mOnRefreshListener2.onPullDownToRefresh(this),交给业务层处理。之后业务层再调用:
public final void onRefreshComplete() {
if (isRefreshing()) {
setState(State.RESET);
}
}
该方法将mState重置为RESET,并且调用如下方法:
protected void onReset() {
mIsBeingDragged = false;
mLayoutVisibilityChangesEnabled = true;
// Always reset both layouts, just in case...
mHeaderLayout.reset();
mFooterLayout.reset();
smoothScrollTo(0);
}
onReset将布局滚动到初始位置,这样Header再次回到隐藏状态。
至此已经基本分析完毕PullToRefresh的下拉刷新原理,下面再深入一下PullToRefreshListView。
PullToRefreshListView的下拉刷新
在PullToRefreshBase的init方法中调用了handleStyledAttributes方法,我们看一下handleStyledAttributes关于这个空方法的重写:
protected void handleStyledAttributes(TypedArray a) {
super.handleStyledAttributes(a);
mListViewExtrasEnabled = a.getBoolean(R.styleable.PullToRefresh_ptrListViewExtrasEnabled, true);
if (mListViewExtrasEnabled) {
final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL);
// Create Loading Views ready for use later
FrameLayout frame = new FrameLayout(getContext());
mHeaderLoadingView = createLoadingLayout(getContext(), Mode.PULL_FROM_START, a);
mHeaderLoadingView.setVisibility(View.GONE);
frame.addView(mHeaderLoadingView, lp);
mRefreshableView.addHeaderView(frame, null, false);
mLvFooterLoadingFrame = new FrameLayout(getContext());
mFooterLoadingView = createLoadingLayout(getContext(), Mode.PULL_FROM_END, a);
mFooterLoadingView.setVisibility(View.GONE);
mLvFooterLoadingFrame.addView(mFooterLoadingView, lp);
/**
* If the value for Scrolling While Refreshing hasn't been
* explicitly set via XML, enable Scrolling While Refreshing.
*/
if (!a.hasValue(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled)) {
setScrollingWhileRefreshingEnabled(true);
}
}
}
首先ptrListViewExtrasEnabled属性默认为true,因此mHeaderLoadingView和mFooterLoadingView被创建,并且默认为GONE。注意这两个布局最后被嵌套在两个FrameLayout中,并且作为ListView的首尾布局。
那么问题来了,为何在PullToRefreshBase中已有Header和Footer,在ListView中还要重复设置Header和Footer?
观察一下PullToRefreshListView重写的onRefreshing方法,该方法在onTouchEvent执行ACTION_UP时调用:
protected void onRefreshing(final boolean doScroll) {
/**
* If we're not showing the Refreshing view, or the list is empty, the
* the header/footer views won't show so we use the normal method.
*/
ListAdapter adapter = mRefreshableView.getAdapter();
if (!mListViewExtrasEnabled || !getShowViewWhileRefreshing() || null == adapter || adapter.isEmpty()) {
super.onRefreshing(doScroll);
return;
}
super.onRefreshing(false);
final LoadingLayout origLoadingView, listViewLoadingView, oppositeListViewLoadingView;
final int selection, scrollToY;
switch (getCurrentMode()) {
case MANUAL_REFRESH_ONLY:
case PULL_FROM_END:
origLoadingView = getFooterLayout();
listViewLoadingView = mFooterLoadingView;
oppositeListViewLoadingView = mHeaderLoadingView;
selection = mRefreshableView.getCount() - 1;
scrollToY = getScrollY() - getFooterSize();
break;
case PULL_FROM_START:
default:
origLoadingView = getHeaderLayout();
listViewLoadingView = mHeaderLoadingView;
oppositeListViewLoadingView = mFooterLoadingView;
selection = 0;
scrollToY = getScrollY() + getHeaderSize();
break;
}
// Hide our original Loading View
origLoadingView.reset();
origLoadingView.hideAllViews();
// Make sure the opposite end is hidden too
oppositeListViewLoadingView.setVisibility(View.GONE);
// Show the ListView Loading View and set it to refresh.
listViewLoadingView.setVisibility(View.VISIBLE);
listViewLoadingView.refreshing();
if (doScroll) {
// We need to disable the automatic visibility changes for now
disableLoadingLayoutVisibilityChanges();
// We scroll slightly so that the ListView's header/footer is at the
// same Y position as our normal header/footer
setHeaderScroll(scrollToY);
// Make sure the ListView is scrolled to show the loading
// header/footer
mRefreshableView.setSelection(selection);
// Smooth scroll as normal
smoothScrollTo(0);
}
}
还是以垂直下拉为例,PULL_FROM_START情况中,原始的Header是PullToRefreshBase的Header,即origLoadingView对象,该对象被重置并隐藏。
同时oppositeListViewLoadingView为相对的ListView的底部布局,被设置为隐藏。ListView的Header被设置为可见,并处于刷新状态。
当松手时,布局已经滚动到getScrollY()的位置,此时若直接隐藏线性布局的Header,而显示ListView的Header,会给用户一种多滑动出一部分的视觉体验,这个多滑动的距离就是隐藏的Header的高度,所以getScrollY() = getScrollY() + getHeaderSize(),执行setHeaderScroll(scrollToY)让用户感觉不到看见的Header从线性切换到ListView,然后调用ListView的setSelection将列表可见的第一个位置回到ListView的Header,然后smoothScrollTo(0),将多下拉的部分滚动到可见区域上方,此时用户只能看见展示Header的ListView。
当刷新完成,调用PullToRefreshListView重写的onReset方法:
protected void onReset() {
/**
* If the extras are not enabled, just call up to super and return.
*/
if (!mListViewExtrasEnabled) {
super.onReset();
return;
}
final LoadingLayout originalLoadingLayout, listViewLoadingLayout;
final int scrollToHeight, selection;
final boolean scrollLvToEdge;
switch (getCurrentMode()) {
case MANUAL_REFRESH_ONLY:
case PULL_FROM_END:
originalLoadingLayout = getFooterLayout();
listViewLoadingLayout = mFooterLoadingView;
selection = mRefreshableView.getCount() - 1;
scrollToHeight = getFooterSize();
scrollLvToEdge = Math.abs(mRefreshableView.getLastVisiblePosition() - selection) <= 1;
break;
case PULL_FROM_START:
default:
originalLoadingLayout = getHeaderLayout();
listViewLoadingLayout = mHeaderLoadingView;
scrollToHeight = -getHeaderSize();
selection = 0;
scrollLvToEdge = Math.abs(mRefreshableView.getFirstVisiblePosition() - selection) <= 1;
break;
}
// If the ListView header loading layout is showing, then we need to
// flip so that the original one is showing instead
if (listViewLoadingLayout.getVisibility() == View.VISIBLE) {
// Set our Original View to Visible
originalLoadingLayout.showInvisibleViews();
// Hide the ListView Header/Footer
listViewLoadingLayout.setVisibility(View.GONE);
/**
* Scroll so the View is at the same Y as the ListView
* header/footer, but only scroll if: we've pulled to refresh, it's
* positioned correctly
*/
if (scrollLvToEdge && getState() != State.MANUAL_REFRESHING) {
mRefreshableView.setSelection(selection);
setHeaderScroll(scrollToHeight);
}
}
// Finally, call up to super
super.onReset();
}
首先将listView的Header隐藏,接着显示线性的Header。当ListView展示的是第一或第二项,将布局垂直滚动-getHeaderSize()以至于可以完整显示出线性的Header,并将ListView设置为展示第一项。
最后调用父类的onReset,上面已经分析过,该方法将Header中的文字还原到最初态,停止动画的播放,并且调用smoothScrollTo(0)将线性布局滚动到内容区域,将Header区域隐藏起来。
下面分析一下为什么在刷新时切换到ListView的Header
将mListViewExtrasEnabled设置为false,则禁止使用ListView的Header和Footer,此时再下拉刷新,发现ListView不能滑动了。因为PullToRefreshListView默认执行了setScrollingWhileRefreshingEnabled(true),该方法再PullToRefreshBase中将mScrollingWhileRefreshingEnabled属性设置为true,默认为false。顾名思义就是是否允许再刷新时滚动。
在PullToRefreshBase的onInterceptTouchEvent中,如果mScrollingWhileRefreshingEnabled为false,在ACTION_MOVE时,事件会被截断,子View即ListView无法消费事件,因此不会做出响应:
case MotionEvent.ACTION_MOVE: {
// If we're refreshing, and the flag is set. Eat all MOVE events
if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
return true;
}
即使我们设置mScrollingWhileRefreshingEnabled为true,尝试效果是,线性的Header固定在顶部,ListView可以滚动,当我们需要顶部位置固定,内容区域可滑动的效果时,可以这么做。而PullToRefreshListView的默认效果是ListView在滑动时,顶部的刷新区域一样可以滚动出可视区域。
总结
1、PullToRefreshListView是怎么布局的?
PullToRefreshListView是一个垂直线性布局,顶部是Header,中间是ListView,底部是Footer。
2、初始状态为何看不到Header?
通过将PullToRefreshListView的paddingTop设置为负数,将Header区域隐藏起来。
3、下拉刷新过程描述
在onInterceptTouchEvent中判断当前位置是否在ListView的顶部,如果是则拦截下面所有的事件交给onTouchEvent处理,onTouchEvent根据下拉的距离设置布局向上滚动出Header区域。
如果下拉的距离大于mInnerLayout的高度,则松手刷新布局,否则重置到初始态。
进入刷新状态,线性布局的Header被隐藏,ListView的Header被展示,并且ListView可滑动,滑动中Header不固定,随着ListView上下滑可滚动出可视区域。刷新结束后,将ListView的Header隐藏,将线性布局的Header展示出替换,并回到最初态。
遗留分析:
ListView滚动到边界时,Header的回弹效果,详见ListView的overScrollBy方法,具体实现在OverscrollHelper的overScrollBy方法中。