RV 多樣式 MultiType 聊天界面 消息類型 MD


Markdown版本筆記 我的GitHub首頁 我的博客 我的微信 我的郵箱
MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

RV 多樣式 MultiType 聊天界面 消息類型 MD
Demo


目錄

純原生實現多樣式

addHeaderView 方式

Activity

public class Activity1 extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ListView listView = new ListView(this);

        List<Model1> mList = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            mList.add(new Model1(("包青天" + i), R.drawable.icon));
        }

        //給ListView添加頭尾
        TextView mTextView = new TextView(this);
        mTextView.setText("我是頭部\n必須在listview.setAdapter前添加");
        mTextView.setBackgroundColor(Color.YELLOW);
        listView.addHeaderView(mTextView);//必須在listview.setAdapter前添加。添加以后,listView的position=0的View是此View

        ImageView mImageView = new ImageView(this);
        mImageView.setImageResource(R.drawable.icon);
        mImageView.setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, 300));
        listView.addHeaderView(mImageView);
        listView.setHeaderDividersEnabled(new Random().nextBoolean());//控制頭部是否顯示分割線。默認為true

        View footerView = new View(this);
        footerView.setBackgroundColor(Color.GREEN);
        footerView.setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, 50));
        listView.addFooterView(footerView);
        listView.setFooterDividersEnabled(new Random().nextBoolean());

        listView.setAdapter(new MyAdapter1(this, mList));//addHeaderView要放在setAdapter之前,而addFooterView放在前后都可以
        listView.setDivider(new ColorDrawable(Color.RED));
        listView.setDividerHeight(2);//如果調用了setDivider,也需調用setDividerHeight才行
        listView.setOnItemClickListener((parent, view, p, id) -> Toast.makeText(this, "p=" + p, Toast.LENGTH_SHORT).show());

        setContentView(listView);
    }
}

Adapter

public class MyAdapter1 extends BaseAdapter {
    private Context mContext;
    private List<Model1> mList;

    public MyAdapter1(Context context, List<Model1> list) {
        this.mContext = context;
        this.mList = list;
    }

    @Override
    public int getCount() {
        return mList.size();
    }

    @Override
    public Object getItem(int position) {
        return mList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder mViewHolder;
        if (convertView != null) {
            mViewHolder = (ViewHolder) convertView.getTag();
        } else {
            convertView = LayoutInflater.from(mContext).inflate(R.layout.item, parent, false);
            mViewHolder = new ViewHolder();
            mViewHolder.iv_head = (ImageView) convertView.findViewById(R.id.holder2_iv);
            mViewHolder.tv_name = (TextView) convertView.findViewById(R.id.holder2_title);
            convertView.setTag(mViewHolder);
        }
        Model1 mBean = mList.get(position);
        mViewHolder.iv_head.setImageResource(mBean.resId);
        mViewHolder.tv_name.setText(mBean.name + "  position=" + position);
        return convertView;
    }

    public static class ViewHolder {
        public ImageView iv_head;// 頭像
        public TextView tv_name;// 名字
    }
}

Model

public class Model1 {
    public String name;
    public int resId;

    public Model1(String name, int resId) {
        this.name = name;
        this.resId = resId;
    }
}

getItemViewType 方式

Activity

public class Activity2 extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ListView listView = new ListView(this);
        List<Model2> mList = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            if (new Random().nextBoolean()) mList.add(new Model2(Model2.ITEM_FIRST, "第一種樣式 " + i));
            else mList.add(new Model2(Model2.ITEM_SECOND, "第二種樣式 " + i, R.drawable.icon));
        }

        listView.setAdapter(new MyAdapter2(this, mList));
        listView.setOnItemClickListener((parent, view, p, id) -> Toast.makeText(this, "p=" + p, Toast.LENGTH_SHORT).show());
        setContentView(listView);
    }
}

Adapter

public class MyAdapter2 extends BaseAdapter {

    private Context context;
    private List<Model2> mList;

