您的位置:首页 > 移动开发 > Android开发

[置顶] android 仿SwipeRefreshLayout实现下拉刷新上拉加载控件(PullRefreshLayout)

2016-04-29 10:43 489 查看
一、我为什么要自己实现下拉刷新上拉加载控件

       公司的项目之前一直用的是PullToRefresh,先不说PullToRefresh已经停止维护了,PullToRefresh扩展起来很麻烦,但并不是说它的扩展性不好,就程序本身的设计思想和设计模式而言是非常好的,非常值得学习,只是PullToRefresh难以适应各种各样的需求罢了。如果项目中使用了像StickyHeaderListView,SwipeListview这样的开源项目,而且需要下拉刷新和上拉加载的功能,PullToRefresh就显得格外尴尬。官方的SwipeRefreshLayout就很好,继承自ViewGroup,基本上能为任何一个view提供下拉刷新功能,但是它并没有实现上拉加载,而且它的下拉样式有时间也不是产品想要的。现在已经有很多开源项目都是以SwipeRefreshLayout这种形式实现的,但我还是决定自己写一个,原因就是为了满足产品的需求,并且自己写的这个没有多样的刷新样式,代码不复杂便于维护,只是为了适应自己的项目。好了扯了这么多没用的,还是让我们开始正题吧!

二、PullRefreshLayout的实现原理

       像SwipeRefreshLayout一样,我们的PullRefreshLayout也继承ViewGroup。PullRefreshLayout应该包含三个部分:refreshHeader(刷新头部视图),refreshView(需要刷新和加载功能的view),refreshFooter(加载尾部视图)。refreshHeader和refreshFooter在初始布局时应该是隐藏的,我们先通过重写onMeasure方法确定这三个部分的大小,首先refreshView应该填充整个PullRefreshLayout可视区域,再通过重写onLayout方法对着三个部分进行排版,将refreshHeaderView和refreshFooterView的位置放置在PullRefreshLayout可视区域外就达到了隐藏的效果,排版要达到的效果如下图所示



下面贴出onMeasure和onLayout方法的代码 @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

if (mRefreshView == null) {
ensureRefreshView();
}
if (mRefreshView == null) {
return;
}
mRefreshView.measure(MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));

measureView(mRefreshHeaderView);
measureView(mLoadingFooterView);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (0 == getChildCount()) {
return;
}

int childLeft = getPaddingLeft();
int childTop = getPaddingTop();
int childRight = getPaddingRight();
int childBottom = getPaddingBottom();

int width = getMeasuredWidth();
int height = getMeasuredHeight();
int refreshViewWidth = width - childLeft - childRight;
int refreshViewHeight = height - childTop - childBottom;
mRefreshView.layout(childLeft, childTop, childLeft + refreshViewWidth, childTop + refreshViewHeight);

int headerHeight = mRefreshHeaderView.getMeasuredHeight();
int headerWidth = mRefreshHeaderView.getMeas
bbb4
uredWidth() - childLeft - childRight;
mRefreshHeaderView.layout(childLeft, - headerHeight, childLeft + headerWidth, 0);

int footerHeight = mLoadingFooterView.getMeasuredHeight();
int footerWidth = mLoadingFooterView.getMeasuredWidth() - childLeft - childRight;
int footerTop = refreshViewHeight + childBottom;
mLoadingFooterView.layout(childLeft, footerTop, childLeft + footerWidth, footerTop + footerHeight);
}
private void measureView(View targetView) {
if (targetView == null) {
return;
}

LayoutParams p = targetView.getLayoutParams();
if (p == null) {
p = new LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT);
}

int lpHeight = p.height;
int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
int childWidthSpec = MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY);
targetView.measure(childWidthSpec, childHeightSpec);
}
好了,布局已经搞定了,接下来无可厚非要根据用户的touchEvent事件来控制刷新头尾视图的展示与隐藏了,先不急,让我们先来看两张效果图


          


很明显,第一张图刚开始是向上拉,事件是交由RefreshView处理的,接下来向下拉,直到满足需要展示RefreshHeaderView的条件时,事件转交由PullRefreshLayout处理。第二张图也一样,事件由RefreshView和RefreshHeaderView交替处理。出于这种场景,我重写了onInterceptTouchEvent方法,将Touch事件直接拦截,在需要RefreshView处理时,才将事件分发。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownX = ev.getX();
mDownY = ev.getY();
mLastY = mDownY;
break;
case MotionEvent.ACTION_MOVE:
float dx = ev.getX() - mDownX;
float dy = ev.getY() - mDownY;
RefreshHeaderView.PullDownRefreshStatus refreshStatus = mRefreshHeaderView.getRefreshStatus();

if ((refreshStatus == RefreshHeaderView.PullDownRefreshStatus.REFRESHING || mIsLoadingMore)) {
if (Math.abs(dy) > 5) {
return true;
}
} else {
if (Math.abs(dy) > Math.abs(dx)) {
return true;
}
}
}
return false;
}
可以看到并不是所有的事件都拦截了,当页面正在刷新时,并认为是有效的move时拦截事件,反之只有垂直方向的位移大于水平方向的位移时拦截(这样做是为了兼容有侧滑功能的ListView,但当正在刷新时如果不拦截水平方向的Touch事件会出现问题,因为这种事件普通listview也是处理的,会出现刷新视图不能隐藏的情况。这里有点瑕疵

)。只要onInterceptTouchEvent方法返回true,onTouchEvent方法会立即调用
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mIsHeaderCollapseAnimating || mIsFooterExpandAnimating || mIsFooterCollapseAnimating) {
return true;
}

