android 自定义listview实现下拉刷新(一)

时间:2020-12-16 19:42:09

今天准备写一个关于listview下拉刷新的,我的实现思路:

1:我们知道实现上拉刷新是在listview头部添加一个headerView,下拉刷新是listview底部添加一个view

2:当我们下拉的时候让headerview随着我们的手在屏幕上移动的距离headerview慢慢显示出来,我们如果在布局问题中如果使用过android:layout_marginTop="-100dp"或者android:paddingTop="-100dp",现在我写一个demo,

布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/ll_root"
android:background="#ff00ff"
>
<TextView
android:id="@+id/tv"
android:layout_width="fill_parent"
android:layout_height="100dp"
android:text="helloworld"
android:gravity="center"
android:textColor="#000000"
android:textSize="30sp"
/>
</LinearLayout>
</LinearLayout>
预览效果:

android  自定义listview实现下拉刷新(一)

现在写几行代码把它隐藏掉:

LinearLayout ll_root = (LinearLayout) findViewById(R.id.ll_root);
<span style="white-space:pre"></span>ll_root.measure(0, 0);
<span style="white-space:pre"></span>int measuredHeight = ll_root.getMeasuredHeight();
<span style="white-space:pre"></span>ll_root.setPadding(0, -measuredHeight, 0, 0);

现在再预览屏幕就是一片空白了,如果把ll_root.setPadding(0, 0, 0, 0);就显示正常了,使用到我们listview头部headerview是不是可以和这样一样控制它的显示,现在画一个图,对着这个图想通了就好写代码了,

android  自定义listview实现下拉刷新(一)


分析了这么多,我们开始新建一个android项目Refreshlistview,写一个RefreshListView类去继承ListView,

首先我们得做一些初始化的工作,先把头部的布局文件和底部加载更多布局写好,并初始化,因此需要再构造函数中写个init()方法了,而且我们一进来,headerview应该让它隐藏,init()方法代码如下:

private void init() {
headerView = View.inflate(getContext(), R.layout.layout_header, null);
initHeaderView(headerView);
headerView.measure(0, 0);
measuredHeaderViewHeight = headerView.getMeasuredHeight();
headerView.setPadding(0, -measuredHeaderViewHeight, 0, 0);//一进来让它隐藏
addHeaderView(headerView);//把头部view添加到listview中
initFooterView();//把底部view添加到listview中
}

/**
* 初始化底部view
*/
private void initFooterView() {
footerView = View.inflate(getContext(), R.layout.layout_footer, null);
footerView.measure(0, 0);//主动通知系统去测量该view;
measuredFooterViewHeight = footerView.getMeasuredHeight();
footerView.setPadding(0, -measuredFooterViewHeight, 0, 0);
addFooterView(footerView);
}

/**
* 初始化头部view
* @param headerView
*/
private void initHeaderView(View headerView) {
iv_arrow = (ImageView) headerView.findViewById(R.id.iv_arrow);
pb_rotate = (ProgressBar) headerView.findViewById(R.id.pb_rotate);
tv_state = (TextView) headerView.findViewById(R.id.tv_state);
tv_time = (TextView) headerView.findViewById(R.id.tv_time);
}

现在我们就要实现下拉的过程中慢慢的把headerview显示出来了,因此listview需要复写onTouchEvent()方法了,上面分析了当我们手指在屏幕上滑动的距离,得先记录你手指按下的点,然后move的点,因此onTouchEvent()方法就是这样了,

@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDowny = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
int distanceY = (int) (ev.getY()-mDowny);//表示在y轴上移动的距离
int paddingTop = -measuredHeaderViewHeight+distanceY;//算出headerview要显示的距离
headerView.setPadding(0, paddingTop, 0, 0);
break;
case MotionEvent.ACTION_UP:

break;
}
return super.onTouchEvent(ev);
}

现在运行起来看下效果,

android  自定义listview实现下拉刷新(一)