    public MyAdapter2(Context context, List<Model2> list) {
        this.context = context;
        mList = list;
    }

    //**************************************************************************************************************************
    @Override
    public int getCount() {
        return mList.size();
    }

    @Override
    public Object getItem(int position) {
        return mList.get(position);
    }

    //重寫方法一:返回值代表的是某一個樣式的 Type(是一個需要我們自己定義的,用於區分不同樣式的int類型的值)
    @Override
    public int getItemViewType(int position) {
        return mList.get(position).type;
    }

    //重寫方法一:返回的是你有幾種類型的樣式
    @Override
    public int getViewTypeCount() {
        return 2;
    }

    @Override
    public long getItemId(int paramInt) {
        return paramInt;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        int type = getItemViewType(position);
        //要使用不同類型的ViewHolder
        Holder1 holder1 = null;
        Holder2 holder2 = null;

        //************************************************初始化和復用******************************************
        if (convertView != null) {
            switch (type) {
                case Model2.ITEM_FIRST:
                    holder1 = (Holder1) convertView.getTag();
                    Log.i("bqt", position + "  復用  " + type);
                    break;
                case Model2.ITEM_SECOND:
                    holder2 = (Holder2) convertView.getTag();
                    Log.i("bqt", position + "  復用  " + type);
                    break;
            }
        } else {
            switch (type) {
                case Model2.ITEM_FIRST:
                    convertView = View.inflate(context, R.layout.head, null);
                    holder1 = new Holder1();
                    holder1.holder1_title = (TextView) convertView.findViewById(R.id.holder1_title);
                    holder1.holder1_time = (TextView) convertView.findViewById(R.id.holder1_time);
                    convertView.setTag(holder1);
                    Log.i("bqt", position + "  初始化  " + type);
                    break;
                case Model2.ITEM_SECOND:
                    convertView = View.inflate(context, R.layout.item, null);
                    holder2 = new Holder2();
                    holder2.holder2_title = (TextView) convertView.findViewById(R.id.holder2_title);
                    holder2.holder2_iv = (ImageView) convertView.findViewById(R.id.holder2_iv);
                    convertView.setTag(holder2);
                    Log.i("bqt", position + "  初始化  " + type);
                    break;
            }
        }

        //*************************************************填充數據*****************************************
        switch (type) {
            case Model2.ITEM_FIRST:
                if (holder1 != null) {
                    holder1.holder1_title.setText(mList.get(position).title);
                    holder1.holder1_time.setText(new SimpleDateFormat("yyyy.MM.dd HH:mm:ss", Locale.getDefault()).format(new Date()));
                }
                break;
            case Model2.ITEM_SECOND:
                if (holder2 != null) {
                    holder2.holder2_title.setText(mList.get(position).title);
                    holder2.holder2_iv.setImageResource(mList.get(position).resId);
                }
                break;
        }
        return convertView;
    }

    //**************************************************************************************************************************
    private class Holder1 {
        TextView holder1_title;
        TextView holder1_time;
    }

    private class Holder2 {
        TextView holder2_title;
        ImageView holder2_iv;
    }
}

Model

public class Model2 {
    public static final int ITEM_FIRST = 0;//第一個樣式
    public static final int ITEM_SECOND = 1;//第二個樣式
    public int type;//記錄是哪種樣式
    public String title;//標題
    public int resId;//圖片,僅第二個樣式可以獲取圖片

    public Model2(int type, String str) {
        this.type = type;
        title = str;
    }

    public Model2(int type, String str, int resId) {
        this.type = type;
        title = str;
        this.resId = resId;
    }
}

MultiType 簡介

GitHub

compile 'me.drakeet.multitype:multitype:3.1.0'

在開發我的 TimeMachine 時,我有一個復雜的聊天頁面,於是我設計了我的類型池系統,它是完全解耦的,因此我能夠輕松將它抽離出來分享,並給它取名為 MultiType.

