自定义控件实现ListView下拉刷新和上拉加载
下拉刷新和上拉加载时一个很常用的功能,刚好今天学了,好好的总结一把!
下拉刷新实现思路:
- 第一步:创建一个类继承ListView
- 第二步:写一个头部,添加到listview中,先将其隐藏
- 第三步:设置监听触屏事件,判断是否滑动到顶部
- 第四步:当到顶部的时候,通过下拉的位移来设置头部的显示高度,并根据头部显示的高度来设置对应的文字和动画效果
- 第五步:当松手时,判断需要进行哪一种更新并进行相应的操作
上拉加载实现思路:
由于两个功能是写在一个类里面的,这里就不需要再创建一个类了。
- 第一步:写一个尾部,添加到listview中,先将其隐藏
- 第二步:设置监听滚动监听事件,判断是否滑动到底部
- 第三部:加载数据
具体代码实现
代码里面思路也很清楚,而且注释很多,相信很容易看明白。
自定义控件类:
public class PullToRefresh extends ListView {
private int downY;
private View header;
private int headerHeight;
private ImageView ivArrow;
private TextView tvTitle;
private TextView tvTime;
private RotateAnimation downAnin;
private RotateAnimation upAnin;
private boolean isLoading = false;
private OnPullToRefreshListener onPullToRefreshListener;
private static final int PULL_TO_REFRESH = 1; // 下拉刷新状态
private static final int RELEASE_TO_REFRESH = 2; // 释放刷新状态
private static final int REFRESHING = 3; // 正在刷新状态
public PullToRefresh(Context context, AttributeSet attrs) {
super(context, attrs);
addHeader();
initComponent();
addFooter();
// 设置滑动监听事件
setOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
int count = view.getAdapter().getCount(); // 获取条目数
switch (scrollState) {
case OnScrollListener.SCROLL_STATE_FLING:
break;
case OnScrollListener.SCROLL_STATE_IDLE:
// 在闲置状态时判断是否到达了底部
if (getLastVisiblePosition() == count - 1 && !isLoading) {
footer.setPadding(0, 0, 0, 0);
setSelection(count - 1);
isLoading = true;
if (onPullToRefreshListener != null) {
onPullToRefreshListener.loadMore();
}
}
break;
case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
break;
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
}
});
}
/**
* 为控件添加一个尾部
* */
private void addFooter() {
footer = View.inflate(getContext(), R.layout.footer, null);
footer.measure(0, 0); // 手动测量尾部的高度,否则在这里无法获取到尾部的高度
footerrHeight = header.getMeasuredHeight();
footer.setPadding(0, -footerrHeight, 0, 0); // 通过设置尾部的paddingTop来将尾部先隐藏
addFooterView(footer);
}
/**
* 设置接口的引用
* */
public void setOnPullToRefreshListener(
OnPullToRefreshListener onPullToRefreshListener) {
this.onPullToRefreshListener = onPullToRefreshListener;
}
/**
* 初始化组件找到它们
* */
private void initComponent() {
ivArrow = (ImageView) findViewById(R.id.iv_arrow);
ivLeft = (ImageView) findViewById(R.id.iv_left);
tvTitle = (TextView) findViewById(R.id.tv_title);
tvTime = (TextView) findViewById(R.id.tv_time);
upAnin = new RotateAnimation(0, 180, RotateAnimation.RELATIVE_TO_SELF,
0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f);
upAnin.setDuration(200);
upAnin.setFillAfter(true);// 动画完成时停留在那里
downAnin = new RotateAnimation(180, 0,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
downAnin.setDuration(200);
downAnin.setFillAfter(true);// 动画完成时停留在那里
loadingAnim = new RotateAnimation(0, 360,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
loadingAnim.setRepeatCount(RotateAnimation.INFINITE);
loadingAnim.setRepeatMode(RotateAnimation.RESTART);
loadingAnim.setInterpolator(new LinearInterpolator());
loadingAnim.setDuration(1000);
}
/**
* 第一步:为listview添加一个头部
* */
private void addHeader() {
header = View.inflate(getContext(), R.layout.header, null);
header.measure(0, 0); // 手动测量头部的高度,否则在这里无法获取到头部的高度
headerHeight = header.getMeasuredHeight();
header.setPadding(0, -headerHeight, 0, 0); // 通过设置头部的paddingTop来将头部先隐藏
addHeaderView(header);
}
// 第二步:监听触屏事件,判断是否滑动到顶部并进行相应的操作
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
// 记录下当前滑动的垂直坐标
int moveY = (int) ev.getY();
int dy = moveY - downY; // 垂直位移
downY = moveY;
int paddingTop = header.getPaddingTop();
// 不是正在刷新状态、到达头部并且头部在显示的时候
if (state != REFRESHING // 不是正在刷新状态
&& getFirstVisiblePosition() == 0 // 到达头部
&& (dy > 0 // 下滑
|| paddingTop > -headerHeight)) { // 上滑,头部在显示
header.setPadding(0, paddingTop + dy, 0, 0); // 设置头部显示状态
/**
* 第三步:通过下拉的高度来判断进行何种刷新动作 1.头部没有完全露出来,进行下拉刷新 2.头部完全露出来,则进行释放刷新
* */
if (paddingTop >= 0) {
// 进入释放刷新状态
setState(RELEASE_TO_REFRESH);
} else {
// 进入下拉刷新状态
setState(PULL_TO_REFRESH);
}
return true;
}
break;
case MotionEvent.ACTION_UP:
/**
* 第四步:当松开手之后,判断需要进行哪种刷新 这里我们在resetHeader()方法里面进行判断
*
* */
resetHeader();
break;
default:
break;
}
return super.onTouchEvent(ev);
}
/**
* 设置刷新状态
* */
private int state = PULL_TO_REFRESH; // 记录当前状态
private ImageView ivLeft;
private RotateAnimation loadingAnim;
private View footer;
private int footerrHeight;
private void setState(int state) {
if (this.state != state) {
if (state == PULL_TO_REFRESH) {
tvTitle.setText("下拉刷新");
ivArrow.startAnimation(downAnin);
} else if (state == RELEASE_TO_REFRESH) {
tvTitle.setText("释放刷新");
ivArrow.startAnimation(upAnin);
} else if (state == REFRESHING) {
tvTitle.setText("正在刷新");
tvTime.setText("最后刷新时间:"
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
ivArrow.clearAnimation();// 清除箭头动画
ivArrow.setVisibility(View.GONE);// 将箭头隐藏
ivLeft.setVisibility(View.VISIBLE); // 将正在刷新图标显示出来
ivLeft.startAnimation(loadingAnim);// 开始刷新动画
}
this.state = state;
}
}
/**
* 下拉刷新操作完成
* */
public void refreshFinish() {
ivLeft.clearAnimation();
ivLeft.setVisibility(View.GONE);
ivArrow.setVisibility(View.VISIBLE);
setState(PULL_TO_REFRESH);
header.setPadding(0, -headerHeight, 0, 0);// 重新隐藏头
}
/**
* 上拉加载操作完成
* */
public void loadMoreFinish() {
isLoading = false;
footer.setPadding(0, -footerrHeight, 0, 0);
}
/**
* 重新设置头部
*
* */
private void resetHeader() {
if (state == PULL_TO_REFRESH) {
header.setPadding(0, -headerHeight, 0, 0); // 重新将头部隐藏
} else if (state == RELEASE_TO_REFRESH) {
header.setPadding(0, 0, 0, 0); // 刚好将头部完全显示出来
setState(REFRESHING); // 设置为正在刷新状态
// 开始加载数据
if (onPullToRefreshListener != null) {
onPullToRefreshListener.refresh();
}
}
}
public interface OnPullToRefreshListener {
public void refresh(); // 刷新
public void loadMore(); // 加载更多
}
}
总结一下写的过程中碰到的坑:
- 由于我们添加头部和尾部的操作是在构造方法里面进行的,系统还没来得及为我们测量控件的高宽,因此我们在获取控件高宽的时候必须自己调用
measure()
方法来测量,否则获取到的控件高宽始终为0; - 在监听触屏事件中,我们做了一个操作:
downY = moveY;
这样我们所获取到的垂直位移才是实时移动的位移。 - 在
if (state != REFRESHING && getFirstVisiblePosition() == 0 && (dy > 0 || paddingTop > -headerHeight))
这语句中,paddingTop > -headerHeight
这句是用来判断向上滑动并且头部处于显示状态。如果没有这个判断,我们高频率的小幅度滑动屏幕时,会发现头部上面的空白区域会不断的增大。 - 在
setState(int state)
方法处,我们需要设置一个变量来记录当前控件所处的状态,并通过这个变量状态来决定是否执行方法里面的代码,否则你会发现,当你下拉控件的时候,那个指示图标会不停的上下转动,那是它在不断的反复执行动画的效果。 - 加载数据属于耗时操作,必须在子线程中进行,否则会出现“卡卡”的现象,反正我由于忘记了这个东东找了好长时间。
- 在刷新完成之后,即在
refreshFinish()
方法里面,我们需要进行动画清除,不然再次下拉的时候会出现图标“残影”,还要将控件状态还原到下拉刷新状态setState(PULL_TO_REFRESH)
,不然保证你拉不了第二次,就这么给力。 - 最后一点,为了降低代码之间的耦合性,用到了接口回调,那么,我们在需要加载数据的时候(控件类代码中)必须先进行非空判断,不然空指针异常等着你。
header.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal" >
<ImageView
android:id="@+id/iv_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher"
android:visibility="gone"/>
<ImageView
android:id="@+id/iv_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/common_listview_headview_red_arrow"/>
<LinearLayout
android:layout_marginLeft="10dp"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textColor="#f00"
android:text="下拉刷新"/>
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#f00"
/>
</LinearLayout>
</LinearLayout>
footer.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="horizontal" >
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_gravity="center_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="正在加载中..."/>
</LinearLayout>
在其他项目里面测试的代码:
布局文件activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="${relativePackage}.${activityClass}" >
<com.example.pulltorefreshdemo.PullToRefresh
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/ptrlist">
</com.example.pulltorefreshdemo.PullToRefresh>
</RelativeLayout>
MainActivity
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final PullToRefresh ptr = (PullToRefresh) findViewById(R.id.ptrlist);
final List<String> data = getData();
final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, android.R.id.text1,data);
ptr.setAdapter(adapter);
/**
* 刷新加载数据,接口回调,大大降低了代码间的耦合性
* */
ptr.setOnPullToRefreshListener(new OnPullToRefreshListener() {
@Override
public void refresh() {
//加载数据属于耗时操作,需要开启子线程
new Thread(new Runnable() {
public void run() {
SystemClock.sleep(1000); //模拟数据
String newData = "下拉刷新更多的数据";
data.add(0,newData);
//更新UI的动作只能在主线程中完成
runOnUiThread(new Runnable() {
public void run() {
adapter.notifyDataSetChanged();
ptr.refreshFinish();
}
});
}
}).start();
}
@Override
public void loadMore() {
// TODO Auto-generated method stub
new Thread(new Runnable() {
public void run() {
SystemClock.sleep(1000);
String newData = "上拉加载更多的数据";
data.add(newData);
//更新UI的动作只能在主线程中完成
runOnUiThread(new Runnable() {
public void run() {
adapter.notifyDataSetChanged();
ptr.loadMoreFinish();
}
});
}
}).start();
}
});
}
/**
* 初始化数据
* */
private List<String> getData() {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 15; i++) {
list.add("原始数据"+i);
}
return list;
}
}