Markdown版本筆記 | 我的GitHub首頁 | 我的博客 | 我的微信 | 我的郵箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
RV 多樣式 MultiType 聊天界面 消息類型 MD
Demo
目錄
純原生實現多樣式
addHeaderView 方式
Activity
Adapter
Model
getItemViewType 方式
Activity
Adapter
Model
MultiType 簡介
特性
基礎用法
設計思想
使用插件自動生成代碼
一個類型對應多個 ItemViewBinder
添加HeaderView、FooterView
使用斷言以方便調試
聊天界面模板代碼
聊天界面 ChatActivity
消息模型的父類 ContentModel
VH的父類 ContentHolder
IVB的父類 ChatFrameBinder
消息模型的子類 SimpleImage
SimpleImageViewBinder
消息模型的特殊子類 BigImage
BigImageViewBinder
左框架布局 item_frame_left
左圖片 item_simple_image_left
純原生實現多樣式
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 簡介
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