從前,比如我們寫一個類似微博列表頁面,這樣的列表是十分復雜的:有純文本的、帶轉發原文的、帶圖片的、帶視頻的、帶文章的等等,甚至穿插一條可以橫向滑動的好友推薦條目。不同的 item 類型眾多,而且隨着業務發展,還會更多。如果我們使用傳統的開發方式,經常要做一些繁瑣的工作,代碼可能都堆積在一個 Adapter 中:我們需要覆寫 RecyclerView.Adapter 的 getItemViewType 方法,羅列一些 type 整型常量,並且 ViewHolder 轉型、綁定數據也比較麻煩。一旦產品需求有變,或者產品設計說需要增加一種新的 item 類型,我們需要去代碼堆里找到原來的邏輯去修改,或找到正確的位置去增加代碼。這些過程都比較繁瑣,侵入較強,需要小心翼翼,以免改錯影響到其他地方。

現在好了,我們有了 MultiType,簡單來說,MultiType 就是一個多類型列表視圖的中間分發框架,它能幫助你快速並且清晰地開發一些復雜的列表頁面。 它本是為聊天頁面開發的,聊天頁面的消息類型也是有大量不同種類,且新增頻繁,而 MultiType 能夠輕松勝任。

MultiType 以靈活直觀為第一宗旨進行設計,它內建了 類型 - View 的復用池系統,支持 RecyclerView,隨時可拓展新的類型進入列表當中,使用簡單,令代碼清晰、模塊化、靈活可變。

因此,我寫了這篇文章,目的有幾個:一是以作者的角度對 MultiType 進行入門和進階詳解。二是傳遞我開發過程中的思想、設計理念,這些偏細膩的內容,即使不使用 MultiType,想必也能帶來很多啟發。最后就是把自我覺得不錯的東西分享給大家,試想如果你制造的東西很多人在用,即使沒有帶來任何收益,也是一件很自豪的事情。

特性

  • 輕盈,整個類庫只有 14 個類文件,aar 或 jar 包大小只有 13 KB
  • 周到,支持 data type <--> item view binder 之間 一對一 和 一對多 的關系綁定
  • 靈活,幾乎所有的部件(類)都可被替換、可繼承定制,面向接口 / 抽象編程
  • 純粹,只負責本分工作,專注多類型的列表視圖 類型分發,絕不會去影響 views 的內容或行為
  • 高效,沒有性能損失,內存友好,最大限度發揮 RecyclerView 的復用性
  • 可讀,代碼清晰干凈、設計精巧,極力避免復雜化,可讀性很好,為拓展和自行解決問題提供了基礎

基礎用法

compile 'me.drakeet.multitype:multitype:3.1.0'

注:MultiType 內部引用了 recyclerview-v7:25.3.1,如果你不想使用這個版本,可以使用 exclude 將它排除掉,再自行引入你選擇的版本。示例如下:

dependencies {
    compile('me.drakeet.multitype:multitype:3.1.0', {
       exclude group: 'com.android.support'
    })
    compile 'com.android.support:recyclerview-v7:你選擇的版本'
}

1、創建一個類,它將是你的數據類型或 Java bean / model. 對這個類的內容沒有任何限制(建議這些model統一繼承於一個父類)

public class SimpleImage extends ContentModel {
    public int resId;
​
    public SimpleImage(int resId) {
        super(ContentModel.TYPE_SIMPLE_IMAGE);
        this.resId = resId;
    }
}

2、創建一個類 繼承 ItemViewBinder
ItemViewBinder 是個抽象類,其中 onCreateViewHolder 方法用於生產你的 Item View Holder,onBindViewHolder 用於綁定數據到 Views。
一般一個 ItemViewBinder 類在內存中只會有一個實例對象,MultiType 內部將復用這個 binder 對象來生產所有相關的 item views 和綁定數據。

public class SimpleImageViewBinder extends ItemViewBinder<SimpleImage, SimpleImageViewBinder.ViewHolder> {
​
    @NonNull
    @Override
    protected ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
        View contentView = inflater.inflate(R.layout.item_weibo_simple_image, parent, false);
        return new ViewHolder(contentView);
    }
