1. 程式人生 > >RecyclerView 通用介面卡封裝

RecyclerView 通用介面卡封裝

有很久沒有寫部落格了,主要還是因為小 baby 的誕生,忙前忙後,跑來跑去的很少有整片整片的時間靜下心來寫部落格,現在小楠終於回北京了,自己利用 SpringBoot 搭建部落格也基本完成了,終於有了自己的個人部落格。這段時間雖然沒有寫部落格,但是平時也不是沒有積累,只是零零散散的記在本子裡,現在準備慢慢整理成部落格記錄下來。
第一篇還是來一個我覺得比較滿意的,在做公司專案時,吸收了網上其他人的思想,封裝了 RecyclerView 的通用介面卡。

序言

RecyclerView 是用來替代 ListView 和 GridView 的控制元件,功能非常強大,有很強的擴充套件性,不過每次寫介面卡的時候,總會覆寫那麼幾個相同的方法。雖然都是熟悉的配方,熟悉的味道,並沒有什麼難度,但是寫多了也未免覺得繁瑣,於是就萌生了封裝 RecyclerView 通用介面卡的想法,在此先感謝前輩的思想:

為RecyclerView打造通用Adapter 讓RecyclerView更加好用

MultiType-Adapter 優雅的實現RecyclerVIew中的複雜佈局

一、要實現的小目標

1.可以很快速的實現簡單資料的 RecyclerView 的介面卡,這裡的簡單不是指實體類的屬性少,而是指介面卡的資料型別只有一種。
2.可以很方便的實現一對多的資料型別,即一種資料對應多種佈局,像很多聊天介面就有這樣的需求。
3.可以很方便的實現多對多的資料型別,即多種資料對應多種佈局。

二、思路

寫程式就是理清了思路邏輯,然後再用程式碼實現這個邏輯就 OK 了,首先看看一般我們寫一個介面卡需要怎麼做。

public class TestAdapter extends RecyclerView.Adapter<TestAdapter.MyViewHolder> {
    private List<String> stringList;

    public void setDatas(List<String> stringList) {
        this.stringList = stringList;
        notifyDataSetChanged();
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_rv_test, parent, false);
        MyViewHolder vh = new MyViewHolder(v);
        return vh;
    }

    @Override
    public void onBindViewHolder(final MyViewHolder holder, int position) {
        holder.tvTest.setText(stringList.get(position));
    }


    @Override
    public int getItemCount() {
        return stringList == null ? 0 : stringList.size();
    }

    class MyViewHolder extends RecyclerView.ViewHolder {
        public TextView tvTest;

        public MyViewHolder(View itemView) {
            super(itemView);
            tvTest = (TextView) itemView.findViewById(R.id.tv_test);
        }
    }
}

是的,這幾個方法是必不可少的,但是多寫幾個介面卡後你會發現
onCreateViewHolder()方法 和 getItemCount() 方法基本是一樣的,setDatas() 這種我們自己寫的設定資料的方法也是一樣的,還有複用的 ViewHolder 也差不多,只是裡面定義的控制元件不同罷了,OK 我們就從這幾個方法入手。

三、通用 BaseViewHolder

繼承 RecyclerView.ViewHolder 時,它需要我們提供一個 itemView 作為每一個 item 的檢視,然後在裡面通過這個 itemView 的 findViewById() 方法來找到裡面的子控制元件,用於我們設定資料,所以可以這樣封裝。

public class BaseViewHolder extends RecyclerView.ViewHolder {
    private Context context;
    private View itemView;
    private SparseArray<View> viewSparseArray;

    public BaseViewHolder(Context context, View itemView) {
        super(itemView);
        this.context = context;
        this.itemView = itemView;
        this.viewSparseArray = new SparseArray<View>();
    }

    public View getItemView() {
        return itemView;
    }

    public <T extends View> T findViewById(int viewId) {
        View view = viewSparseArray.get(viewId);
        if (view == null) {
            view = itemView.findViewById(viewId);
            viewSparseArray.put(viewId, view);
        }
        return (T) view;
    }
}

SparseArray 是 Android 特有的容器,相當於 key 為 int 型的 Map,控制元件的 id 正好是 int 型的,所以用這個容器來存放子控制元件,當呼叫 findViewById() 時,如果 SparseArray 中有則直接取出來返回,如果沒有再呼叫 itemView 的 findViewById() 方法找到該控制元件返回,並存到容器中。這個通用的 BaseViewHolder 還可以繼續封裝,擴充套件很多常用的方法,稍後再說。

