【Android Adapter】是時候開啟Adapter新時代了

panpf發表於2019-04-15

更新日誌:

  • 2016.08.28日更新,基於2.2.0版本,主要更新了header和footer支援隱藏或顯示
  • 2016.06.21日更新,基於2.1.0版本,主要更新了header和footer的支援,以及新版載入更多的API

做Android開發的都感同身受,寫列表時最麻煩的就是要針對每個不同的列表寫一個Adapter,這樣下來一般的專案都得有幾十個Adapter,要是Adapter中還有其它的功能比如多Item,以及載入更多那就更麻煩了。要是同樣的內容在不同的頁面還要寫多次那就更苦逼了

其實我們仔細分析一下就可得知每個Adapter無非包含兩部分

  • 第一是Adapter本身,包含處理getView、itemType、載入更多以及維護資料
  • 第二是Item,包含建立convertView、設定資料、設定並處理事件

那我們到底能不能寫一個通用的能夠處理所有ItemView的Adapter呢?根據我多年的經驗告訴大家這絕逼是不可能的,這裡面唯一的障礙就是ItemView,每個ItemView都是有很大不同的。

那麼我們理想中的Adapter應該是什麼樣子的呢?

  • Item部分完全獨立,滿足一處定義處處使用
  • 支援header和footer,讓GridView、RecyclerView也支援header和footer
  • 自帶載入更多功能

縱觀能搜尋到的那些號稱通用的Adapter(這裡有個比較完整的一個示例)最後還不都是讓你傳一個佈局ID,然後通過一個介面讓你來設定資料。其實這樣的實現方式應付一些簡單的場景尚可,但只要涉及到itemType複雜度就成幾何倍數升級了,而現在稍微複雜點兒的列表幾乎都離不開itemType,並且無法做到一處定義處處使用

AssemblyAdapter

今天就給大家介紹一款全新的Adapter庫AssemblyAdapter(GitHub傳送門),先來看一下其特性:

  • Item一處定義處處使用. 你只需為每一個item layout寫一個ItemFactory,然後使用ItemFactory即可
  • 便捷的組合式多Item. 可以使用多個ItemFactory,每個ItemFactory就代表一種itemType
  • 支援header和footer. 使用AssemblyAdapter可以讓ExpandableListView、GridView、RecyclerView、ViewPager等也支援header和footer
  • 隨意隱藏、顯示header或footer. header和footer還支援通過其setEnabled(boolean)方法控制隱藏或顯示
  • 自帶載入更多功能. 自帶滑動到列表底部觸發載入更多功能,你只需定義一個專門用於載入更多的ItemFactory即可
  • 支援常用Adapter. 支援BaseAdapter、RecyclerView.Adapter、BaseExpandableListAdapter、PagerAdapter、 FragmentPagerAdapter和FragmentStatePagerAdapter,涵蓋了Android開發中常用的大部分Adapter
  • 無效能損耗. 沒有使用任何反射相關的技術,因此無須擔心效能問題

共有6種Adapter:

Adapter 父類 適用於 支援功能
AssemblyAdapter BaseAdapter ListView、GridView、Spinner、Gallery 多Item、header和footer、載入更多
AssemblyRecyclerViewAdapter RecyclerView.Adapter RecyclerView 多Item、header和footer、載入更多
AssemblyExpandableAdapter BaseExpandableListAdapter ExpandableListView 多Item、header和footer、載入更多
AssemblyPagerAdapter PagerAdapter ViewPager + View 多Item、header和footer
AssemblyFragmentPagerAdapter FragmentPagerFragment ViewPager + Fragment 多Item、header和footer
AssemblyFragmentStatePagerAdapter FragmentStatePagerFragment ViewPager + Fragment 多Item、header和footer

接下來以AssemblyAdapter為例講解具體的用法,其它Adapter你只需照葫蘆畫瓢,然後ItemFactory和Item繼承各自專屬的類即可,詳情請參考sample原始碼

AssemblyAdapter分為三部分:

  • Adapter:負責維護資料、itemType以及載入更多的狀態
  • ItemFactory:負責匹配資料和建立Item
  • Item:負責itemView的一切,包括建立itemView、設定資料、設定並處理事件

AssemblyAdapter與其它萬能Adapter最根本的不同就是其把item相關的處理全部定義在了一個ItemFactory類裡面,在使用的時候只需通過Adapter的addItemFactory(AssemblyItemFactory)方法將ItemFactory加到Adapter中即可。

這樣的好處就是真正做到了一處定義處處使用,並且可以方便的在一個頁面通過多次呼叫addItemFactory(AssemblyItemFactory)方法使用多個ItemFactory(每個ItemFactory就代表一種ItemType),這正體現了AssemblyAdapter名字中Assembly所表達的意思

另外由於支援多Item,一個Adapter又只有一個資料列表,所以資料列表的資料型別就得是Object

3. 建立ItemFactory

在使用AssemblyAdapter之前得先建立ItemFactory和Item,如下:

public class UserItemFactory extends AssemblyItemFactory<UserItemFactory.UserItem> {

