下拉刷新界面最初流行于iphone应用界面,如图:
然后在Android中也逐渐被应用,比如微博,资讯类。
所以,今天要实现的结果应该也是类似的,先贴出最终完成效果,如下图,接下来我们一步一步实现。
1. 流程分析
下拉刷新最主要的流程是:
(1). 下拉,显示提示头部界面(HeaderView),这个过程提示用户"下拉刷新"
(2). 下拉到一定程度,超出了刷新最基本的下拉界限,我们认为达到了刷新的条件,提示用户可以"松手刷新"了,效果上允许用户继续下拉
(3). 用户松手,可能用户下拉远远不止提示头部界面,所以这一步,先反弹回仅显示提示头部界面,然后提示用户"正在加载"。
(4). 加载完成后,隐藏提示头部界面。
示意图如下:
->->
2. 实现分析
当前我们要实现上述流程,是基于ListView的,所以对应ListView本身的功能我们来分析一下实现原理:
(1). 下拉,显示提示头部界面,这个过程提示用户"下拉刷新"
a. 下拉的操作,首先是监听滚动,ListView提供了onScroll()方法
b. 与下拉类似一个动作向下飞滑,所以ListView的scrollState有3种值:SCROLL_STATE_IDLE, SCROLL_STATE_TOUCH_SCROLL, SCROLL_STATE_FLING,意思容易理解,而我们要下拉的触发条件是SCROLL_STATE_TOUCH_SCROLL。判断当前的下拉操作状态,ListView提供了public void onScrollStateChanged(AbsListView view, int scrollState) {}。
c. 下拉的过程中,我们可能还需要下拉到多少的边界值处理,重写onTouchEvent(MotionEvent ev){}方法,可依据ACTION_DOWN,ACTION_MOVE,ACTION_UP实现更精细的判断。
(2). 下拉到一定程度,超出了刷新最基本的下拉界限,我们认为达到了刷新的条件,提示用户可以"松手刷新"了,效果上允许用户继续下拉
a. 达到下拉刷新界限,一般指达到header的高度的,所以有两步,第一,获取header的高度,第二,当header.getBottom()>=header的高度时,我们认为就达到了刷新界限值
b. 继续允许用户下拉,当header完全下拉后,默认无法继续下拉,但是可以增加header的PaddingTop实现这种效果
(3). 用户松手,可能用户下拉远远不止提示头部界面,所以这一步,先反弹回仅显示提示头部界面,然后提示用户"正在加载"。
a. 松手后反弹,这个不能一下子弹回去,看上去太突然,需要一步一步柔性的弹回去,像弹簧一样,我们可以new一个Thread循环计算减少PaddingTop,直到PaddingTop为0,反弹结束。
b. 正在加载,在子线程里处理后台任务
(4). 加载完成后,隐藏提示头部界面。
a. 后台任务完成后,我们需要隐藏header,setSelection(1)即实现了从第2项开始显示,间接隐藏了header。
上面我们分析了实现过程的轮廓,接下来,通过细节说明和代码具体实现。
3. 初始化
一切状态显示都是用HeaderView显示的,所以我们需要一个HeaderView的layout,使用addHeaderView方法添加到ListView中。
同时,默认状态下,HeaderView是不显示的,只是在下拉后才显示,所以我们需要隐藏HeaderView且不影响后续的下拉显示,用setSelection(1)。
refresh_list_header.xml布局如下:
|
|
代码中在构造函数中添加init()方法加载如下:
|
|
默认就显示完成了。
4. HeaderView的默认高度测量
因为下拉到HeaderView全部显示出来,就由提示"下拉刷新"变为"松手刷新",全部显示的出来的测量标准就是header.getBottom()>=header的高度。
所以,首先我们需要测量HeaderView的默认高度。
|
|
然后在init的上述代码后面加上调用measureView后,使用getMeasureHeight()方法获取header的高度:
|
|
private int mHeaderHeight;
void init(final Context context) {
... ...
measureView(mHeaderLinearLayout);
mHeaderHeight = mHeaderLinearLayout.getMeasuredHeight();
}
后面我们就会用到这个mHeaderHeight.
5. scrollState监听记录
scrollState有3种,使用onScrollStateChanged()方法监听记录。
|
|
private int mCurrentScrollState;
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
mCurrentScrollState = scrollState;
}
然后即可使用mCurrentScrollState作为后面判断的条件了。
6. 刷新状态分析
因为一些地方需要知道我们处在正常状态下还是进入下拉刷新状态还是松手反弹状态,比如,
(1). 在非正常的状态下,我们不小心飞滑了一下(松手的瞬间容易出现这种情况),我们不能setSelection(1)的,否则总是松手后header跳的一下消失掉了。
(2). 下拉后要做一个下拉效果的特殊处理,需要用到OVER_PULL_REFRESH(松手刷新状态下)
(3). 松手反弹后要做一个反弹效果的特殊处理,需要用到OVER_PULL_REFRESH和ENTER_PULL_REFRESH。
|
|
private final static int NONE_PULL_REFRESH = 0; //正常状态
private final static int ENTER_PULL_REFRESH = 1; //进入下拉刷新状态
private final static int OVER_PULL_REFRESH = 2; //进入松手刷新状态
private final static int EXIT_PULL_REFRESH = 3; //松手后反弹后加载状态
private int mPullRefreshState = 0; //记录刷新状态
@Override
public void onScroll(AbsListView view, intfirstVisibleItem, intvisibleItemCount, inttotalItemCount) {
if(mCurrentScrollState ==SCROLL_STATE_TOUCH_SCROLL
&& firstVisibleItem == 0
&& (mHeaderLinearLayout.getBottom() >= 0&& mHeaderLinearLayout.getBottom() < mHeaderHeight)) {
//进入且仅进入下拉刷新状态
if (mPullRefreshState == NONE_PULL_REFRESH) {
mPullRefreshState = ENTER_PULL_REFRESH;
}
}else if(mCurrentScrollState ==SCROLL_STATE_TOUCH_SCROLL
&& firstVisibleItem == 0
&& (mHeaderLinearLayout.getBottom() >= mHeaderHeight)) {
//下拉达到界限,进入松手刷新状态
if (mPullRefreshState == ENTER_PULL_REFRESH || mPullRefreshState == NONE_PULL_REFRESH) {
mPullRefreshState = OVER_PULL_REFRESH;
//下面是进入松手刷新状态需要做的一个显示改变
mDownY = mMoveY;//用于后面的下拉特殊效果
mHeaderTextView.setText("松手刷新");
mHeaderPullDownImageView.setVisibility(View.GONE);
mHeaderReleaseDownImageView.setVisibility(View.VISIBLE);
}
}else if(mCurrentScrollState ==SCROLL_STATE_TOUCH_SCROLL && firstVisibleItem !=0) {
//不刷新了
if (mPullRefreshState == ENTER_PULL_REFRESH) {
mPullRefreshState = NONE_PULL_REFRESH;
}
}else if(mCurrentScrollState == SCROLL_STATE_FLING && firstVisibleItem ==0) {
//飞滑状态,不能显示出header,也不能影响正常的飞滑
//只在正常情况下才纠正位置
if (mPullRefreshState == NONE_PULL_REFRESH) {
setSelection(1);
}
}
}
mPullRefreshState将是后面我们处理边界的重要变量。
6. 下拉效果的特殊处理
所谓的特殊处理,当header完全显示后,下拉只按下拉1/3的距离下拉,给用户一种艰难下拉,该松手的弹簧感觉。
这个在onTouchEvent里处理比较方便:
onScroll里监听到了进入松手刷新状态,onTouchEvent就开始在ACTION_MOVE中处理1/3折扣问题。
7. 反弹效果的特殊处理
松手后我们需要一个柔性的反弹效果,意味着我们弹回去的过程需要分一步步走,我的解决方案是:
在子线程里计算PaddingTop,并减少到原来的3/4,循环通知主线程,直到PaddingTop小于1(这个值取一个小值,合适即可)。
松手后,当然是在onTouchEvent的ACTION_UP条件下处理比较方便:
为了一下子看的明确,我把效果中的数据处理代码也贴出来了。
8. 切入数据加载过程
上面数据后台处理我们用sleep(2000)来处理,实际处理中,作为公共组件,我们也不好把具体代码直接写在这里,我们需要一个更灵活的分离:
(1). 定义接口
(2). 注入接口
在其他地方我们就可以不修改这个listview组件的代码,使用如下:
很方便了。
9. 扩展"更多"功能
下拉刷新之外,我们也可以通过相同方法使用FooterView切入底部"更多"过程,这里我就不详细说明了
10. 源码
上面的每段代码都看做是"零部件",需要组合一下。
因为我们上面实现了下拉刷新,还增加了"更多"功能,我们直接命名这个类为RefreshListView吧: