ListView絕對可以稱得上是 Android 中最常用的控件之一,幾乎所有的應用程序都會用到它。由於手機屏幕空間都比較有限,能夠一次性在屏幕上顯示的內容並不多,當我們的程序中有大量的數據需要展示的時候,就可以借助 ListView來實現。ListView允許用戶通過手指上下滑動的方式將屏幕外的數據滾動到屏幕內,同時屏幕上原有的數據則會滾動出屏幕。相信你其實每天都在使用這個控件,比如查看手機聯系人列表,翻閱微博的最新消息等等。不過比起前面介紹的幾種控件,ListView的用法也相對復雜了很多。
ListView 的簡單用法
首先新建一個 ListViewTest 項目,並讓 ADT 自動幫我們創建好活動。然后修改activity_main.xml 中的代碼,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <ListView android:id="@+id/list_view" android:layout_width="match_parent" android:layout_height="match_parent" > </ListView> </LinearLayout>
在布局中加入 ListView 控件還算非常簡單,先為 ListView 指定了一個 id,然后將寬度和高度都設置為 match_parent,這樣 ListView也就占據了整個布局的空間。接下來修改 MainActivity 中的代碼,如下所示:
public class MainActivity extends Activity { private String[] data = { "Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango" }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ArrayAdapter<String> adapter = new ArrayAdapter<String>( MainActivity.this, android.R.layout.simple_list_item_1, data); ListView listView = (ListView) findViewById(R.id.list_view); listView.setAdapter(adapter); } }
既然 ListView是用於展示大量數據的,那我們就應該先將數據提供好。這些數據可以是從網上下載的,也可以是從數據庫中讀取的,應該視具體的應用程序場景來決定。這里我們就簡單使用了一個 data 數組來測試,里面包含了很多水果的名稱。不過,數組中的數據是無法直接傳遞給 ListView 的,我們還需要借助適配器來完成。Android 中提供了很多適配器的實現類,其中我認為最好用的就是 ArrayAdapter。它可以通過泛型來指定要適配的數據類型,然后在構造函數中把要適配的數據傳入即可。ArrayAdapter有多個構造函數的重載,你應該根據實際情況選擇最合適的一種。這里由於我們提供的數據都是字符串,因此將 ArrayAdapter 的泛型指定為 String,然后在 ArrayAdapter 的構造函數中依次傳入當前上下文、ListView 子項布局的 id,以及要適配的數據。注意我們使用了android.R.layout.simple_list_item_1 作為 ListView子項布局的 id,這是一個 Android 內置的布局文件,里面只有一個TextView,可用於簡單地顯示一段文本。這樣適配器對象就構建好了。最后,還需要調用 ListView 的 setAdapter()方法,將構建好的適配器對象傳遞進去,這樣 ListView和數據之間的關聯就建立完成了。

