From 3747c10dc1f80d4f68afa2e98b744ce16709fe42 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 23 Feb 2024 14:36:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=AE=8C=E6=88=90=E9=A2=84=E7=95=99?= =?UTF-8?q?=E6=B4=BB=E5=8A=A8=EF=BC=88=E6=A8=A1=E7=89=88=E6=B6=88=E6=81=AF?= =?UTF-8?q?=EF=BC=89=E5=85=AC=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../erban/avroom/widget/MessageView.java | 53 +++++++ .../avroom/widget/TemplateMessageAdapter.kt | 133 ++++++++++++++++++ .../home/bean/BannerInfo.java | 4 + .../home/bean/IRouterData.kt | 2 + .../manager/IMNetEaseManager.java | 8 ++ .../yizhuan/xchat_android_core/bean/I18N.kt | 23 +++ .../im/custom/bean/CustomAttachParser.java | 6 + .../im/custom/bean/CustomAttachment.java | 4 + .../im/custom/bean/TemplateMessage.kt | 132 +++++++++++++++++ .../custom/bean/TemplateMessageAttachment.kt | 32 +++++ .../java/com/chuhai/utils/StringUtils.kt | 64 +++++++++ 11 files changed, 461 insertions(+) create mode 100644 app/src/main/java/com/yizhuan/erban/avroom/widget/TemplateMessageAdapter.kt create mode 100644 core/src/main/java/com/yizhuan/xchat_android_core/bean/I18N.kt create mode 100644 core/src/main/java/com/yizhuan/xchat_android_core/im/custom/bean/TemplateMessage.kt create mode 100644 core/src/main/java/com/yizhuan/xchat_android_core/im/custom/bean/TemplateMessageAttachment.kt create mode 100644 library/src/module_utils/java/com/chuhai/utils/StringUtils.kt diff --git a/app/src/main/java/com/yizhuan/erban/avroom/widget/MessageView.java b/app/src/main/java/com/yizhuan/erban/avroom/widget/MessageView.java index f0d24d640..d350be902 100644 --- a/app/src/main/java/com/yizhuan/erban/avroom/widget/MessageView.java +++ b/app/src/main/java/com/yizhuan/erban/avroom/widget/MessageView.java @@ -5,6 +5,7 @@ import static com.yizhuan.xchat_android_core.im.custom.bean.CustomAttachment.CUS import static com.yizhuan.xchat_android_core.im.custom.bean.CustomAttachment.CUSTOM_MSG_GUARDIAN_PLANET; import static com.yizhuan.xchat_android_core.im.custom.bean.CustomAttachment.CUSTOM_MSG_RED_PACKAGE; import static com.yizhuan.xchat_android_core.im.custom.bean.CustomAttachment.CUSTOM_MSG_ROOM_ALBUM; +import static com.yizhuan.xchat_android_core.im.custom.bean.CustomAttachment.CUSTOM_MSG_ROOM_TEMPLATE; import static com.yizhuan.xchat_android_core.im.custom.bean.CustomAttachment.CUSTOM_MSG_SUB_BOX_ME; import static com.yizhuan.xchat_android_core.im.custom.bean.CustomAttachment.CUSTOM_MSG_SUB_CONVERT_L1; import static com.yizhuan.xchat_android_core.im.custom.bean.CustomAttachment.CUSTOM_MSG_SUB_CONVERT_L2; @@ -145,6 +146,7 @@ import com.yizhuan.xchat_android_core.im.custom.bean.RoomReceivedLuckyGiftAttach import com.yizhuan.xchat_android_core.im.custom.bean.RoomTipAttachment; import com.yizhuan.xchat_android_core.im.custom.bean.TarotAttachment; import com.yizhuan.xchat_android_core.im.custom.bean.TarotMsgBean; +import com.yizhuan.xchat_android_core.im.custom.bean.TemplateMessageAttachment; import com.yizhuan.xchat_android_core.im.custom.bean.User; import com.yizhuan.xchat_android_core.im.custom.bean.VipMessageAttachment; import com.yizhuan.xchat_android_core.im.custom.bean.WelcomeAttachment; @@ -255,6 +257,7 @@ public class MessageView extends FrameLayout { private OnClick onClick; private OnMsgLongClickListener onLongClickListener; + private TemplateMessageAdapter templateMessageAdapter; public MessageView(Context context) { this(context, null); @@ -435,7 +438,17 @@ public class MessageView extends FrameLayout { } } }); + } + private TemplateMessageAdapter getTemplateMessageAdapter() { + if (templateMessageAdapter == null) { + templateMessageAdapter = new TemplateMessageAdapter(uid -> { + if (clickConsumer != null) { + Single.just(String.valueOf(uid)).subscribe(clickConsumer); + } + }); + } + return templateMessageAdapter; } public void onCurrentRoomReceiveNewMsg(List messages) { @@ -619,6 +632,19 @@ public class MessageView extends FrameLayout { builder.setSpan(imageSpan, start, builder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); return this; } + /** + * @param drawable -icon url + * @return -返回一個spannableStringBuilder + */ + public SpannableBuilder appendImg(String drawable, Object what) { + if (TextUtils.isEmpty(drawable)) return this; + int start = builder.length(); + builder.append("-"); + CustomImageSpan imageSpan = new CustomImageSpan(new ColorDrawable(Color.TRANSPARENT), textView, drawable); + builder.setSpan(imageSpan, start, builder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + builder.setSpan(what, start, builder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + return this; + } /** * @param drawable -icon url @@ -635,6 +661,16 @@ public class MessageView extends FrameLayout { return this; } + public SpannableBuilder append(String drawable, int width, int height, Object what) { + if (TextUtils.isEmpty(drawable)) return this; + int start = builder.length(); + builder.append("-"); + CustomImageSpan imageSpan = new CustomImageSpan(new ColorDrawable(Color.TRANSPARENT), textView, drawable, width, height); + builder.setSpan(imageSpan, start, builder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + builder.setSpan(what, start, builder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + return this; + } + /** * 文本和背景分離的情況 */ @@ -676,6 +712,16 @@ public class MessageView extends FrameLayout { return this; } + public SpannableBuilder append(String imgUrl, int height, Object what) { + if (TextUtils.isEmpty(imgUrl)) return this; + int start = builder.length(); + builder.append("-"); + builder.setSpan(new CustomAutoWidthImageSpan(new ColorDrawable(Color.TRANSPARENT), textView, imgUrl, height) + , start, builder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + builder.setSpan(what, start, builder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + return this; + } + /** * @param drawable -icon * @param width 寬 @@ -1036,6 +1082,13 @@ public class MessageView extends FrameLayout { setRoomAlbumMsg(chatRoomMessage, baseViewHolder); } else if (first == CUSTOM_MSG_GUARDIAN_PLANET) { setGuardianPlanetMsg(chatRoomMessage, tvContent); + } else if (first == CUSTOM_MSG_ROOM_TEMPLATE) { + TemplateMessageAttachment templateMessageAttachment = (TemplateMessageAttachment) chatRoomMessage.getAttachment(); + if (templateMessageAttachment != null) { + getTemplateMessageAdapter().convert(tvContent, templateMessageAttachment.getTemplateMessage()); + } else { + getTemplateMessageAdapter().convert(tvContent, null); + } } else { tvContent.setTextColor(Color.WHITE); tvContent.setText(tvContent.getResources().getText(R.string.not_support_message_tip)); diff --git a/app/src/main/java/com/yizhuan/erban/avroom/widget/TemplateMessageAdapter.kt b/app/src/main/java/com/yizhuan/erban/avroom/widget/TemplateMessageAdapter.kt new file mode 100644 index 000000000..e3e849c73 --- /dev/null +++ b/app/src/main/java/com/yizhuan/erban/avroom/widget/TemplateMessageAdapter.kt @@ -0,0 +1,133 @@ +package com.yizhuan.erban.avroom.widget + +import android.content.Context +import android.graphics.Color +import android.text.method.LinkMovementMethod +import android.text.style.ForegroundColorSpan +import android.view.View +import android.widget.TextView +import com.chuhai.utils.UiUtils +import com.yizhuan.erban.common.widget.OriginalDrawStatusClickSpan +import com.yizhuan.erban.utils.CommonJumpHelper +import com.yizhuan.xchat_android_core.home.bean.BannerInfo +import com.yizhuan.xchat_android_core.im.custom.bean.TemplateMessage +import com.yizhuan.xchat_android_core.im.custom.bean.TemplateMessage.TemplateNode + +/** + * Created by Max on 2024/2/22 17:20 + * Desc:模版消息适配器 + **/ +class TemplateMessageAdapter(val listener: Listener) { + + fun convert(textView: TextView, attachment: TemplateMessage?) { + if (attachment == null) { + textView.text = "" + return + } + val nodeList = attachment.getNodeList() + val textBuilder = MessageView.SpannableBuilder(textView) + nodeList.forEach { + if (it is TemplateNode.NormalNode) { + val textColor = parseColor(it.textColor) + if (textColor != null) { + textBuilder.append(it.text, ForegroundColorSpan(textColor)) + } else { + textBuilder.append(it.text) + } + } else if (it is TemplateNode.SpecialNode) { + when (it.content.type) { + TemplateMessage.Content.TEXT -> { + val text = it.content.text?.getFirstText() + if (!text.isNullOrEmpty()) { + val textColor = parseColor(it.content.textColor) + val clickSpan = createClickSpan(textView.context, it.content) + val list = ArrayList() + if (textColor != null) { + list.add(ForegroundColorSpan(textColor)) + } + if (clickSpan != null) { + list.add(clickSpan) + } + textBuilder.append(text, *list.toArray()) + } + } + + TemplateMessage.Content.IMAGE -> { + val image = it.content.image + val width = it.content.width ?: 0 + val height = it.content.height ?: 0 + val clickSpan = createClickSpan(textView.context, it.content) + if (height > 0 && width == 0) { + if (clickSpan != null) { + textBuilder.append( + image, + UiUtils.dip2px(height.toFloat()), + clickSpan + ) + } else { + textBuilder.append(image, UiUtils.dip2px(height.toFloat())) + } + } else if (height > 0 && width > 0) { + if (clickSpan != null) { + textBuilder.append( + image, + UiUtils.dip2px(width.toFloat()), + UiUtils.dip2px(height.toFloat()), clickSpan + ) + } else { + textBuilder.append( + image, + UiUtils.dip2px(width.toFloat()), + UiUtils.dip2px(height.toFloat()) + ) + } + } else { + if (clickSpan != null) { + textBuilder.appendImg(image, clickSpan) + } else { + textBuilder.appendImg(image) + } + } + } + } + } + } + textView.text = textBuilder.build() + textView.setOnClickListener(null) + textView.movementMethod = LinkMovementMethod() + } + + private fun createClickSpan( + context: Context, + content: TemplateMessage.Content + ): OriginalDrawStatusClickSpan? { + val skipType = content.getSkipType() + val skipUri = content.getSkipUri() + if (skipType > 0 && !skipUri.isNullOrEmpty()) { + return object : OriginalDrawStatusClickSpan() { + override fun onClick(widget: View) { + if (skipType == BannerInfo.SKIP_TYPE_ROOM_USER_CARD) { + listener.onShowUserCard(skipUri) + } else { + CommonJumpHelper.bannerJump(context, content) + } + } + } + } else { + return null + } + } + + private fun parseColor(color: String?): Int? { + try { + return Color.parseColor(color) + } catch (e: java.lang.Exception) { + e.printStackTrace() + } + return null + } + + interface Listener { + fun onShowUserCard(uid: String) + } +} \ No newline at end of file diff --git a/core/src/diff_src_erban/java/com/yizhuan/xchat_android_core/home/bean/BannerInfo.java b/core/src/diff_src_erban/java/com/yizhuan/xchat_android_core/home/bean/BannerInfo.java index 54eeb3cf4..ca783f6b7 100644 --- a/core/src/diff_src_erban/java/com/yizhuan/xchat_android_core/home/bean/BannerInfo.java +++ b/core/src/diff_src_erban/java/com/yizhuan/xchat_android_core/home/bean/BannerInfo.java @@ -31,6 +31,10 @@ public class BannerInfo implements Parcelable, Serializable, IRouterData { * routerhandler跳转规则 */ public final static transient int SKIP_TYPE_ROUTER = 5; + /** + * 房间用户资料卡 + */ + public final static transient int SKIP_TYPE_ROOM_USER_CARD = 6; /* bannerId:1 //id diff --git a/core/src/diff_src_erban/java/com/yizhuan/xchat_android_core/home/bean/IRouterData.kt b/core/src/diff_src_erban/java/com/yizhuan/xchat_android_core/home/bean/IRouterData.kt index df0daec7a..febde7deb 100644 --- a/core/src/diff_src_erban/java/com/yizhuan/xchat_android_core/home/bean/IRouterData.kt +++ b/core/src/diff_src_erban/java/com/yizhuan/xchat_android_core/home/bean/IRouterData.kt @@ -12,7 +12,9 @@ interface IRouterData : Serializable { fun getSkipType(): Int + @Deprecated("SkipType==5时用到该值,后台讲这种已经没用到了") fun getRouterType(): String? + @Deprecated("SkipType==5时用到该值,后台讲这种已经没用到了") fun getRouterValue(): String? } \ No newline at end of file diff --git a/core/src/diff_src_erban/java/com/yizhuan/xchat_android_core/manager/IMNetEaseManager.java b/core/src/diff_src_erban/java/com/yizhuan/xchat_android_core/manager/IMNetEaseManager.java index 4d7b960c1..67870e860 100644 --- a/core/src/diff_src_erban/java/com/yizhuan/xchat_android_core/manager/IMNetEaseManager.java +++ b/core/src/diff_src_erban/java/com/yizhuan/xchat_android_core/manager/IMNetEaseManager.java @@ -1009,6 +1009,14 @@ public final class IMNetEaseManager { break; } break; + case CUSTOM_MSG_ROOM_TEMPLATE: + switch (second){ + case CUSTOM_MSG_ROOM_TEMPLATE_SUB_ROOM: + case CUSTOM_MSG_ROOM_TEMPLATE_SUB_ALL_ROOM: + addMessages(msg); + break; + } + break; case CUSTOM_MSG_RADISH: RoomBoxPrizeAttachment boxPrizeAttachment = ((RoomBoxPrizeAttachment) msg.getAttachment()); UserInfo userInfo = UserModel.get().getCacheLoginUserInfo(); diff --git a/core/src/main/java/com/yizhuan/xchat_android_core/bean/I18N.kt b/core/src/main/java/com/yizhuan/xchat_android_core/bean/I18N.kt new file mode 100644 index 000000000..72ccc3304 --- /dev/null +++ b/core/src/main/java/com/yizhuan/xchat_android_core/bean/I18N.kt @@ -0,0 +1,23 @@ +package com.yizhuan.xchat_android_core.bean + +import java.io.Serializable + + +/** + * Created by Max on 2024/2/22 18:36 + * Desc: + **/ +class I18N : HashMap(), Serializable { + /** + * 获取优先显示文本 + */ + fun getFirstText(): String? { + // 目前应用只支持繁体,后续支持其他语言,这里需要调整 + val content = get("zh-TW") + return if (content.isNullOrEmpty()) { + this.values.firstOrNull() + } else { + content + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/yizhuan/xchat_android_core/im/custom/bean/CustomAttachParser.java b/core/src/main/java/com/yizhuan/xchat_android_core/im/custom/bean/CustomAttachParser.java index e4161c257..77a6650e3 100644 --- a/core/src/main/java/com/yizhuan/xchat_android_core/im/custom/bean/CustomAttachParser.java +++ b/core/src/main/java/com/yizhuan/xchat_android_core/im/custom/bean/CustomAttachParser.java @@ -659,6 +659,12 @@ public class CustomAttachParser implements MsgAttachmentParser { attachment = new RoomAlbumAttachment(); } break; + case CustomAttachment.CUSTOM_MSG_ROOM_TEMPLATE: + if (second == CustomAttachment.CUSTOM_MSG_ROOM_TEMPLATE_SUB_ROOM + || second == CustomAttachment.CUSTOM_MSG_ROOM_TEMPLATE_SUB_ALL_ROOM) { + attachment = new TemplateMessageAttachment(first, second); + } + break; default: LogUtils.e(ResUtil.getString(R.string.custom_bean_customattachparser_01) + first + " second=" + second); break; diff --git a/core/src/main/java/com/yizhuan/xchat_android_core/im/custom/bean/CustomAttachment.java b/core/src/main/java/com/yizhuan/xchat_android_core/im/custom/bean/CustomAttachment.java index 319de4b0d..49dd308cb 100644 --- a/core/src/main/java/com/yizhuan/xchat_android_core/im/custom/bean/CustomAttachment.java +++ b/core/src/main/java/com/yizhuan/xchat_android_core/im/custom/bean/CustomAttachment.java @@ -485,6 +485,10 @@ public class CustomAttachment implements MsgAttachment { public static final int CUSTOM_MSG_ROOM_ALBUM = 101; public static final int CUSTOM_MSG_ROOM_ALBUM_SUB = 1011; + // 模版消息 + public static final int CUSTOM_MSG_ROOM_TEMPLATE = 103; + public static final int CUSTOM_MSG_ROOM_TEMPLATE_SUB_ROOM = 1031; + public static final int CUSTOM_MSG_ROOM_TEMPLATE_SUB_ALL_ROOM = 1032; /** * 自定义消息附件的类型,根据该字段区分不同的自定义消息 diff --git a/core/src/main/java/com/yizhuan/xchat_android_core/im/custom/bean/TemplateMessage.kt b/core/src/main/java/com/yizhuan/xchat_android_core/im/custom/bean/TemplateMessage.kt new file mode 100644 index 000000000..0ffd3c6f4 --- /dev/null +++ b/core/src/main/java/com/yizhuan/xchat_android_core/im/custom/bean/TemplateMessage.kt @@ -0,0 +1,132 @@ +package com.yizhuan.xchat_android_core.im.custom.bean + +import com.chuhai.utils.StringUtils +import com.yizhuan.xchat_android_core.bean.I18N +import com.yizhuan.xchat_android_core.home.bean.IRouterData +import java.io.Serializable +import java.util.regex.Pattern + +/** + * Created by Max on 2024/2/22 18:51 + * Desc: + **/ +data class TemplateMessage( + var template: I18N? = null, + var textColor: String? = null, + val contents: List? = null +) : Serializable { + + private val nodeList: List? = null + + fun getNodeList(): List { + var nodeList = this.nodeList + if (nodeList == null) { + nodeList = parseNodes() + } + return nodeList + } + + // 转化为方便渲染的节点列表 + private fun parseNodes(): List { + val templateText = template?.getFirstText() + val contentList = this.contents + val defTextColor = this.textColor + if (templateText.isNullOrEmpty()) { + return emptyList() + } + if (contentList.isNullOrEmpty()) { + return listOf( + TemplateNode.NormalNode( + text = templateText, + textColor = defTextColor + ) + ) + } + val list = ArrayList() + StringUtils.split( + content = templateText, + pattern = Pattern.compile("\\{.+?\\}"), + onNormalNode = { + list.add( + TemplateNode.NormalNode( + text = it, + textColor = defTextColor + ) + ) + }, + onMatchNode = { text -> + val content = contentList.firstOrNull { + "{${it.key}}" == text + } + if (content?.type == Content.TEXT && content.textColor.isNullOrEmpty()) { + // 默认文本色 + content.textColor = textColor + } + if (content != null) { + list.add( + TemplateNode.SpecialNode( + content = content + ) + ) + } + }) + return list + } + + data class Content( + /** + * 公共字段 + */ + val key: String? = null, + // TEXT,IMAGE + val type: String? = null, + val skipType: Int? = null, + val skipContent: String? = null, + + /** + * 文本相关字段(type=TEXT) + */ + val text: I18N? = null, + var textColor: String? = null, + + /** + * 图片相关字段(type=IMAGE) + */ + val image: String? = null, + val width: Int? = null, + val height: Int? = null + ) : Serializable, IRouterData { + + override fun getSkipType(): Int { + return skipType ?: 0 + } + + override fun getRouterType(): String? { + return null + } + + override fun getRouterValue(): String? { + return null + } + + override fun getSkipUri(): String? { + return skipContent + } + + companion object { + const val TEXT = "TEXT" + const val IMAGE = "IMAGE" + } + } + + interface TemplateNode : Serializable { + data class NormalNode( + var text: String, + var textColor: String? = null + ) : TemplateNode + + data class SpecialNode( + var content: Content + ) : TemplateNode + } +} \ No newline at end of file diff --git a/core/src/main/java/com/yizhuan/xchat_android_core/im/custom/bean/TemplateMessageAttachment.kt b/core/src/main/java/com/yizhuan/xchat_android_core/im/custom/bean/TemplateMessageAttachment.kt new file mode 100644 index 000000000..336aabb24 --- /dev/null +++ b/core/src/main/java/com/yizhuan/xchat_android_core/im/custom/bean/TemplateMessageAttachment.kt @@ -0,0 +1,32 @@ +package com.yizhuan.xchat_android_core.im.custom.bean + +import com.alibaba.fastjson.JSONObject +import com.google.gson.Gson + +/** + * Created by Max on 2024/2/22 17:28 + * Desc: + **/ +class TemplateMessageAttachment : CustomAttachment { + + constructor() : super() + constructor(first: Int, second: Int) : super(first, second) + + private var templateMessage: TemplateMessage? = null + + fun getTemplateMessage() = templateMessage + + override fun parseData(data: JSONObject?) { + super.parseData(data) + if (data != null) { + try { + templateMessage = Gson().fromJson( + data.toJSONString(), + TemplateMessage::class.java + ) + } catch (e: Exception) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/library/src/module_utils/java/com/chuhai/utils/StringUtils.kt b/library/src/module_utils/java/com/chuhai/utils/StringUtils.kt new file mode 100644 index 000000000..45afb5cf4 --- /dev/null +++ b/library/src/module_utils/java/com/chuhai/utils/StringUtils.kt @@ -0,0 +1,64 @@ +package com.chuhai.utils + +import java.util.regex.Pattern + +/** + * Created by Max on 2/10/21 4:56 PM + * Desc:字符串工具 + */ +object StringUtils { + + /** + * 拆分字符串(根据匹配规则,按顺序拆分出来) + * @param pattern 匹配节点的规则模式 + * @param onNormalNode<节点内容> 普通节点 + * @param onMatchNode<节点内容> 匹配节点 + */ + fun split( + content: String, + pattern: Pattern, + onNormalNode: (String) -> Unit, + onMatchNode: (String) -> Unit, + ) { + try { + if (content.isEmpty()) { + onNormalNode.invoke(content) + return + } + val matcher = pattern.matcher(content) + // 最后一个匹配项的结束位置 + var lastItemEnd = 0 + var noMatch = true + while (matcher.find()) { + noMatch = false + // 匹配元素的开启位置 + val start = matcher.start() + // 匹配元素的结束位置 + val end = matcher.end() + // 匹配元素的文本 + val text = matcher.group() + // 匹配元素的对应索引 +// logD("split() start:$start ,end:$end ,text:$text") + if (start > lastItemEnd) { + // 普通节点 + val nodeContent = content.substring(lastItemEnd, start) + onNormalNode.invoke(nodeContent) + } + // 匹配节点显示内容 + onMatchNode.invoke(text) + lastItemEnd = end + } + if (lastItemEnd > 0 && lastItemEnd < content.length) { + // 最后的匹配项不是尾部(追加最后的尾部) + val nodeContent = content.substring(lastItemEnd, content.length) + onNormalNode.invoke(nodeContent) + } + if (noMatch) { + // 无匹配 + onNormalNode.invoke(content) + } + } catch (e: Exception) { + e.printStackTrace() + } + } +}