請參考教材,全面理解和完成本章節內容... ...
復制工程ch8,將工程目錄改名為ch9。
我們經常會在應用程序中使用列表的形式來展現一些內容,所以學好ListView控件是非常必要的。ListView功能強大、用法靈活,需要我們花些時間才能掌握,不過本章將是一個非常好的的開始。
當前,「陋習手記」應用的模型層僅包含一個Crime實例。本章,我們將更新「陋習手記」應用以包含一個crime列表(通過ListView控件實現的),如圖9-1所示。列表顯示每一個Crime實例的標題、發生日期以及處理狀態。(本章功能需求)
圖9-1crime列表
圖9-2展示了本章「陋習手記」應用的整體規划設計。
圖9-2 「陋習手記」應用對象圖
應用的模型層將新增一個CrimeLab對象,該對象是一個數據集中存儲池,用來存儲Crime對象。
為了顯示crime的列表,應用還需在的控制層新增一個activity和一個fragment,即CrimeListActivity和CrimeListFragment。
其中CrimeListFragment是ListFragment的子類,ListFragment是Fragment的子類, Fragment內置的功能支持列表顯示。
9.1 更新 CriminalIntent 應用的模型層
首先,我們來更新應用的模型層,從原來的單個Crime對象變為可容納多個Crime對象的ArrayList。
單例與數據集中存儲
在「陋習手記」應用中,crime數組對象將存儲在一個單例里。
提示:
java中單例模式是一種常見的設計模式,單例模式確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例。如,同一個手機僅能登錄一個微信。
為什么將crime數組對象存儲在一個單例對象里?應用能夠在內存里存在多久,單例對象就能存在多久,因此將對象列表保存在單例里可保持crime數據的一直存在,不管activity、fragment及它們的生命周期發生什么變化。
要創建單例對象,需創建一個帶有私有構造方法及get()方法的類,其中get()方法返回實例。如實例已存在,get()方法則直接返回它;如實例還未存在,get()方法會調用構造方法來創建它。
右鍵單擊com.jet.criminalintent類包,選擇New → Class菜單項。在隨后出現的對話框中,命名類為CrimeLab,然后單擊OK按鈕完成。
在打開的CrimeLab.java文件中,編碼實現CrimeLab類為帶有私有構造方法和get(Context)方法的單例,如代碼清單9-1所示。
代碼清單9-1 創建單例(CrimeLab.java)
CrimeLab類的構造方法需要一個Context參數。這在Android開發里很常見,使用Context參數,單例可完成啟動activity、獲取項目資源,查找應用的私有存儲空間等任務。
下面,我們將一些Crime對象保存到CrimeLab中去。在CrimeLab的構造方法里,創建一個空的用來保存Crime對象的ArrayList。此外,再添加兩個方法,即getCrimes()方法和getCrime(UUID)方法。前者返回數組列表(多個crime),后者返回帶有指定ID的Crime對象。具體代碼如代碼清單9-2所示。
代碼清單9-2 創建可容納Crime對象的ArrayList(CrimeLab.java)
新建的ArrayList將內含用戶自建的Crime,用戶既可以存入Crime,也可以從中調取Crime。但實現用戶手工存入Crime還需要許多工作,我們放的后續章節完成。現在,我們暫時先往數組列表中批量存入100個Crime對象,如代碼清單9-3所示。
代碼清單9-3 生成100個crime(CrimeLab.java)
現在我們擁有了一個滿是數據的模型層,和100個可在屏幕上顯示的crime。
9.2 創建 ListFragment
創建一個名為CrimeListFragment的類, 使用ListFragment類作為她的超類,導入支持庫中的android.support.v4.app.ListFragment類包,避免不同系統版本的兼容性問題。
在CrimeListFragment.java中,覆蓋onCreate(Bundle)方法,設置托管CrimeListFragment的activity標題,如代碼清單9-4所示。
代碼清單9-4 為新Fragment添加onCreate(Bundle)方法(CrimeListFragment.java)(原翻譯為“為新Activity”不對)
注意查看getActivity()方法,該方法不僅可以便利地返回托管activity,且允許fragment處理更多的與activity相關的事務。這里,我們就用它調用Activity.setTitle(int)方法,將顯示在操作欄上的標題文字替換為傳入的字符串資源中設定的文字。
現在,無需覆蓋onCreateView()方法或為CrimeListFragment生成布局。ListFragment類默認實現方法已生成了一個全屏ListView布局。我們先暫時使用該布局,后續章節中我們會覆蓋CrimeListFragment.onCreateView()方法,從而添加更多的高級功能。
在strings.xml文件中,為列表activity標題添加字符串資源,如代碼清單9-5所示。
代碼清單9-5 為新的activity標題添加字符串資源(strings.xml)
CrimeListFragment需要獲取存儲在CrimeLab中的crime列表。在CrimeListFragment.onCreate()方法中,先獲取CrimeLab單例,再獲取其中的crime列表,如代碼清單9-6所示。
代碼清單9-6 在CrimeListFragment中獲取crime(CrimeListFragment.java)
9.3 使用抽象 activity 托管 fragment
下面我們來創建一個用於托管CrimeListFragment的CrimeListActivity類。首先為CrimeListActivity創建一個視圖。
9.3.1 通用的fragment托管布局
對於CrimeListActivity,我們仍可使用定義在activity_crime.xml文件中的布局。該布局提供了一個用來放置fragment的FrameLayout容器視圖,其中的fragment可在activity中使用代碼獲取,如代碼清單9-7所示。
代碼清單9-7 通用的布局定義文件activity_crime.xml
因為activity_crime.xml布局文件並沒有指定給一個特定的fragment,因此你可以使用它托管任一個fragment。為反映出該布局的通用性,我們把它重命名為activity_fragment.xml。
首先,如果已打開了activity_crime.xml文件,請先關閉它。接下來,右鍵單擊activity_crime.xml文件。在彈出的菜單里,選擇Refactor → Rename菜單項將activity_crime.xml改名為activity_fragment.xml,點擊Refactor確認。重命名資源時,IDE會自動更新資源文件的所有引用。如代碼清單9-8所示。(太幸福了,這就是重構好處!)
代碼清單9-8 為CrimeActivity更新布局文件引用(CrimeActivity.java)
9.3.2 抽象 activity 類
我們可以通過復用CrimeActivity的代碼來創建CrimeListActivity類。
回顧一下前面寫的CrimeActivity類代碼,該代碼簡單且幾近通用。事實上,CrimeActivity類代碼唯一不通用的地方是實例化CrimeFragment類哪行代碼(用下划線標識的部分),如代碼清單9-9所示。
代碼清單9-9 幾近通用的CrimeActivity類(CrimeActivity.java)
本書中幾乎每一個創建的activity都需要同樣的代碼。為避免不必要的重復性輸入,我們將這些重復代碼置入一個抽象類,以供使用。
首先,在CriminalIntent類包里創建一個名為SingleFragmentActivity的新類,單擊OK按鈕完成創建,如圖9-3所示。
圖9-3 創建SingleFragmentActivity抽象類
接下來,使用FragmentActivity類作為它的超類。
然后,使用abstract關鍵字,讓SingleFragmentActivity類成為一個抽象類,添加代碼清單9-10所示代碼到SingleFragmentActivity.java文件。可以看到,除了兩處加下划線的代碼,其余代碼和原來的CrimeActivity代碼完全一樣。
代碼清單9-10 添加一個通用超類(SingleFragmentActivity.java)
在以上代碼里,我們設置從activity_fragment.xml布局里生成activity視圖。然后在容器中尋找FragmentManager里的fragment。如fragment不存在,則創建一個新的fragment並將其添加到容器中。
代碼清單9-10與原來的CrimeActivity代碼唯一的區別就是,新增了一個名為createFragment()的抽象方法,我們可使用它實例化新的fragment。SingleFragmentActivity的子類會實現該方法返回一個由activity托管的fragment實例。
1. 使用抽象類
下面我們來測試一下抽象類的使用。首先創建一個名為CrimeListActivity的新類,如圖9-4所示。
圖9-4CrimeListActivity類
新建的CrimeListActivity類是SingleFragmentActivity類的子類。
首先,參考代碼清單9-11完成CrimeListActivity類的創建。
代碼清單9-11 代碼實現CrimeListActivity(CrimeListActivity.java)
如代碼清單9-11所示,代碼實現了父類的抽象方法createFragment()。該方法返回一個新的CrimeListFragment實例。
如果CrimeActivity類也可以繼承通用類來實現,那就再好不過了。返回到CrimeActivity.java文件中。參照代碼清單9-12,刪除CrimeActivity類的現有代碼,重新編寫代碼,使其成為SingleFragmentActivity的子類。
代碼清單9-12 清理CrimeActivity類(CrimeActivity.java)
在本書的后續章節中,我們會發現使用SingleFragmentActivity抽象類可大大減少代碼輸入量,節約開發時間。現在,世界清靜了,我們的activity代碼看起來簡練又整潔。
2. 在配置文件中聲明CrimeListActivity
CrimeListActivity創建完成后,記得在配置文件中聲明它(為什么呢?)。
為實現CriminalIntent應用啟動后,用戶看到的主界面是crime列表,我們還需配置CrimeListActivity為啟動activity。
如代碼清單9-13所示,在manifest配置文件中,首先聲明CrimeListActivity,然后刪除CrimeActivity的啟動activity配置,改配CrimeListActivity為啟動activity。
代碼清單9-13 聲明CrimeListActivity為啟動activity(AndroidManifest.xml)
現在,CrimeListActivity被配置為啟動activity。運行CriminalIntent應用,會看到CrimeListActivity的FrameLayout托管了一個無內容的CrimeListFragment,如圖9-5所示。
圖9-5 沒有內容的CrimeListActivity用戶界面
當ListView沒有內容可以顯示時,ListFragment會通過內置的ListView顯示一個圓形進度條。CrimeListFragment已經被賦予了訪問Crime數組的能力,接下來,我們要將crime列表通過ListView顯示在屏幕上。
9.4 ListFragment、ListView 及 ArrayAdapter
我們需要通過CrimeListFragment的ListView將列表項展示給用戶,而不是什么圓形進度條。每一個列表項都應該包含一個Crime實例的數據。
ListView是ViewGroup的子類,每一個列表項都是作為ListView的一個View子對象顯示的。這些View子對象既可以是復雜的View對象,也可以是簡單的View對象,這取決於我們對列表顯示復雜度的需要。
簡單起見,我們先實現一個簡單形式的列表項顯示,即每個列表項只顯示Crime的標題,並且View對象是一個簡單的TextView,如圖9-6所示。(你可能有疑義,這些陋習項目不是逐條添加的嗎?Good Idear!)
圖9-6 帶有子TextView的ListView
上圖中,我們可以看到6個TextView。要是能滾動截圖屏幕的話,ListView還可顯示出更多的TextView,如 陋習 7 、陋習 8等。
這些View對象來自哪里?ListView會提前准備好所有要顯示的View對象嗎?倘若這樣,效率可就太低了。其實View對象只有在屏幕上顯示時才有必要存在。列表的數據量非常大,為整個列表創建並儲存所有視圖對象不僅沒有必要,而且會導致嚴重的系統性能及內存占用問題。
因此,比較聰明的做法是在需要顯示的時候才創建視圖對象。即當ListView需要顯示某個列表項時,它才會去申請一個可用的視圖對象。
ListView找誰去申請視圖對象呢?答案是adapter。
adapter是一個控制器對象,從模型層獲取數據,並將之提供給ListView顯示,起到溝通橋梁的作用。adapter負責:
- 創建必要的視圖對象;
- 用模型層數據填充視圖對象;
- 將准備好的視圖對象返回給ListView。
ListView需要顯示視圖對象時,會與其adapter展開會話溝通(自動)。圖9-8展示了ListView向其數組adapter啟動會話的例子。
圖9-8 模擬ListView與Adapter會話過程
首先,通過調用adapter的getCount()方法,ListView詢問數組列表中包含多少個對象。(為避免出現數組越界的錯誤,獲取對象數目信息非常重要。)
緊接着,ListView就調用adapter的getView(int, View, ViewGroup)方法。該方法的第一個參數是ListView要查找的列表項在數組列表中的位置。
在getView()方法的內部實現里,adapter使用數組列表中指定位置的列表項創建一個視圖對象,並將該對象返回給ListView。ListView繼而將其設置為自己的子視圖,並刷新顯示在屏幕上。
稍后,通過覆蓋getView()方法創建定制列表項的學習,我們將會了解到更多有關它的實現機制。
9.4.1 創建ArrayAdapter<T>類實例
首先,使用以下構造方法為CrimeListFragment創建一個默認的ArrayAdapter<T>類實現:
publicArrayAdapter(Context context,int textViewResourceId, T[] objects)
在數組adapter構造方法中:
- 第一個參數是一個Context對象,是上下文,就是當前的Activity;
- 第二個參數是android sdk中自己內置的一個布局,它里面只有一個TextView,這個參數是表明我們數組中每一條數據的布局是這個view,就是將每一條數據都顯示在這個view上面;
- 第三個參數是數據集(Crime數組對象),就是我們要顯示的數據。
listView會根據這三個參數,遍歷adapter里面的每一條數據,讀出一條,顯示到第二個參數對應的布局中,這樣就形成了我們看到的listView。
在CrimeListFragment.java中,創建一個ArrayAdapter<T>實例,並設置其為CrimeListFragment中ListView的adapter,如代碼清單9-14所示。
代碼清單9-14 創建ArrayAdapter(CrimeListFragment.java)
setListAdapter(ListAdapter)是ListFragment類內含的一個易用的方法,使用此方法可為ListFragment類內置的ListView設置(或綁定)adapter(這個ListView由CrimeListFragment管理)。
(ListFragment類默認實現方法生成一個全屏ListView布局)
我們在adapter的構造方法中指定的布局(android.R.layout.simple_list_item_1)是Android SDK 提供的預定義布局資源(稍后我們用自己的布局替換它)。該布局擁有一個TextView根元素。布局的源碼如代碼清單9-15所示。
代碼清單9-15 android.R.layout.simple_list_item_1資源的源碼
也可在adapter的構造方法中指定其他布局,只要滿足布局的根元素是TextView條件即可。
得益於ListFragment的默認行為,我們現在可以運行應用了。ListView隨即會被實例化,並顯示在屏幕上,然后開啟與adapter間的會話。
運行CriminalIntent應用。這次屏幕上出現的是列表項,而不再是圓形進度條了。不過,視圖上顯示的內容對用戶來說莫名其妙(為什么?),如圖9-9所示。
圖9-9 混合了類名和內存地址的列表項
默認的ArrayAdapter<T>.getView()實現方法依賴於toString()方法。它首先生成布局視圖,然后找到指定位置的Crime對象並對其調用toString()方法,最后得到字符串信息並傳遞給TextView。Crime類當前並沒有覆蓋toString()方法,因此,它默認使用了java.lang.Object類的實現方法,直接返回了混和對象類名和內存地址的字符串信息。這就是視圖上顯示“莫名其妙”內容的原因!
為讓adapter針對Crime對象創建更實用的視圖,可打開Crime.java文件,覆蓋toString()方法返回crime標題,如代碼清單9-16所示。
代碼清單9-16 覆蓋Crime.toString()方法(Crime.java)
再次運行CriminalIntent應用。上下滾動列表,查看更多的Crime對象,如圖9-10所示。
圖9-10 顯示crime標題的簡單列表項
在我們上下滾動列表時,ListView調用adapter的getView()方法,按需獲得要顯示的視圖。
9.4.2 響應列表項的點擊事件
要響應用戶對列表項的點擊事件,我們僅需要覆蓋ListFragment類的另一方法:
public void onListItemClick(ListView l, View v, int position, long id)
無論用戶是單擊或是手指的觸摸,都會觸發onListItemClick()方法。
OK,我們開始處理列表項的點擊事件,首先,在CrimeListFragment.java中,覆蓋onListItemClick()方法,使adapter返回被點擊的列表項所對應的Crime對象;然后,日志會記錄Crime對象的標題,如代碼清單9-17所示。
代碼清單9-17 覆蓋onListItemClick()方法,日志記錄Crime對象的標題(CrimeListFragment.java)
getListAdapter()方法是ListFragment類中的一個便利的方法,該方法可返回設置在ListFragment列表視圖上的adapter。然后,使用onListItemClick()方法的position參數調用adapter的getItem(int)方法,最后把結果轉換成Crime對象。
再次運行CriminalIntent應用。點擊某個列表項,查看日志,確認Crime對象已被正確獲取。
9.5 定制列表項
截至目前,每個列表項只是顯示了Crime的標題(Crime.toString()方法的返回結果)。
如不滿足於此,也可以創建定制列表項。實現顯示定制列表項需完成以下任務:
- 創建定義列表項視圖的XML布局文件;
- 創建
ArrayAdapter<T>的子類,用來創建、填充並返回定義在新布局中的視圖。
9.5.1 創建列表項布局
在CriminalIntent應用中,每個列表項的視圖布局應包含crime的三項內容,即標題、創建日期,以及是否已解決的狀態,如圖9-11所示。這要求該視圖布局包含兩個TextView和一個CheckBox。
圖9-11 一些定制的列表項
如同創建activity或fragment視圖一樣,為列表項創建一個新的布局視圖。在項目導航視圖中,右鍵單擊res\layout目錄,選擇New -> XML-> Layout XML File。在隨后出現的對話框中,命名布局文件為list_item_crime.xml,設置其根元素為RelativeLayout,最后單擊Finish按鈕完成。
在RelativeLayout里,子視圖相對於根布局以及子視圖相對於子視圖的布置排列,需使用一些布局參數加以布置控制。對於列表項新布局,需布置CheckBox對齊RelativeLayout布局的右手邊,布置兩個TextView相對於CheckBox左對齊。
圖9-12展示了定制列表項布局的全部組件。CheckBox子視圖須首先被定義,因為雖然它出現在布局的最右邊,但TextView需使用CheckBox的資源ID作為屬性值。
現在是利用圖形布局工具的時候了,依據圖9-12完成布局文件list_item_crime.xml。
圖9-12 定制列表項的布局(list_item_crime.xml)
基於同樣的理由,顯示標題的TextView也必須定義在顯示日期的TextView之前。總而言之,在布局文件里,一個組件必須首先被定義,這樣,其他組件才能在定義時使用它的資源ID。定制列表項的布局如圖9-12所示。
注意,在其他組件的定義中使用某個組件的ID時,符號+不應該包括在內(只是應用不是定義)。符號+是在組件首次出現在布局文件中時,用來創建資源ID的,一般出現在android:id屬性值里。
另外要注意的是,我們在布局定義中使用的是固定字符串,而不是android:text屬性的字符串資源。這些字符串是用作開發和測試的示例文字。adapter會提供用戶想看到的信息。由於這些字符串不會顯示給用戶,所以也就沒有必要為它們創建字符串資源了。
定制列表項布局創建就完成了。接下來,我們繼續學習創建定制adapter。
9.5.2 創建adapter子類
定制布局用來顯示特定Crime對象信息的列表項。列表項要顯示的數據信息必須使用Crime類的getter方法才能獲取,因此,我們需創建一個知道如何與Crime對象交互的adapter。
在CrimeListFragment.java中,創建一個ArrayAdapter的子類作為CrimeListFragment的內部類,如代碼清單9-18所示。
代碼清單9-18 添加定制的adapter內部類(CrimeListFragment.java)
這里需調用超類的構造方法來綁定Crime對象的數組列表。由於不打算使用預定義布局,我們傳入0作為布局ID參數。
創建並返回定制列表項是在ArrayAdapter<T>的方法getView()里實現的,如下所示:
public View getView(int position, View convertView, ViewGroup parent)
convertView參數是一個已存在的列表項,adapter可重新配置並返回它,因此我們無需再創建全新的視圖對象。復用視圖對象可避免反復創建、銷毀同一類對象的開銷,應用性能因此得到了提高。ListView一次性需顯示大量列表項,因此,沒有理由產生大量暫不使用的視圖對象來耗盡寶貴的內存資源。
在CrimeAdapter內部類中覆蓋getView()方法, 返回產生於定制布局的視圖對象,並填充對應的Crime數據如代碼清單9-19所示。
注意:重寫的是CrimeAdapter內部類中的getView()方法,不是CrimeListFragment類的方法;
提示:
ListView需要顯示視圖對象時,會與其adapter自動展開會話溝通,如自動調用getView()方法,類似購買火車票,系統會自動查詢余額
代碼清單9-19 覆蓋getView()方法(CrimeListFragment.java)
在getView()實現方法里,首先檢查傳入的視圖對象是否是復用對象。如不是,則從定制布局里產生一個新的視圖對象。
無論是新對象還是復用對象,都應調用Adapter的getItem(int)方法獲取列表中當前position的Crime對象。
獲取Crime對象后,引用視圖對象中的各個組件,並以Crime的數據信息對應配置視圖對象。最后,把視圖對象返回給ListView。
現在可以在CrimeListFragment中綁定定制的adapter了。在CrimeListFragment.java文件頭部,參照代碼清單9-20,更新onCreate()和onListItemClick()實現方法以使用CrimeAdapter。
代碼清單9-20 使用CrimeAdapter(CrimeListFragment.java)
既然已轉換為CrimeAdapter類,自然也獲得了類型檢查的能力。CrimeAdapter只能容納Crime對象,因此Crime類的強制類型轉換也就不需要了。
通常情況下,現在就可以准備運行應用了。但由於列表項中存在着一個CheckBox,因此還需進行一處調整。CheckBox默認是可聚焦的。這意味着,點擊列表項會被解讀為切換CheckBox的狀態,自然也就無法觸發onListItemClick()方法了。
由於ListView的這種內部特點,出現在列表項布局內的任何可聚焦組件(如CheckBox或Button)都應設置為非聚焦狀態,從而保證用戶在點擊列表項后能夠獲得預期效果。
當前CheckBox沒有綁定應用邏輯,只是用來顯示Crime信息的,因此,解決方法很簡單。只需更新list_item_crime.xml布局文件,將CheckBox定義為非聚焦狀態組件即可,如代碼清單9-21所示。
代碼清單9-21 配置CheckBox為非聚焦狀態(list_item_crime.xml)
運行CriminalIntent應用。滾動查看定制列表項,如圖9-13所示。點擊某個列表項並查看日志,確認CrimeAdapter返回了正確的crime信息。如應用可運行但布局顯示不正確,請返回list_item_crime.xml布局文件,檢查是否存在輸入或拼寫等錯誤。
圖9-13 具有定制列表項的用戶界面

































