前面在介紹列表視圖和網格視圖時,它們的適配器代碼都存在視圖持有者ViewHolder,因為Android對列表類視圖提供了回收機制,如果某些列表項在屏幕上看不到了,則系統會自動回收相應的視圖對象。隨著用戶的下拉或者上拉手勢,已經被回收的列表項要重新載入到界面上,倘若每次載入都得從頭創建視圖對象,勢必 ...
前面在介紹列表視圖和網格視圖時,它們的適配器代碼都存在視圖持有者ViewHolder,因為Android對列表類視圖提供了回收機制,如果某些列表項在屏幕上看不到了,則系統會自動回收相應的視圖對象。隨著用戶的下拉或者上拉手勢,已經被回收的列表項要重新載入到界面上,倘若每次載入都得從頭創建視圖對象,勢必增加了系統的資源開銷。所以ViewHolder便應運而生,它在列表項首次初始化時,就將其視圖對象保存起來,後面再次載入該視圖時,即可直接從持有者處獲得先前的視圖對象,從而減少了系統開銷,提高了系統的運行效率。
視圖持有者的設計理念固然美好,卻苦了Android開發者,每次由BaseAdapter派生新的適配器類,都必須手工處理視圖持有者的相關邏輯,實在是個沉重的負擔。有鑒於此,迴圈視圖的適配器把視圖持有者的重用邏輯剝離出來,由系統自行判斷並處理持有者的重用操作。開發者繼承RecyclerView.Adapter之後,只要完成業務上的代碼邏輯即可,無需進行BaseAdapter視圖持有者的手工重用。
現在由Kotlin實現迴圈視圖的適配器類,綜合前面兩小節提到的優化技術,加上視圖持有者的自動重用,適配器代碼又得到了進一步的精簡。由於迴圈視圖適配器並不提供列表項的點擊事件,因此開發者要自己編寫包括點擊、長按在內的事件處理代碼。為方便理解迴圈適配器的Kotlin編碼,下麵以微信的公眾號消息列表為例,給出對應的消息列表Kotlin代碼:
//ViewHolder在構造時初始化佈局中的控制項對象 class RecyclerLinearAdapter(private val context: Context, private val infos: MutableList<RecyclerInfo>) : RecyclerView.Adapter<ViewHolder>(), OnItemClickListener, OnItemLongClickListener { val inflater: LayoutInflater = LayoutInflater.from(context) //獲得列表項的數目 override fun getItemCount(): Int = infos.size //創建整個佈局的視圖持有者 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view: View = inflater.inflate(R.layout.item_recycler_linear, parent, false) return ItemHolder(view) } //綁定每項的視圖持有者 override fun onBindViewHolder(holder: ViewHolder, position: Int) { val vh: ItemHolder = holder as ItemHolder vh.iv_pic.setImageResource(infos[position].pic_id) vh.tv_title.text = infos[position].title vh.tv_desc.text = infos[position].desc // 列表項的點擊事件需要自己實現 vh.ll_item.setOnClickListener { v -> itemClickListener?.onItemClick(v, position) } vh.ll_item.setOnLongClickListener { v -> itemLongClickListener?.onItemLongClick(v, position) true } } //ItemHolder中的屬性在構造時初始化 inner class ItemHolder(view: View) : RecyclerView.ViewHolder(view) { var ll_item = view.findViewById(R.id.ll_item) as LinearLayout var iv_pic = view.findViewById(R.id.iv_pic) as ImageView var tv_title = view.findViewById(R.id.tv_title) as TextView var tv_desc = view.findViewById(R.id.tv_desc) as TextView } private var itemClickListener: OnItemClickListener? = null fun setOnItemClickListener(listener: OnItemClickListener) { this.itemClickListener = listener } private var itemLongClickListener: OnItemLongClickListener? = null fun setOnItemLongClickListener(listener: OnItemLongClickListener) { this.itemLongClickListener = listener } override fun onItemClick(view: View, position: Int) { val desc = "您點擊了第${position+1}項,標題是${infos[position].title}" context.toast(desc) } override fun onItemLongClick(view: View, position: Int) { val desc = "您長按了第${position+1}項,標題是${infos[position].title}" context.toast(desc) } }
以上的適配器代碼初步實現了公眾號消息列表的展示頁面,具體的列表效果如下圖所示。
可是這個迴圈適配器RecyclerLinearAdapter仍然體量龐大,細細觀察發現其實它有著數個與具體業務無關的屬性與方法,譬如上下文對象context、佈局載入對象inflater、點擊監聽器itemClickListener、長按監聽器itemLongClickListener等等,故而完全可以把這些通用部分提取到一個基類,然後具體業務再從該基類派生出特定的業務適配器類。根據這種設計思路,提取出了迴圈視圖基礎適配器,它的Kotlin代碼如下所示:
//迴圈視圖基礎適配器 abstract class RecyclerBaseAdapter<VH : RecyclerView.ViewHolder>(val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), OnItemClickListener, OnItemLongClickListener { val inflater: LayoutInflater = LayoutInflater.from(context) //獲得列表項的個數,需要子類重寫 override abstract fun getItemCount(): Int //根據佈局文件創建視圖持有者,需要子類重寫 override abstract fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder //綁定視圖持有者中的各個控制項對象,需要子類重寫 override abstract fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) override fun getItemViewType(position: Int): Int = 0 override fun getItemId(position: Int): Long = position.toLong() var itemClickListener: OnItemClickListener? = null fun setOnItemClickListener(listener: OnItemClickListener) { this.itemClickListener = listener } var itemLongClickListener: OnItemLongClickListener? = null fun setOnItemLongClickListener(listener: OnItemLongClickListener) { this.itemLongClickListener = listener } override fun onItemClick(view: View, position: Int) {} override fun onItemLongClick(view: View, position: Int) {} }
一旦有了這個基礎適配器,實際業務的適配器即可由此派生而來,真正需要開發者編寫的代碼一下精簡了不少。下麵便是個迴圈視圖的網格適配器,它實現了類似淘寶主頁的網格頻道欄目,具體的Kotlin代碼如下所示:
//把公共屬性和公共方法剝離到基類RecyclerBaseAdapter, //此處僅需實現getItemCount、onCreateViewHolder、onBindViewHolder三個方法,以及視圖持有者的類定義 class RecyclerGridAdapter(context: Context, private val infos: MutableList<RecyclerInfo>) : RecyclerBaseAdapter<RecyclerView.ViewHolder>(context) { override fun getItemCount(): Int = infos.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val view: View = inflater.inflate(R.layout.item_recycler_grid, parent, false) return ItemHolder(view) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val vh = holder as ItemHolder vh.iv_pic.setImageResource(infos[position].pic_id) vh.tv_title.text = infos[position].title } inner class ItemHolder(view: View) : RecyclerView.ViewHolder(view) { var ll_item = view.findViewById(R.id.ll_item) as LinearLayout var iv_pic = view.findViewById(R.id.iv_pic) as ImageView var tv_title = view.findViewById(R.id.tv_title) as TextView } }
改進後的迴圈網格適配器,運行之後的界面效果如下圖所示,無縫實現了原來需要數十行Java代碼才能實現的功能。
然而基類不過是雕蟲小技,Java也照樣能夠運用,所以這根本不入Kotlin的法眼,要想超越Java,還得擁有獨門秘笈才行。註意到適配器代碼仍然通過findViewById方法獲得控制項對象,可是號稱在Anko庫的支持之下,Kotlin早就無需該方法就能直接訪問控制項對象了呀,為啥這裡依舊靠老牛拉破車呢?其中的緣由是Anko庫僅僅實現了Activity活動頁面的控制項自動獲取,並未實現適配器內部的自動獲取。不過Kotlin早就料到了這一手,為此專門提供了一個插件名叫LayoutContainer,只要開發者讓自定義的ViewHolder繼承該介面,即可在視圖持有者內部無需獲取就能使用控制項對象了。這下不管是在Activity代碼,還是在適配器代碼中,均可將控制項名稱拿來直接調用了。這麼神奇的魔法,快來看看Kotlin的適配器代碼是如何書寫的:
//利用Kotlin的插件LayoutContainer,在適配器中直接使用控制項對象,而無需對其進行顯式聲明 class RecyclerStaggeredAdapter(context: Context, private val infos: MutableList<RecyclerInfo>) : RecyclerBaseAdapter<RecyclerView.ViewHolder>(context) { override fun getItemCount(): Int = infos.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val view: View = inflater.inflate(R.layout.item_recycler_staggered, parent, false) return ItemHolder(view) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { (holder as ItemHolder).bind(infos[position]) } //註意這裡要去掉inner,否則運行報錯“java.lang.NoSuchMethodError: No virtual method _$_findCachedViewById” class ItemHolder(override val containerView: View?) : RecyclerView.ViewHolder(containerView), LayoutContainer { fun bind(item: RecyclerInfo) { iv_pic.setImageResource(item.pic_id) tv_title.text = item.title } } }
當然,為了能夠正常使用該功能,需要在適配器代碼頭部加上以下兩行代碼,其中第一行代碼表示引用了Kotlin的擴展插件LayoutContainer,第二行代碼與Activity的一樣表示導入了指定佈局文件裡面所有控制項對象:
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.item_recycler_staggered.*
另外,因為LayoutContainer是Kotlin針對性提供給Android的擴展插件,所以需要修改模塊的build.gradle,在文件末尾添加下麵幾行配置,表示允許引用安卓插件庫:
androidExtensions { experimental = true }
即使修改後的適配器代碼用了新插件,外部仍舊同原來一樣給迴圈視圖設置適配器,調用代碼並無任何變化:
//第一種方式:使用採取了LayoutContainer的插件適配器 val adapter = RecyclerStaggeredAdapter(this, RecyclerInfo.defaultStag) rv_staggered.adapter = adapter
採用了新的適配器插件,似乎已經大功告成,可是依然要書寫單獨的適配器代碼,仔細研究發現這個RecyclerStaggeredAdapter還有三個要素是隨著具體業務而變化的,包括:
1、列表項的佈局文件資源編碼,如R.layout.item_recycler_staggered;
2、列表項信息的數據結構名稱,如RecyclerInfo;
3、對各種控制項對象的設置操作,如ItemHolder類的bind方法;
除了以上三個要素,RecyclerStaggeredAdapter內部的其餘代碼都是允許復用的,因此,接下來的工作就是想辦法把這三個要素抽象為公共類的某種變數。對於第一個的佈局編碼,可以考慮將其作為一個整型的輸入參數;對於第二個的數據結構,可以考慮定義一個模板類,在外部調用時再指定具體的數據類;對於第三個的bind方法,若是Java編碼早已束手無策,現用Kotlin編碼正好將該方法作為一個函數參數傳入。依照三個要素的三種處理對策,進而提煉出來了迴圈適配器的通用類RecyclerCommonAdapter,詳細的Kotlin代碼示例如下:
//迴圈視圖通用適配器 //將具體業務中會變化的三類要素抽取出來,作為外部傳進來的變數。這三類要素包括: //佈局文件對應的資源編號、列表項的數據結構、各個控制項對象的初始化操作 class RecyclerCommonAdapter<T>(context: Context, private val layoutId: Int, private val items: List<T>, val init: (View, T) -> Unit): RecyclerBaseAdapter<RecyclerCommonAdapter.ItemHolder<T>>(context) { override fun getItemCount(): Int = items.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val view: View = inflater.inflate(layoutId, parent, false) return ItemHolder<T>(view, init) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val vh: ItemHolder<T> = holder as ItemHolder<T> vh.bind(items.get(position)) } //註意init是個函數形式的輸入參數 class ItemHolder<in T>(val view: View, val init: (View, T) -> Unit) : RecyclerView.ViewHolder(view) { fun bind(item: T) { init(view, item) } } }
有了這個通用適配器,外部使用適配器只需像函數調用那樣傳入這三種變數就好了,具體調用的Kotlin代碼如下所示:
//第二種方式:使用把三類可變要素抽象出來的通用適配器 val adapter = RecyclerCommonAdapter(this, R.layout.item_recycler_staggered, RecyclerInfo.defaultStag, {view, item -> val iv_pic = view.findViewById(R.id.iv_pic) as ImageView val tv_title = view.findViewById(R.id.tv_title) as TextView iv_pic.setImageResource(item.pic_id) tv_title.text = item.title }) rv_staggered.adapter = adapter
最終出爐的適配器僅有十行代碼不到,其中的關鍵技術——函數參數真是不鳴則已、一鳴驚人。至此本節的適配器實現過程終於落下帷幕,一路上可謂是過五關斬六將,硬生生把數十行的Java代碼壓縮到不到十行的Kotlin代碼,經過不斷迭代優化方取得如此彪炳戰績。尤其是最後的兩種實現方式,分別運用了Kotlin的多項綜合技術,才能集Kotlin精妙語法之大成。