​
    @Override
    protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull SimpleImage item) {
        holder.simpleImage.setImageResource(item.resId);
    }
​
    static class ViewHolder extends RecyclerView.ViewHolder {
        private ImageView simpleImage;
        ViewHolder(View itemView) {
            super(itemView);
            simpleImage = (ImageView) itemView.findViewById(R.id.simple_image);
        }
    }
}

3、為RecyclerView指定所使用的MultiTypeAdapter,注冊你的類型,綁定數據。完畢。

MultiTypeAdapter adapter = new MultiTypeAdapter();
adapter.register(SimpleImage.class, new SimpleImageViewBinder());
adapter.register(SimpleText.class, new SimpleTextViewBinder());
recyclerView.setAdapter(adapter);
​
List<Simple_Content> items = new ArrayList<>();
adapter.setItems(items);

設計思想

MultiType 設計伊始,我給它定了幾個原則:

1、要簡單,便於他人閱讀代碼
因此我極力避免將它復雜化,避免加入許多不相干的內容。我想寫人人可讀的代碼,使用簡單的方式,去實現復雜的需求。過多不相干、沒必要的代碼,將會使項目變得令人暈頭轉向,難以閱讀,遇到需要定制、解決問題的時候,無從下手。

2、要靈活,便於拓展和適應各種需求
很多人會得意地告訴我,他們把 MultiType 源碼精簡成三四個類,甚至一個類,以為代碼越少就是越好,這我不能贊同。MultiType 考慮得更遠,這是一個提供給大眾使用的類庫,過度的精簡只會使得大幅失去靈活性。它或許不是使用起來最簡單的,但很可能是使用起來最靈活的。 在我看來,"直觀"、"靈活"優先級大於"簡單"。因此,MultiType 以接口或抽象進行連接,這意味着它的角色、組件都可以被替換,或者被拓展和繼承。如果你覺得它使用起來還不夠簡單,完全可以通過繼承封裝出更具體符合你使用需求的方法。它已經暴露了足夠豐富、周到的接口以供拓展,我們不應該直接去修改源碼,這會導致一旦后續發現你的精簡版滿足不了你的需求時,已經沒有回頭路了。

3、要直觀,使用起來能令項目代碼更清晰可讀,一目了然
MultiType 提供的 ItemViewBinder 沿襲了 RecyclerView Adapter 的接口命名,使用起來更加舒適,符合習慣。另外,MultiType 很多地方放棄使用反射而是讓用戶顯式指明一些關系,如:MultiTypeAdapter#register 方法,需要傳遞一個數據模型 class 和 ItemViewBinder 對象,雖然有很多方法可以把它精簡成單一參數方法,但我們認為顯式聲明數據模型類與對應關系,更具直觀。

使用插件自動生成代碼

MultiType 提供了 Android Studio 插件 MultiTypeTemplates 來自動生成代碼,源碼也是開源的 。

這個插件不僅提供了一鍵生成 item 類文件和 ItemViewBinder,而且是一個很好的利用代碼模版自動生成代碼的示例。其中使用到了官方提供的代碼模版 API,也用到了我自己發明的更靈活修改模版內容的方法,有興趣做這方面插件的可以看看。

使用方式:右鍵點擊你的 package,選擇 New -> MultiType Item,然后輸入你的 item 名字,它就會自動生成 item 模型類 和 ItemViewBinder 文件和代碼。特別方便,相信你會很喜歡它。未來這個插件也將會支持自動生成布局文件,這是目前欠缺的,但不要緊,其實 AS 在這方面已經很方便了,對布局 R.layout.item_category 使用 alt + enter 快捷鍵即可自動生成布局文件。

一個類型對應多個 ItemViewBinder

MultiType 天然支持一個類型對應多個 ItemViewBinder,注冊方式也很簡單,如下:

