天天看點

Android萬能擴充卡base-adapter-helper的源碼分析

項目位址:https://github.com/JoanZapata/base-adapter-helper

1. 功能介紹

1.1. base-adapter-helper

base-adapter-helper 是對傳統的 BaseAdapter ViewHolder 模式的一個封裝。主要功能就是簡化我們書寫 AbsListView 的 Adapter 的代碼,如 ListView,GridView。

1.2 基本使用

mListView.setAdapter(mAdapter = new QuickAdapter<Bean>(MainActivity.this, R.layout.item_list, mDatas) {

    @Override
    protected void convert(BaseAdapterHelper helper, Bean item) {
        helper.setText(R.id.tv_title, item.getTitle());
        helper.setImageUrl(R.id.id_icon, item.getUrl());
        helper.setText(R.id.tv_describe, item.getDesc());
        helper.setText(R.id.tv_phone, item.getPhone());
        helper.setText(R.id.tv_time, item.getTime());
    }
});
           

1.3 優點

(1) 提供 QucikAdapter,省去類似 getCount() 等抽象函數的書寫,隻需關注 Model 到 View 的顯示。

(2) BaseAdapterHelper 中封裝了大量用于為 View 操作的輔助方法,例如從網絡加載圖檔:

helper.setImageUrl(R.id.iv_photo, item.getPhotoUrl());

1.4 缺點

(1) 與 Picasso 耦合,想替換為其他圖檔緩存需要修改源碼。

可通過接口方式,供三方根據自己的圖檔緩存庫實作圖檔擷取,或者直接去掉

helper.setImageUrl(…)

函數。

(2) 與内部添加的進度條偶爾,導緻不支援多種類型布局

在本文最後給出不改動進度條的解決方法。更好的實作方式應該是通過接口方式暴露,供三方自己設定。

(3) 目前的方案也不支援

HeaderViewListAdapter

總體來說這個庫比較簡單,實作也有待改進。

2. 總體設計

由于 base-adapter-helper 本質上仍然是 ViewHolder 模式,下面分别是 base-adapter-helper 的總體設計圖和 ViewHolder 模式的設計圖,通過兩圖的比較,可以看出 base-adapter-helper 對傳統的

BaseAdapter

進行了初步的實作(

QuickAdapter

),并且其子類僅需實作

convert(…)

方法,在

convert(…)

中可以拿到

BaseAdapterHelper

,

BaseAdapterHelper

就相當于

ViewHolder

,但其内部提供了大量的輔助方法,用于設定 View 上的資料及事件等。

base-adapter-helpr
Android萬能擴充卡base-adapter-helper的源碼分析
ViewHolder Pattern
Android萬能擴充卡base-adapter-helper的源碼分析

3. 詳細設計

3.1 類關系圖

Android萬能擴充卡base-adapter-helper的源碼分析

這是 base-adapter-helper 庫的主要類關系圖。

(1) 在 BaseQucikAdapter 中實作了 BaseAdapter 中通用的抽象方法;

(2) BaseQuickAdapter 中兩個泛型,其中 T 表示資料實體類(Bean)類型,H 表示 BaseAdapterHelper 或其子類;

(3) QucikAdapter 繼承自 BaseQuickAdapter,并且傳入 BaseAdapterHelper 作為 H 泛型;

(4) EnhancedQuickAdapter 主要為

convert(…)

方法添加了一個 itemChanged 參數,表示 item 對應資料是否發生變化;

(5) BaseAdapterHelper 為用于擷取 View 并進行内容、事件設定等相關操作的輔助類。其中多數用于設定的方法都采用鍊式程式設計,友善書寫;

(6) 可以根據自己需要繼承 BaseAdapterHelper 來擴充,做為 BaseQuickAdapter 子類的 H 泛型。

3.2 核心類源碼分析

3.2.1 BaseQucikAdapter.java

該類繼承自 BaseAdapter,完成 BaseAdapter 中部分通用抽象方法的實作,類似

ArrayAdapter

該類聲明了兩個泛型,其中 T 表示資料實體類(Bean)類型,H 表示 BaseAdapterHelper 或其子類,主要在擴充

BaseAdapterHelper

時使用。

(1) 構造方法
public BaseQuickAdapter(Context context, int layoutResId) {
    this(context, layoutResId, null);
}