四、通用 BaseAdapter

public abstract class RcvBaseAdapter<T> extends RecyclerView.Adapter<BaseViewHolder> {

    private Context context;
    private int layoutId;
    private List<T> dataList = new ArrayList<>();

    public RcvBaseAdapter(Context context, int layoutId) {
        this.context = context;
        this.layoutId = layoutId;
    }

    @Override
    public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(context).inflate(layoutId, parent, false);
        return new BaseViewHolder(context, itemView);
    }

    @Override
    public void onBindViewHolder(final BaseViewHolder holder, int position) {
        bindViewHolder(holder, dataList.get(position), position);
    }

    @Override
    public int getItemCount() {
        return dataList == null ? 0 : dataList.size();
    }

    public abstract void bindViewHolder(BaseViewHolder holder, T itemData, int position);

    public Context getContext() {
        return context;
    }

    public List<T> getDataList() {
        return dataList;
    }

    public void setDataList(List<T> dataList) {
        this.dataList = dataList;
        notifyDataSetChanged();
    }
}

現在我們寫介面卡的時候只需要繼承這個通用介面卡,實現 bindViewHolder() 方法繫結我們自己的資料即可:

public class TestAdapter extends RcvBaseAdapter<String> {

    public TestAdapter(Context context) {
        super(context, R.layout.item_rv_test);
    }

    @Override
    public void bindViewHolder(BaseViewHolder holder, String itemData, int position) {
        TextView tvTest = holder.findViewById(R.id.tv_test);
        tvTest.setText(itemData);
    }
}

效果:

比起以前的介面卡可是簡化了太多。

五、擴充套件

在上面我們在對 TextView 設定文字時需要先找到這個控制元件再呼叫 TextView 的 setText() 方法,還有點小麻煩,作為程式設計師當然是能省則省,所以通用 BaseViewHolder 還可以擴充套件,增加一些常用的設定文字、設定圖片、設定背景的方法,也可以為通用介面卡 BaseAdapter 設定 Item 的點選事件的監聽器,這些功能比較簡單,就不貼程式碼了,如果需要可以看看文末的 Demo 地址中去檢視完整的 BaseViewHolder 程式碼,這裡主要看看比較麻煩的單資料對多佈局,多資料對多佈局。

六、多資料對多佈局

多資料對多佈局比較簡單,先說這個,首先我們知道 Adapter 有一個 getItemViewType(int position) 方法,傳入 Item 的 position 返回一個 int 值,預設的是所有的都會返回 0,也就是說所有的 Item 都是同一型別。Adapter 中建立 Holder 的方法 onCreateViewHolder(ViewGroup parent,int viewType) 返回一個 ViewHolder,沒錯,這裡面的引數 viewType 就是上面那個方法返回的 int 值,那就是說我們可以在 onCreateViewHolder() 方法中根據不同的 viewType 來返回不同的 ViewHolder,從而實現不同的佈局,也就是說重寫 getItemViewType(int position) 方法。為什麼說多對多比較簡單,因為多對多就是一種實體類對應一個佈局,不同的實體類的 hashCode 肯定不一樣,Class 也不一樣,我們就可以將 hashCode 作為 viewType 返回,在建立資料和繫結資料時根據 hashCode 來決定取哪個佈局。用程式碼來說事:

public class RcvMultipleBaseAdapter extends RcvBaseAdapter {
    private SparseArray<BaseItemView> itemViewSparseArray;

    public RcvMultipleBaseAdapter(Context context) {
        super(context, 0);
        itemViewSparseArray = new SparseArray<>();
    }