    @Override
    public boolean isTarget(Object itemObject) {
        return itemObject instanceof User;
    }

    @Override
    public UserListItem createAssemblyItem(ViewGroup parent) {
        return new UserListItem(R.layout.list_item_user, parent);
    }

    public class UserItem extends AssemblyItem<User> {
        private ImageView headImageView;
        private TextView nameTextView;
        private TextView sexTextView;
        private TextView ageTextView;
        private TextView jobTextView;

        public UserListItem(int itemLayoutId, ViewGroup parent) {
            super(itemLayoutId, parent);
        }

        @Override
        protected void onFindViews(View itemView) {
            headImageView = (ImageView) findViewById(R.id.image_userListItem_head);
            nameTextView = (TextView) findViewById(R.id.text_userListItem_name);
            sexTextView = (TextView) findViewById(R.id.text_userListItem_sex);
            ageTextView = (TextView) findViewById(R.id.text_userListItem_age);
            jobTextView = (TextView) findViewById(R.id.text_userListItem_job);
        }

        @Override
        protected void onConfigViews(Context context) {
            getItemView().setOnClickListener(new View.OnClickListener(){
                @Override
                public void onClick(View v) {
                    Toast.makeText(v.getConext(), "第" + (getPosition() + 1) + "條資料", Toast.LENGTH_LONG).show();
                }
            });
        }

        @Override
        protected void onSetData(int position, User user) {
            headImageView.setImageResource(user.headResId);
            nameTextView.setText(user.name);
            sexTextView.setText(user.sex);
            ageTextView.setText(user.age);
            jobTextView.setText(user.job);
        }
    }
}
複製程式碼

詳解:

  • ItemFactory的泛型是為了限定其createAssemblyItem(ViewGroup)方法返回的型別
  • ItemFactory的isTarget()方法是用來匹配資料列表中的資料的,Adapter從資料列表中拿到當前位置的資料後會依次呼叫其所有的ItemFactory的isTarget(Object)方法,誰返回true就用誰處理當前這條資料
  • ItemFactory的createAssemblyItem(ViewGroup)方法用來建立Item,返回的型別必須跟你在ItemFactory上配置的泛型一樣
  • Item的泛型是用來指定對應的資料型別,會在onSetData和getData()方法中用到
  • Item的onFindViews(View)onConfigViews(Context)方法分別用來初始化View和配置View,只會在建立Item的時候呼叫一次,另外在onFindViews方法中你可以直接使用findViewById(int)法獲取View
  • Item的onSetData()方法是用來設定資料的,在每次getView()的時候都會呼叫
  • 你可以通過Item的getPosition()getData()方法可直接獲取當前所對應的位置和資料,因此你在處理click的時候不再需要通過setTag()來繫結位置和資料了,直接獲取即可
  • 你還可以通過過Item的getItemView()方法獲取當前的itemView

另外你還可以通過ContentSetter簡化你的程式碼,上述例子使用ContentSetter簡化後如下:

public class UserItemFactory extends AssemblyItemFactory<UserListItemFactory.UserListItem> {

    @Override
    public boolean isTarget(Object itemObject) {
        return itemObject instanceof User;
    }

    @Override
    public UserListItem createAssemblyItem(ViewGroup parent) {
        return new UserListItem(R.layout.list_item_user, parent);
    }

    public class UserItem extends AssemblyItem<User> {
        public UserListItem(int itemLayoutId, ViewGroup parent) {
            super(itemLayoutId, parent);
        }

        @Override
        protected void onFindViews(View itemView) { }

        @Override
        protected void onConfigViews(Context context) {
            getItemView().setOnClickListener(new View.OnClickListener(){
                @Override
                public void onClick(View v) {
                    Toast.makeText(v.getConext(), "第" + (getPosition() + 1) + "條資料", Toast.LENGTH_LONG).show();
                }
            });
        }

        @Override
        protected void onSetData(int position, User user) {
            getSetter()
                .setImageResource(R.id.image_userListItem_head, user.headResId)
                .setText(R.id.text_userListItem_name, user.name)
                .setText(R.id.text_userListItem_sex, user.sex)
                .setText(R.id.text_userListItem_age, user.age)
                .setText(R.id.text_userListItem_job, user.job);
        }
    }
}
複製程式碼

4. 使用ItemFactory

首先你要準備好資料並new一個AssemblyAdapter,然後通過Adapter的addItemFactory(AssemblyItemFactory)方法新增ItemFactory即可,如下:

ListView listView = ...;

List<Object> dataList = new ArrayList<Object>;
dataList.add(new User("隔離老王"));
dataList.add(new User("隔壁老李"));

AssemblyAdapter adapter = new AssemblyAdapter(dataList);
adapter.addItemFactory(new UserItemFactory());

listView.setAdapter(adapter);
複製程式碼

你還可以一次使用多個ItemFactory,如下:

ListView listView = ...;

List<Object> dataList = new ArrayList<Object>;
dataList.add(new User("隔離老王"));
dataList.add(new Game("英雄聯盟"));
dataList.add(new User("隔壁老李"));
dataList.add(new Game("守望先鋒"));