adapter.register(Data.class).to(
    new DataType1ViewBinder(),
    new DataType2ViewBinder()
).withClassLinker(new ClassLinker<Data>() {
    @NonNull @Override
    public Class<? extends ItemViewBinder<Data, ?>> index(@NonNull Data data) {
        if (data.type == Data.TYPE_2) return DataType2ViewBinder.class;
        else return DataType1ViewBinder.class;
    }
});

或者:

adapter.register(Data.class).to(
    new DataType1ViewBinder(),
    new DataType2ViewBinder()
).withLinker(new Linker<Data>() {
    @Override
    public int index(@NonNull Data data) {
        if (data.type == Data.TYPE_2) return 1;
        else return 0;
    }
});

如上示例代碼,對於一對多,我們需要使用 MultiType#register(class) 方法,它會返回一個 OneToManyFlow 讓你緊接着綁定多個 ItemViewBinder 實例,最后再調用 OneToManyEndpoint#withLinker 或 OneToManyEndpoint#withClassLinker 操作符方法類設置 linker. 所謂 linker,是負責動態連接這個 "一" 對應 "多" 中哪一個 binder 的角色。

這個方案具有很好的性能表現,而且可謂十分直觀。另外,我使用了 @CheckResult 注解來讓編譯器督促開發者一定要完整調用方法鏈才不至於出錯。

添加HeaderView、FooterView

MultiType 其實本身就支持 HeaderView、FooterView,只要創建一個 Header.class - HeaderViewBinder 和 Footer.class - FooterViewBinder 即可,然后把 new Header() 添加到 items 第一個位置,把 new Footer() 添加到 items 最后一個位置。
需要注意的是,如果使用了 Footer View,在底部插入數據的時候,需要添加到 最后位置 - 1,即倒二個位置,或者把 Footer remove 掉,再添加數據,最后再插入一個新的 Footer.
PS:聽他這么說,這哪叫"支持"啊,HeaderView、FooterView完全就是item中的【普通一員】了。不過話又說回來,它本來不就是嗎?

使用斷言以方便調試

眾所周知,如果一個傳統的 RecyclerView Adapter 內部有異常導致崩潰,它的異常棧是不會指向到你的 Activity,這給我們開發調試過程中帶來了麻煩。如果我們的 Adapter 是復用的,就不知道是哪一個頁面崩潰。而對於 MultiTypeAdapter,我們顯然要用於多個地方,而且可能出現開發者忘記注冊類型等等問題。為了便於調試,開發期快速定位失敗,MultiType 提供了很方便的斷言 API: MultiTypeAsserts,使用方式如下:
assertHasTheSameAdapter(recyclerView, adapter);//斷言 recyclerView 使用的是正確的 adapter,必須在setAdapter(adapter) 之后調用
assertAllRegistered(adapter, items);//斷言所有使用的類型都已注冊,需要在加載或更新數據之后調用
這兩個API都是可選擇性使用的。
這樣做以后,MultiTypeAdapter 相關的異常都會報到你的 Activity,並且會詳細注明出錯的原因,而如果符合斷言,斷言代碼不會有任何副作用或影響你的代碼邏輯,這時你可以把它當作廢話。關於這個類的源代碼是很簡單的,有興趣可以直接看看源碼:MultiTypeAsserts.java。

聊天界面模板代碼

聊天界面 ChatActivity