public BaseQuickAdapter(Context context, int layoutResId, List<T> data) {
    this.data = data == null ? new ArrayList<T>() : new ArrayList<T>(data);
    this.context = context;
    this.layoutResId = layoutResId;
}
           

Adapter 的必須元素 ItemView 的布局檔案通過 layoutResId 指定,待展示資料通過 data 指定。

(2) 已經實作的主要方法
@Override
public int getCount() {
    int extra = displayIndeterminateProgress ? 1 : 0;
    return data.size() + extra;
}

@Override
public int getViewTypeCount() {
    return 2;
}

@Override
public int getItemViewType(int position) {
    return position >= data.size() ? 1 : 0;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    if (getItemViewType(position) == 0) {
        final H helper = getAdapterHelper(position, convertView, parent);
        T item = getItem(position);
        helper.setAssociatedObject(item);
        convert(helper, item);
        return helper.getView();
    }

    return createIndeterminateProgressView(convertView, parent);
}
           

上面列出了 BaseQucikAdapter 中已經實作的主要方法,跟一般 BaseAdapter 類似,我們重點看以下幾個點:

a. 重寫了

getViewTypeCount()

getItemViewType()

,這裡 type 為 2,通過

getView(…)

可以看出,主要是為了在 AbsListView 最後顯示一個進度條,這裡也暴露了一個弊端,無法支援多種 Item 樣式的布局;

b.

getView(…)

方法的實作中首先通過抽象函數

getAdapterHelper(…)

得到 BaseAdapterHelper 及 item,然後通過抽象函數

convert(…)

實作 View 和 資料的綁定。

這樣

BaseQucikAdapter

子類隻需要實作抽象函數

getAdapterHelper(…)

convert(…)

即可。

(3) 待實作的抽象方法
protected abstract void convert(H helper, T item);

protected abstract H getAdapterHelper(int position, View convertView, ViewGroup parent);
           

a. convert(H helper, T item)

通過

helper

将 View 和 資料綁定。

helper

參數表示 BaseQuickAdapter 或其子類,用于擷取 View 并進行内容、事件設定等相關操作,由

getAdapterHelper(…)

函數傳回;

item

表示對應的資料。

b. getAdapterHelper(int position, View convertView, ViewGroup parent)

傳回 BaseQuickAdapter 或其子類,綁定 item,然後傳回值傳遞給上面的

convert(…)

函數。

關于

getAdapterHelper(…)

的實作見下面

QuickAdapter

的介紹。

3.2.2 QucikAdapter.java

這個類繼承自

BaseQuickAdapter

,沒什麼代碼,主要用于提供一個可快速使用的 Adapter。

對于

getAdapterHelper(…)

函數直接傳回了

BaseAdapterHelper

,一般情況下直接用此類作為 Adapter 即可,如

1.2 基本使用

的示例。

但如果你擴充了

BaseAdapterHelper

,重寫

getAdapterHelper(…)

函數将其傳回,即可實作自己的 Adapter。

3.2.3 EnhancedQuickAdapter.java

繼承自

QuickAdapter

,僅僅是為

convert(…)

添加了一個參數

itemChanged

,表示 item 對應資料是否發生變化。

@Override
protected final void convert(BaseAdapterHelper helper, T item) {
    boolean itemChanged = helper.associatedObject == null || !helper.associatedObject.equals(item);
    helper.associatedObject = item;
    convert(helper, item, itemChanged);
}

protected abstract void convert(BaseAdapterHelper helper, T item, boolean itemChanged);
           

可以看到它的實作是通過

helper.associatedObject

equals()

方法判斷資料是否發生變化,associatedObject 即我們的 bean。在

BaseQuickAdapter.getView(…)

可以看到其指派的代碼。

3.2.4 BaseAdapterHelper.java

可用于擷取 View 并進行内容設定等相關操作的輔助類,該類的功能有:

(1) 充當了 ViewHolder 角色,KV 形式儲存 convertView 中子 View 的 id 及其引用,友善查找。和 convertView 通過 tag 關聯;

(2) 提供了一堆輔助方法,用于為子 View 設定内容、樣式、事件等。

(1) 構造相關方法
protected BaseAdapterHelper(Context context, ViewGroup parent, int layoutId, int position) {
    this.context = context;
    this.position = position;
    this.views = new SparseArray<View>();
    convertView = LayoutInflater.from(context) //
            .inflate(layoutId, parent, false);
    convertView.setTag(this);
}

