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

RV 多样式 MultiType 聊天界面 消息类型 MD的更多相关文章

  1. Android—简单的仿QQ聊天界面

    最近仿照QQ聊天做了一个类似界面,先看下界面组成(画面不太美凑合凑合呗,,,,):

  2. Android学习笔记(十二)——实战:制作一个聊天界面

    //此系列博文是<第一行Android代码>的学习笔记,如有错漏,欢迎指正! 运用简单的布局知识,我们可以来尝试制作一个聊天界面. 一.制作 Nine-Patch 图片 : Nine-Pa ...

  3. 聊天界面之气泡文本cell(一)

    在实现qq聊天界面的过程中,使用UITableViewCell碰到了不少问题,这里还是记录一下以免遗忘. 气泡聊天cell的实现,网上最多的方法还是: 1.手动计算设置frame的值,文本的size使 ...

  4. [iOS基础控件 - 6.9] 聊天界面Demo

    A.需求 做出一个类似于QQ.微信的聊天界面 1.每个cell包含发送时间.发送人(头像).发送信息 2.使用对方头像放在左边,我方头像在右边 3.对方信息使用白色背景对话框,我方信息使用蓝色背景对话 ...

  5. iOS开发——UI_swift篇&TableView自定义聊天界面

    TableView自定义聊天界面   1,下面是一个放微信聊天界面的消息展示列表,实现的功能有: (1)消息可以是文本消息也可以是图片消息 (2)消息背景为气泡状图片,同时消息气泡可根据内容自适应大小 ...

  6. 自定义android精美聊天界面

    编写精美聊天界面,那就肯定要有收到的消息和发送的消息. 首先还是编写主界面,修改activity_chat.xml中的代码,如下所示: <?xml version="1.0" ...

  7. QQ聊天界面的布局和设计(IOS篇)-第一季

    我写的源文件整个工程会再第二季中发上来~,存在百度网盘, 感兴趣的童鞋, 可以关注我的博客更新,到时自己去下载~.喵~~~ QQChat Layout - 第一季 一.准备工作 1.将假数据messa ...

  8. Swift - 自定义单元格实现微信聊天界面

    1,下面是一个放微信聊天界面的消息展示列表,实现的功能有: (1)消息可以是文本消息也可以是图片消息 (2)消息背景为气泡状图片,同时消息气泡可根据内容自适应大小 (3)每条消息旁边有头像,在左边表示 ...

  9. 自定义一个ListView实现聊天界面

    摘要 ListView可以称得上Android中最常用也最难用的控件了,几乎所有的应用程序都会用到它.由于手机屏幕空间都比较有限,能够一次性在屏幕上显示的内容并不多,当我们的程序中有大量的数据需要展示 ...

随机推荐

  1. 第二波分析:德国是2018世界杯夺冠最大热门? Python数据分析来揭开神秘面纱… (附源代码)

    2018年,世界杯小组赛已经在如火如荼的进行中.在上篇文章的基础上[2018世界杯:用Python分析热门夺冠球队],我们继续分析世界杯32强的实力情况,以期能够更进一步分析本次世界杯的夺冠热门球队. ...

  2. Redis 连接命令

    1.AUTH password 验证密码是否正确 2.ECHO message 打印字符串 3.PING 查看服务是否运行 4.QUIT 关闭当前连接 5.SELECT index 切换到指定的数据库

  3. [BZOJ3560]DZY Loves Math V(欧拉函数)

    https://www.cnblogs.com/zwfymqz/p/9332753.html 由于欧拉函数是积性函数,可以用乘法分配律变成对每个质因子分开算最后乘起来.再由欧拉函数公式和分配律发现就是 ...

  4. Java并发(十七):ConcurrentHashMap

    先做总结: 1.HashMap HashTable ConcurrentHashMap HashMap:线程不安全 HashTable:线程安全,每个方法都加了 synchronized 修饰.类似 ...

  5. bzoj 2850

    比较基础的KD树.每个节点维护一个BOX,包含包含当当前子树的点的最小矩形,以及点权和,然后用“整个矩形都在直线的一侧”和“整个矩形都不在直线的一侧”剪枝. /******************** ...

  6. 使用清华大学开源软件镜像AOSP的“每月更新初始化包”更新指定版本的Android源码

    参照官方教程:Tsinghua Open Source Mirror 1. 下载了repo工具 mkdir  ~/bin PATH = ~/bin:$PATH curl  https://storag ...

  7. MySQL多表联查之ThinkPHP中的实现

    创建两个表如图: sp_user表: sp_dept表: 目的:通过sp_user的dept_id查询所属部门即sp_dept中的name. 原生sq方法一:select t1.*,t2.name a ...

  8. 使用Chrome快速实现数据的抓取(五)—— puppeteer

    如果要以自动化的方式驱动Chrome进行数据抓取,必须实现Chrome Dev Protocol协议的客户端.这个协议本身并不复杂,我在之前的文章中也简单的介绍过一下. Google本身有一个Node ...

  9. HDU 4717 The Moving Points (三分)

    The Moving Points Time Limit: 6000/3000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others ...

  10. centos7 通过kvm+vnc 实现远程桌面虚拟化和创建windows、Linux虚拟机

    感谢朋友支持本博客.欢迎共同探讨交流,因为能力和时间有限.错误之处在所难免,欢迎指正! 假设转载.请保留作者信息. 博客地址:http://blog.csdn.net/qq_21398167 原博文地 ...