    @Override
    public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return itemViewSparseArray.get(viewType).onCreateViewHolder(parent);
    }

    @Override
    public int getItemViewType(int position) {
        for (int i = 0; i < itemViewSparseArray.size(); i++) {
            BaseItemView baseItemView = itemViewSparseArray.valueAt(i);
            if (baseItemView.isForViewType(getDataList().get(position), position)) {
                return itemViewSparseArray.keyAt(i);
            }
        }
        throw new IllegalArgumentException("No ItemView added that matches position=" + position + " in data source");
    }

    @Override
    public void bindViewHolder(BaseViewHolder holder, Object itemData, int position) {
        for (int i = 0; i < itemViewSparseArray.size(); i++) {
            BaseItemView baseItemView = itemViewSparseArray.valueAt(i);
            if (baseItemView.isForViewType(itemData, position)) {
                baseItemView.bindViewHolder(holder, itemData, position);
                return;
            }
        }
    }

    public void addItemView(BaseItemView baseItemView) {
        itemViewSparseArray.put(baseItemView.hashCode(), baseItemView);
    }

    public void removeItemView(BaseItemView baseItemView) {
        itemViewSparseArray.remove(baseItemView.hashCode());
    }
}

用一個 SparseArray 來儲存不同的 Item 佈局,鍵就是多資料型別的 hashCode 值,正好是 int 型的滿足 SparseArray,值當然就是 Item 佈局了。由於是不同的佈局繫結資料的操作肯定不一樣,所以繫結資料肯定不能也沒辦法在 Adapter 中進行,交給繼承 BaseItemView 的每一個佈局自己去繫結,我們需要做的是呼叫 addItemView() 方法新增一種佈局,然後介面卡會在 getItemViewType() 方法時遍歷 SparseArray 容器裡的所有佈局,當有佈局匹配上時,則將該佈局的資料型別的 hashCode 值作為 getItemViewType() 方法的返回值返回,如果遍歷完都沒有匹配上則主動丟擲一個異常。onCreateViewHolder() 建立 ViewHolder 時根據 viewType 獲取不同的 BaseItemView,然後讓 BaseItemView 自己去建立 BaseViewHolder。同樣在繫結資料時也是拿到 BaseItemView 讓其自己去繫結資料。如何判斷是否有與當前 position 的資料匹配的佈局是在 BaseItemView 中的 isForViewType() 方法中通過反射拿到泛型來判斷的:

public abstract class BaseItemView<T> {
    private Context context;
    private int layoutId;

    public BaseItemView(Context context, int layoutId) {
        this.context = context;
        this.layoutId = layoutId;
    }

    public BaseViewHolder onCreateViewHolder(ViewGroup parent) {
        View itemView = LayoutInflater.from(context).inflate(layoutId, parent, false);
        return new BaseViewHolder(context, itemView);
    }

    /**
     * Description:     子類可以覆蓋此方法決定引用該子佈局的時機
     * Date:2018/8/6
     *
     * @param item     該position對應的資料
     * @param position position
     * @return 是否屬於子佈局
     */
    public boolean isForViewType(T item, int position) {
        Type type = getClass().getGenericSuperclass();
        ParameterizedType parameterizedType = (ParameterizedType) type;
        Class clazz = (Class) parameterizedType.getActualTypeArguments()[0];
        return item.getClass() == clazz;
    }

    /**
     * Description:繫結 UI 和資料
     * Date:2018/8/6
     */
    public abstract void bindViewHolder(BaseViewHolder holder, T itemData, int position);
}

BaseItemView 在多對多時需要指定泛型,這個泛型就是判斷引入不同佈局的條件,通過反射拿到泛型中的真實型別,然後與傳入的 item 的型別比較,如果相同則返回 true 表示匹配成功,每一種佈局的建立 VeiwHolder 和繫結資料的真正操作都是在這個 BaseItemView 中做的,RcvMultipleBaseAdapter 其實相當於起了一箇中間分發者的作用。看看如何用吧:

MainActivity 中設定不同型別的資料(因為是不同型別,所以資料容器就沒辦法指定泛型了):

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        RecyclerView rvTest = (RecyclerView) findViewById(R.id.rv_test);
        TestAdapter testAdapter = new TestAdapter(this);
        rvTest.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
        rvTest.setAdapter(testAdapter);
        testAdapter.setDataList(getList());
    }

    private List getList() {
        List list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                String string = "我是字串" + i;
                list.add(string);
            } else {
                list.add(i);
            }
        }
        return list;
    }
}

TestAdapter,說了它只相當於一箇中間分發者的角色,所以程式碼很簡單,只是新增不同的佈局型別,真正的繫結資料操作在 ItemView 中做:

public class TestAdapter extends RcvMultipleBaseAdapter {

