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,或者到CSDN下載區下載。

任何問題,歡迎留言交流!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章