版权声明:个人原创,欢迎转载。 https://blog.csdn.net/chuyangchangxi/article/details/90773186
Android高仿iOS Messages聊天气泡
在《iOS Messages显示图片功能分析》一文中,我们总结了iOS Messages的气泡形状。现在着手实现之。
一、目标
实现iOS Messages聊天气泡。
二、功能分析
神马笔记当前只能发送信息,不会收到信息,因此只考虑发送消息的气泡形状。
除了没有气泡外,共有6种气泡形状。
考虑消息时间的连续性,有4种——单独、开始、中间、结束,使用4种气泡形状已经足够。
同时考虑消息类型的连续型,同样有4种——单独、开始、中间、结束。
二者存在重叠情况,最终16种情况合并为6种气泡形状。
- 气泡形状
尖角气泡总是出现在对话告一段落的情况。
定义 | 描述 |
---|---|
BUBBLE_NONE | 没有气泡,如日期 |
BUBBLE_SINGLE | 单个气泡 |
BUBBLE_SINGLE_TAIL | 单个尖角气泡 |
BUBBLE_START | 开始 |
BUBBLE_MIDDLE | 中间 |
BUBBLE_END | 结束气泡 |
BUBBLE_END_TAIL | 结束尖角气泡 |
- 时间类型
定义 | 描述 |
---|---|
STYLE_SINGLE | 单独时间点 |
STYLE_START | 开始时间点 |
STYLE_MIDDLE | 中间时间点 |
STYLE_END | 结束时间点 |
- 消息类型
定义 | 描述 |
---|---|
STYLE_SINGLE | 单独类型 |
STYLE_START | 开始 |
STYLE_MIDDLE | 中间 |
STYLE_END | 结束 |
- 关系表
时间类型 | 消息类型 | 气泡类型 |
---|---|---|
STYLE_SINGLE | ||
STYLE_SINGLE | BUBBLE_SINGLE_TAIL | |
STYLE_START | BUBBLE_SINGLE_TAIL | |
STYLE_MIDDLE | BUBBLE_SINGLE_TAIL | |
STYLE_END | BUBBLE_SINGLE_TAIL | |
STYLE_START | ||
STYLE_SINGLE | BUBBLE_SINGLE | |
STYLE_START | BUBBLE_START | |
STYLE_MIDDLE | BUBBLE_START | |
STYLE_END | BUBBLE_SINGLE | |
STYLE_MIDDLE | ||
STYLE_SINGLE | BUBBLE_SINGLE | |
STYLE_START | BUBBLE_START | |
STYLE_MIDDLE | BUBBLE_MIDDLE | |
STYLE_END | BUBBLE_END | |
STYLE_END | ||
STYLE_SINGLE | BUBBLE_SINGLE_TAIL | |
STYLE_START | BUBBLE_SINGLE_TAIL | |
STYLE_MIDDLE | BUBBLE_END_TAIL | |
STYLE_END | BUBBLE_END_TAIL |
三、实现代码
1. ChatItem
方法 | 描述 | 依赖 |
---|---|---|
public int getBubble() | 获取气泡形状 | getTimeStyle() getTypeStyle() |
protected int getTimeStyle() | 获取时间类型 | isTimeContinuous() |
protected int getTypeStyle() | 获取消息类型 | isTypeContinuous() |
protected boolean isTimeContinuous(ChatItem item) | 判断时间是否连续,当前设定为3分钟。 | |
protected boolean isTypeContinuous(ChatItem item) | 判断类型是否连续 |
public class ChatItem<E extends MessageEntity> {
public static final int TYPE_NONE = 0;
public static final int TYPE_DATE = 1;
public static final int TYPE_TEXT = 2;
public static final int TYPE_PHOTO = 3;
public static final int BUBBLE_NONE = 0;
public static final int BUBBLE_SINGLE = 1;
public static final int BUBBLE_SINGLE_TAIL = 2;
public static final int BUBBLE_START = 3;
public static final int BUBBLE_MIDDLE = 4;
public static final int BUBBLE_END = 5;
public static final int BUBBLE_END_TAIL = 6;
public static final int STYLE_SINGLE = 0;
public static final int STYLE_START = 1;
public static final int STYLE_MIDDLE = 2;
public static final int STYLE_END = 3;
int type;
E entity;
protected ChatProvider parent;
public ChatItem(ChatProvider parent, E entity, int type) {
this.parent = parent;
this.entity = entity;
this.type = type;
}
public E getEntity() {
return entity;
}
public int getType() {
return this.type;
}
public DateTime getCreated() {
return entity.getCreated();
}
public int getBubble() {
int bubble = BUBBLE_SINGLE_TAIL;
int time = getTimeStyle();
int type = getTypeStyle();
switch (time) {
case STYLE_SINGLE: {
bubble = BUBBLE_SINGLE_TAIL;
break;
}
case STYLE_START: {
if (type == STYLE_SINGLE) {
bubble = BUBBLE_SINGLE;
} else if (type == STYLE_START) {
bubble = BUBBLE_START;
} else if (type == STYLE_MIDDLE) {
bubble = BUBBLE_START;
} else if (type == STYLE_END) {
bubble = BUBBLE_SINGLE;
}
break;
}
case STYLE_MIDDLE: {
if (type == STYLE_SINGLE) {
bubble = BUBBLE_SINGLE;
} else if (type == STYLE_START) {
bubble = BUBBLE_START;
} else if (type == STYLE_MIDDLE) {
bubble = BUBBLE_MIDDLE;
} else if (type == STYLE_END) {
bubble = BUBBLE_END;
}
break;
}
case STYLE_END: {
if (type == STYLE_SINGLE) {
bubble = BUBBLE_SINGLE_TAIL;
} else if (type == STYLE_START) {
bubble = BUBBLE_SINGLE_TAIL;
} else if (type == STYLE_MIDDLE) {
bubble = BUBBLE_END_TAIL;
} else if (type == STYLE_END) {
bubble = BUBBLE_END_TAIL;
}
break;
}
}
return bubble;
}
protected int getTimeStyle() {
boolean pre = false;
boolean next = false;
int position = parent.indexOf(this);
if (position > 0) {
pre = isTimeContinuous(parent.get(position - 1));
}
if (position >= 0 && position < (parent.size() - 1)) {
next = isTimeContinuous(parent.get(position + 1));
}
int style = STYLE_SINGLE;
if (pre && next) {
style = STYLE_MIDDLE;
} else if (pre && !next) {
style = STYLE_END;
} else if (!pre && next) {
style = STYLE_START;
} else if (!pre && !next) {
style = STYLE_SINGLE;
}
return style;
}
protected int getTypeStyle() {
boolean pre = false;
boolean next = false;
int position = parent.indexOf(this);
if (position > 0) {
pre = isTypeContinuous(parent.get(position - 1));
}
if (position >= 0 && position < (parent.size() - 1)) {
next = isTypeContinuous(parent.get(position + 1));
}
int style = STYLE_SINGLE;
if (pre && next) {
style = STYLE_MIDDLE;
} else if (pre && !next) {
style = STYLE_END;
} else if (!pre && next) {
style = STYLE_START;
} else if (!pre && !next) {
style = STYLE_SINGLE;
}
return style;
}
protected boolean isTypeContinuous(ChatItem item) {
return this.getType() == item.getType();
}
protected boolean isTimeContinuous(ChatItem item) {
return isTimeContinuous(this, item, 3);
}
static boolean isTimeContinuous(ChatItem current, ChatItem next, int minutes) {
DateTime date = current.getCreated();
DateTime now = next.getCreated();
int diff = Minutes.minutesBetween(date, now).getMinutes();
diff = Math.abs(diff);
boolean result = (diff < minutes);
return result;
}
}
2. DateItem
没有气泡。
public class DateItem extends ChatItem<MessageEntity> {
public DateItem(ChatProvider parent, MessageEntity entity) {
super(parent, entity, TYPE_DATE);
}
@Override
public int getBubble() {
return BUBBLE_NONE;
}
}
3. TextItem
重载getTypeStyle
方法,文本消息类型总看作单独的。
public class TextItem extends ChatItem<TextEntity> {
public TextItem(ChatProvider parent, TextEntity entity) {
super(parent, entity, TYPE_TEXT);
}
@Override
protected int getTypeStyle() {
return STYLE_SINGLE;
}
public String getText() {
return entity.getText();
}
}
4. PhotoItem
未来在判断类型连续性上会进行扩展,重载之。
public class PhotoItem extends ChatItem<PhotoEntity> {
public PhotoItem(ChatProvider parent, PhotoEntity entity) {
super(parent, entity, TYPE_PHOTO);
}
@Override
protected boolean isTypeContinuous(ChatItem item) {
return super.isTypeContinuous(item);
}
public int getWidth() {
return entity.getWidth();
}
public int getHeight() {
return entity.getHeight();
}
public Uri getUri() {
return entity.getUri();
}
public File getFile() {
return entity.getFile();
}
public String getSignature() {
return entity.getSignature();
}
}
5. ChatViewHolder
根据气泡形状,获取对应的图片资源。
public abstract class ChatViewHolder<E extends ChatItem> extends BridgeViewHolder<E> {
ViewStub viewStub;
@Keep
public ChatViewHolder(View itemView) {
super(itemView);
}
@Override
public void onViewCreated(@NonNull View view) {
this.viewStub = view.findViewById(R.id.stub);
}
public int getBubble(ChatItem item) {
int resId = R.drawable.ic_outgoing_bubble_single_tail;
switch (item.getBubble()) {
case ChatItem.BUBBLE_NONE: {
resId = 0;
break;
}
case ChatItem.BUBBLE_SINGLE: {
resId = R.drawable.ic_outgoing_bubble_single;
break;
}
case ChatItem.BUBBLE_SINGLE_TAIL: {
resId = R.drawable.ic_outgoing_bubble_single_tail;
break;
}
case ChatItem.BUBBLE_START: {
resId = R.drawable.ic_outgoing_bubble_start;
break;
}
case ChatItem.BUBBLE_MIDDLE: {
resId = R.drawable.ic_outgoing_bubble_middle;
break;
}
case ChatItem.BUBBLE_END: {
resId = R.drawable.ic_outgoing_bubble_end;
break;
}
case ChatItem.BUBBLE_END_TAIL: {
resId = R.drawable.ic_outgoing_bubble_end_tail;
break;
}
}
return resId;
}
}
四、开发过程回顾
从6种气泡形状开始,发现决定气泡形状的2个参数——时间类型、消息类型。
根据时间类型和消息类型组合出对应管理。
再根据时间和类型的连续性计算出对应的类型。
从而最终计算出每条消息对应的气泡形状。
五、接下来
组合所有功能,实现神马笔记在对话中插入图片消息。
六、Finally
~若是经典所在之处~即为有佛~若尊重弟子~