int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_MOVE:
if (!mIsFooterCollapseAnimating && !mIsFooterExpandAnimating && !mIsHeaderCollapseAnimating) {
scrollRefreshLayout(event);
}
mLastY = event.getY();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
RefreshHeaderView.PullDownRefreshStatus refreshStatus = mRefreshHeaderView.getRefreshStatus();
// 根据下拉状态,判断是隐藏header,还是触发下拉刷新
if (refreshStatus == RefreshHeaderView.PullDownRefreshStatus.RELEASE_REFRESH) {
collapseRefreshHeaderView(RefreshHeaderView.PullDownRefreshStatus.REFRESHING);
} else if (refreshStatus == RefreshHeaderView.PullDownRefreshStatus.PULL_DOWN_REFRESH && getScrollY() < 0) {
collapseRefreshHeaderView(refreshStatus);
}
mPullDownDistance = 0;

if (mIsDispatchDown) {
mRefreshView.dispatchTouchEvent(event);
mIsDispatchDown = false;
}
mFlag = false;
break;
}
return true;

这里我们主要看一下scrollRefreshLayout方法,它控制着事件的分发和刷新视图的隐藏和显示
private void scrollRefreshLayout(MotionEvent event) {
int scrollY = getScrollY();
float dy = mLastY - event.getY();
RefreshHeaderView.PullDownRefreshStatus refreshStatus = mRefreshHeaderView.getRefreshStatus();

if (refreshStatus == RefreshHeaderView.PullDownRefreshStatus.REFRESHING) {
if (scrollY == 0) {
// scrollY == 0时,下拉刷新view已经不可见,此时需要判断事件由谁来处理
if (dy < 0 && !canChildScrollUp()) {
//方向为下拉,并且refreshView不能继续往下滚时自己处理事件,否则事件分发给refreshView处理
scrollRefreshLayoutWithRefreshing((int) dy, scrollY);
} else {
dispatchTouchEventToRefreshView(event);
}
} else {
scrollRefreshLayoutWithRefreshing((int) dy, scrollY);
}
} else if (mIsLoadingMore) {
if (scrollY == 0) {
// scrollY == 0时,上拉加载view已经不可见,此时需要判断事件由谁来处理
if (dy > 0 && !canChildScrollDown()) {
// 方向为上拉,并且refreshView不能继续往上滚时自己处理事件,否则事件分发给refreshView处理
scrollRefreshLayoutWithLoading((int) dy, scrollY);
} else {
dispatchTouchEventToRefreshView(event);
}
} else {
scrollRefreshLayoutWithLoading((int) dy, scrollY);
}
} else {
if (dy < 0 && !canChildScrollUp() || getScrollY() < 0) {
// 下拉刷新
if (mMode == PULL_FROM_START || mMode == BOTH) {
mPullDownDistance += -dy;

if (mPullDownDistance > 0) {
// 阻尼
float damping = (float) (mPullDownDistance / (Math.log(mPullDownDistance) / Math.log(FACTOR)));
damping = Math.max(0, damping);
scrollTo(0, (int) -damping);

if (damping >= mRefreshHeaderView.getHeight()) {
// 滚动距离超过下拉刷新视图的高,将状态变为释放
mRefreshHeaderView.setRefreshStatus(RefreshHeaderView.PullDownRefreshStatus.RELEASE_REFRESH);
} else {
mRefreshHeaderView.setRefreshStatus(RefreshHeaderView.PullDownRefreshStatus.PULL_DOWN_REFRESH);
}
} else {
scrollTo(0, 0);
mPullDownDistance = 0;
}
}
} else if (dy > 0 && !canChildScrollDown() && getScrollY() == 0 && !mIsFooterCollapseAnimating) {
if (!mFlag && (mMode == PULL_FROM_END || mMode == BOTH) && !mIsFooterExpandAnimating) {
mFlag = true;
// 上拉加载
expandLoadingFooterView();
}
} else {
if (!(!canChildScrollDown() && dy > 0)) {
if (Math.abs(dy) > 5) {
dispatchTouchEventToRefreshView(event);
}
}
}
}
}刷新视图的滚动主要借助了ViewGroup的getScrollY和scrollBy方法,这里先对这两个方法做下简单的说明
1.scrollBy
以现在的位置为基础,根据传入的dx或dy在水平或垂直方向滚动一段距离,dy为负向下滚,dy为正向上滚。
2.getScrollY
这个主要说下它的参照原点和正负方向,它的y轴正方向和数学中的坐标系一样,向上为正(这个坐标系官方自己搞的真心乱),参照原点是layout之后,top或lef值最小的子view的坐定点,这样说很难理解,直接看下图



好了,开始讲我们的逻辑。我们这里就先撇开上拉加载,我们只讲下拉刷新,上拉加载与下拉刷新类似。首先分出两种状态
1.下拉刷新正在执行
此时,当getScrollY == 0时,下拉刷新view已经完全不可见,此时需要判断事件由谁来处理。方向为下拉,并且refreshView不能继续往下滚时PullRefreshLayout自己处理事件,否则事件分发给refreshView处理。当getScrollY != 0时,下拉刷新view还可见,这时要根据move的距离,移动PullRefreshLayout。

2.下拉刷新没有执行
此时,当方向为下拉并且RefreshView不能再往上滚动时(dy < 0 && !canChildScrollUp()),触发下拉刷新,刷新头拉下来一段距离后,再往上拉(dy > 0),此时要慢慢隐藏刷新头,所以又加了一个getScrollY < 0的判断。当下移的距离大于等于刷新头的高度时,将刷新头的状态改为释放状态,否则为下拉状态。在action up 是判断当前是否处于释放状态,处于释放状态则触发刷新。
好了,就讲到这里,大家可以去下载完整的代码https://github.com/weijia1991/PullRefreshLayout
欢迎大家来拍砖!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: