SuperSwipeRefreshLayout源码分析
源码及DEMO
特性
- 支持下拉刷新和上拉加载更多
- 非侵入式,对原来的ListView、RecyclerView没有任何影响,用法和SwipeRefreshLayout类似。
- 可自定义头部View的样式,调用setHeaderView方法即可
- 可自定义页尾View的样式,调用setFooterView方法即可
- 支持RecyclerView,ListView,ScrollView,GridView等等。
- 被包含的View(RecyclerView,ListView etc.)可跟随手指的滑动而滑动
默认是跟随手指的滑动而滑动,也可以设置为不跟随:setTargetScrollWithLayout(false)
- 回调方法更多
比如:onRefresh() onPullDistance(int distance)和onPullEnable(boolean enable)
开发人员可以根据下拉过程中distance的值做一系列动画。
思路
自定义一个ViewGroup,往其中添加headerView和footerView,然后在onMeasure中确定它们的大小,在onLayout中确定它们的位置。当子View滑动到最上方的时候,或者最下方的时候,拦截事件,自己处理onTouchEvent事件;其他情况,交给子View自己处理onTouchEvent事件。
基于以上分析:需要重点关注的方法有:
- addView
- onMeasure
- getChildDrawingOrder
- onLayout
- isChildScrollToTop
- isChildScrollToBottom
- onInterceptTouchEvent
- onTouchEvent
addView
主要是添加headerView和footerView,这是第一步。
addView(mHeadViewContainer);
...
addView(mFooterViewContainer);
onMeasure
重写ViewGroup的onMeasure方法:
注意:onMeasure方法中只决定子View的大小,并不决定View的位置
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
mTarget.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth()
- getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight()
- getPaddingTop() - getPaddingBottom(),
MeasureSpec.EXACTLY));
mHeadViewContainer.measure(MeasureSpec.makeMeasureSpec(
mHeaderViewWidth, MeasureSpec.EXACTLY), MeasureSpec
.makeMeasureSpec(mHeaderViewHeight, MeasureSpec.EXACTLY));
mFooterViewContainer.measure(MeasureSpec.makeMeasureSpec(
mFooterViewWidth, MeasureSpec.EXACTLY), MeasureSpec
.makeMeasureSpec(mFooterViewHeight, MeasureSpec.EXACTLY));
...
//查找在ViewGroup的index
mHeaderViewIndex = -1;
for (int index = 0; index < getChildCount(); index++) {
if (getChildAt(index) == mHeadViewContainer) {
mHeaderViewIndex = index;
break;
}
}
mFooterViewIndex = -1;
for (int index = 0; index < getChildCount(); index++) {
if (getChildAt(index) == mFooterViewContainer) {
mFooterViewIndex = index;
break;
}
}
}
getChildDrawingOrder
重写ViewGroup的getChildDrawingOrder,由于HeaderView和FooterView刚开始是不显示的,分别隐藏在屏幕的上方和下方,因此,需要将它们两个的绘制顺序调整到最后。getChildDrawingOrder方法的含义是第i次应该绘制哪一个childView
/**
* 孩子节点绘制的顺序
*
* @param childCount
* @param i
* @return
*/
@Override
protected int getChildDrawingOrder(int childCount, int i) {
// 将新添加的View,放到最后绘制
if (mHeaderViewIndex < 0 && mFooterViewIndex < 0) {
return i;
}
if (i == childCount - 2) {
return mHeaderViewIndex;
}
if (i == childCount - 1) {
return mFooterViewIndex;
}
int bigIndex = mFooterViewIndex > mHeaderViewIndex ? mFooterViewIndex
: mHeaderViewIndex;
int smallIndex = mFooterViewIndex < mHeaderViewIndex ? mFooterViewIndex
: mHeaderViewIndex;
if (i >= smallIndex && i < bigIndex - 1) {
return i + 1;
}
if (i >= bigIndex || (i == bigIndex - 1)) {
return i + 2;
}
return i;
}
onLayout
onMeasure方法中决定了子View的大小,onLayout决定了View的位置和大小
因此,我们可以通过重写onLayout方法。当拦截到事件后,我们可以根据滑动的距离,动态更新View的位置。所以,HeaderView和FooterView的layout跟滑动距离有关系。
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
if (getChildCount() == 0) {
return;
}
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
int distance = mCurrentTargetOffsetTop + mHeadViewContainer.getHeight();
if (!targetScrollWithLayout) {
// 判断标志位,如果目标View不跟随手指的滑动而滑动,将下拉偏移量设置为0
distance = 0;
}
final View child = mTarget;
final int childLeft = getPaddingLeft();
final int childTop = getPaddingTop() + distance - pushDistance;// 根据偏移量distance更新
final int childWidth = width - getPaddingLeft() - getPaddingRight();
final int childHeight = height - getPaddingTop() - getPaddingBottom();
Log.d(LOG_TAG, "debug:onLayout childHeight = " + childHeight);
child.layout(childLeft, childTop, childLeft + childWidth, childTop
+ childHeight);// 更新目标View的位置
int headViewWidth = mHeadViewContainer.getMeasuredWidth();
int headViewHeight = mHeadViewContainer.getMeasuredHeight();
mHeadViewContainer.layout((width / 2 - headViewWidth / 2),
mCurrentTargetOffsetTop, (width / 2 + headViewWidth / 2),
mCurrentTargetOffsetTop + headViewHeight);// 更新头布局的位置
int footViewWidth = mFooterViewContainer.getMeasuredWidth();
int footViewHeight = mFooterViewContainer.getMeasuredHeight();
mFooterViewContainer.layout((width / 2 - footViewWidth / 2), height
- pushDistance, (width / 2 + footViewWidth / 2), height
+ footViewHeight - pushDistance);
}
isChildScrollToTop
该方法可谓是下拉刷新的核心方法,如何判断目标View是否滑到顶点了呢?ListView和GridView的判断类似,而RecyclerView和ScrollView则与其不同。代码如下:
/**
* 判断目标View是否滑动到顶部-还能否继续滑动
*
* @return
*/
public boolean isChildScrollToTop() {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mTarget instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mTarget;
return !(absListView.getChildCount() > 0 && (absListView
.getFirstVisiblePosition() > 0 || absListView
.getChildAt(0).getTop() < absListView.getPaddingTop()));
} else {
return !(mTarget.getScrollY() > 0);
}
} else {
return !ViewCompat.canScrollVertically(mTarget, -1);
}
}
isChildScrollToBottom
改方法是上拉加载更多的核心方法,要判断子View是否已经滑到底部,RecyclerView不同的LayoutManager的判断情况都不同。具体代码如下:
/**
* 是否滑动到底部
*
* @return
*/
public boolean isChildScrollToBottom() {
if (isChildScrollToTop()) {
return false;
}
if (mTarget instanceof RecyclerView) {//RecyclerView
RecyclerView recyclerView = (RecyclerView) mTarget;
LayoutManager layoutManager = recyclerView.getLayoutManager();
int count = recyclerView.getAdapter().getItemCount();
if (layoutManager instanceof LinearLayoutManager && count > 0) {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
if (linearLayoutManager.findLastCompletelyVisibleItemPosition() == count - 1) {
return true;
}
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
int[] lastItems = new int[2];
staggeredGridLayoutManager
.findLastCompletelyVisibleItemPositions(lastItems);
int lastItem = Math.max(lastItems[0], lastItems[1]);
if (lastItem == count - 1) {
return true;
}
}
return false;
} else if (mTarget instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mTarget;
int count = absListView.getAdapter().getCount();
int fristPos = absListView.getFirstVisiblePosition();
if (fristPos == 0
&& absListView.getChildAt(0).getTop() >= absListView
.getPaddingTop()) {
return false;
}
int lastPos = absListView.getLastVisiblePosition();
if (lastPos > 0 && count > 0 && lastPos == count - 1) {
return true;
}
return false;
} else if (mTarget instanceof ScrollView) {
ScrollView scrollView = (ScrollView) mTarget;
View view = (View) scrollView
.getChildAt(scrollView.getChildCount() - 1);
if (view != null) {
int diff = (view.getBottom() - (scrollView.getHeight() + scrollView
.getScrollY()));
if (diff == 0) {
return true;
}
}
}
return false;
}
onInterceptTouchEvent
重写该方法,用于事件拦截的判断,该方法决定了事件到底交给谁处理 。
1.当return true时,表示ViewGroup自己来处理onTouchEvent事件,子View接收不到onTouchEvent事件
2.当return false时,表示ViewGroup不拦截事件,直接交给子View处理
我们不仅需要在点击时判断是否拦截事件,还需要在滑动过程中判断是否需要拦截事件,比如滑动的距离过小,则不需要拦截。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
final int action = MotionEventCompat.getActionMasked(ev);
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
if (!isEnabled() || mReturningToStart || mRefreshing || mLoadMore
|| (!isChildScrollToTop() && !isChildScrollToBottom())) {
// 如果子View可以滑动,不拦截事件,交给子View处理-下拉刷新
// 或者子View没有滑动到底部不拦截事件-上拉加载更多
return false;
}
// 下拉刷新判断
switch (action) {
case MotionEvent.ACTION_DOWN:
setTargetOffsetTopAndBottom(
mOriginalOffsetTop - mHeadViewContainer.getTop(), true);// 恢复HeaderView的初始位置
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsBeingDragged = false;
final float initialMotionY = getMotionEventY(ev, mActivePointerId);
if (initialMotionY == -1) {
return false;
}
mInitialMotionY = initialMotionY;// 记录按下的位置
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_POINTER) {
Log.e(LOG_TAG,
"Got ACTION_MOVE event but don‘t have an active pointer id.");
return false;
}
final float y = getMotionEventY(ev, mActivePointerId);
if (y == -1) {
return false;
}
float yDiff = 0;
if (isChildScrollToBottom()) {
yDiff = mInitialMotionY - y;// 计算上拉距离
if (yDiff > mTouchSlop && !mIsBeingDragged) {// 判断是否下拉的距离足够
mIsBeingDragged = true;// 正在上拉
}
} else {
yDiff = y - mInitialMotionY;// 计算下拉距离
if (yDiff > mTouchSlop && !mIsBeingDragged) {// 判断是否下拉的距离足够
mIsBeingDragged = true;// 正在下拉
}
}
break;
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
break;
}
return mIsBeingDragged;// 如果正在拖动,则拦截子View的事件
}
onTouchEvent
在SuperSwipeRefreshLayout中,onTouchEvent只有当onInterceptTouchEvent返回true的时候才执行。它根据下拉或者上拉的距离,动态的修改headerView或footerView的位置,通过调用offsetTopAndBottom刷新onLayout方法。
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
if (!isEnabled() || mReturningToStart
|| (!isChildScrollToTop() && !isChildScrollToBottom())) {
// 如果子View可以滑动,不拦截事件,交给子View处理
return false;
}
if (isChildScrollToBottom()) {// 上拉加载更多
return handlerPushTouchEvent(ev, action);
} else {// 下拉刷新
return handlerPullTouchEvent(ev, action);
}
}
private boolean handlerPullTouchEvent(MotionEvent ev, int action){
...
}
...
More
你可能不注意的细节,但是有肯能引起奇怪的BUG
- 不起眼的requestDisallowInterceptTouchEvent
@Override
public void requestDisallowInterceptTouchEvent(boolean b) {
// Nope.
}
- MotionEventCompat.ACTION_POINTER_UP
总结
分析SuperSwipeRefreshLayout的源码,可以让我们明白一些事情:
- 1.写自定义ViewGroup需要注意的onMeasure和onLayout
- 2.事件拦截onInterceptTouchEvent的使用,到底什么时候该拦截,什么时候该交给子View处理。
很多复杂的交互需求,也许能从onInterceptTouchEvent中找到你要的解决方案。
- 3.如何判断一个ListView或RecyclerView或ScrollView是否已经滑动到底部或顶部。
提示一句,SuperSwipeRefreshLayout是在读懂SwipeRefreshLayout源码的基础上写出来的~
版权声明:本文为博主原创文章,未经博主允许不得转载。