    public TestAdapter(Context context) {
        super(context);
        addItemView(new StringItemView(context));
        addItemView(new IntegerItemView(context));
    }
}

StringItemView(String 型別的資料的佈局):

public class StringItemView extends BaseItemView<String> {

    public StringItemView(Context context) {
        super(context, R.layout.item_rv_test_string);
    }

    @Override
    public void bindViewHolder(BaseViewHolder holder, String itemData, int position) {
        holder.setTvText(R.id.tv_test_string, "String 的 ItemView" + itemData);
    }
}

IntegerItemView(Integer 型別的資料的佈局):

public class IntegerItemView extends BaseItemView<Integer> {

    public IntegerItemView(Context context) {
        super(context, R.layout.item_rv_test_integer);
    }

    @Override
    public void bindViewHolder(BaseViewHolder holder, Integer itemData, int position) {
        holder.setTvText(R.id.tv_test_integer, "Integer 的 ItemView" + itemData);
    }
}

xml 檔案就不貼了,只是簡單的背景顏色不同而已,執行結果如下:

這樣多資料對多佈局就實現了,而且這樣封裝後使用起來非常方便,各佈局型別之間也沒有耦合,非常方便管理。

七、單資料對多佈局

為什麼單資料對多佈局更麻煩呢,因為單資料的 hashCode 和 Class 都是一樣的,所以沒辦法很明確的判斷何時引入哪種佈局,多對多時只要使用上面的介面卡,不用自己寫判斷條件就可以自動判斷佈局的引入時機,單資料時就得自己判斷了,所以我們需要重寫 BaseItemView 的 isForViewType() 方法,下面是例子:

MessageEntity:

public class MessageEntity {
    private String message;
    private int userType;   //0 表示傳送的訊息,1 表示接收的訊息
    //省略構造方法,get、set、toString 方法
}

MainActivity 中設定資料的方法小改一下:

    private List<MessageEntity> getList() {
        List<MessageEntity> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            MessageEntity messageEntity = new MessageEntity();
            messageEntity.setMessage("你好");
            messageEntity.setUserType(i % 2);
            list.add(messageEntity);
        }
        return list;
    }

MessageItemView1,重寫 isForViewType() 方法,userType 為 0 時表示是發出的訊息:

public class MessageItemView1 extends BaseItemView<MessageEntity> {

    public MessageItemView1(Context context) {
        super(context, R.layout.item_rv_test_message1);
    }

    @Override
    public boolean isForViewType(MessageEntity item, int position) {
        if (item.getUserType() == 0) {
            return true;
        }
        return false;
    }

    @Override
    public void bindViewHolder(BaseViewHolder holder, MessageEntity itemData, int position) {
        holder.setTvText(R.id.tv_test_message1, "發出的訊息" + itemData.getMessage());
    }
}

MessageItemView2,重寫 isForViewType() 方法,userType 為 1 時表示是接收的訊息:

public class MessageItemView2 extends BaseItemView<MessageEntity> {

    public MessageItemView2(Context context) {
        super(context, R.layout.item_rv_test_message2);
    }

    @Override
    public boolean isForViewType(MessageEntity item, int position) {
        Log.i("daolema", "position--->" + position);
        if (item.getUserType() == 1) {
            return true;
        }
        return false;
    }

    @Override
    public void bindViewHolder(BaseViewHolder holder, MessageEntity itemData, int position) {
        holder.setTvText(R.id.tv_test_message2, "接收的訊息" + itemData.getMessage());
    }
}

執行結果如下:

八、總結

所有的程式都應該是方便的、易使用的,這次的封裝雖然是借鑑的前輩們的思想,但是自己看過後能夠根據自己封裝一遍,也算是理解了。我一直都覺得應該是這樣,之前喜歡研究自定義控制元件的時候也是,就算那些炫酷的開源控制元件可以拿來就用了,但是也想自己實現一遍,畢竟理解後再用,就算出了什麼 Bug 也容易找到問題,知道從何改起。這次封裝 RecyclerView 介面卡更不談了,我覺得封裝雖然功能上並不會有什麼大的突破,但是在結構上有很大的改善,方便易用低耦合。RecyclerView 真的很強大也很好用,這段時間積累了很多 RecyclerView 的用法,慢慢記錄吧。

九、github 傳送門

完整的 Demo