[置顶] Android 轻松实现仿QQ消息下拉刷新

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

看完本文,您可以学到:


1.下拉刷新的实现原理

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

3.ScrollListener滚动监听

4.Adapter适配器的使用


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

[置顶] Android 轻松实现仿QQ消息下拉刷新_第1张图片


[置顶] Android 轻松实现仿QQ消息下拉刷新_第2张图片[置顶] Android 轻松实现仿QQ消息下拉刷新_第3张图片


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


一、图文并茂的ListViewItem

看一下这一步的效果图:  

[置顶] Android 轻松实现仿QQ消息下拉刷新_第4张图片


首先,我们要实现的是带下拉刷新效果的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:

<?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="match_parent"
    android:gravity="center_vertical"
    android:orientation="horizontal" >

    <ImageView
        android:id="@+id/image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp" />

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:textSize="25sp" />

</LinearLayout>

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

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

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=".MainActivity" >

    <com.example.mylistviewrefresh.RefreshListView
        android:id="@+id/listview"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:cacheColorHint="#00000000"
        android:dividerHeight="5dip" />

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

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

public class MainActivity extends Activity {
	private RefreshListView listView;
	private SimpleAdapter simple_adapter;
	private List<Map<String, Object>> 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<Map<String, Object>> iniData() {
		list = new ArrayList<Map<String, Object>>();
		for (int i = 0; i < 20; i++) {
			Map<String, Object> map = new HashMap<String, Object>();
			//解释下这里的数据,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:

<?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="vertical" >

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingBottom="10dip"
        android:paddingTop="10dip" >

        <LinearLayout
            android:id="@+id/layout"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:gravity="center"
            android:orientation="vertical" >

            <TextView
                android:id="@+id/tip"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="下拉可以刷新!" />

            <TextView
                android:id="@+id/lastupdate_time"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" />
        </LinearLayout>

        <ImageView
            android:id="@+id/arrow"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toLeftOf="@id/layout"
            android:layout_marginRight="20dip"
            android:src="@drawable/pull_to_refresh_arrow" />

        <ProgressBar
            android:id="@+id/progress"
            style="?android:attr/progressBarStyleSmall"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toLeftOf="@id/layout"
            android:layout_marginRight="20dip"
            android:visibility="gone" />
    </RelativeLayout>

</LinearLayout>


这个布局中包含了提示语“下拉可以刷新”、最新更新时间、下拉箭头的图片(已经预先放在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的那行代码注释了,我们看看现在的效果:


如果去除注释,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<Map<String, Object>> 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<Map<String, Object>> iniData() {
		list = new ArrayList<Map<String, Object>>();
		for (int i = 0; i < 20; i++) {
			Map<String, Object> map = new HashMap<String, Object>();
			//解释下这里的数据,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<String, Object> map = new HashMap<String, Object>();
				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下载区下载。

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


你可能感兴趣的:(android,ListView,布局,控件,下拉刷新)