public class ChatActivity extends Activity {
    private static final String TEXT = "不懂左右逢源,不喜趨炎附勢,不會隨波逐流,不狡辯,不恭維,不把妹";
    private static final String PATH1 = "http://img.mmjpg.com/2015/74/33.jpg";
    private static final String PATH2 = "http://img.mmjpg.com/2015/74/35.jpg";
​
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        RecyclerView recyclerView = new RecyclerView(this);
        MultiTypeAdapter adapter = new MultiTypeAdapter();
​
        //一對多,都有相同的父框架結構(頭像、昵稱、時間……等)
        adapter.register(ContentModel.class).to(
                new SimpleTextViewBinder(ContentModel.SEND_TYPE_OTHERS),//左邊的布局(別人發的消息)
                new SimpleTextViewBinder(ContentModel.SEND_TYPE_YOURSELF),//右邊的布局(自己發的消息)
                new SimpleImageViewBinder(ContentModel.SEND_TYPE_OTHERS),
                new SimpleImageViewBinder(ContentModel.SEND_TYPE_YOURSELF),
                new SimpleVoiceViewBinder(ContentModel.SEND_TYPE_OTHERS),
                new SimpleVoiceViewBinder(ContentModel.SEND_TYPE_YOURSELF)
        ).withLinker(model -> {
            if (model.msgType == ContentModel.MSG_TYPE_SIMPLE_TEXT
                    && model.sendType == ContentModel.SEND_TYPE_OTHERS) return 0;//左邊的布局(別人發的消息)
            else if (model.msgType == ContentModel.MSG_TYPE_SIMPLE_TEXT
                    && model.sendType == ContentModel.SEND_TYPE_YOURSELF) return 1;//右邊的布局(自己發的消息)
            else if (model.msgType == ContentModel.MSG_TYPE_SIMPLE_IMAGE
                    && model.sendType == ContentModel.SEND_TYPE_OTHERS) return 2;
            else if (model.msgType == ContentModel.MSG_TYPE_SIMPLE_IMAGE
                    && model.sendType == ContentModel.SEND_TYPE_YOURSELF) return 3;
            else if (model.msgType == ContentModel.MSG_TYPE_SIMPLE_VOICE
                    && model.sendType == ContentModel.SEND_TYPE_OTHERS) return 4;
            else if (model.msgType == ContentModel.MSG_TYPE_SIMPLE_VOICE
                    && model.sendType == ContentModel.SEND_TYPE_YOURSELF) return 5;
            return 0;
        });
        //一個獨立的結構,沒有父框架結構
        adapter.register(BigImage.class, new BigImageViewBinder());
​
        recyclerView.setLayoutManager(new LinearLayoutManager(this));//new GridLayoutManager(this,2)
        recyclerView.setAdapter(adapter);
        assertHasTheSameAdapter(recyclerView, adapter);//斷言 recyclerView 使用的是正確的 adapter,可選擇性使用
​
        User other = new User("other", PATH1);
        User yourself = new User("包青天", PATH2);
        List<ContentModel> items = new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            int sendType = new Random().nextBoolean() ? ContentModel.SEND_TYPE_OTHERS : ContentModel.SEND_TYPE_YOURSELF;
            User user = sendType == ContentModel.SEND_TYPE_OTHERS ? other : yourself;
            String path = sendType == ContentModel.SEND_TYPE_OTHERS ? PATH1 : PATH2;
​
            int random = new Random().nextInt(4);
            if (random == 0) items.add(new SimpleText(user, sendType, i + "、" + TEXT));
            else if (random == 1) items.add(new SimpleImage(user, sendType, path));
            else items.add(new SimpleVoice(user, sendType, path, new Random().nextInt(60)));
        }
        items.add(new BigImage(other, ContentModel.SEND_TYPE_OTHERS, PATH1));
        items.add(new BigImage(yourself, ContentModel.SEND_TYPE_YOURSELF, PATH2));
​
        adapter.setItems(items);
        adapter.notifyDataSetChanged();
        assertAllRegistered(adapter, items);//斷言所有使用的類型都已注冊,需要在加載或更新數據之后調用,可選擇性使用
​
        setContentView(recyclerView);
    }
}

消息模型的父類 ContentModel

/**
 * 各種消息類型的基類
 */