public static BaseAdapterHelper get(Context context, View convertView, ViewGroup parent, int layoutId) {
    return get(context, convertView, parent, layoutId, -1);
}

/** This method is package private and should only be used by QuickAdapter. */
static BaseAdapterHelper get(Context context, View convertView, ViewGroup parent, int layoutId, int position) {
    if (convertView == null) {
        return new BaseAdapterHelper(context, parent, layoutId, position);
    }

    // Retrieve the existing helper and update its position
    BaseAdapterHelper existingHelper = (BaseAdapterHelper) convertView.getTag();
    existingHelper.position = position;
    return existingHelper;
}
           

QuickAdapter

中,通過上面的 5 個參數的靜态函數

get(…)

得到

BaseAdapterHelper

的執行個體。4 個參數的

get(…)

方法,隻是将 position 預設傳入了 -1,即不關注 postion 方法。

這裡可以對比下我們平時在

getView

中編寫的 ViewHolder 模式的代碼。在一般的 ViewHolder 模式中,先判斷

convertView

是否為空:

a. 如果是,則通過

LayoutInflater

inflate 一個布局檔案,然後建立 ViewHolder 存儲布局中各個子 View,通過 tag 綁定該 ViewHolder 到

convertView

,傳回我們的

convertView

b. 否則直接得到 tag 中的 ViewHolder。

結合

BaseQuickAdapter

getView(…)

代碼,看下 base-adapter-helper 的實作。

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    if (getItemViewType(position) == 0) {
        final H helper = getAdapterHelper(position, convertView, parent);
        T item = getItem(position);
        helper.setAssociatedObject(item);
        convert(helper, item);
        return helper.getView();
    }

    return createIndeterminateProgressView(convertView, parent);
}
           

先利用

getAdapterHelper(…)

得到

BaseAdapterHelper

或其子類,對于

QuickAdapter

而言,這個函數直接調用上面

BaseAdapterHelper

get(…)

函數。我們可以看到同樣是先判斷

convertView

是否為空,以确定是否需要建立

BaseAdapterHelper

,否則從 tag 中擷取更新 position 後重用。

在構造方法中 inflate 了一個布局作為

convertView

,并且儲存 context 及 postion,将

convertView

BaseAdapterHelper

通過

tag

關聯。

(2) 幾個重要的方法

一般情況下,在我們重寫

BaseQuickAdapter

convert(…)

時,需要得到 View,這時我們可以通過其入參

BaseAdapterHelper

getView(int viewId)

得到該

View

,代碼如下:

public <T extends View> T getView(int viewId) {
    return retrieveView(viewId);
}

@SuppressWarnings("unchecked")
protected <T extends View> T retrieveView(int viewId) {
    View view = views.get(viewId);
    if (view == null) {
        view = convertView.findViewById(viewId);
        views.put(viewId, view);
    }
    return (T) view;
}
           

通過 viewId 去 views 中進行尋找,找到則傳回,找不到則添加并傳回。views 是一個 SparseArray,key為 view id,value 為 view,緩存已經查找到的子 view。

每個

convertView

與一個

BaseAdapterHelper

綁定,每個

BaseAdapterHelper

中包含一個

views

屬性,

views

中存儲

convertView

的子 View 的引用。

(3) 輔助方法

一般情況下,通過

getView(int viewId)

拿到該

View

,然後進行指派就可以了。但是此庫考慮:既然是拿到 View 然後指派,不如直接提供一些指派的輔助方法。于是産生了一堆類似

setText(int viewId, String value)

的代碼,内部首先通過 viewId 找到該 View,轉為

TextView

然後調用

setText(value)

。部分代碼如下:

public BaseAdapterHelper setText(int viewId, String value) {
    TextView view = retrieveView(viewId);
    view.setText(value);
    return this;
}

public BaseAdapterHelper setImageResource(int viewId, int imageResId) {…}

public BaseAdapterHelper setBackgroundRes(int viewId, int backgroundRes) {…}

public BaseAdapterHelper setTextColorRes(int viewId, int textColorRes) {…}

public BaseAdapterHelper setImageDrawable(int viewId, Drawable drawable) {…}

public BaseAdapterHelper setImageUrl(int viewId, String imageUrl) {…}

