带你一步一步实现listview上拉加载下拉刷新
思路:
- 要是实现这个效果我们需要,有一个view能在头部和底部存在(通过查找的只listview有addheard,和addfoot的方法可以将一个view添加到头部的底部)
- 如何进来的时候不显示头部的view呢?(我门可以设置View的初始高度为0,或者设置setPadding设置他距离上面的距离为-的高度就可以隐藏了,我们使用setPadding方法)
- 如何隐藏底部的view?(同上)
- 如何能让view跟着我们的手势移动呢?(第一种我们可以使用OverScroller这个类,第二种我们可以通过动态的改变头部view和底部view的高度来实现移动的的效果,我们使用后者)
- 如何实现view的自动回弹呢?(我们可以使用OverScroller这个类的startScroll方法,然后实现computeScroll方法在这个里面去动态的改变view的高度)
第一步我们创建HeaderView这个类当顶部的刷新的view
public class HeaderView extends LinearLayout {
/** 刷新状态 */
private LoadState mState = LoadState.NORMAL;
private View mHeader = null;
private ImageView mArrow = null;
private ProgressBar mProgressBar = null;
private TextView mRefreshTips = null;
private TextView mRefreshLastTime = null;
private RotateAnimation mRotateUp = null;
private RotateAnimation mRotateDown = null;
private final static int ROTATE_DURATION = 250;
/** 一分钟的毫秒值,用于判断上次的更新时间. */
private final long ONE_MINUTE = 60 * 1000;
/** 一小时的毫秒值,用于判断上次的更新时间. */
private final long ONE_HOUR = 60 * ONE_MINUTE;
/** 一天的毫秒值,用于判断上次的更新时间. */
private final long ONE_DAY = 24 * ONE_HOUR;
/** 一月的毫秒值,用于判断上次的更新时间. */
private final long ONE_MONTH = 30 * ONE_DAY;
/** 一年的毫秒值,用于判断上次的更新时间. */
private final long ONE_YEAR = 12 * ONE_MONTH;
public HeaderView(Context context) {
this(context, null);
}
public HeaderView(Context context, AttributeSet attrs) {
super(context, attrs);
initHeaderView(context);
}
private void initHeaderView(Context context) {
LinearLayout.LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0);
mHeader = LayoutInflater.from(context).inflate(R.layout.g_refresh_header, null);
addView(mHeader, lp);
setGravity(Gravity.BOTTOM);
mArrow = (ImageView) mHeader.findViewById(R.id.ivArrow);
mProgressBar = (ProgressBar) mHeader.findViewById(R.id.pbWaiting);
mRefreshTips = (TextView) mHeader.findViewById(R.id.refresh_tips);
mRefreshLastTime = (TextView) mHeader.findViewById(R.id.refresh_last_time);
//初始化旋转的动画需要反转箭头
mRotateUp = new RotateAnimation(0.0f, -180.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
mRotateUp.setDuration(ROTATE_DURATION);
mRotateUp.setFillAfter(true);
mRotateDown = new RotateAnimation(-180.0f, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
mRotateDown.setDuration(ROTATE_DURATION);
mRotateDown.setFillAfter(true);
Log.e("HeaderView","HeaderView初始化了");
}
public void setHeaderState(LoadState state) {
if (mState == state) {
return;
}
mArrow.clearAnimation();
if (state == LoadState.LOADING) {
mArrow.setVisibility(View.GONE);
mProgressBar.setVisibility(View.VISIBLE);
} else {
mProgressBar.setVisibility(View.GONE);
mArrow.setVisibility(View.VISIBLE);
}
switch (state) {
case NORMAL:
mArrow.startAnimation(mRotateDown);
mRefreshTips.setText(R.string.g_pull_down_for_refresh);
break;
case WILL_RELEASE:
//旋转当前的箭头的状态
mArrow.startAnimation(mRotateUp);
mRefreshTips.setText(R.string.g_release_for_refresh);
break;
case LOADING:
mRefreshTips.setText(R.string.g_refreshing);
break;
default:
break;
}
mState = state;
}
public LoadState getCurrentState() {
return mState;
}
public void setHeaderHeight(int height) {
if (height <= 0) {
height = 0;
}
LayoutParams lp = (LayoutParams) mHeader.getLayoutParams();
lp.height = height;
mHeader.setLayoutParams(lp);
}
public int getHeaderHeight() {
return mHeader.getHeight();
}
我们可以看到HeaderView这个类很简单,就是初始化一个view,然后提供了设置高度,和不同状态的判断显示的不同字体的逻辑
我们创建FooterView这个类其实和heardview一样
/**
* 底部
*/
public class FooterView extends LinearLayout {
/** 加载更多. */
private LoadState mState = LoadState.NORMAL;
private View mFooter = null;
private ImageView mArrow = null;
private ProgressBar mProgressBar = null;
private TextView mLoaderTips = null;
private RotateAnimation mRotateUp = null;
private RotateAnimation mRotateDown = null;
private final static int ROTATE_DURATION = 250;
public FooterView(Context context) {
this(context, null);
}
public FooterView(Context context, AttributeSet attrs) {
super(context, attrs);
initFooterView(context);
}
private void initFooterView(Context context) {
LinearLayout.LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0);
mFooter = LayoutInflater.from(context).inflate(R.layout.g_loader_footer, null);
addView(mFooter, lp);
mArrow = (ImageView) mFooter.findViewById(R.id.ivLoaderArrow);
mProgressBar = (ProgressBar) mFooter.findViewById(R.id.pbLoaderWaiting);
mLoaderTips = (TextView) mFooter.findViewById(R.id.loader_tips);
mRotateDown = new RotateAnimation(0.0f, 180.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
mRotateDown.setDuration(ROTATE_DURATION);
mRotateDown.setFillAfter(true);
mRotateUp = new RotateAnimation(180.0f, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
mRotateUp.setDuration(ROTATE_DURATION);
mRotateUp.setFillAfter(true);
show();
}
public void setFooterState(LoadState state) {
if (mState == state) {
return;
}
mArrow.clearAnimation();
if (state == LoadState.LOADING) {
mProgressBar.setVisibility(View.VISIBLE);
mArrow.setVisibility(View.GONE);
} else if (state == LoadState.NODATA) {
mProgressBar.setVisibility(View.GONE);
mArrow.setVisibility(View.GONE);
} else {
mProgressBar.setVisibility(View.GONE);
mArrow.setVisibility(View.VISIBLE);
}
switch (state) {
case NORMAL:
mArrow.startAnimation(mRotateUp);
mLoaderTips.setText(R.string.g_pull_up_for_more);
break;
case WILL_RELEASE:
mArrow.startAnimation(mRotateDown);
mLoaderTips.setText(R.string.g_release_for_more);
break;
case LOADING:
mLoaderTips.setText(R.string.g_loading);
break;
case NODATA:
mLoaderTips.setText(R.string.g_nodata);
break;
default:
break;
}
mState = state;
}
public LoadState getCurrentState() {
return mState;
}
public void setFooterHeight(int height) {
if (height <= 0) {
height = 0;
}
LayoutParams lp = (LayoutParams) mFooter.getLayoutParams();
lp.height = height;
mFooter.setLayoutParams(lp);
}
public int getFooterHeight() {
return mFooter.getHeight();
}
}
逻辑也是一样,设置高度,提供不同状态的判断,显示不同的内容
接下来上我们的主要类XrefershListview
/**
* 作者:liujingyuan on 2015/12/21 13:14
* 邮箱:[email protected]
* 自定义的下拉刷新上拉加载的Listview
*/
public class XrefershListview extends ListView implements AbsListView.OnScrollListener {
private final static String TAG = "[XrefershListview]";
private final static float OFFSET_Y = 0.7f;
private OverScroller mScroller;
private final static int SCROLL_HEADER = 0;
private final static int SCROLL_FOOTER = 1;
//刷新的监听
public XrefershListviewListener mListViewListener;
/** 滑动项. */
private int iScrollWhich = SCROLL_HEADER;
//是否显示footview
public boolean isShowLoadeFooterView=false;
HeaderView mHeaderView;
FooterView mFooterView;
int iHeaderHeight;
int iFooterHeight;
float EndRawy;
float mStartY;
float dy;
private float mLastY;
public XrefershListview(Context context) {
super(context);
initHeardView(context);
}
public XrefershListview(Context context, AttributeSet attrs) {
super(context, attrs);
initHeardView(context);
}
public XrefershListview(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initHeardView(context);
}
//初始化头部的view
private void initHeardView(Context context) {
//初始化scmScroller
mScroller = new OverScroller(context,new DecelerateInterpolator());
mHeaderView = new HeaderView(context);
mFooterView = new FooterView(context);
//初始化hearview的高度
mHeaderView.setHeaderHeight(200);
//初始化footview
initFooterView(context);
final RelativeLayout header_content = (RelativeLayout) mHeaderView.findViewById(R.id.header_content);
addHeaderView(mHeaderView);
this.setOnScrollListener(this);
//监听到view加载完毕获取view的高度
mHeaderView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
iHeaderHeight =mHeaderView.getMeasuredHeight();
Log.d(TAG, "iHeaderHeight = " + iHeaderHeight);
//刚进来的时候我们隐藏heardview
mHeaderView.setPadding(0,-iHeaderHeight,0,0);
getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//按下的时候我们需要记住起始点的Y轴的坐标
mLastY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
//移动的时候获取当前的y轴的坐标
mStartY = ev.getY();
//计算滑动的距离
dy=mStartY-mLastY;
//再次获取一次,因为我们是增量增加宽度的,每次获取的是相对于上一点的位移
mLastY = ev.getY();
if (getFirstVisiblePosition()==0&& (mHeaderView.getMeasuredHeight() > 0 || dy > 0)){
//通过改变heardview的高度
upDataHeardView(dy*0.7f);
}else if (getLastVisiblePosition() == getCount() - 1 && (mFooterView.getFooterHeight() > 0 || dy < 0)){
//通过改变footview的高度
updateFooterState(-dy);
}
Log.e(TAG,"dy============"+dy);
break;
case MotionEvent.ACTION_UP:
EndRawy = ev.getRawY();
//如果当前的可见的第一个条目是0,并且当前的hreadview的高度不为0,滑动的距离大于0就证明是在下拉刷新
if (getFirstVisiblePosition()==0&& (mHeaderView.getMeasuredHeight() > 0 || dy > 0)){
//判断当前的状态不是正在刷新的状态就设置为刷新的状态
if (mHeaderView.getCurrentState() != LoadState.LOADING) {
if (mHeaderView.getHeaderHeight() > iHeaderHeight) {
mHeaderView.setHeaderState(LoadState.LOADING);
//调用是界面的刷新,延时1秒
mListViewListener.onRefresh();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
resetHeaderHeight();
}
},1000);
}
}
//如果当前已经显示到最后一个条目了就是上拉加载,并且当前footview的高度大于0,移动的距离也大于0
}else if(getLastVisiblePosition() == getCount() - 1 && (mFooterView.getFooterHeight() > 0 || dy < 0)){
//判断当前的状态不是正在刷新的状态就设置为刷新的状态
if (mFooterView.getCurrentState() != LoadState.LOADING) {
if (mFooterView.getFooterHeight() > iFooterHeight) {
mFooterView.setFooterState(LoadState.LOADING);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//调用是界面的刷新,延时1秒
mListViewListener.onLoadMore();
resetFooter();
}
},3000);
}
}
}
break;
default:
break;
}
return super.onTouchEvent(ev);
}
/**
* 更新底部footview的状态
*
* @param delta
*/
private void updateFooterState(float delta) {
if (null == mFooterView) {
return;
}
//设置footview的高度移动的时候,这个高度是增加每次移动的点相当于上一个点的位置的距离
mFooterView.setFooterHeight((int) (delta + mFooterView.getFooterHeight()));
//当前的状态不是在刷新的状态!
if (mFooterView.getCurrentState() != LoadState.LOADING) {
if (mFooterView.getFooterHeight() > iFooterHeight) {
//设置当前的显示的内容为松开加载更多
mFooterView.setFooterState(LoadState.WILL_RELEASE);
} else {
mFooterView.setFooterState(LoadState.NORMAL);
}
}
}
/**
* 初始化底部footview
*/
private void initFooterView(Context context) {
mFooterView = new FooterView(context);
mFooterView.setFooterHeight(200);
//隐藏footview
mFooterView.setPadding(0,0,0,-200);
addFooterView(mFooterView);
mFooterView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//获取测量的footview的高度
iFooterHeight = mFooterView.getMeasuredHeight();
getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
}
/**
* 重置footview
*/
private void resetFooter() {
//重置刷新状态
mFooterView.setFooterState(LoadState.NORMAL);
if (null == mFooterView) {
return;
}
int height = mFooterView.getMeasuredHeight();
if (height == 0) {
return;
}
int finalHeight = 0;
if (height > iFooterHeight && mFooterView.getCurrentState() != LoadState.NORMAL) {
finalHeight = iFooterHeight;
} else if (mFooterView.getCurrentState() == LoadState.LOADING) {
return;
}
iScrollWhich = SCROLL_FOOTER;
mScroller.startScroll(0, height, 0, finalHeight - height, 300);
invalidate();
}
/**
* 重置heardview的状态
*/
private void resetHeaderHeight() {
//重置刷新的状态
mHeaderView.setHeaderState(LoadState.NORMAL);
//获取测量得到的heardview的高度
int height = mHeaderView.getMeasuredHeight();
//如果获取当前的View的高度等于0代表是没有移动就不做处理
if (height == 0) // not visible.
return;
int finalHeight = 0;
//如果超过HeaderView高度,则回滚到HeaderView高度即可
if (height > iHeaderHeight && mHeaderView.getCurrentState() != LoadState.NORMAL) {
finalHeight = iHeaderHeight;
}
//标记当前是heardview在移动
iScrollWhich = SCROLL_HEADER;
//inalHeight - height, finalHeight代表heardview抬起的时候高度-heardview初始化的高度=heardview在y轴上移动的距离,280毫秒内移动完毕
//int startX,开始的位置
// int mLastY,y位置开始的位置
// int dx, x滑动的距离
// int dy, y滑动的距离
// int duration执行完毕需要的时间
mScroller.startScroll(0, height, 0, finalHeight - height, 300);
//手动调用刷新移动
invalidate();
}
/**
* 设置头部的view的高度,来达到下移的效果
*/
private void upDataHeardView(float dy) {
//如果当前的状态不是正在加载中,就改变状态
if (mHeaderView.getCurrentState() != LoadState.LOADING) {
//如果当前的heardview的高度大于原始的高度就代表用户下拉刷新了要该表状态,变成下拉刷新的状态
if (mHeaderView.getHeaderHeight() > iHeaderHeight) {
mHeaderView.setHeaderState(LoadState.WILL_RELEASE);
} else {
mHeaderView.setHeaderState(LoadState.NORMAL);
}
}
//移动距离等于move的距离加上heardview的高度
mHeaderView.setHeaderHeight((int) (dy + mHeaderView.getHeaderHeight()));
}
/**
* 设置刷新的监听
*/
public void setXrefershListviewListener(XrefershListviewListener l) {
mListViewListener = l;
}
//每次移动都会调用computeScroll
@Override
public void computeScroll() {
//判断是否滑动结束
if (mScroller.computeScrollOffset()) {
//代表是下拉刷新
if (iScrollWhich == SCROLL_HEADER) {
//获取到当前滚动的位置,来动态的设置mHeaderView的高度
mHeaderView.setHeaderHeight(mScroller.getCurrY());
//代表是上拉加载改变footview
} else if (iScrollWhich == SCROLL_FOOTER) {
//不断去改变footview的高度
mFooterView.setFooterHeight(mScroller.getCurrY());
}
invalidate();
}
}
//监听listview的滑动监听
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
//监听listview的滑动监听
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
}
上面的注释也非常的清楚,大体思路:
- 首先我们直接初始化HeaderView和FooterView然后设置她们的默认高度为200dp
- addHeaderView然后我们添加头部的view
- 设置addOnGlobalLayoutListener的监听mHeaderView.getMeasuredHeight();在回调的方法中获取测量的heardview的高度
- mHeaderView.setPadding(0,-iHeaderHeight,0,0);这步我们就隐藏了heardview,设置他距离上面的距离为-iHeaderHeight
- 然后我们重写onTouchEvent来实现具体的移动的逻辑,首先我们在ACTION_DOWN里面获取当前的坐标,然后在ACTION_MOVE中我们去计算滑动的距离,详情看代码的注释
- 通过 upDataHeardView(dy*0.7f);这个方法我们实现头部view下拉的效果
/**
* 设置头部的view的高度,来达到下移的效果
*/
private void upDataHeardView(float dy) {
//如果当前的状态不是正在加载中,就改变状态
if (mHeaderView.getCurrentState() != LoadState.LOADING) {
//如果当前的heardview的高度大于原始的高度就代表用户下拉刷新了要该表状态,变成下拉刷新的状态
if (mHeaderView.getHeaderHeight() > iHeaderHeight) {
mHeaderView.setHeaderState(LoadState.WILL_RELEASE);
} else {
mHeaderView.setHeaderState(LoadState.NORMAL);
}
}
//移动距离等于move的距离加上heardview的高度
mHeaderView.setHeaderHeight((int) (dy + mHeaderView.getHeaderHeight()));
}
- 然后我们在看ACTION_UP,抬起的时候我们去判断当前的是否是下拉刷新,如果是就直接调用onRefresh是刷新界面的数据在resetHeaderHeight我们重置头部view的状态
/**
* 重置heardview的状态
*/
private void resetHeaderHeight() {
//重置刷新的状态
mHeaderView.setHeaderState(LoadState.NORMAL);
//获取测量得到的heardview的高度
int height = mHeaderView.getMeasuredHeight();
//如果获取当前的View的高度等于0代表是没有移动就不做处理
if (height == 0) // not visible.
return;
int finalHeight = 0;
//如果超过HeaderView高度,则回滚到HeaderView高度即可
if (height > iHeaderHeight && mHeaderView.getCurrentState() != LoadState.NORMAL) {
finalHeight = iHeaderHeight;
}
//标记当前是heardview在移动
iScrollWhich = SCROLL_HEADER;
//inalHeight - height, finalHeight代表heardview抬起的时候高度-heardview初始化的高度=heardview在y轴上移动的距离,280毫秒内移动完毕
//int startX,开始的位置
// int mLastY,y位置开始的位置
// int dx, x滑动的距离
// int dy, y滑动的距离
// int duration执行完毕需要的时间
mScroller.startScroll(0, height, 0, finalHeight - height, 300);
//手动调用刷新移动
invalidate();
}
- 我们需要重写computeScroll这个方法来实现阻尼的效果
@Override
public void computeScroll() {
//判断是否滑动结束
if (mScroller.computeScrollOffset()) {
//代表是下拉刷新
if (iScrollWhich == SCROLL_HEADER) {
//获取到当前滚动的位置,来动态的设置mHeaderView的高度
mHeaderView.setHeaderHeight(mScroller.getCurrY());
//代表是上拉加载改变footview
} else if (iScrollWhich == SCROLL_FOOTER) {
//不断去改变footview的高度
mFooterView.setFooterHeight(mScroller.getCurrY());
}
invalidate();
}
}
底部的刷新逻辑和头部的都一样,这样我们就实现了上拉刷新下拉加载的效果了!
代码地址点击这