可以通過滾動的方式來查看屏幕外的數據。
定制 ListView 的界面
只能顯示一段文本的 ListView 實在是太單調了,我們現在就來對 ListView 的界面進行定制,讓它可以顯示更加豐富的內容。首先需要准備好一組圖片,分別對應上面提供的每一種水果,待會我們要讓這些水果名稱的旁邊都有一個圖樣。接着定義一個實體類,作為 ListView適配器的適配類型。新建類 Fruit,代碼如下所示:
public class Fruit { private String name; private int imageId; public Fruit(String name, int imageId) { this.name = name; this.imageId = imageId; } public String getName() { return name; } public int getImageId() { return imageId; } }
Fruit 類中只有兩個字段,name 表示水果的名字,imageId 表示水果對應圖片的資源 id。然后需要為 ListView 的子項指定一個我們自定義的布局,在 layout 目錄下新建fruit_item.xml,代碼如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <ImageView android:id="@+id/fruit_image" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:id="@+id/fruit_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginLeft="10dip" /> </LinearLayout>
在這個布局中,我們定義了一個 ImageView 用於顯示水果的圖片,又定義了一個TextView用於顯示水果的名稱。接下來需要創建一個自定義的適配器,這個適配器繼承自 ArrayAdapter,並將泛型指定為 Fruit 類。新建類 FruitAdapter,代碼如下所示:
public class FruitAdapter extends ArrayAdapter<Fruit> { private int resourceId; public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects) { super(context, textViewResourceId, objects); resourceId = textViewResourceId; } @Override public View getView(int position, View convertView, ViewGroup parent) { Fruit fruit = getItem(position); // 獲取當前項的Fruit實例 View view = LayoutInflater.from(getContext()).inflate(resourceId, null); ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image); TextView fruitName = (TextView) view.findViewById(R.id.fruit_name); fruitImage.setImageResource(fruit.getImageId()); fruitName.setText(fruit.getName()); return view; } }
FruitAdapter 重寫了父類的一組構造函數,用於將上下文、ListView 子項布局的 id 和數據都傳遞進來。另外又重寫了 getView()方法,這個方法在每個子項被滾動到屏幕內的時候會被調用。在 getView 方法中,首先通過 getItem()方法得到當前項的 Fruit 實例,然后使用LayoutInflater 來為這個子項加載我們傳入的布局,接着調用 View 的 findViewById()方法分別獲取到 ImageView 和 TextView 的實例,並分別調用它們的 setImageResource()和 setText()方法來設置顯示的圖片和文字,最后將布局返回,這樣我們自定義的適配器就完成了。下面修改 MainActivity 中的代碼,如下所示:
public class MainActivity extends Activity { private List<Fruit> fruitList = new ArrayList<Fruit>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initFruits(); // 初始化水果數據 FruitAdapter adapter = new FruitAdapter(MainActivity.this, R.layout.fruit_item, fruitList); ListView listView = (ListView) findViewById(R.id.list_view); listView.setAdapter(adapter); } private void initFruits() { Fruit apple = new Fruit("Apple", R.drawable.apple_pic); fruitList.add(apple); Fruit banana = new Fruit("Banana", R.drawable.banana_pic); fruitList.add(banana); Fruit orange = new Fruit("Orange", R.drawable.orange_pic); fruitList.add(orange); Fruit watermelon = new Fruit("Watermelon", R.drawable.watermelon_pic); fruitList.add(watermelon); Fruit pear = new Fruit("Pear", R.drawable.pear_pic); fruitList.add(pear); Fruit grape = new Fruit("Grape", R.drawable.grape_pic); fruitList.add(grape); Fruit pineapple = new Fruit("Pineapple", R.drawable.pineapple_pic); fruitList.add(pineapple); Fruit strawberry = new Fruit("Strawberry", R.drawable.strawberry_pic); fruitList.add(strawberry); Fruit cherry = new Fruit("Cherry", R.drawable.cherry_pic); fruitList.add(cherry); Fruit mango = new Fruit("Mango", R.drawable.mango_pic); fruitList.add(mango); } }
可以看到,這里添加了一個 initFruits()方法,用於初始化所有的水果數據。在 Fruit 類的構造函數中將水果的名字和對應的圖片 id 傳入,然后把創建好的對象添加到水果列表中。接着我們在 onCreate()方法中創建了 FruitAdapter對象,並將 FruitAdapter 作為適配器傳遞給了 ListView。這樣定制 ListView界面的任務就完成了。

雖然目前我們定制的界面還是很簡單,但是相信聰明的你已經領悟到了訣竅,只要修改fruit_item.xml 中的內容,就可以定制出各種復雜的界面了。
提升 ListView 的運行效率
之所以說 ListView這個控件很難用,就是因為它有很多的細節可以優化,其中運行效率就是很重要的一點。目前我們ListView的運行效率是很低的,因為在 FruitAdapter的getView()方法中每次都將布局重新加載了一遍,當 ListView快速滾動的時候這就會成為性能的瓶頸。仔細觀察,getView()方法中還有一個 convertView 參數,這個參數用於將之前加載好的布局進行緩存,以便之后可以進行重用。修改 FruitAdapter中的代碼,如下所示:
public class FruitAdapter extends ArrayAdapter<Fruit> { …… @Override public View getView(int position, View convertView, ViewGroup parent) { Fruit fruit = getItem(position); View view; if (convertView == null) { view = LayoutInflater.from(getContext()).inflate(resourceId, null); } else { view = convertView; } ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image); TextView fruitName = (TextView) view.findViewById(R.id.fruit_name); fruitImage.setImageResource(fruit.getImageId()); fruitName.setText(fruit.getName()); return view; } }
可以看到,現在我們在 getView()方法中進行了判斷,如果 convertView 為空,則使用LayoutInflater 去加載布局,如果不為空則直接對 convertView進行重用。這樣就大大提高了ListView的運行效率,在快速滾動的時候也可以表現出更好的性能。
不過,目前我們的這份代碼還是可以繼續優化的,雖然現在已經不會再重復去加載布局,但是每次在getView()方法中還是會調用View的findViewById()方法來獲取一次控件的實例。我們可以借助一個 ViewHolder來對這部分性能進行優化,修改 FruitAdapter 中的代碼,如下所示:
public class FruitAdapter extends ArrayAdapter<Fruit> { …… @Override public View getView(int position, View convertView, ViewGroup parent) { Fruit fruit = getItem(position); View view; ViewHolder viewHolder; if (convertView == null) { view = LayoutInflater.from(getContext()).inflate(resourceId, null); viewHolder = new ViewHolder(); viewHolder.fruitImage = (ImageView) view.findViewById(R.id.fruit_image); viewHolder.fruitName = (TextView) view.findViewById(R.id.fruit_name); view.setTag(viewHolder); // 將ViewHolder 存儲在View 中 } else { view = convertView; viewHolder = (ViewHolder) view.getTag(); // 重新獲取ViewHolder } viewHolder.fruitImage.setImageResource(fruit.getImageId()); viewHolder.fruitName.setText(fruit.getName()); return view; } class ViewHolder { ImageView fruitImage; TextView fruitName; } }
我們新增了一個內部類 ViewHolder,用於對控件的實例進行緩存。當 convertView為空的時候,創建一個 ViewHolder對象,並將控件的實例都存放在 ViewHolder 里,然后調用 View的 setTag()方法,將 ViewHolder 對象存儲在 View 中。當 convertView 不為空的時候則調用View的 getTag()方法,把 ViewHolder 重新取出。這樣所有控件的實例都緩存在了 ViewHolder里,就沒有必要每次都通過 findViewById()方法來獲取控件實例了。通過這兩步的優化之后,我們 ListView的運行效率就已經非常不錯了。
ListView 的點擊事件
話說回來,ListView 的滾動畢竟只是滿足了我們視覺上的效果,可是如果 ListView 中的子項不能點擊的話,這個控件就沒有什么實際的用途了。因此,本小節中我們就來學習一下ListView如何才能響應用戶的點擊事件。修改 MainActivity 中的代碼,如下所示:
public class MainActivity extends Activity { private List<Fruit> fruitList = new ArrayList<Fruit>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initFruits(); FruitAdapter adapter = new FruitAdapter(MainActivity.this,R.layout.fruit_item, fruitList); ListView listView = (ListView) findViewById(R.id.list_view); listView.setAdapter(adapter); listView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Fruit fruit = fruitList.get(position); Toast.makeText(MainActivity.this, fruit.getName(), Toast.LENGTH_SHORT).show(); } }); } …… }
可以看到,我們使用了 setOnItemClickListener()方法來為 ListView 注冊了一個監聽器,當用戶點擊了 ListView中的任何一個子項時就會回調 onItemClick()方法,在這個方法中可以通過 position 參數判斷出用戶點擊的是哪一個子項,然后獲取到相應的水果,並通過 Toast將水果的名字顯示出來。重新運行程序,並點擊一下西瓜。