public BaseAdapterHelper setImageBitmap(int viewId, Bitmap bitmap) {…}

@SuppressLint("NewApi")
public BaseAdapterHelper setAlpha(int viewId, float value) {…}

public BaseAdapterHelper setVisible(int viewId, boolean visible) {…}

public BaseAdapterHelper linkify(int viewId) {…}

public BaseAdapterHelper setProgress(int viewId, int progress, int max) {…}

public BaseAdapterHelper setRating(int viewId, float rating, int max) {…}

public BaseAdapterHelper setTag(int viewId, int key, Object tag) {…}

public BaseAdapterHelper setChecked(int viewId, boolean checked) {…}

public BaseAdapterHelper setAdapter(int viewId, Adapter adapter) {…}
……
           

實作都是根據 viewId 找到 View,然後為 View 指派的代碼。

這裡隻要注意下:

setImageUrl(int viewId, String imageUrl)

這個方法,預設是通過

Picasso

去加載圖檔的,當然你可以更改成你項目中使用的圖檔加載架構 Volley,UIL 等,如果不希望繼續耦合,可參考

1.4 缺點

的建議改法。

也可以為子 View 去設定一個事件監聽,部分代碼如下:

public BaseAdapterHelper setOnClickListener(int viewId, View.OnClickListener listener) {
    View view = retrieveView(viewId);
    view.setOnClickListener(listener);
    return this;
}

public BaseAdapterHelper setOnTouchListener(int viewId, View.OnTouchListener listener) {…}

public BaseAdapterHelper setOnLongClickListener(int viewId, View.OnLongClickListener listener) {…}
           

這裡僅僅列出一些常用的方法,如果有些控件的方法這裡沒有封裝,可以通過

BaseAdapterHelper.getView(viewId)

得到控件去操作,或者繼承

BaseAdapterHelper

實作自己的

BaseAdapterHelper

4. 雜談

4.1 耦合嚴重

(1) 與 Picasso 耦合,想替換為其他圖檔緩存需要修改源碼

可通過新增接口方式,供三方自己根據自己的圖檔緩存庫實作圖檔擷取,或者直接去掉

helper.setImageUrl(…)

函數。

(2) 與内部添加的進度條耦合,導緻不支援多種類型布局

在下面給出不改動進度條的解決方法。更好的實作方式應該是通過接口方式暴露,供三方自己設定。

總體來說這個庫比較簡單,實作也有待改進。

4.2 目前的方案也不支援

HeaderViewListAdapter

4.3 擴充多種 Item 布局

通過

3.2.1 BaseQucikAdapter.java

的分析,可以看出 base-adapter-helper 并不支援多種布局 Item 的情況,雖然大多數情況下一個種樣式即可,但是要是讓我用這麼簡單的方式寫 Adapter,忽然來個多種布局 Item 的 ListView 又要 按傳統的方式去寫,這反差就太大了。下面我們介紹,如何在本庫的基礎上添加多布局 Item 的支援。

(1) 分析

對于多種布局的 Item,大家都清楚,需要去複寫

BaseAdapter

getViewTypeCount()

getItemViewType()

。并且需要在

getView()

裡面進行判斷并選取不同布局檔案,不同的布局也需要采用不同的

ViewHolder

我們可以在構造

QucikAdapter

時,去設定

getViewTypeCount()

getItemViewType()

的值,進一步将其抽象為一個接口,提供幾個方法,如果需要使用多種 Item 布局,進行設定即可。

(2) 擴充
  • 添加接口

    MultiItemTypeSupport

    public interface MultiItemTypeSupport<T> {
    
      int getLayoutId(int position, T t);
    
      int getViewTypeCount();
    
      int getItemViewType(int postion, T t);
    }
               
  • 分别在

    QuickAdapter

    BaseQuickAdapter

    中添加新的構造函數

BaseQuickAdapter

新增構造函數如下:

protected MultiItemTypeSupport<T> multiItemSupport;

public BaseQuickAdapter(Context context, ArrayList<T> data,
        MultiItemTypeSupport<T> multiItemSupport) {
    this.multiItemSupport = multiItemSupport;
    this.data = data == null ? new ArrayList<T>() : new ArrayList<T>(data);
    this.context = context;
}
           

QuickAdapter

新增構造函數如下:

