Android:打造“万能”Adapter与ViewHolder
##寫在前面
最近一直忙著各種結課大作業,重新看起Android還有種親切感。前段時間寫項目的時候,學習了一個萬能Adapter與ViewHolder的寫法。說是“萬能”其實就是在各種情況下都能通用。
我們知道,在寫項目的時候,項目中肯定有很多的ListView或者RecyclerView,這個時候我們就要寫大量的Adapter與ViewHolder。盡管重復寫的難度并不大,但是這會讓項目看起來十分冗余,因為存在大量的重復代碼。
所以能不能有一個通用的ViewHolder與Adapter,讓項目中只存在一個ViewHolder與Adapter呢?
當然可以,現在就通過一個小Demo將我學習的知識分享給大家。下面是本文的目錄:
- 項目介紹
- 傳統寫法分析
- 簡單認識SparseArray
- 萬能ViewHolder
- 萬能Adapter
- 結語
- 項目源碼
##項目介紹
先來看這個Demo,很簡單,我就不多說了。
這是項目結構,為了方便后期對比,我將三種Adapter分離開了:
- MainActivity:模擬新聞頁面
- NewsBean:封裝了新聞的Bean
- CommonViewHolder:通用ViewHolder
- CommonAdapter:通用Adapter
- TraditionAdapterWithTraditionHolder:基于傳統Holder的傳統Adapter
- TraditionAdapterWithCommonHolder:基于通用ViewHolder的傳統Adapter
- CommonAdapterWithCommoeHolder:基于通用ViewHolder的通用Adapter
##傳統寫法分析
至于頁面布局、模擬加載數據在這里我就不提了,十分簡單。現在主要看一下傳統的Adapter的寫法。
/*** 基于傳統Holder的傳統Adapter*/ public class TraditionAdapterWithTraditionHolder extends BaseAdapter {private Context context;private List<NewsBean> list;public TraditionAdapterWithTraditionHolder(Context context, List<NewsBean> list) {this.context = context;this.list = list;}@Overridepublic int getCount() {return list.size();}@Overridepublic Object getItem(int position) {return list.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {ViewHolder viewHolder ;if (convertView == null) {convertView = View.inflate(context, R.layout.item_list, null);viewHolder = new ViewHolder();viewHolder.titleText = (TextView) convertView.findViewById(R.id.tv_title);viewHolder.descText = (TextView) convertView.findViewById(R.id.tv_desc);viewHolder.timeText = (TextView) convertView.findViewById(R.id.tv_time);viewHolder.phoneText = (TextView) convertView.findViewById(R.id.tv_phone);convertView.setTag(viewHolder);}else{viewHolder = (ViewHolder) convertView.getTag();}NewsBean bean = list.get(position);viewHolder.titleText.setText(bean.getTitle());viewHolder.descText.setText(bean.getDesc());viewHolder.timeText.setText(bean.getTime());viewHolder.phoneText.setText(bean.getPhone());return convertView;}private class ViewHolder {TextView titleText;TextView descText;TextView timeText;TextView phoneText;} } 復制代碼由于代碼也比較簡單,基本都是 套路 代碼,大家都會寫,都能看懂,所以我就不加以詳細注釋了。
我們知道,如果需要一個通用的Adapter,肯定要對之前的代碼進行封裝。所以現在主要來分析一下這個傳統寫法,看到底哪個地方可以進行封裝。
依次來看,首先是構造函數。只要稍微有點經驗的開發者都知道,一般來說,這里面傳遞的參數幾乎都是一個 Context 與 List ,而List中通常都裝了一個具體內容的 Bean 。
public TraditionAdapterWithTraditionHolder(Context context, List<NewsBean> list) {this.context = context;this.list = list; } 復制代碼所以設想,既然這個Bean每次都需要,那我們是否能否將這個Bean直接給自定義的Adapter呢?比如這樣:
public MyAdapter<T>(Context context, List<T> list) {this.context = context;this.list = list; } 復制代碼先把這個問題拋向天空,來看固定的三個方法,這也沒啥好說的,依舊是套路,所以可以封裝成固定方法,內部實現它,不需暴露出來再復寫:
@Override public int getCount() {return list.size(); }@Override public Object getItem(int position) {return list.get(position); }@Override public long getItemId(int position) {return position; } 復制代碼然后在重頭戲 getView 方法中需要一個ViewHolder,來復用已有的View。然后 new 出List中的 Bean ,賦值后顯示在View上。這就是基本的套路。
private class ViewHolder {TextView titleText;TextView descText;TextView timeText;TextView phoneText; }Override public View getView(int position, View convertView, ViewGroup parent) {ViewHolder viewHolder ;if (convertView == null) {convertView = View.inflate(context, R.layout.item_list, null);viewHolder = new ViewHolder();viewHolder.titleText = (TextView) convertView.findViewById(R.id.tv_title);viewHolder.descText = (TextView) convertView.findViewById(R.id.tv_desc);viewHolder.timeText = (TextView) convertView.findViewById(R.id.tv_time);viewHolder.phoneText = (TextView) convertView.findViewById(R.id.tv_phone);convertView.setTag(viewHolder);}else{viewHolder = (ViewHolder) convertView.getTag();}NewsBean bean = list.get(position);viewHolder.titleText.setText(bean.getTitle());viewHolder.descText.setText(bean.getDesc());viewHolder.timeText.setText(bean.getTime());viewHolder.phoneText.setText(bean.getPhone());return convertView; } 復制代碼而這個ViewHolder套路就更深了,先定義一個ViewHolder類,類中是布局中所需的控件,然后在getView方法中new一個ViewHolder出來,通過這個ViewHolder找到對應的控件,找到后需要設置個Tag,方便之后復用。最后就是通過ViewHolder設置控件的內容了。
既然熟悉了過程,那封裝起來就簡單了許多。首先肯定需要封裝ViewHolder類,不然怎么算的上通用,但是每一個ListView中item布局可能不一樣,肯定不能將控件寫死,那么如何定義控件呢?當控件定義好后,又如何找到這些控件呢?控件找到后又如何設置控件內容呢?
仍然將這些問題拋向天空,接下來再考慮convertView的復用問題,固定寫法,當然也可以封裝。
所以目前來看,如果想要一個Adapter與ViewHolder可以通用,那么 至少 必須做如下工作:
- 將List的泛型參數轉移到Adapter中
- 封裝 getCount、getItem、getItemId方法
- 封裝ViewHolder,并解決不同布局控件不統一問題
- 通用ViewHolder需要找到相應的控件
- 通用ViewHolder需要提供方法來設置相應控件的內容
##簡單認識SparseArray
在寫萬能ViewHolder之前,先來了解一個新的API。我們知道,在Java中一般會用HashMap以鍵值對的形式來存儲一些數據。但是Android給我們提供了一種工具類 SparseArray ,它是Android框架獨有的類,在標準的JDK中不存在這個類。
為什么需要用SparseArray代替HashMap呢?
SparseArray要比 HashMap 節省內存,某些情況下比HashMap性能更好
那為什么SparseArray性能更好呢?按照官方的解釋,原因有以下幾點:
- SparseArray不需要對key和value進行自動裝箱
- 結構比HashMap簡單
- SparseArray內部主要使用兩個一維數組來保存數據,一個用來存key,一個用來存value
- 不需要額外的數據結構(主要是針對HashMap中的HashMapEntry 而言的)
從源碼的構造函數來看,與List一樣,可以通過new的形式來創建一個SparseArray,與Map一樣,可以通過 put(int key, E value) 的形式來添加鍵值對。也可以通過 get(int key) 的方式來獲取值。
好了,就介紹這么多,關于具體的用法,文末附有參考資料鏈接,如有需要可以自行查看。
##萬能ViewHolder
現在就來打造萬能ViewHolder,打造之前再次明確我們需要做的事情:
- 提供方法返回ViewHolder
- 提供方法獲取控件
- 提供方法對控件進行設置
- 提供方法返回復用的View,也就是convertView
先來看如何解決不同布局有不同控件的問題。由于每個控件都有自己固定的ID和控件類型,那么我們可以通過鍵值對的形式來存儲這些控件。在之前可以看到SparseArray能夠提高性能,所以就用SparseArray來存儲控件。
這樣可以先寫出構造函數,在構造函數中,初始化SparseArray,并設置一些內容。
/*** 通用ViewHolder*/ public class CommonViewHolder {//所有控件的集合private SparseArray<View> mViews;//記錄位置 可能會用到private int mPosition;//復用的Viewprivate View mConvertView; 復制代碼?
/*** 構造函數** @param context 上下文對象* @param parent 父類容器* @param layoutId 布局的ID* @param position item的位置*/public CommonViewHolder(Context context, ViewGroup parent, int layoutId, int position) {this.mPosition = position;this.mViews = new SparseArray<>();//構造方法中就指定布局mConvertView = LayoutInflater.from(context).inflate(layoutId, parent, false);//設置TagmConvertView.setTag(this);} } 復制代碼接下來我們就需要得到一個ViewHolder,這個比較簡單,大家都能看懂,就是對Adapter中的getView方法進行一定的封裝:
/*** 得到一個ViewHolder** @param context 上下文對象* @param convertView 復用的View* @param parent 父類容器* @param layoutId 布局的ID* @param position item的位置* @return*/ public static CommonViewHolder get(Context context, View convertView, ViewGroup parent, int layoutId, int position) {//如果為空 直接新建一個ViewHolderif (convertView == null) {return new CommonViewHolder(context, parent, layoutId, position);} else {//否則返回一個已經存在的ViewHolderCommonViewHolder viewHolder = (CommonViewHolder) convertView.getTag();//記得更新條目位置viewHolder.mPosition = position;return viewHolder;} } 復制代碼再接下來就是一個重難點,如何得到布局中的控件?因為我們肯定知道控件的ID,那么可以通過控件的ID來從SparseArray得到具體的控件類型。而Android中所有的控件都是繼承自 View ,所以可以如下這樣寫:
/*** 通過ViewId獲取控件** @param viewId View的Id* @param <T> View的子類* @return 返回View*/ public <T extends View> T getView(int viewId) {View view = mViews.get(viewId);if (view == null) {view = mConvertView.findViewById(viewId);mViews.put(viewId, view);}return (T) view; } 復制代碼通過上述方法,就能得到對應的控件類型。既然得到了,那么設置控件內容就比較簡單了,在本例中都是TextView,所以我封裝了下面的方法:
/*** 為文本設置text** @param viewId view的Id* @param text 文本* @return 返回ViewHolder*/ public CommonViewHolder setText(int viewId, String text) {TextView tv = getView(viewId);tv.setText(text);return this; } 復制代碼最后提供一個方法返回復用的convertView,這也比較簡單。
/*** @return 返回復用的View*/ public View getConvertView() {return mConvertView; } 復制代碼好了,再來看全部的代碼,是不是清晰了很多:
/*** 通用ViewHolder*/ public class CommonViewHolder {//所有控件的集合private SparseArray<View> mViews;//記錄位置 可能會用到private int mPosition;//復用的Viewprivate View mConvertView; 復制代碼?
/*** 構造函數** @param context 上下文對象* @param parent 父類容器* @param layoutId 布局的ID* @param position item的位置*/public CommonViewHolder(Context context, ViewGroup parent, int layoutId, int position) {this.mPosition = position;this.mViews = new SparseArray<>();mConvertView = LayoutInflater.from(context).inflate(layoutId, parent, false);mConvertView.setTag(this);}/*** 得到一個ViewHolder** @param context 上下文對象* @param convertView 復用的View* @param parent 父類容器* @param layoutId 布局的ID* @param position item的位置* @return*/public static CommonViewHolder get(Context context, View convertView, ViewGroup parent, int layoutId, int position) {//如果為空 直接新建一個ViewHolderif (convertView == null) {return new CommonViewHolder(context, parent, layoutId, position);} else {//否則返回一個已經存在的ViewHolderCommonViewHolder viewHolder = (CommonViewHolder) convertView.getTag();//記得更新條目位置viewHolder.mPosition = position;return viewHolder;}}/*** @return 返回復用的View*/public View getConvertView() {return mConvertView;}/*** 通過ViewId獲取控件** @param viewId View的Id* @param <T> View的子類* @return 返回View*/public <T extends View> T getView(int viewId) {View view = mViews.get(viewId);if (view == null) {view = mConvertView.findViewById(viewId);mViews.put(viewId, view);}return (T) view;}/*** 為文本設置text** @param viewId view的Id* @param text 文本* @return 返回ViewHolder*/public CommonViewHolder setText(int viewId, String text) {TextView tv = getView(viewId);tv.setText(text);return this;} } 復制代碼接下來我們就重寫一個基于萬能ViewHolder的Adapter,其他方法都不變,主要是getView方法。
@Override public View getView(int position, View convertView, ViewGroup parent) {//得到一個ViewHolderCommonViewHolder viewHolder = CommonViewHolder.get(context, convertView, parent, R.layout.item_list, position);NewsBean bean = list.get(position);//直接設置控件內容,鏈式調用viewHolder.setText(R.id.tv_title, bean.getTitle()).setText(R.id.tv_desc, bean.getDesc()).setText(R.id.tv_time, bean.getTime()).setText(R.id.tv_phone, bean.getPhone());//返回復用的Viewreturn viewHolder.getConvertView(); } 復制代碼現在來與之前的方法對比,是不是簡單了很多,只需三步:
- 得到一個ViewHolder
- 通過這個ViewHolder直接設置控件內容
- 返回復用的View
看到這里大家肯定有個疑問,在上面ViewHolder中只提供了TextView設置文本的方法,那如果控件不是TextView呢?沒關系,繼續在萬能ViewHolder中封裝就好了:
/*** 設置ImageView** @param viewId view的Id* @param resId 資源Id* @return*/ public CommonViewHolder setImageResource(int viewId, int resId) {ImageView iv = getView(viewId);iv.setImageResource(resId);return this; }/*** 還可以添加更多的方法*/ 復制代碼至此,我們就搞定了一個通用的“萬能”ViewHolder。
##萬能Adapter
有了萬能ViewHolder,我們就可以來打造萬能Adapter了,在文章開頭已經分析過,需要做的事情有一下幾點:
- 將Bean對象直接設置成Adapter的泛型
- 封裝三個固定方法
- 封裝getView方法
- 提供方法設置控件內容
先直接上代碼,其實比較簡單,大家應該能看懂:
/*** 通用Adapter抽象類*/ public abstract class CommonAdapter<T> extends BaseAdapter {protected Context context;protected List<T> list;private int layoutId;public CommonAdapter(Context context, List<T> list, int layoutId) {this.context = context;this.list = list;this.layoutId = layoutId;}@Overridepublic int getCount() {return list.size();}@Overridepublic T getItem(int position) {return list.get(position);}@Overridepublic long getItemId(int position) {return position;}/*** 封裝getView方法*/@Overridepublic View getView(int position, View convertView, ViewGroup parent) {//得到一個ViewHolderCommonViewHolder viewHolder = CommonViewHolder.get(context, convertView, parent, layoutId, position);//設置控件內容setViewContent(viewHolder, (T) getItem(position));//返回復用的Viewreturn viewHolder.getConvertView();}/*** 提供抽象方法,來設置控件內容** @param viewHolder 一個ViewHolder* @param t 一個數據集*/public abstract void setViewContent(CommonViewHolder viewHolder, T t); } 復制代碼這里可以看到我們先自定義一個Adapter繼承BaseAdapter,并將Bean換成Adapter的泛型T了,然后封裝了四個方法。又由于各個控件不一樣,所以提供抽象方法來設置控件內容,我們只要復寫就行了。
此時我們再來看基于萬能ViewHolder的萬能Adapter應該怎樣寫:
/*** 繼承通用Adapter且使用通用Holder的適配器*/ public class CommonAdapterWithCommonHolder extends CommonAdapter<NewsBean> {public CommonAdapterWithCommonHolder(Context context, List<NewsBean> list) {super(context, list,R.layout.item_list);}/*** 復寫抽象方法* @param viewHolder 一個ViewHolder* @param bean Bean對象*/@Overridepublic void setViewContent(CommonViewHolder viewHolder, NewsBean bean) {//直接設置內容 鏈式調用viewHolder.setText(R.id.tv_title, bean.getTitle()).setText(R.id.tv_desc, bean.getDesc()).setText(R.id.tv_time, bean.getTime()).setText(R.id.tv_phone, bean.getPhone());} } 復制代碼看到這里,是不是有點神奇,對比之前的Adapter,這里只要幾行代碼就OK了。
##結語
由于本文說明的不是一種固定的知識,而是一種設計的思想,所以理解起來比較晦澀難懂。我自己在學這個的時候,也是消化了很久,現在回頭看看真的是很巧妙。
不過值得注意的是,這里說的“萬能”其實就是一個俗稱,代表一種通用的Adapter,能避免項目中的大量的重復代碼,提高代碼質量。而這種通用,不一定就是文中的這樣的格式,這里只是提供一個設計思想與大致流程,大家可以自己寫一個通用的、更加強大的Adapter。
最后由于我水平有限與篇幅限制等原因,在寫文章的過程中,有很多地方寫的不夠詳細或者有明顯的疏漏與錯誤,歡迎大家交流與指正。
##參考資料
Android應用性能優化之使用SparseArray替代HashMap
SparseArray替代HashMap來提高性能
如何打造萬能適配器
##項目源碼 CommonAdapter-GitHub-IamXiaRui
個人博客:www.iamxiarui.com 原文鏈接:http://www.iamxiarui.com/?p=727
總結
以上是生活随笔為你收集整理的Android:打造“万能”Adapter与ViewHolder的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: a标签中href=javacript:;
- 下一篇: jenkins+maven+ansibl