AssemblyAdapter adapter = new AssemblyAdapter(dataList);
adapter.addItemFactory(new UserItemFactory());
adapter.addItemFactory(new GameItemFactory());

listView.setAdapter(adapter);
複製程式碼

5. 使用header和footer

AssemblyAdapter支援新增header和footer,可以方便的固定顯示內容在列表的頂部或尾部。 Adapter支援header和footer的重要性在於可以讓GridView、RecyclerView等也支援header和footer

首先定義好一個用於header或footer的ItemFactory

新增header、footer:

然後呼叫addHeaderItem(AssemblyItemFactory, Object)addFooterItem(AssemblyItemFactory, Object)方法新增即可,如下:

AssemblyAdapter adapter = new AssemblyAdapter(objects);

adapter.addHeaderItem(new HeaderItemFactory(), "我是小額頭呀!");
...
adapter.addFooterItem(new HeaderItemFactory(), "我是小尾巴呀!");
複製程式碼

addHeaderItem(AssemblyItemFactory, Object)和addFooterItem(AssemblyItemFactory, Object)的第二個引數是Item需要的資料,直接傳進去即可

隱藏或顯示header、footer

addHeaderItem()或addFooterItem()都會返回一個用於控制header或footer的FixedItemInfo物件,如下:

AssemblyAdapter adapter = new AssemblyAdapter(objects);

FixedItemInfo userFixedItemInfo = adapter.addHeaderItem(new HeaderItemFactory(), "我是小額頭呀!");

// 隱藏
userFixedItemInfo.setEnabled(false);

// 顯示
userFixedItemInfo.setEnabled(true);
複製程式碼

由於有了header和footer那麼Item.getPosition()方法得到的位置就是Item在Adapter中的位置,要想得到其在所屬部分的真實位置可通過Adapter的getPositionInPart(int)獲取

6. 使用載入更多功能

首先你需要建立一個繼承自AssemblyLoadMoreItemFactory的ItemFactory,AssemblyLoadMoreItemFactory已經將載入更多相關邏輯部分的程式碼寫好了,你只需關心介面即可,如下:

public class LoadMoreItemFactory extends AssemblyLoadMoreItemFactory {

    public LoadMoreListItemFactory(OnLoadMoreListener eventListener) {
        super(eventListener);
    }

    @Override
    public AssemblyLoadMoreItem createAssemblyItem(ViewGroup parent) {
        return new LoadMoreListItem(R.layout.list_item_load_more, parent);
    }

    public class LoadMoreItem extends AssemblyLoadMoreItem {
        private View loadingView;
        private View errorView;
        private View endView;

        public LoadMoreListItem(int itemLayoutId, ViewGroup parent) {
            super(itemLayoutId, parent);
        }

        @Override
        protected void onFindViews(View itemView) {
            loadingView = findViewById(R.id.text_loadMoreListItem_loading);
            errorView = findViewById(R.id.text_loadMoreListItem_error);
            endView = findViewById(R.id.text_loadMoreListItem_end);
        }

        @Override
        public View getErrorRetryView() {
            return errorView;
        }

        @Override
        public void showLoading() {
            loadingView.setVisibility(View.VISIBLE);
            errorView.setVisibility(View.INVISIBLE);
            endView.setVisibility(View.INVISIBLE);
        }

        @Override
        public void showErrorRetry() {
            loadingView.setVisibility(View.INVISIBLE);
            errorView.setVisibility(View.VISIBLE);
            endView.setVisibility(View.INVISIBLE);
        }

        @Override
        public void showEnd() {
            loadingView.setVisibility(View.INVISIBLE);
            errorView.setVisibility(View.INVISIBLE);
            endView.setVisibility(View.VISIBLE);
        }
    }
}
複製程式碼

然後呼叫Adapter的setLoadMoreItem(AssemblyLoadMoreItemFactory)方法設定載入更多ItemFactory即可,如下:

AssemblyAdapter adapter = ...;
adapter.setLoadMoreItem(new LoadMoreItemFactory(new OnLoadMoreListener(){
    @Override
    public void onLoadMore(AssemblyAdapter adapter) {
        // 訪問網路載入資料
        ...
        
        boolean loadSuccess = ...;
        if (loadSuccess) {
            // 載入成功時判斷是否已經全部載入完畢,然後呼叫Adapter的setLoadMoreEnd(boolean)方法設定載入更多是否結束
            boolean loadMoreEnd = ...;
            adapter.setLoadMoreEnd(loadMoreEnd);
        } else {
            // 載入失敗時呼叫Adapter的loadMoreFailed()方法顯示載入失敗提示,使用者點選失敗提示則會重新觸發載入更多
            adapter.loadMoreFailed();
        }
    }
}));
複製程式碼

你還可以通過setDisableLoadMore(boolean)方法替代setLoadMoreEnd(boolean)來控制是否禁用載入更多功能,兩者的區別在於setLoadMoreEnd(boolean)為true時會在列表尾部顯示end提示,而setDisableLoadMore(boolean)則是完全不顯示載入更多尾巴

相關文章