public abstract class ContentModel {
    //消息類型
    public static final int MSG_TYPE_SIMPLE_TEXT = 0;
    public static final int MSG_TYPE_SIMPLE_IMAGE = 1;
    public static final int MSG_TYPE_SIMPLE_VOICE = 2;
    public static final int MSG_TYPE_BIG_IMAGE = 3;
​
    //消息是誰發的
    public static final int SEND_TYPE_OTHERS = 0;
    public static final int SEND_TYPE_YOURSELF = 1;
    public static final int SEND_TYPE_YSTEM = 2;
​
    public int msgType;
    public int sendType;
    public String createTime;
    /**
     * 所有信息都可以封裝到user中
     */
    public User user;
​
    protected ContentModel(User user, int msgType, int sendType) {
        this.user = user;
        this.msgType = msgType;
        this.sendType = sendType;
        this.createTime = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss SSS E", Locale.getDefault()).format(new Date());
    }
}

VH的父類 ContentHolder

public class ContentHolder {
​
    public ChatFrameBinder.FrameHolder frameHolder;
​
    public final View itemView;
​
    public ContentHolder(final View itemView) {
        this.itemView = itemView;
    }
​
    public ChatFrameBinder.FrameHolder getParent() {
        return frameHolder;
    }
​
    public final int getAdapterPosition() {
        return getParent().getAdapterPosition();
    }
​
    public final int getLayoutPosition() {
        return getParent().getLayoutPosition();
    }
​
    public final int getOldPosition() {
        return getParent().getOldPosition();
    }
​
    public final boolean isRecyclable() {
        return getParent().isRecyclable();
    }
​
    public final void setIsRecyclable(boolean recyclable) {
        getParent().setIsRecyclable(recyclable);
    }
}

IVB的父類 ChatFrameBinder

/**
 * 此種方式非常適合聊天頁面。
 * 對於聊天頁面,left和right的元素基本是完全相同的,唯一(會最大)的不同就是元素放置的位置不同
 */
public abstract class ChatFrameBinder<T extends ContentModel, H extends ContentHolder>
        extends ItemViewBinder<ContentModel, ChatFrameBinder.FrameHolder> {
    protected int sendType;
​
    public ChatFrameBinder(int sendType) {
        super();
        this.sendType = sendType;
    }
​
    protected abstract ContentHolder onCreateContentViewHolder(LayoutInflater inflater, ViewGroup parent);
​
    protected abstract void onBindContentViewHolder(H holder, T content);
​
    @NonNull
    @Override
    protected FrameHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
        View root;
        if (sendType == ContentModel.SEND_TYPE_OTHERS) root = inflater.inflate(R.layout.item_frame_left, parent, false);
        else root = inflater.inflate(R.layout.item_frame_right, parent, false);
        ContentHolder subViewHolder = onCreateContentViewHolder(inflater, parent);
        return new FrameHolder(root, subViewHolder);
    }
​
    @Override
    @SuppressWarnings("unchecked")
    protected void onBindViewHolder(@NonNull FrameHolder holder, @NonNull ContentModel model) {
        Glide.with(holder.avatar.getContext()).load(model.user.avatar).into(holder.avatar);
        holder.username.setText(model.user.name);
        holder.createTime.setText(model.createTime);
        onBindContentViewHolder((H) holder.subViewHolder, (T) model);
    }
​
    public static class FrameHolder extends RecyclerView.ViewHolder {
​
        private ImageView avatar;
        private TextView username;
        private FrameLayout container;
        private TextView createTime;
        private ContentHolder subViewHolder;
​
        FrameHolder(View itemView, final ContentHolder subViewHolder) {
            super(itemView);
            avatar = (ImageView) itemView.findViewById(R.id.avatar);
            username = (TextView) itemView.findViewById(R.id.username);
            container = (FrameLayout) itemView.findViewById(R.id.container);
            createTime = (TextView) itemView.findViewById(R.id.create_time);
​
            container.addView(subViewHolder.itemView);
            this.subViewHolder = subViewHolder;
            this.subViewHolder.frameHolder = this;
​
            itemView.setOnClickListener(v -> Toast.makeText(v.getContext(), "Position=" + getAdapterPosition(), LENGTH_SHORT).show());
        }
    }
}

消息模型的子類 SimpleImage

