本文详细讲解了下拉刷新、上拉加载控件的写法,如有需要完整代码可以到我的资源下载
先说一下心得:
这是一个自定义控件,自定义控件分为两种:
一种是完全人为定义的,比如说常见的开关按钮,这个是原生
控件没有的;另一种是在原生的控件上添加一些新的功能,比如说上拉加载下拉刷新控件就是继承了ListView而增加了一些新功能
构造函数——一个参数的构造函数用于java代码文件使用控件 两个参数的构造函数用于布局文件使用控件 一般这两个构造函数都重写一下
ListView可以添加头布局和脚布局
this.addHeaderView(headerView);
如何隐藏头布局★关键点 headerView.setPadding(0, -headerViewHeight, 0, 0);
如何得到头布局的高度headerView.measure(0, 0);// 给0,0是让系统框架去帮我们测量头布局的高度
headerViewHeight = headerView.getMeasuredHeight();// 获
得一个测量后的高度,注意:只有在measure方法调用完毕后才能得到具体的高度
Animation动画 setFillAfter(true);// ★让控件定格在动画结束的状态,否则就自己回到原先状态了
ListView的触摸事件onTouchEvent(关键点★) 在这里完成主要的逻辑,写代码的时候,要一点一点尝试,直达调出想要的效果,不是一蹴而就的 另外 ★★★★很重要★★★★return true,表示按照我们的意愿去处理触摸事件,否则return super.onTouchEvent(ev);// listview默认处理滑动效果
★★★我们可以模仿设置点击事件的效果来给本自定义控件设置一个刷新事件的监听器,里面使用了接口,强制用户重写抽象方法,来实现用户自己想要的效果
ListView有一个OnScrollListener用来监听滚动状态 当前滚动状态分为3种: IDLE 停止状态 TOUCH_SCROLL 手指触摸在屏幕上滚动 FLING 手指快速滑动 有惯性
MainActivity
package com.example.pullrefreshlistviewdemo;
import java.util.ArrayList;
import java.util.List;
import com.example.pullrefreshlistviewdemo.view.RefreshListView;
import com.example.pullrefreshlistviewdemo.view.RefreshListView.OnRefreshListener;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends Activity {
private RefreshListView mListView;
private List<String> dataList;
public Context context = MainActivity.this;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (RefreshListView) findViewById(R.id.refresh_listview);
// 模拟数据
dataList = new ArrayList<String>();
for (int i = 0; i < 30; i++) {
dataList.add("Listview数据" + i);
}
// 上拉刷新控件就是一个改良的listview,所以像使用listview一样配置适配器
final MyAdapter adapter = new MyAdapter();
mListView.setAdapter(adapter);
// 设置一个当ListView刷新时的监听
mListView.setOnRefreshListener(new OnRefreshListener() {
@Override
public void onPullDownRefresh() {
Toast.makeText(context, "下拉刷新数据啦啦啦", 0).show();
// 模拟从网络请求下来的数据
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// 延时3秒后执行
dataList.add(0, "我是下拉刷新出来的数据...");
adapter.notifyDataSetChanged();
// 隐藏头布局
mListView.onRefreshFinish();
}
}, 3000);
}
@Override
public void onLoadingMore() {
// TODO Auto-generated method stub
Toast.makeText(context, "开始加载更多", 0).show();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// 模拟家加载更多的数据
dataList.add("我是上拉加载更多出来的数据1...");
dataList.add("我是上拉加载更多出来的数据2...");
dataList.add("我是上拉加载更多出来的数据3...");
dataList.add("我是上拉加载更多出来的数据4...");
dataList.add("我是上拉加载更多出来的数据5...");
// 隐藏脚布局
mListView.onRefreshFinish();
// ★为了让屏幕自动完全显示出所有加载更多出来的数据,要设置一个大于listview条目的数字,就会直接显示脚部局(否则要再拉一次listview才会显示新加载出来的数据)
mListView.setSelection(mListView.getCount());
}
}, 3000);
}
});
}
class MyAdapter extends BaseAdapter {
@Override
public int getCount() {
return dataList.size();
}
@Override
public Object getItem(int arg0) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView tv = new TextView(context);
tv.setText(dataList.get(position));
tv.setTextSize(18);
tv.setTextColor(Color.BLACK);
tv.setPadding(0, 5, 0, 5);
return tv;
}
}
}
RefreshListView
package com.example.pullrefreshlistviewdemo.view;
import java.text.SimpleDateFormat;
import java.util.Date;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.example.pullrefreshlistviewdemo.R;
/** * 自定义Listview的下拉刷新和加载更多 * */
public class RefreshListView extends ListView implements OnScrollListener {
/** * 头布局的高度 */
private int headerViewHeight;
/** * 头布局对象 */
private View headerView;
/** * 按下时Y轴的偏移量 */
private int downY;
/** * 头布局状态:下拉刷新 */
private final int DOWN_PULL = 0;
/** * 头布局状态:释放刷新 */
private final int RELEASE_REFRESH = 1;
/** * 头布局状态:正在刷新中 */
private final int REFRESHING = 2;
/** * 头布局当前的状态:默认为下拉刷新 */
private int currentState = DOWN_PULL;
/** * 头布局向上旋转的动画 */
private RotateAnimation upRotateAnimation;
/** * 头布局向下旋转的动画 */
private RotateAnimation downRotateAnimation;
/** * 头布局的箭头 */
private ImageView ivArrow;
/** * 头布局的进度圈 */
private ProgressBar myProgressBar;
/** * 头布局的刷新状态 */
private TextView tvState;
/** * 头布局的最后刷新时间 */
private TextView tvLastUpdateTime;
/** * ★★★使用者的回调事件 */
private OnRefreshListener mOnRefreshListener;
/** * 脚部局对象 */
private View footerVeiw;
/** * 脚部局高度 */
private int footerViewHeight;
/** * 是否正在加载更多中。。。,默认不是正在加载中 */
private boolean isLoadingMore = false;
public RefreshListView(Context context, AttributeSet attrs) {// 两个参数的构造函数用于布局文件使用控件
super(context, attrs);
// 布局文件声明这个自定义控件,必须写这个带AttributeSet的构造函数
initHeadView();
initFootView();
// 设置listview滚动状态的监听器
setOnScrollListener(this);
}
public RefreshListView(Context context) {// 一个参数的构造函数用于java代码文件使用控件
super(context);
// 用于代码new自定义控件
initHeadView();
initFootView();
// 设置listview滚动状态的监听器
setOnScrollListener(this);
}
/** * 初始化脚部局 */
private void initFootView() {
footerVeiw = View.inflate(getContext(), R.layout.listview_footer, null);
// 设置脚部局的padding为自己高度的负数(即默认隐藏脚部局)
footerVeiw.measure(0, 0);
footerViewHeight = footerVeiw.getMeasuredHeight();
footerVeiw.setPadding(0, -footerViewHeight, 0, 0);
this.addFooterView(footerVeiw);
}
/** * 初始化ListView下拉刷新头 */
private void initHeadView() {
headerView = View.inflate(getContext(), R.layout.listview_header, null);
ivArrow = (ImageView) headerView
.findViewById(R.id.iv_listview_header_arrow);
myProgressBar = (ProgressBar) headerView
.findViewById(R.id.pb_listview_header);
tvState = (TextView) headerView
.findViewById(R.id.tv_listview_header_state);
tvLastUpdateTime = (TextView) headerView
.findViewById(R.id.tv_listview_header_last_update_time);
tvLastUpdateTime.setText("最后刷新时间:" + getCurrentTime());
// 取到头布局的高度
headerView.measure(0, 0);// 给0,0是让系统框架去帮我们测量头布局的高度
headerViewHeight = headerView.getMeasuredHeight();// 获得一个测量后的高度,注意:只有在measure方法调用完毕后才能得到具体的高度
// 隐藏头布局,-paddingTop
headerView.setPadding(0, -headerViewHeight, 0, 0);
// 把头布局加到listview的头部
this.addHeaderView(headerView);
initAnimation();
}
/** * 初始化动画,主要是让箭头去按照此动画旋转 */
private void initAnimation() {
upRotateAnimation = new RotateAnimation(0, -180,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
upRotateAnimation.setDuration(500);
upRotateAnimation.setFillAfter(true);// ★让控件定格在动画结束的状态,否则就自己回到原先状态了
downRotateAnimation = new RotateAnimation(-180, -360,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
downRotateAnimation.setDuration(500);
downRotateAnimation.setFillAfter(true);// ★让控件定格在动画结束的状态,否则就自己回到原先状态了
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
// TODO Auto-generated method stub
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
// 当前的状态是否是正在刷新中,如果是,直接跳出,执行listview自身的触摸事件,而不再走我们自定义的触摸事件(否则头布局会一直往下拉,体验不好)
if (currentState == REFRESHING) {
break;
}
int moveY = (int) ev.getY();
// 间距=移动y-按下y
int diffY = moveY - downY;
// 计算头布局最新的paddingTop=-头布局高度+间距
int paddingTop = -headerViewHeight + diffY;
// 如果paddingTop值<-headerViewHeight,不进行下拉刷新头的滑动操作
// 并且顶部第一个显示的条目的索引为0时,才可以进行滑动
// 获取listview的顶部第一个显示条目的索引
int firstVisiblePosition = getFirstVisiblePosition();
// 总之,必须是满足下列两个条件,才可以下拉头布局
// 1.paddingTop必须大于-headerViewHeight(这个需要在理解一下,只有大于才有需求通过向下滑动让其减少到-headerViewHeight,达到隐藏效果),
// 否则会发现只能看到第一屏的数据,向上拉,不会显示listview下面的条目
// 当你向上滑动时,你的paddingTop已经小于-headerViewHeight就意味着头布局已经隐藏了,没有必要将再小的值设置给头布局,让他深入隐藏,
// 这倒是其次,最主要的是,向上滑动的事件屏蔽了listview自身的向上滑动显示下面条目的需求,这样就永远再设置paddingTop,而看不到listview下面的条目
// 2.第一个显示的条目不是0,下拉时,不会下拉头布局,只是响应listview自身的下拉事件,否则会发现当你向下拉倒第二个条目后,想要在往上拉时,listview前面的条目永远拉不下来
if (paddingTop > -headerViewHeight && firstVisiblePosition == 0) {
if (paddingTop > 0 && currentState == DOWN_PULL) {// ★头布局完全显示,★并且当前状态是下拉刷新,进入松开刷新状态
System.out.println("松开刷新状态");
currentState = RELEASE_REFRESH;// ★保证了进入松开刷新状态只执行一次,否则箭头会一直转圈
refreshHeaderViewState();
} else if (paddingTop < 0 && currentState == RELEASE_REFRESH) {// ★头布局没有完全显示,★并且当前状态是松开刷新,进入下拉刷新状态
System.out.println("下拉刷新状态");
currentState = DOWN_PULL;// ★保证了进入下拉刷新状态只执行一次,否则箭头会一直转圈
refreshHeaderViewState();
}
// 动态改变头布局的padding值,即头布局的隐藏高度
headerView.setPadding(0, paddingTop, 0, 0);
return true;// 自己处理用户的触摸滑动事件
}
break;
case MotionEvent.ACTION_UP:
// 松开手时,要根据当前头布局的状态,作出相应的动作
if (currentState == DOWN_PULL) {
// 当前状态是在下拉刷新状态下松开了,什么都不做,把头布局隐藏就可以了
headerView.setPadding(0, -headerViewHeight, 0, 0);
} else if (currentState == RELEASE_REFRESH) {
// 当前状态属于释放刷新下松开了,应该把头布局正常显示,把currentState改为正在刷新状态
headerView.setPadding(0, 0, 0, 0);
currentState = REFRESHING;
refreshHeaderViewState();
// ★★★调用用户的监听事件(认真体会回调的写法)
if (mOnRefreshListener != null) {
mOnRefreshListener.onPullDownRefresh();
}
}
break;
}
return super.onTouchEvent(ev);// listview默认处理滑动效果
}
/** * 根据当前的状态currentState来刷新头布局的状态 */
private void refreshHeaderViewState() {
switch (currentState) {
case DOWN_PULL:// 下拉刷新
ivArrow.startAnimation(downRotateAnimation);
tvState.setText("下拉刷新");
break;
case RELEASE_REFRESH:// 松开刷新
ivArrow.startAnimation(upRotateAnimation);
tvState.setText("松开刷新");
break;
case REFRESHING:// 正在刷新中
ivArrow.clearAnimation();// 清除自己身上的所有动画
ivArrow.setVisibility(View.INVISIBLE);
myProgressBar.setVisibility(View.VISIBLE);
tvState.setText("正在刷新中...");
break;
}
}
/** * 刷新完成,用户调用此方法,把对应的★★★★★★★★头布局或者★★★★★★★脚布局隐藏掉,这里要做很多事情 */
public void onRefreshFinish() {
if (isLoadingMore) {// 当前属于加载更多状态
// 隐藏脚部局
footerVeiw.setPadding(0, -footerViewHeight, 0, 0);
// 别忘了把isLoadingMore置为false,否则再也不会进入加载更多了
isLoadingMore = false;
} else {
// 隐藏头布局
headerView.setPadding(0, -headerViewHeight, 0, 0);
// 改变当前状态为下拉刷新
currentState = DOWN_PULL;
// 进度圈不可见
myProgressBar.setVisibility(View.INVISIBLE);
// 箭头可见
ivArrow.setVisibility(View.VISIBLE);
// 把文本改为“下拉刷新”
tvState.setText("下拉刷新");
// 更改最后刷新时间
tvLastUpdateTime.setText("最后刷新时间:" + getCurrentTime());
}
}
/** * 获取最后刷新时间 * * @return */
private String getCurrentTime() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(new Date());
}
/** * ★★★提供给使用者设置刷新的监听事件 */
public void setOnRefreshListener(OnRefreshListener listener) {
mOnRefreshListener = listener;
}
/** * ★★★当ListView刷新时的监听事件 */
public interface OnRefreshListener {
/** * ★★★当下拉刷新时回调此方法 */
public void onPullDownRefresh();
/** * ★★★当上拉加载更多时回调此方法 */
public void onLoadingMore();
}
@Override
public void onScroll(AbsListView arg0, int arg1, int arg2, int arg3) {
// TODO Auto-generated method stub
}
/** * 当滚动状体改变时,触发此方法 scrollState 指的是当前的滚动状态 * * 当前滚动状态分为3种: IDLE 停止状态 TOUCH_SCROLL 手指触摸在屏幕上滚动 FLING 手指快速滑动 有惯性 */
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// 当前的状态是★停止或者★快速滑动时,且屏幕上显示的最后一个条目为(条目总数-1)时,才加载更多
if ((scrollState == OnScrollListener.SCROLL_STATE_IDLE || scrollState == OnScrollListener.SCROLL_STATE_FLING)
&& getLastVisiblePosition() == (getCount() - 1)
&& !isLoadingMore) {
Toast.makeText(getContext(), "可以加载更多了", 0).show();
// 为了防止已经正在加载了,还会再次出发正在加载,设置了一个状态,并当已经开始加载时改变加载状态
isLoadingMore = true;
// 显示脚部局
footerVeiw.setPadding(0, 0, 0, 0);
// ★★★★★★★★★如果想实现上拉直接显示新数据,而不显示脚布局,只要把padding设为-footerViewHeight即可
// footerVeiw.setPadding(0, -footerViewHeight, 0, 0);
// ★为了让屏幕自动显示出脚部局,要设置一个大于listview条目的数字,就会直接显示脚部局(否则要再拉一次listview才会显示脚部局)
setSelection(getCount());
if (mOnRefreshListener != null) {
mOnRefreshListener.onLoadingMore();
}
}
}
}
布局
activity_main
<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" >
<com.example.pullrefreshlistviewdemo.view.RefreshListView
android:id="@+id/refresh_listview"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
</com.example.pullrefreshlistviewdemo.view.RefreshListView>
</RelativeLayout>
listview_header
<?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:orientation="horizontal" >
<!-- 帧布局 箭头和圆圈 -->
<FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="5dip" >
<ImageView android:id="@+id/iv_listview_header_arrow" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:src="@drawable/common_listview_headview_red_arrow" />
<ProgressBar android:indeterminateDrawable="@drawable/custom_progressbar" android:id="@+id/pb_listview_header" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" />
</FrameLayout>
<LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:gravity="center_horizontal" android:orientation="vertical" >
<TextView android:id="@+id/tv_listview_header_state" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下拉刷新" android:textColor="#FF0000" android:textSize="20sp" />
<TextView android:id="@+id/tv_listview_header_last_update_time" android:layout_marginTop="5dip" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="最后刷新时间:2015-11-12" android:textSize="16sp" android:textColor="@android:color/darker_gray" />
</LinearLayout>
</LinearLayout>
listview_footer
<?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" >
<ProgressBar android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="5dip" android:indeterminateDrawable="@drawable/custom_progressbar" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:text="加载更多中..." android:textColor="#FF0000" android:textSize="25sp" />
</LinearLayout>
custom_progressbar
<rotate xmlns:android="http://schemas.android.com/apk/res/android" android:fromDegrees="0" android:pivotX="50%" android:pivotY="50%" android:toDegrees="360" >
<!-- 定义一个环形,注意半径比率的意思为,当前控件的宽度除以3为半径,例如控件宽100,比率为3,则半径为33.333 注意所有控件都是矩形 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:innerRadiusRatio="3" android:shape="ring" android:thicknessRatio="10" android:useLevel="false" >
<!-- 渐变色,指定圆环的起始颜色和type -->
<gradient android:centerColor="#FF6A6A" android:endColor="#FF0000" android:startColor="#FFFFFF" android:type="sweep" />
</shape>
</rotate>