Android高仿iOS Messages聊天氣泡
-
- 一、目标
- 二、功能分析
- 三、實作代碼
-
- 1. ChatItem
- 2. DateItem
- 3. TextItem
- 4. PhotoItem
- 5. ChatViewHolder
- 四、開發過程回顧
- 五、接下來
- 六、Finally
在《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
~若是經典所在之處~即為有佛~若尊重弟子~