public class SimpleImage extends ContentModel {
​
    public String imagePath;
​
    public SimpleImage(User user, int sendType, String imagePath) {
        super(user, ContentModel.MSG_TYPE_SIMPLE_IMAGE, sendType);
        this.imagePath = imagePath;
    }
}

SimpleImageViewBinder

public class SimpleImageViewBinder extends ChatFrameBinder<SimpleImage, SimpleImageViewBinder.ViewHolder> {
​
    public SimpleImageViewBinder(int sendType) {
        super(sendType);
    }
​
    @Override
    protected ContentHolder onCreateContentViewHolder(LayoutInflater inflater, ViewGroup parent) {
        View root;
        if (sendType == ContentModel.SEND_TYPE_OTHERS) root = inflater.inflate(R.layout.item_simple_image_left, parent, false);
        else root = inflater.inflate(R.layout.item_simple_image_right, parent, false);
        return new SimpleImageViewBinder.ViewHolder(root);
    }
​
    @Override
    protected void onBindContentViewHolder(ViewHolder holder, SimpleImage simpleImage) {
        Glide.with(holder.simpleImage.getContext()).load(simpleImage.imagePath).into(holder.simpleImage);
    }
​
    static class ViewHolder extends ContentHolder {
​
        private ImageView simpleImage;
​
        ViewHolder(View itemView) {
            super(itemView);
            simpleImage = (ImageView) itemView.findViewById(R.id.simple_image);
        }
    }
}

消息模型的特殊子類 BigImage

public class BigImage extends ContentModel {
​
    public String imagePath;
​
    public BigImage(User user, int sendType, String imagePath) {
        super(user, ContentModel.MSG_TYPE_BIG_IMAGE, sendType);
        this.imagePath = imagePath;
    }
}

BigImageViewBinder

public class BigImageViewBinder extends ItemViewBinder<BigImage, BigImageViewBinder.ViewHolder> {

    @NonNull
    @Override
    protected ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
        View root = inflater.inflate(R.layout.item_big_image, parent, false);
        return new ViewHolder(root);
    }

    @Override
    protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull BigImage bigImage) {
        Glide.with(holder.iv_pic.getContext()).load(bigImage.imagePath).into(holder.iv_pic);
        holder.tv_path.setText(bigImage.imagePath);
    }

    static class ViewHolder extends RecyclerView.ViewHolder {
        private ImageView iv_pic;
        private TextView tv_path;
​
        ViewHolder(View itemView) {
            super(itemView);
            iv_pic = (ImageView) itemView.findViewById(R.id.iv_pic);
            tv_path = (TextView) itemView.findViewById(R.id.tv_path);
        }
    }
}

左框架布局 item_frame_left

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                style="@style/Weibo.Frame"
                tools:ignore="UnusedAttribute, RtlHardcoded, ContentDescription">
​
    <ImageView
        android:id="@+id/avatar"
        style="@style/Weibo.Avatar"
        android:layout_marginRight="16dp"
        android:src="@drawable/icon"/>
​
    <TextView
        android:id="@+id/username"
        style="@style/Weibo.Username"
        android:layout_alignTop="@id/avatar"
        android:layout_toRightOf="@id/avatar"
        tools:text="drakeet"/>
​
    <FrameLayout
        android:id="@+id/container"
        style="@style/Weibo.SubView"
        android:layout_alignLeft="@id/username"
        android:layout_below="@id/username"
        tools:background="@android:color/darker_gray"
        tools:layout_height="72dp"/>
​
    <TextView
        android:id="@+id/create_time"
        style="@style/Weibo.CreateTime"
        android:layout_alignLeft="@id/username"
        android:layout_below="@id/container"
        tools:text="2017-7-18 11:53:59 星期二"/>
​
</RelativeLayout>

左圖片 item_simple_image_left

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
​
​
    <ImageView
        android:id="@+id/simple_image"
        style="@style/WeiboContent.SimpleImage"
        tools:src="@drawable/icon"/>
</RelativeLayout>

2017-7-18


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM