在Android app开发过程中,使用下拉刷新控件的机会是非常多的,比如列表页或是首页,一般都是要下拉刷新的。在Github中下拉刷新控件有很多,但是我现在介绍的是已经停更很久的XListView,因为我觉得这个库写的简介明了,功能稳定,bug少。非常适合自己学习下拉刷新的原理。面试的时候也通常会问到某些控件的原理,所以,了解一下还是很有必要的。
XListView在github中的仓库:https://github.com/Maxwin-z/XListView-Android
本人根据@Maxwin-z大神的库,添加了一下功能,主要是类似支付宝首页的在刷新Header上添加一个header,往上滑会跟着滑的功能,希望大家也多多支持,仓库地址是:https://github.com/mengchaoshen/SListView-Android/
首先介绍一下XListView的简单使用:
因为XListView它是继承自ListView的,所以使用方法和普通ListView没什么两样,
1.使用<XListView>标签,放入你的布局文件中
2.使用Adapter,设置数据源
3.不同的地方是,它可以设置下拉刷新的callback,也就是说,出发下拉刷新时,需要你去刷新数据,并且让你去更新数据源,然后告诉它,我已经刷新完毕
mXListView.setXListViewListener(new XListView.IXListViewListener() {
@Override
public void onRefresh() {
new Thread() {
@Override
public void run() {
super.run();//这里可以去调用接口获取数据,我这里简单的延迟2000ms,看看效果。
mHandler.sendEmptyMessageDelayed(0, 2000);
}
}.run();
}
@Override
public void onLoadMore() {
}
});
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);//这里是数据获取成功后,要调用stopRefresh()来改变状态
mSListView.stopRefresh();
}
}
简单的使用,就大概这样,在这里不具体展开了。
接下来详细讲解下XListView的实现原理:
首先大家应该知道ListView是包含一个HeaderView的,下拉刷新就是围绕这个展开的。XListView只有三个类,XListView.java,XListViewHeader.java,XListViewFooter.java。
先介绍一下主要流程
主要流程,主要是在XListView.java中,大致原理就是,当ListView滚动到顶部时,这个时候手指再往下拉,就开始触发下拉刷新的第一个步骤,(首先HeaderView默认设置height=0,也就是完全不显示的)通过onTouch事件逐步传递滑动间距,来设置HeaderView的高度值,这样看起来HeaderView就是一点点显示出来了。当headerView已经完全显示出来时,继续往下拉,就触发第二个步骤,开始变为准备状态,文案也变为“松开刷新数据”,就是文案的意思,这个时候松开手指,就会去回调你设置的刷新接口。当你松开手指,就触发了第三个步骤,文案变为“数据加载中...”。知道数据加载完成,你调用了stopRefresh()方法,就又回到第一个状态,HeaderView隐藏回去。
主要过程就是这样,是不是很简单,但是细细看源码,发现还是有很多值的回味的地方。比如如何使显示的HeaderView逐渐隐藏回去,手指下拉和HeaderView显示有一个阻尼效果是如何实现的,HeaderView有那么多中状态,是如何来控制的,,带着这些问题,来详细看看源码
首先看看HeaderView源码
它是一个基础自LinearLayout的自定义布局,里面包含状态文案,刷新事件,箭头提示,progressBar等包含状态文案,刷新事件,箭头提示,progressBar等。
它的主要功能就是根据XListView的命令,来显示不同的状态和高度
首先它定义了三种状态,
//normal状态,显示“下拉刷新”,箭头朝下,如果是ready状态转换而来,需要执行箭头从上到下的动画
public final static int STATE_NORMAL = 0;
//ready状态,显示“松开刷新数据”,需要执行箭头向上的动画
public final static int STATE_READY = 1;
//refreshing状态,显示“正在加载...”隐藏箭头,显示progressBar
public final static int STATE_REFRESHING = 2;
实现代码如下:
public void setState(int state) {
if (state == mState) return ;
if (state == STATE_REFRESHING) { // 显示进度
mArrowImageView.clearAnimation();
mArrowImageView.setVisibility(View.INVISIBLE);
mProgressBar.setVisibility(View.VISIBLE);
} else { // 显示箭头图片
mArrowImageView.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.INVISIBLE);
}
switch(state){
case STATE_NORMAL:
if (mState == STATE_READY) {//如果是ready状态变为normal状态,需要开始动画,把箭头变为向下
mArrowImageView.startAnimation(mRotateDownAnim);
}
if (mState == STATE_REFRESHING) {
mArrowImageView.clearAnimation();
}
mHintTextView.setText(R.string.xlistview_header_hint_normal);
break;
case STATE_READY:
if (mState != STATE_READY) {//如果是其他状态变为ready状态,需要开始动画,把箭头变为向上
mArrowImageView.clearAnimation();
mArrowImageView.startAnimation(mRotateUpAnim);
mHintTextView.setText(R.string.xlistview_header_hint_ready);
}
break;
case STATE_REFRESHING:
mHintTextView.setText(R.string.xlistview_header_hint_loading);
break;
default:
}
mState = state;
}
还有一个就是设置HeaderView的高度,这也是很关键的
public void setVisibleHeight(int height) {//设置为传入的高度,如果传入的高度小于0,就设置为0
if (height < 0)
height = 0;
LayoutParams lp = (LayoutParams) mContainer
.getLayoutParams();
lp.height = height;
mContainer.setLayoutParams(lp);
}
再看看最关键的XListView的源码
一些简单的初始化就直接跳过了,大家可以直接看源码,直接步入主题,查看onTouch部分源码
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mLastY == -1) {
mLastY = ev.getRawY();
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = ev.getRawY();//ACTION_DOWN事件时,记录y坐标值
break;
case MotionEvent.ACTION_MOVE://手指移动
final float deltaY = ev.getRawY() - mLastY;//ACTION_MOVE事件时,记录与上一事件y坐标的差值
mLastY = ev.getRawY();
if (getFirstVisiblePosition() == 0//如果ListView已经拉到顶部
&& (mHeaderView.getVisiableHeight() > 0 || deltaY > 0)) {//HeaderView已经显示,或手指继续往下拉
// the first item is showing, header has shown or pull down.
updateHeaderHeight(deltaY / OFFSET_RADIO);//开始逐步把HeaderView显示出来,并且带有阻尼系数
invokeOnScrolling();
} else if (getLastVisiblePosition() == mTotalItemCount - 1//ListView已经滑动到底部
&& (mFooterView.getBottomMargin() > 0 || deltaY < 0)) {//FooterView已经离开底部或手指继续往上拉
// last item, already pulled up or want to pull up.
updateFooterHeight(-deltaY / OFFSET_RADIO);//更新FooterView的高度
}
break;
default://手指松开
mLastY = -1; // reset
if (getFirstVisiblePosition() == 0) {//ListView在顶部
// invoke refresh
if (mEnablePullRefresh//开启下拉刷新功能
&& mHeaderView.getVisiableHeight() > mHeaderViewHeight) {//HeaderView可见高度已经比自身高度要高
mPullRefreshing = true;
mHeaderView.setState(XListViewHeader.STATE_REFRESHING);//触发刷新,HeaderView进入刷新中状态if (mListViewListener != null) {
mListViewListener.onRefresh();//回调你设置的刷新方法
}
}
resetHeaderHeight();//重新回到HeaderView的初始状态
} else if (getLastVisiblePosition() == mTotalItemCount - 1) {//下拉刷新也是类似的
// invoke load more.
if (mEnablePullLoad
&& mFooterView.getBottomMargin() > PULL_LOAD_MORE_DELTA
&& !mPullLoading) {
startLoadMore();
}
resetFooterHeight();
}
break;
}
return super.onTouchEvent(ev);
}
上面的源码和注释,已经把整个流程讲得很清楚了,这里还有一个需要介绍的就是resetHeaderHeight()方法,这里使用了Scroller来把HeaderView滑动回初始位置或者全部显示位置(刷新中状态)
private void resetHeaderHeight() {
int height = mHeaderView.getVisiableHeight();
if (height == 0) // not visible. 如果HeaderView是不可见状态,则不需要滑动
return;
// refreshing and header isn't shown fully. do nothing.
if (mPullRefreshing && height <= mHeaderViewHeight) {//如果正在刷新中,并且可见高度是小于固有高度的,不需要滑动
return;
}
int finalHeight = 0; // default: scroll back to dismiss header.默认把HeaderView滑动回原位
// is refreshing, just scroll back to show all the header.
if (mPullRefreshing && height > mHeaderViewHeight) {//如果是刷新中,并且可见高度是大于固有高度,需要滚动到全部显示位置
finalHeight = mHeaderViewHeight;//这个时候,需要把HeaderView滑动回全部显示的位置
}
mScrollBack = SCROLLBACK_HEADER;
mScroller.startScroll(0, height, 0, finalHeight - height,
SCROLL_DURATION);//根据之前设置的finalHeight来滑动到目标位置(初始位置或全部显示位置)
// trigger computeScroll
invalidate();//这里触发滑动
}
这里还有一些关于设置,是否可以刷新等,就不一一介绍了,看懂上面的,一些细节都能迎刃而解。