(本文讲解了在Android中实现列表下拉刷新的动态效果的过程,文末附有源码。)

看完本文,您可以学到:


1.下拉刷新的实现原理

2.自定义Android控件,重写其ListView

3.ScrollListener滚动监听

4.Adapter适配器的使用


话不多说,先来看看效果图:

Android 轻松实现仿QQ消息下拉刷新_第1张图片Android 轻松实现仿QQ消息下拉刷新_第2张图片


Android 轻松实现仿QQ消息下拉刷新_第3张图片Android 轻松实现仿QQ消息下拉刷新_第4张图片


接下来我们一步一步地实现以上的效果。


一、图文并茂的ListViewItem

看一下这一步的效果图:  

Android 轻松实现仿QQ消息下拉刷新_第5张图片Android 轻松实现仿QQ消息下拉刷新_第6张图片


首先,我们要实现的是带下拉刷新效果的ListView。所以我们选择自己重写原生控件ListView。只需要写一个类继承它就可以了,先不添加任何的具体实现。

RefreshListView.java:

public class RefreshListView extends ListView { 	public RefreshListView(Context context) { 		super(context); 		// TODO Auto-generated constructor stub 	}  	public RefreshListView(Context context, AttributeSet attrs) { 		super(context, attrs); 		// TODO Auto-generated constructor stub 	}  	public RefreshListView(Context context, AttributeSet attrs, int defStyle) { 		super(context, attrs, defStyle); 		// TODO Auto-generated constructor stub 	} }

要实现图文并茂的ListViewItem,接下去就要自己定义它Item的布局,这个可以无限发挥,我这里就只取图和文做一个简单的实现:

listview_item.xml:

               

在这个布局中,我就只放了一个Image和一个Text。您可以自己定义地更复杂。

然后需要我们注意的是,既然我们自己定义了ListView,那我们主界面的布局也要响应地修改了:

activity_main.xml:

        
可以一眼看出我们修改了它的控件标签,改为我们自己定义的类的完全路径。

最后是主角MainActivity.java,里面的一些代码我详细地给了注释。这里要注意的是适配器的使用。

public class MainActivity extends Activity { 	private RefreshListView listView; 	private SimpleAdapter simple_adapter; 	private List> list;  	@Override 	protected void onCreate(Bundle savedInstanceState) { 		super.onCreate(savedInstanceState); 		setContentView(R.layout.activity_main); 		listView = (RefreshListView) findViewById(R.id.listview); 		iniData();   //初始化数据,我们给它加20条Item 		// 设置SimpleAdapter监听器 		/** 		 * SimpleAdapter的五个参数的含义: 		 * 第一个:context上下文 		 * 第二个:用于显示的数据,map的list 		 * 第三个:Item的布局,即我们自定义的那个文件 		 * 第四个:与第二个参数紧密联系,与第五个紧密联系,是在map中的键值 		 * 第五个:我们看到是id(int类型)的数组,这个数组里的东西是哪里来的?是我们自己在布局文件中定义的,忘记的读者可以回过头去看一下 		 * 这几个参数独立开来可能不知道是干吗的,但是我觉得联合在一起就挺好理解了。 		 */ 		simple_adapter = new SimpleAdapter(MainActivity.this, list, 				R.layout.listview_item, new String[] { "image", "text" }, 				new int[] { R.id.image, R.id.text }); 		//设置适配器 		listView.setAdapter(simple_adapter);  	}  	// 初始化SimpleAdapter数据集 	private List> iniData() { 		list = new ArrayList>(); 		for (int i = 0; i < 20; i++) { 			Map map = new HashMap(); 			//解释下这里的数据,key对应SimpleAdapter的第三个参数,必须都包含它们。值对应第五个参数,分别是图片和文字 			map.put("text", i); 			map.put("image", R.drawable.ic_launcher); 			list.add(map); 		}  		return list; 	}  	@Override 	public boolean onCreateOptionsMenu(Menu menu) { 		// Inflate the menu; this adds items to the action bar if it is present. 		getMenuInflater().inflate(R.menu.main, menu); 		return true; 	}  } 

到了这一步,应该能够实现图片+文字的listview了吧,喝口茶,我们继续看下去。


二、加入隐藏的Header

这里我们要说一下下拉刷新的实现思路了:

首先,我们平常用到的下拉刷新,都是在下拉后屏幕上方显示出一些之前被隐藏的控件,类似下拉的箭头、progress bar等等。

那我们可以直接把它们设置为不可见吗?显然是不可以的。因为这些空间的显示与否,有一个渐变的过程,不是刷一下就出来的。

所以我们应该这样做:

加入一个隐藏的布局,放在屏幕上方。根据下拉的范围来显示响应的控件。

这一步,我们要实现的是加入隐藏的布局,具体怎样根据下拉的状态来实时调整Header的显示状态,我们在下文细说。


我们为了需要隐藏的header再自定义个新的布局,header.xml:

                                                                                  


这个布局中包含了提示语“下拉可以刷新”、最新更新时间、下拉箭头的图片(已经预先放在drawable文件夹中了,读者可以自己找个图片放进去,命名为pull_to_refresh_arrow.phg)、一个更新时才显示的progressbar(现在是隐藏的)。

为了把这个布局加到我们定义的List,我们需要改写之前自定义的RefreshLIstview控件:

public class RefreshListView extends ListView { 	View header;// 顶部布局文件; 	int headerHeight;// 顶部布局文件的高度; 	public RefreshListView(Context context) { 		super(context); 		// TODO Auto-generated constructor stub 		initView(context); 	}  	public RefreshListView(Context context, AttributeSet attrs) { 		super(context, attrs); 		// TODO Auto-generated constructor stub 		initView(context); 	}  	public RefreshListView(Context context, AttributeSet attrs, int defStyle) { 		super(context, attrs, defStyle); 		// TODO Auto-generated constructor stub 		initView(context); 	}  	/** 	 * 初始化界面,添加顶部布局文件到 listview 	 */ 	private void initView(Context context) { 		LayoutInflater inflater = LayoutInflater.from(context); 		header = inflater.inflate(R.layout.header, null); 		measureView(header); 		headerHeight = header.getMeasuredHeight(); 		Log.i("tag", "headerHeight = " + headerHeight); 		//topPadding(-headerHeight);   //这一行被我注释了,如果你去除注释,就可以显示出来了 		this.addHeaderView(header); 	}  	/** 	 * 通知父布局,占用的宽,高; 	 */ 	private void measureView(View view) { 		ViewGroup.LayoutParams p = view.getLayoutParams(); 		if (p == null) { 			p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 					ViewGroup.LayoutParams.WRAP_CONTENT); 		} 		int width = ViewGroup.getChildMeasureSpec(0, 0, p.width); 		int height; 		int tempHeight = p.height; 		if (tempHeight > 0) { 			height = MeasureSpec.makeMeasureSpec(tempHeight, 					MeasureSpec.EXACTLY); 		} else { 			height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 		} 		view.measure(width, height); 	}  	/** 	 * 设置header布局 上边距; 	 */ 	private void topPadding(int topPadding) { 		header.setPadding(header.getPaddingLeft(), topPadding, 				header.getPaddingRight(), header.getPaddingBottom()); 		header.invalidate(); 	} }

主要实现的是把新的header加入进去,同时把它隐藏。现在我把隐藏header的那行代码注释了,我们看看现在的效果:

Android 轻松实现仿QQ消息下拉刷新_第7张图片

如果去除注释,header就被隐藏。


三、加入滚动监听实时调整Header显示状态以及刷新后添加数据

思路:

添加屏幕触摸监听和屏幕滚动监听。

触摸监听尤其重要:  

在触摸时记录下触摸坐标的Y值即startY,然后在移动过程中监听当前的Y值,根据两者的插值判断当前的移动距离,与一些临界值做比较

比较之后得出当前的状态:提示下拉状态、提示释放状态、刷新状态。根据当前的状态来刷新header布局的显示情况。

滚动监听的作用是判断当前是否是列表的顶端(通过判断当前可见的第一个item的position是否为0),以及在之后判断屏幕的滚动状态。


另外在自定义的Listview类中定义了一个接口,在mainactivity中实现这个接口,用来对数据进行刷新。我们在刷新的时候用了Handler延迟了两秒,以清晰地看到刷新的效果。

修改后的MainActivity以及ListView:

ListView:  (里面很多注释,自己看着应该很好理解)

public class RefreshListView extends ListView implements OnScrollListener { 	View header;// 顶部布局文件; 	int headerHeight;// 顶部布局文件的高度; 	int firstVisibleItem;// 当前第一个可见的item的位置; 	int scrollState;// listview 当前滚动状态; 	boolean isRemark;// 标记,当前是在listview最顶端摁下的; 	int startY;// 摁下时的Y值;  	int state;// 当前的状态; 	final int NONE = 0;// 正常状态; 	final int PULL = 1;// 提示下拉状态; 	final int RELEASE = 2;// 提示释放状态; 	final int REFRESHING = 3;// 刷新状态; 	IRefreshListener iRefreshListener;//刷新数据的接口 	public RefreshListView(Context context) { 		super(context); 		// TODO Auto-generated constructor stub 		initView(context); 	}  	public RefreshListView(Context context, AttributeSet attrs) { 		super(context, attrs); 		// TODO Auto-generated constructor stub 		initView(context); 	}  	public RefreshListView(Context context, AttributeSet attrs, int defStyle) { 		super(context, attrs, defStyle); 		// TODO Auto-generated constructor stub 		initView(context); 	}  	/** 	 * 初始化界面,添加顶部布局文件到 listview 	 *  	 * @param context 	 */ 	private void initView(Context context) { 		LayoutInflater inflater = LayoutInflater.from(context); 		header = inflater.inflate(R.layout.header, null); 		measureView(header); 		headerHeight = header.getMeasuredHeight(); 		Log.i("tag", "headerHeight = " + headerHeight); 		topPadding(-headerHeight); 		this.addHeaderView(header); 		this.setOnScrollListener(this); 	}  	/** 	 * 通知父布局,占用的宽,高; 	 *  	 * @param view 	 */ 	private void measureView(View view) { 		ViewGroup.LayoutParams p = view.getLayoutParams(); 		if (p == null) { 			p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 					ViewGroup.LayoutParams.WRAP_CONTENT); 		} 		int width = ViewGroup.getChildMeasureSpec(0, 0, p.width); 		int height; 		int tempHeight = p.height; 		if (tempHeight > 0) { 			height = MeasureSpec.makeMeasureSpec(tempHeight, 					MeasureSpec.EXACTLY); 		} else { 			height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 		} 		view.measure(width, height); 	}  	/** 	 * 设置header 布局 上边距; 	 *  	 * @param topPadding 	 */ 	private void topPadding(int topPadding) { 		header.setPadding(header.getPaddingLeft(), topPadding, 				header.getPaddingRight(), header.getPaddingBottom()); 		header.invalidate(); 	}  	@Override 	public void onScroll(AbsListView view, int firstVisibleItem, 			int visibleItemCount, int totalItemCount) { 		// TODO Auto-generated method stub 		this.firstVisibleItem = firstVisibleItem; 	}  	@Override 	public void onScrollStateChanged(AbsListView view, int scrollState) { 		// TODO Auto-generated method stub 		this.scrollState = scrollState; 	} 	 	/** 	 * 对屏幕触摸的监控, 	 * 先判断当前是否是在顶端。如果是在最顶端,记录下你开始滑动的Y值 	 * 然后在滑动过程中(监听到的是ACTION_MOVE),不断地判断当前滑动的范围是否到达应该刷新的程度。 	 * (根据当前的Y-之前的startY的值 与我们的控件的高度之间关系来判断) 	 * 然后在监听到手指松开时,根据当前的状态(我们在onmove()中计算的),做相应的操作。 	 */ 	@Override 	public boolean onTouchEvent(MotionEvent ev) { 		// TODO Auto-generated method stub 		switch (ev.getAction()) { 		case MotionEvent.ACTION_DOWN: 			if (firstVisibleItem == 0) { 				isRemark = true; 				startY = (int) ev.getY(); 			} 			break;  		case MotionEvent.ACTION_MOVE: 			onMove(ev); 			break; 		case MotionEvent.ACTION_UP: 			if (state == RELEASE) { 				//即提示松开刷新的状态,一旦松开,进入到正在刷新;这时候就可以加载数据了! 				state = REFRESHING; 				// 加载最新数据; 				refreshViewByState(); 				iRefreshListener.onRefresh(); 			} else if (state == PULL) { 				//提示下拉状态状态,如果放掉的话,把一切还原,什么都没有做 				state = NONE; 				isRemark = false; 				refreshViewByState(); 			} 			break; 		} 		return super.onTouchEvent(ev); 	}  	/** 	 * 判断移动过程操作: 	 * 如果不是顶端,不需要做任何的操作 	 * 否则就获取当前的Y值,与开始的Y值做比较。 	 * 判断下拉的高度,与我们定义的一些临界值做判断(其实这个临界值你可以自己定义) 	 *  	 * @param ev 	 */ 	private void onMove(MotionEvent ev) { 		if (!isRemark) { 			return; 		} 		int tempY = (int) ev.getY(); 		int space = tempY - startY; 		int topPadding = space - headerHeight; 		switch (state) { 		case NONE: 			if (space > 0) {    				state = PULL;  //正在下拉 				refreshViewByState(); 			} 			break; 		case PULL: 			topPadding(topPadding); 			//如果大于一定高度,并且滚动状态是正在滚动时,就到了松开可以刷新的状态 			if (space > headerHeight + 30 					&& scrollState == SCROLL_STATE_TOUCH_SCROLL) { 				state = RELEASE; 				refreshViewByState(); 			} 			break; 		case RELEASE: 			topPadding(topPadding); 			//在提示松开刷新时,如果你往上拖,距离小于一定高度时,提示下拉可以刷新 			if (space < headerHeight + 30) {   				state = PULL; 				refreshViewByState(); 			}  			break; 		} 	}  	/** 	 * 根据当前状态,改变界面显示; 	 */ 	private void refreshViewByState() { 		//如果要提高性能,这些应该在oncreate中写,但是。。那里面参数太多了,为了大家读代码更舒服,就写在这里了。 		TextView tip = (TextView) header.findViewById(R.id.tip); 		ImageView arrow = (ImageView) header.findViewById(R.id.arrow); 		ProgressBar progress = (ProgressBar) header.findViewById(R.id.progress); 		RotateAnimation anim = new RotateAnimation(0, 180, 				RotateAnimation.RELATIVE_TO_SELF, 0.5f, 				RotateAnimation.RELATIVE_TO_SELF, 0.5f); 		anim.setDuration(500); 		anim.setFillAfter(true); 		RotateAnimation anim1 = new RotateAnimation(180, 0, 				RotateAnimation.RELATIVE_TO_SELF, 0.5f, 				RotateAnimation.RELATIVE_TO_SELF, 0.5f); 		anim1.setDuration(500); 		anim1.setFillAfter(true); 		switch (state) { 		case NONE:     //正常状态不显示 			arrow.clearAnimation(); 			topPadding(-headerHeight); 			break;  		case PULL:     //下拉状态显示箭头,隐藏进度条,以下的状态也类似。自己根据实际情况去修改。 			arrow.setVisibility(View.VISIBLE); 			progress.setVisibility(View.GONE); 			tip.setText("下拉可以刷新!"); 			arrow.clearAnimation(); 			arrow.setAnimation(anim1); 			break; 		case RELEASE:   			arrow.setVisibility(View.VISIBLE); 			progress.setVisibility(View.GONE); 			tip.setText("松开可以刷新!"); 			arrow.clearAnimation(); 			arrow.setAnimation(anim); 			break; 		case REFRESHING: 			topPadding(50); 			arrow.setVisibility(View.GONE); 			progress.setVisibility(View.VISIBLE); 			tip.setText("正在刷新..."); 			arrow.clearAnimation(); 			break; 		} 	}  	/** 	 * 获取完数据之后 	 */ 	public void refreshComplete() { 		state = NONE; 		isRemark = false; 		refreshViewByState(); 		TextView lastupdatetime = (TextView) header 				.findViewById(R.id.lastupdate_time); 		SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss"); 		Date date = new Date(System.currentTimeMillis()); 		String time = format.format(date); 		lastupdatetime.setText(time); 	} 	 	public void setInterface(IRefreshListener iRefreshListener){ 		this.iRefreshListener = iRefreshListener; 	} 	 	/** 	 * 刷新数据接口 	 * @author Administrator 	 */ 	public interface IRefreshListener{ 		public void onRefresh(); 	} }

MainActivity: (添加了接口回调,即在listview中调用main的添加数据的方法)

public class MainActivity extends Activity  implements IRefreshListener { 	private RefreshListView listView; 	private SimpleAdapter simple_adapter; 	private List> list;  	@Override 	protected void onCreate(Bundle savedInstanceState) { 		super.onCreate(savedInstanceState); 		setContentView(R.layout.activity_main); 		listView = (RefreshListView) findViewById(R.id.listview); 		iniData();   //初始化数据,我们给它加20条Item 		// 设置SimpleAdapter监听器 		/** 		 * SimpleAdapter的五个参数的含义: 		 * 第一个:context上下文 		 * 第二个:用于显示的数据,map的list 		 * 第三个:Item的布局,即我们自定义的那个文件 		 * 第四个:与第二个参数紧密联系,与第五个紧密联系,是在map中的键值 		 * 第五个:我们看到是id(int类型)的数组,这个数组里的东西是哪里来的?是我们自己在布局文件中定义的,忘记的读者可以回过头去看一下 		 * 这几个参数独立开来可能不知道是干吗的,但是我觉得联合在一起就挺好理解了。 		 */ 		simple_adapter = new SimpleAdapter(MainActivity.this, list, 				R.layout.listview_item, new String[] { "image", "text" }, 				new int[] { R.id.image, R.id.text }); 		//设置适配器 		listView.setAdapter(simple_adapter); 		//设置更新数据的接口         listView.setInterface(this); 	}  	// 初始化SimpleAdapter数据集 	private List> iniData() { 		list = new ArrayList>(); 		for (int i = 0; i < 20; i++) { 			Map map = new HashMap(); 			//解释下这里的数据,key对应SimpleAdapter的第三个参数,必须都包含它们。值对应第五个参数,分别是图片和文字 			map.put("text", i); 			map.put("image", R.drawable.ic_launcher); 			list.add(map); 		}  		return list; 	}  	@Override 	public boolean onCreateOptionsMenu(Menu menu) { 		// Inflate the menu; this adds items to the action bar if it is present. 		getMenuInflater().inflate(R.menu.main, menu); 		return true; 	} 	 	/** 	 * 接口回调,在RefreshListView中可以调用此方法进行数据添加。 	 */ 	@Override 	public void onRefresh() { 		// TODO 自动生成的方法存根 		Handler handler = new Handler(); 		handler.postDelayed(new Runnable() { 			@Override 			public void run() { 				// TODO Auto-generated method stub 				Map map = new HashMap(); 				map.put("text", "滚动添加 "); 				map.put("image", R.drawable.ic_launcher); 				list.add(0, map); 				listView.setAdapter(simple_adapter); 				simple_adapter.notifyDataSetChanged(); 				listView.refreshComplete(); 			} 		}, 2000); 	}  } 

到了这里,我们就实现了文章开头的效果图了。


========================================

写在后面:

源代码已上传到我的Github,或者到CSDN下载区下载。

任何问题,欢迎留言交流!