比如我按住在4这个位置向下拖动listview,当我再往上拖动的时候,发现你手指到了不是在原来的位置,可能到了7或者8的位置,这是一个问题,还有一个问题就是在move的时候我们是不是设置了headerview向上的距离也就是这行代码:

headerView.setPadding(0, paddingTop, 0, 0);
你会发现当你往上一直滑动的时候还会一直执行move中headerView.setPadding(0,paddingTop,0,0),打个比方吧,比如你headerview的高度是100,你往拖动了110,那么这个时候paddingTop是不是-100+110=10,它都超过headerview全部显示的时候了,因为headerview全部显示也就是paddingTop=0的时候,因此你一直执行headerView.setPadding()这个方法,对你功能没任何问题,但是确一直在执行,是不是我们要加个条件进行判断,这样就稍微提高了效率,至于加什么条件,我都说了,所以我们move中是这样写的

 if(paddingTop> -measuredHeaderViewHeight){
headerView.setPadding(0, paddingTop, 0, 0);
}

但是还是没有解决手指问题,这是因为listview本身就自带的滑动事件,因此我们需要把listview本身的touch事件不让它起作用,只是我们手指拖动headerview在移动发生变化,我们就需要在刚才的条件中return true了,

if(paddingTop> -measuredHeaderViewHeight){
headerView.setPadding(0, paddingTop, 0, 0);
return true;
}

这样手指滑动错位问题就解决了!现在问题又来了当我们滑动到底部,再往上滑动的时候,发现滑不上去,那是因此return true,把listview的滑动事件给禁止了,因此我们要知道什么时候才让listview滑动事件禁止,是不是只有我们可见条目=0的时候,才把禁止了,第一个可见条目不等于我们就让listview可以滑动,listview给我们提供了一个api方法:getFirstVisiblePosition

因此if条件要写这样:

if(paddingTop> -measuredHeaderViewHeight&&getFirstVisiblePosition()==0){
headerView.setPadding(0, paddingTop, 0, 0);
return true;
}

ok,这样bug也解决了,现在就是什么下拉刷新,松开刷新,正在刷新,这三个状态改怎么弄?

其实观察很简单的,只要你拖动的距离和headerview的高度进行对比,如果高度小于headerview的高度就是下拉刷新状态,大于headerview的高度就是松开刷新,当手指离开屏幕就要正在刷新,但是正在刷新有个问题,比如我手指拖动的距离小于headerview的高度,这个时候放手就是正在刷新了,就是直接隐藏了,因此正在刷新状态是只有在松开刷新状态之后才是正在刷新,要加条件对其控制,


现在就写三个变量表示其对于的刷新三种状态!  move中的关键代码:

 int distanceY = (int) (ev.getY()-mDowny);//表示在y轴上移动的距离
int paddingTop = -measuredHeaderViewHeight+distanceY;//算出headerview要显示的距离
 if(paddingTop> -measuredHeaderViewHeight&&getFirstVisiblePosition()==0){
headerView.setPadding(0, paddingTop, 0, 0);
 if(paddingTop>0){
              mCurrentState = RELEASE_REFRESH;
}else if(paddingTop<0){
 mCurrentState = PULL_REFRESH;
 }
return true;
}

这里又有个和那个刷新一样的问题,当时处于下拉刷新状态,我一直拉还是会执行这个赋值方法 mCurrentState = PULL_REFRESH;是不是不需要啊,因为我把mCurrentState 初始化状态就是PULL_REFRESH状态,是不是当松开状态转变成下拉刷新状态我再执行这个方法,

if(paddingTop> -measuredHeaderViewHeight&&getFirstVisiblePosition()==0){
headerView.setPadding(0, paddingTop, 0, 0);
if(paddingTop>0&&mCurrentState==PULL_REFRESH){
mCurrentState = RELEASE_REFRESH;
}else if(paddingTop<0&&mCurrentState==RELEASE_REFRESH){
mCurrentState = PULL_REFRESH;
}
return true;
}

因此在这需要加个条件,也是优化了点效率,这个时候就需要根据刷新的状态改变UI了,

private void refreshUI(int state) {
switch (state) {
case PULL_REFRESH:
tv_state.setText("下拉刷新");
iv_arrow.startAnimation(downAnimation);
break;
case RELEASE_REFRESH:
tv_state.setText("松开刷新");
iv_arrow.startAnimation(upAnimation);
break;
case REFRESHING:
iv_arrow.clearAnimation();//因为向上的旋转动画有可能没有执行完
iv_arrow.setVisibility(View.INVISIBLE);
pb_rotate.setVisibility(View.VISIBLE);
tv_state.setText("正在刷新...");
break;
}
}
关于headerview中的进度条旋转动画在这就不讲了,

我们进一步完善这个功能,现在还差一个正在刷新功能了,刚才上面分析了正在刷新的状态是怎么来的,

case MotionEvent.ACTION_UP:
if(mCurrentState ==RELEASE_REFRESH){
mCurrentState = REFRESHING;
refreshUI(mCurrentState);

}
if(mCurrentState == PULL_REFRESH){//当是下拉刷新的状态手指离开这个时候是隐藏headerview
headerView.setPadding(0, -measuredHeaderViewHeight, 0, 0);
}
break;

当时松开状态进去到正在刷新,一般正在刷新是去请求服务器,那你知道什么时候加载数据完成么,listview肯定不知道,所以要用到回调了,

public interface PullRefreshListener{
void onRefresh();
void onLoadMore();
}

因此我们只要在手指(UP)的时候去调用onRefresh(),等数据加载完成再去真正调这个方法,我们现在通过handler去模拟请求网络

refresh_listview.setPullRefreshListener(new PullRefreshListener() {
@Override
public void onRefresh() {
new Handler().postDelayed(new Runnable() {//模拟网络请求
@Override
public void run() {
datas.add(0,"网络请求成功!");
adapter.notifyDataSetChanged();
refresh_listview.completeRefresh();

}
}, 5000);
}
@Override
public void onLoadMore() {

}
});

当完成了加载就调用completeRefresh()方法,这个时候就要把一些状态重置下了,

public void completeRefresh() {
//重置headerView状态
headerView.setPadding(0, -measuredHeaderViewHeight, 0, 0);
mCurrentState = PULL_REFRESH;//回到下拉刷新状态
iv_arrow.setVisibility(View.VISIBLE);//回到下拉刷新状态
pb_rotate.setVisibility(View.INVISIBLE);//回到下拉刷新状态
tv_state.setText("下拉刷新");
tv_time.setText("最后刷新:"+getCurrentTime());
}

不过这个时候有个小bug要处理下,当正处于正在刷新状态还是可以拖动的,所以我们在move的时候先判断是否处于正在刷新,如果处于正在刷新就把让move不执行了,break;就行

现在就是滑动到底部自动加载更多,这个功能简单,因为listview给我们提供了滚动监听,所以只要实现下OnScrollListener只要在onScrollStateChanged()方法中做个判断就ok,然后也是模拟网络请求

@Override
public void onScrollStateChanged(AbsListView arg0, int scrollState) {
if(scrollState==OnScrollListener.SCROLL_STATE_IDLE && getLastVisiblePosition()==(getCount()-1) &&!isLoadingMore){
isLoadingMore = true;
footerView.setPadding(0, 0, 0, 0);//显示出footerView
setSelection(getCount());//让listview最后一条显示出来
if(pullRefreshListener!=null){
pullRefreshListener.onLoadMore();
}
}
}

发现市面上有很多时当在加载的时候是不让加载更多的,当加载更多不能进行下拉刷新的,这个就是加个条件去判断就行了,在这就不弄了!写了一上午,手疼的不行了,要休息了!到此结束