Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 輕松實現仿QQ空間下拉刷新

Android 輕松實現仿QQ空間下拉刷新

編輯:關於Android編程

(本文講解了在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:

 




    

    


在這個布局中,我就只放了一個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的那行代碼注釋了,我們看看現在的效果:

 

\

如果去除注釋,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);
	}

}

到了這裡,我們就實現了文章開頭的效果圖了。

 

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved