Android高仿iOS Messages聊天气泡

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) 判断类型是否连续
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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

没有气泡。

1
2
3
4
5
6
7
8
9
10
11
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方法,文本消息类型总看作单独的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

未来在判断类型连续性上会进行扩展,重载之。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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

根据气泡形状,获取对应的图片资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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

~若是经典所在之处~即为有佛~若尊重弟子~