当前位置: 代码迷 >> Android >> 三、加入滚动监听实时调整Header显示状态以及刷新后添加数据
  详细解决方案

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

热度:66   发布时间:2016-04-28 00:40:18.0
Android 轻松实现仿QQ空间下拉刷新

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

看完本文,您可以学到:


1.下拉刷新的实现原理

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

3.ScrollListener滚动监听

4.Adapter适配器的使用


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




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


一、图文并茂的ListViewItem

看一下这一步的效果图:  



首先,我们要实现的是带下拉刷新效果的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,或者到网盘下载。

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


2楼qq_2865257140分钟前
顶顶顶vfilm.china.com.cn
Re: u01242282911分钟前
回复qq_28652571n...
1楼qq_286509491小时前
ding
  相关解决方案