public QuickAdapter(Context context, ArrayList<T> data,
        MultiItemTypeSupport<T> multiItemSupport) {
    super(context, data, multiItemSupport);
}
           

同時肯定需要改寫

BaseQuickAdapter

getViewTypeCount()

getItemViewType()

以及

getView()

函數。

@Override
public int getViewTypeCount() {
    return multiItemSupport != null ? (mMultiItemSupport.getViewTypeCount() + 1) : 2);
}

@Override
public int getItemViewType(int position) {
    if (position >= data.size()) {
        return 0;
    }
    return (mMultiItemSupport != null) ? 
        mMultiItemSupport.getItemViewType(position, data.get(position)) : 1;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    if (getItemViewType(position) == 0) {
        return createIndeterminateProgressView(convertView, parent);
    }
    final H helper = getAdapterHelper(position, convertView, parent);
    T item = getItem(position);
    helper.setAssociatedObject(item);
    convert(helper, item);
    return helper.getView();
}
           

為了保留其原本提供的添加滾動條的功能,我們在其基礎上進行修改。

  • 改寫

    BaseAdapterHelper

    的構造方法

因為我們不同的布局,肯定要對應不同的

ViewHolder

,這裡

BaseAdapterHelper

其實就扮演了

ViewHolder

的角色。我們的

BaseAdapterHelper

是在

QuickAdapter

getAdapterHelper

中構造的,修改後代碼:

QuickAdapter

protected BaseAdapterHelper getAdapterHelper(int position,
        View convertView, ViewGroup parent) {

    if (mMultiItemSupport != null){
        return get(
                context,
                convertView,
                parent,
                mMultiItemSupport.getLayoutId(position, data.get(position)),
                position);
    } else {
        return get(context, convertView, parent, layoutResId, position);
    }
}
           

BaseAdapterHelper

get

方法也需要修改。

/** This method is package private and should only be used by QuickAdapter. */
static BaseAdapterHelper get(Context context, View convertView,
        ViewGroup parent, int layoutId, int position) {
    if (convertView == null) {
        return new BaseAdapterHelper(context, parent, layoutId, position);
    }

    // Retrieve the existing helper and update its position
    BaseAdapterHelper existingHelper = (BaseAdapterHelper)convertView
            .getTag();

    if (existingHelper.layoutId != layoutId) {
        return new BaseAdapterHelper(context, parent, layoutId, position);
    }

    existingHelper.position = position;
    return existingHelper;
}
           

我們在 helper 中存儲了目前的 layoutId,如果 layoutId 不一緻,則重新建立。

(3) 測試

下面展示核心代碼

mListView = (ListView) findViewById(R.id.id_lv_main);

MultiItemTypeSupport<ChatMessage> multiItemTypeSupport = new MultiItemTypeSupport<ChatMessage>() {
    @Override
    public int getLayoutId(int position, ChatMessage msg) {
        return msg.isComMeg() ? R.layout.main_chat_from_msg : R.layout.main_chat_send_msg;
    }

    @Override
    public int getViewTypeCount() {
        return 2;
    }

    @Override
    public int getItemViewType(int postion, ChatMessage msg) {
        return msg.isComMeg() ? ChatMessage.RECIEVE_MSG : ChatMessage.SEND_MSG;
    }
};

initDatas();

mAdapter = new QuickAdapter<ChatMessage>(ChatActivity.this, mDatas,
        multiItemTypeSupport) {
    @Override
    protected void convert(BaseAdapterHelper helper, ChatMessage item) {
        switch (helper.layoutId) {
            case R.layout.main_chat_from_msg:
                helper.setText(R.id.chat_from_content, item.getContent());
                helper.setText(R.id.chat_from_name, item.getName());
                helper.setImageResource(R.id.chat_from_icon, item.getIcon());
                break;
            case R.layout.main_chat_send_msg:
                helper.setText(R.id.chat_send_content, item.getContent());
                helper.setText(R.id.chat_send_name, item.getName());
                helper.setImageResource(R.id.chat_send_icon, item.getIcon());
                break;
        }
    }
};

mListView.setAdapter(mAdapter);
           

當遇到多種布局 Item 的時候,首先構造一個

MultiItemTypeSupport

接口對象,然後在

convert

中根據 layoutId,擷取不同的布局進行設定。

貼張效果圖:

Android萬能擴充卡base-adapter-helper的源碼分析

繼續閱讀