##寫在前面
最近一直忙著各種結課大作業,重新看起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;
}
@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;
}
@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;
}
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;
//複用的View
private 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);
//設定Tag
mConvertView.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) {
//如果為空 直接新建一個ViewHolder
if (convertView == null) {
return new CommonViewHolder(context, parent, layoutId, position);
} else {
//否則返回一個已經存在的ViewHolder
CommonViewHolder 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;
//複用的View
private 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) {
//如果為空 直接新建一個ViewHolder
if (convertView == null) {
return new CommonViewHolder(context, parent, layoutId, position);
} else {
//否則返回一個已經存在的ViewHolder
CommonViewHolder 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) {
//得到一個ViewHolder
CommonViewHolder 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());
//返回複用的View
return 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;
}
@Override
public int getCount() {
return list.size();
}
@Override
public T getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
/**
* 封裝getView方法
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
//得到一個ViewHolder
CommonViewHolder viewHolder = CommonViewHolder.get(context, convertView, parent, layoutId, position);
//設定控制元件內容
setViewContent(viewHolder, (T) getItem(position));
//返回複用的View
return 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物件
*/
@Override
public 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
##專案原始碼 CommonAdapter-GitHub-IamXiaRui
個人部落格:www.iamxiarui.com 原文連結:http://www.iamxiarui.com/?p=727