(本文講解了在Android中實現列表下拉刷新的動態效果的過程,文末附有源碼。)
看完本文,您可以學到:
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>
爲了把這個佈局加到我們定義的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); } }
到了這裏,我們就實現了文章開頭的效果圖了。
========================================
寫在後面:
任何問題,歡迎留言交流!