Просмотр исходного кода

feat: 语音支持倍速播放

DoggyZhang 3 месяцев назад
Родитель
Сommit
185205e581
20 измененных файлов с 871 добавлено и 129 удалено
  1. 17 0
      app/src/main/java/com/adealink/weparty/commonui/util/DateTimeUtil.kt
  2. 1 0
      app/src/main/java/com/adealink/weparty/module/im/data/Tags.kt
  3. 17 0
      app/src/main/java/com/adealink/weparty/util/DeviceUtil.kt
  4. 7 0
      frame/tuikit/TUIChat/tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/audio/AudioPlayer.java
  5. 13 6
      module/im/src/main/java/com/adealink/weparty/im/session/SessionActivity.kt
  6. 191 0
      module/im/src/main/java/com/adealink/weparty/im/session/SessionFragment.kt
  7. 83 6
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/SessionAdapter.kt
  8. 73 33
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/BaseMessageViewBinder.kt
  9. 38 13
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/ImageMessageViewBinder.kt
  10. 253 26
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/SoundMessageViewBinder.kt
  11. 38 18
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/TextMessageViewBinder.kt
  12. 32 11
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/TipsMessageViewBinder.kt
  13. 34 11
      module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/UnSupportMessageViewBinder.kt
  14. 4 0
      module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionBottomAudioComp.kt
  15. 1 1
      module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionBottomInputComp.kt
  16. 43 3
      module/im/src/main/java/com/adealink/weparty/im/session/widget/MessageSoundPlayView.kt
  17. BIN
      module/im/src/main/res/drawable-xhdpi/session_send_error_ic.png
  18. 14 1
      module/im/src/main/res/layout/layout_session_message_base.xml
  19. 11 0
      module/im/src/main/res/values/colors.xml
  20. 1 0
      module/im/src/main/res/values/strings.xml

+ 17 - 0
app/src/main/java/com/adealink/weparty/commonui/util/DateTimeUtil.kt

@@ -105,4 +105,21 @@ private fun getDayStr(date: Date, day: Int, language: String): String {
 
 private fun getHMStr(date: Date): String {
     return HmFormat.format(date)
+}
+
+fun formatSecondsTo00(timeSeconds: Int): String {
+    val second = timeSeconds % 60
+    val minuteTemp = timeSeconds / 60
+    if (minuteTemp > 0) {
+        val minute = minuteTemp % 60
+        val hour = minuteTemp / 60
+        return if (hour > 0) {
+            ((if (hour >= 10) (hour.toString() + "") else ("0$hour")) + ":" + (if (minute >= 10) (minute.toString() + "") else ("0$minute")) + ":"
+                    + (if (second >= 10) (second.toString() + "") else ("0$second")))
+        } else {
+            (if (minute >= 10) (minute.toString() + "") else ("0$minute")) + ":" + (if (second >= 10) (second.toString() + "") else ("0$second"))
+        }
+    } else {
+        return "00:" + (if (second >= 10) (second.toString() + "") else ("0$second"))
+    }
 }

+ 1 - 0
app/src/main/java/com/adealink/weparty/module/im/data/Tags.kt

@@ -2,6 +2,7 @@ package com.adealink.weparty.module.im.data
 
 const val TAG_IM = "tag_im"
 const val TAG_IM_SESSION ="${TAG_IM}_session"
+const val TAG_IM_SESSION_SOUND ="${TAG_IM}_session_sound"
 
 const val TAG_IM_INPUT_FLOW ="${TAG_IM}_input_flow"
 const val TAG_IM_AUDIO ="${TAG_IM}_audio"

+ 17 - 0
app/src/main/java/com/adealink/weparty/util/DeviceUtil.kt

@@ -0,0 +1,17 @@
+package com.adealink.weparty.util
+
+import android.os.Build
+
+/**
+ * 是否支持多媒体倍速播放
+ */
+fun isSupportSpeedPlay(): Boolean {
+    return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+}
+
+/**
+ * 是否支持录制暂停
+ */
+fun isSupportPauseRecord(): Boolean {
+    return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
+}

+ 7 - 0
frame/tuikit/TUIChat/tuichat/src/main/java/com/tencent/qcloud/tuikit/tuichat/component/audio/AudioPlayer.java

@@ -123,6 +123,13 @@ public class AudioPlayer {
         return mAudioPath;
     }
 
+    public int getPlayPosition() {
+        if (mPlayer == null || !mPlayer.isPlaying()) {
+            return 0;
+        }
+        return mPlayer.getCurrentPosition();
+    }
+
     public interface Callback {
         void onCompletion(Boolean success);
     }

+ 13 - 6
module/im/src/main/java/com/adealink/weparty/im/session/SessionActivity.kt

@@ -1,5 +1,6 @@
 package com.adealink.weparty.im.session
 
+import android.view.inputmethod.InputMethodManager
 import com.adealink.frame.base.fastLazy
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
@@ -116,13 +117,19 @@ class SessionActivity : BaseActivity() {
             }
         }
 
+    }
+
 
-//        val v2TIMMessage =
-//            intent.getSerializableExtra(TUIConstants.TUIChat.LOCATE_MESSAGE) as V2TIMMessage?
-//        val messageInfo = ChatMessageBuilder.buildMessage(v2TIMMessage)
-//        chatInfo.setLocateMessage(messageInfo)
-//        chatInfo.setAtInfoList(intent.getSerializableExtra(TUIConstants.TUIChat.AT_INFO_LIST) as MutableList<V2TIMGroupAtInfo?>?)
-//        chatInfo.setFaceUrl(intent.getStringExtra(TUIConstants.TUIChat.FACE_URL))
+    override fun finish() {
+        hideSoftInput()
+        super.finish()
     }
 
+    private fun hideSoftInput() {
+        val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
+        val window = getWindow()
+        if (window != null) {
+            imm.hideSoftInputFromWindow(window.decorView.windowToken, 0)
+        }
+    }
 }

+ 191 - 0
module/im/src/main/java/com/adealink/weparty/im/session/SessionFragment.kt

@@ -1,9 +1,14 @@
 package com.adealink.weparty.im.session
 
+import android.view.MotionEvent
+import android.view.View
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.updatePadding
 import androidx.fragment.app.activityViewModels
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
 import com.adealink.frame.base.fastLazy
+import com.adealink.frame.ext.safeSubList
 import com.adealink.frame.log.Log
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.weparty.commonui.BaseFragment
@@ -17,12 +22,18 @@ import com.adealink.weparty.im.session.adapter.SessionAdapter
 import com.adealink.weparty.im.session.comp.SessionBottomComp
 import com.adealink.weparty.im.session.comp.viewmodel.SessionInputViewModel
 import com.adealink.weparty.module.im.data.TAG_IM_SESSION
+import com.tencent.qcloud.tuicore.TUIConstants
+import com.tencent.qcloud.tuicore.TUICore
+import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean
+import com.tencent.qcloud.tuikit.timcommon.component.interfaces.IUIKitCallback
+import com.tencent.qcloud.tuikit.timcommon.interfaces.OnItemClickListener
 import com.tencent.qcloud.tuikit.tuichat.TUIChatConstants
 import com.tencent.qcloud.tuikit.tuichat.bean.C2CChatInfo
 import com.tencent.qcloud.tuikit.tuichat.bean.ChatInfo
 import com.tencent.qcloud.tuikit.tuichat.bean.GroupChatInfo
 import com.tencent.qcloud.tuikit.tuichat.component.audio.AudioPlayer
 import com.tencent.qcloud.tuikit.tuichat.component.audio.AudioRecorder
+import com.tencent.qcloud.tuikit.tuichat.interfaces.OnGestureScrollListener
 import com.tencent.qcloud.tuikit.tuichat.presenter.C2CChatPresenter
 import com.tencent.qcloud.tuikit.tuichat.presenter.ChatPresenter
 import com.tencent.qcloud.tuikit.tuichat.presenter.GroupChatPresenter
@@ -38,6 +49,8 @@ class SessionFragment : BaseFragment(R.layout.fragment_session) {
     private val sessionAdapter: SessionAdapter by fastLazy { SessionAdapter() }
     private var sessionPresenter: ChatPresenter? = null
 
+    private var scrollDirection = 0
+
     fun setChatInfo(chatInfo: ChatInfo?) {
         this.chatInfo = chatInfo
     }
@@ -65,7 +78,63 @@ class SessionFragment : BaseFragment(R.layout.fragment_session) {
         }
         binding.rvMessage.setPresenter(sessionPresenter)
         binding.rvMessage.setAdapter(sessionAdapter)
+        sessionAdapter.setOnItemClickListener(onMessageItemClickListener)
         binding.rvMessage.addItemDecoration(VerticalSpaceItemDecoration(8.dp(), 24.dp(), 20.dp()))
+        binding.rvMessage.setOnGestureScrollListener(object : OnGestureScrollListener {
+            override fun onScroll(
+                m1: MotionEvent?,
+                m2: MotionEvent?,
+                distanceX: Float,
+                distanceY: Float
+            ) {
+                if (distanceY < 0) {
+                    scrollDirection = -1
+                } else if (distanceY > 0) {
+                    scrollDirection = 1
+                } else {
+                    scrollDirection = 0
+                }
+            }
+        })
+
+        binding.rvMessage.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+                val linearLayoutManager =
+                    binding.rvMessage.layoutManager as LinearLayoutManager? ?: return
+                val firstVisiblePosition = linearLayoutManager.findFirstVisibleItemPosition()
+                val lastVisiblePosition = linearLayoutManager.findLastVisibleItemPosition()
+                sendMsgReadReceipt(firstVisiblePosition, lastVisiblePosition)
+                notifyMessageDisplayed(firstVisiblePosition, lastVisiblePosition)
+            }
+
+            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+                    if (scrollDirection == -1) {
+                        if (!binding.rvMessage.canScrollVertically(-1)) {
+                            //加载
+                            //sessionAdapter.showLoading()
+                            loadMessages(TUIChatConstants.GET_MESSAGE_FORWARD)
+                        }
+                    } else if (scrollDirection == 1) {
+                        if (!binding.rvMessage.canScrollVertically(1)) {
+                            loadMessages(TUIChatConstants.GET_MESSAGE_BACKWARD)
+//                            displayBackToLastMessage(false)
+//                            displayBackToNewMessage(false, "", 0)
+                            sessionPresenter?.resetCurrentChatUnreadCount()
+                        }
+                    }
+                    scrollDirection = 0
+
+//                    if (binding.rvMessage.isDisplayJumpMessageLayout()) {
+//                        displayBackToLastMessage(true)
+//                    } else {
+//                        displayBackToLastMessage(false)
+//                    }
+                } else if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
+//                    hideBackToAtMessages()
+                }
+            }
+        })
         //sessionAdapter.register()
         //对方正在输入中
         ///sessionPresenter.setTypingListener
@@ -110,6 +179,25 @@ class SessionFragment : BaseFragment(R.layout.fragment_session) {
         )
     }
 
+
+    fun loadMessages(lastMessage: TUIMessageBean?, type: Int) {
+        sessionPresenter?.loadMessage(type, lastMessage)
+    }
+
+    fun loadMessages(type: Int) {
+        if (type == TUIChatConstants.GET_MESSAGE_FORWARD) {
+            loadMessages(
+                sessionAdapter.firstMessageBean,
+                type
+            )
+        } else if (type == TUIChatConstants.GET_MESSAGE_BACKWARD) {
+            loadMessages(
+                sessionAdapter.lastMessageBean,
+                type
+            )
+        }
+    }
+
     override fun initComponents() {
         super.initComponents()
         SessionBottomComp(
@@ -138,4 +226,107 @@ class SessionFragment : BaseFragment(R.layout.fragment_session) {
         sessionPresenter?.markMessageAsRead(chatInfo, false)
     }
 
+    private fun sendMsgReadReceipt(firstPosition: Int, lastPosition: Int) {
+        if (sessionPresenter == null) {
+            return
+        }
+
+        val tuiMessageBeans =
+            sessionAdapter.items.safeSubList(firstPosition, lastPosition) as List<TUIMessageBean>
+        sessionPresenter?.sendMessageReadReceipt(tuiMessageBeans, object : IUIKitCallback<Void?>() {
+            override fun onSuccess(data: Void?) {}
+
+            override fun onError(module: String?, errCode: Int, errMsg: String?) {
+//                if (errCode == TUIConstants.BuyingFeature.ERR_SDK_INTERFACE_NOT_SUPPORT) {
+//                    showNotSupportDialog()
+//                }
+            }
+        })
+    }
+
+    private fun notifyMessageDisplayed(firstPosition: Int, lastPosition: Int) {
+        // *******************************
+
+        // *******************************
+//        markCallingMsgRead(firstPosition, lastPosition)
+
+        // *******************************
+        // *******************************
+        if (sessionPresenter == null) {
+            return
+        }
+        for (bean in sessionAdapter.items.safeSubList(firstPosition, lastPosition)) {
+            val param: MutableMap<String?, Any?> = HashMap<String?, Any?>()
+            param.put(TUIConstants.TUIChat.MESSAGE_BEAN, bean)
+            TUICore.notifyEvent(
+                TUIConstants.TUIChat.EVENT_KEY_MESSAGE_EVENT,
+                TUIConstants.TUIChat.EVENT_SUB_KEY_DISPLAY_MESSAGE_BEAN,
+                param
+            )
+        }
+    }
+
+    private val onMessageItemClickListener = object : OnItemClickListener() {
+        override fun onMessageLongClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+            onMessageLongClicked(view, messageBean)
+        }
+
+        override fun onMessageClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+
+        override fun onUserIconClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+
+        override fun onUserIconLongClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+
+        override fun onReEditRevokeMessage(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+
+        override fun onRecallClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+
+        override fun onReplyMessageClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+
+        override fun onReplyDetailClick(messageBean: TUIMessageBean?) {
+            messageBean ?: return
+        }
+
+        override fun onSendFailBtnClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+
+        override fun onTextSelected(view: View?, position: Int, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+
+        override fun onMessageReadStatusClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+        }
+    }
+
+
+    private fun onMessageLongClicked(view: View, message: TUIMessageBean?) {
+        //展示长按弹窗
+        //chatView.getMessageLayout().showItemPopMenu(message, view)
+    }
+
 }

+ 83 - 6
module/im/src/main/java/com/adealink/weparty/im/session/adapter/SessionAdapter.kt

@@ -1,5 +1,6 @@
 package com.adealink.weparty.im.session.adapter
 
+import android.view.View
 import androidx.recyclerview.widget.RecyclerView
 import com.adealink.weparty.commonui.recycleview.adapter.ExtMultiTypeAdapter
 import com.adealink.weparty.im.session.adapter.data.UnSupportMessageBean
@@ -12,6 +13,7 @@ import com.adealink.weparty.im.session.widget.MessageRecyclerView
 import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean
 import com.tencent.qcloud.tuikit.timcommon.component.highlight.HighlightPresenter
 import com.tencent.qcloud.tuikit.timcommon.interfaces.ICommonMessageAdapter
+import com.tencent.qcloud.tuikit.timcommon.interfaces.OnItemClickListener
 import com.tencent.qcloud.tuikit.timcommon.interfaces.UserFaceUrlCache
 import com.tencent.qcloud.tuikit.timcommon.minimalistui.widget.message.MessageBaseHolder
 import com.tencent.qcloud.tuikit.tuichat.bean.message.ImageMessageBean
@@ -21,7 +23,9 @@ import com.tencent.qcloud.tuikit.tuichat.bean.message.TipsMessageBean
 import com.tencent.qcloud.tuikit.tuichat.interfaces.IMessageAdapter
 import com.tencent.qcloud.tuikit.tuichat.interfaces.IMessageRecyclerView
 
-class SessionAdapter : ExtMultiTypeAdapter(), IMessageAdapter, ICommonMessageAdapter {
+class SessionAdapter : ExtMultiTypeAdapter(),
+    IMessageAdapter,
+    ICommonMessageAdapter {
 
     companion object {
         private const val ITEM_POSITION_UNKNOWN: Int = -1
@@ -30,6 +34,79 @@ class SessionAdapter : ExtMultiTypeAdapter(), IMessageAdapter, ICommonMessageAda
     private var mLoading: Boolean = true
     private var mRecycleView: MessageRecyclerView? = null
 
+    private var onItemClickListener: OnItemClickListener? = null
+
+    fun setOnItemClickListener(listener: OnItemClickListener?) {
+        this.onItemClickListener = listener
+    }
+
+    private val onMessageItemClick = object : OnItemClickListener() {
+        override fun onMessageLongClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+            onItemClickListener?.onMessageLongClick(view, messageBean)
+        }
+
+        override fun onMessageClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+            onItemClickListener?.onMessageClick(view, messageBean)
+        }
+
+        override fun onUserIconClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+            onItemClickListener?.onUserIconClick(view, messageBean)
+        }
+
+        override fun onUserIconLongClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+            onItemClickListener?.onUserIconLongClick(view, messageBean)
+        }
+
+        override fun onReEditRevokeMessage(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+            onItemClickListener?.onReEditRevokeMessage(view, messageBean)
+        }
+
+        override fun onRecallClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+            onItemClickListener?.onRecallClick(view, messageBean)
+        }
+
+        override fun onReplyMessageClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+            onItemClickListener?.onReplyMessageClick(view, messageBean)
+        }
+
+        override fun onReplyDetailClick(messageBean: TUIMessageBean?) {
+            messageBean ?: return
+            onItemClickListener?.onReplyDetailClick(messageBean)
+        }
+
+        override fun onSendFailBtnClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+            onItemClickListener?.onSendFailBtnClick(view, messageBean)
+        }
+
+        override fun onTextSelected(view: View?, position: Int, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+            onItemClickListener?.onTextSelected(view, position, messageBean)
+        }
+
+        override fun onMessageReadStatusClick(view: View?, messageBean: TUIMessageBean?) {
+            view ?: return
+            messageBean ?: return
+            onItemClickListener?.onMessageReadStatusClick(view, messageBean)
+        }
+    }
+
     private val supportMessageTypes = setOf(
         TextMessageBean::class.java,
         ImageMessageBean::class.java,
@@ -39,11 +116,11 @@ class SessionAdapter : ExtMultiTypeAdapter(), IMessageAdapter, ICommonMessageAda
     )
 
     init {
-        register(TextMessageBean::class.java, TextMessageViewBinder())
-        register(ImageMessageBean::class.java, ImageMessageViewBinder())
-        register(SoundMessageBean::class.java, SoundMessageViewBinder())
-        register(TipsMessageBean::class.java, TipsMessageViewBinder())
-        register(UnSupportMessageBean::class.java, UnSupportMessageViewBinder())
+        register(TextMessageBean::class.java, TextMessageViewBinder(onMessageItemClick))
+        register(ImageMessageBean::class.java, ImageMessageViewBinder(onMessageItemClick))
+        register(SoundMessageBean::class.java, SoundMessageViewBinder(onMessageItemClick))
+        register(TipsMessageBean::class.java, TipsMessageViewBinder(onMessageItemClick))
+        register(UnSupportMessageBean::class.java, UnSupportMessageViewBinder(onMessageItemClick))
 
 
 //        addMessageType(FaceMessageBean::class.java, FaceMessageHolder::class.java)

+ 73 - 33
module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/BaseMessageViewBinder.kt

@@ -3,45 +3,62 @@ package com.adealink.weparty.im.session.adapter.viewbinder
 import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.ViewGroup
+import androidx.annotation.CallSuper
 import androidx.viewbinding.ViewBinding
 import com.adealink.frame.aab.util.getCompatDimension
 import com.adealink.frame.util.onClick
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.show
 import com.adealink.weparty.commonui.recycleview.adapter.BindingViewHolder
 import com.adealink.weparty.commonui.recycleview.adapter.multitype.ItemViewBinder
 import com.adealink.weparty.im.R
 import com.adealink.weparty.im.databinding.LayoutSessionMessageBaseBinding
 import com.tencent.qcloud.tuikit.timcommon.bean.TUIMessageBean
+import com.tencent.qcloud.tuikit.timcommon.interfaces.OnItemClickListener
 
 
-open class MessageBinding<T : ViewBinding>(
-    binding: LayoutSessionMessageBaseBinding,
-    val messageBinding: T
-) : BindingViewHolder<LayoutSessionMessageBaseBinding>(binding)
-
 /**
  * 对照: ImageMessageHolder
  */
-abstract class BaseMessageViewBinder<T : TUIMessageBean, MB : ViewBinding>(
+abstract class BaseMessageViewBinder<T : TUIMessageBean, V: ViewBinding, MH : MessageViewHolder<T, V>>(
+    protected var onItemClickListener: OnItemClickListener?
+) : ItemViewBinder<T, MH>() {
 
-) : ItemViewBinder<T, MessageBinding<MB>>() {
+    override fun onBindViewHolder(holder: MH, item: T) {
+        initView(holder, item)
+        setTimeTitle(holder, item)
+        if (item.isSelf) {
+            applySelfStyle(holder, item)
+            holder.onBindSelfMessage(holder.messageBinding, item)
+        } else {
+            applyOtherStyle(holder, item)
+            holder.onBindOtherMessage(holder.messageBinding, item)
+        }
+    }
 
-    override fun onBindViewHolder(
-        holder: MessageBinding<MB>,
-        item: T
+    @CallSuper
+    open fun initView(
+        holder: MH, msg: T
     ) {
         holder.binding.root.onClick {
-
+            onItemClickListener?.onMessageLongClick(holder.binding.root, msg)
+            true
         }
-        if (item.isSelf) {
-            applySelfStyle(holder)
-            onBindSelfMessage(holder.messageBinding, item)
+        if (msg.status == TUIMessageBean.MSG_STATUS_SEND_FAIL) {
+            //消息发送失败触发长按
+            holder.binding.root.onClick {
+                onItemClickListener?.onMessageLongClick(holder.binding.root, msg)
+                true
+            }
         } else {
-            applyOtherStyle(holder)
-            onBindOtherMessage(holder.messageBinding, item)
+            holder.binding.root.onClick {
+                onItemClickListener?.onMessageClick(holder.binding.root, msg)
+                true
+            }
         }
     }
 
-    private fun applySelfStyle(holder: MessageBinding<MB>) {
+    private fun applySelfStyle(holder: MH, msg: T) {
         holder.binding.messageContent.gravity = Gravity.END
         holder.messageBinding.root.setBackgroundResource(R.drawable.im_message_bubble_self_bg)
         holder.messageBinding.root.setPaddingRelative(
@@ -50,9 +67,13 @@ abstract class BaseMessageViewBinder<T : TUIMessageBean, MB : ViewBinding>(
             getCompatDimension(R.dimen.im_message_bubble_self_padding_end).toInt(),
             getCompatDimension(R.dimen.im_message_bubble_self_padding_bottom).toInt(),
         )
+
+        //--------通用消息状态
+        setStatusImage(holder, msg)
+
     }
 
-    private fun applyOtherStyle(holder: MessageBinding<MB>) {
+    private fun applyOtherStyle(holder: MH, msg: T) {
         holder.binding.messageContent.gravity = Gravity.START
         holder.messageBinding.root.setBackgroundResource(R.drawable.im_message_bubble_other_bg)
         holder.messageBinding.root.setPaddingRelative(
@@ -61,31 +82,50 @@ abstract class BaseMessageViewBinder<T : TUIMessageBean, MB : ViewBinding>(
             getCompatDimension(R.dimen.im_message_bubble_other_padding_end).toInt(),
             getCompatDimension(R.dimen.im_message_bubble_other_padding_bottom).toInt(),
         )
+
+        //--------通用消息状态
+        holder.binding.ivMessageStatus.gone()
     }
 
-    abstract fun onBindSelfMessage(binding: MB, item: T)
 
-    abstract fun onBindOtherMessage(binding: MB, item: T)
+    protected fun setStatusImage(holder: MH, msg: T) {
+        if (msg.hasRiskContent()) {
+            holder.binding.ivMessageStatus.show()
+        } else if (msg.status == TUIMessageBean.MSG_STATUS_SEND_FAIL) {
+            holder.binding.ivMessageStatus.show()
+            holder.binding.ivMessageStatus.onClick {
+                onItemClickListener?.onSendFailBtnClick(holder.binding.ivMessageStatus, msg)
+            }
+        } else {
+            holder.binding.ivMessageStatus.gone()
+        }
+    }
 
-    private fun setTimeTitle(
-        holder: BindingViewHolder<LayoutSessionMessageBaseBinding>,
-        item: T
-    ) {
+
+    private fun setTimeTitle(holder: MH, msg: T) {
 
     }
 
-    override fun onCreateViewHolder(
-        inflater: LayoutInflater,
-        parent: ViewGroup
-    ): MessageBinding<MB> {
-        val binding = LayoutSessionMessageBaseBinding.inflate(inflater, parent, false)
-        val messageBinding = onCreateMessageContent(inflater, binding.messageContent)
-        return MessageBinding(binding, messageBinding)
+    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): MH {
+        val rootBinding = LayoutSessionMessageBaseBinding.inflate(inflater, parent, false)
+        return onCreateMessageHolder(rootBinding, inflater, rootBinding.messageContent)
     }
 
-    abstract fun onCreateMessageContent(
+    abstract fun onCreateMessageHolder(
+        rootBinder: LayoutSessionMessageBaseBinding,
         inflater: LayoutInflater,
         parent: ViewGroup
-    ): MB
+    ): MH
+
+}
+
+abstract class MessageViewHolder<T : TUIMessageBean, V : ViewBinding>(
+    rootBinding: LayoutSessionMessageBaseBinding,
+    val messageBinding: V
+) : BindingViewHolder<LayoutSessionMessageBaseBinding>(rootBinding) {
+
+    abstract fun onBindSelfMessage(binding: V, msg: T)
+
+    abstract fun onBindOtherMessage(binding: V, msg: T)
 
 }

+ 38 - 13
module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/ImageMessageViewBinder.kt

@@ -2,38 +2,49 @@ package com.adealink.weparty.im.session.adapter.viewbinder
 
 import android.view.LayoutInflater
 import android.view.ViewGroup
+import com.adealink.weparty.im.databinding.LayoutSessionMessageBaseBinding
 import com.adealink.weparty.im.databinding.LayoutSessionMessageImageBinding
 import com.adealink.weparty.util.formatTimeTo
+import com.tencent.imsdk.v2.V2TIMImageElem
+import com.tencent.qcloud.tuikit.timcommon.interfaces.OnItemClickListener
 import com.tencent.qcloud.tuikit.tuichat.bean.message.ImageMessageBean
 import com.tencent.qcloud.tuikit.tuichat.presenter.ChatFileDownloadPresenter
 
 /**
  * 对照: ImageMessageHolder
  */
-class ImageMessageViewBinder() :
-    BaseMessageViewBinder<ImageMessageBean, LayoutSessionMessageImageBinding>() {
+
+class ImageMessageViewHolder(
+    rootBinding: LayoutSessionMessageBaseBinding,
+    binging: LayoutSessionMessageImageBinding
+) : MessageViewHolder<ImageMessageBean, LayoutSessionMessageImageBinding>(
+    rootBinding,
+    binging
+) {
+
     override fun onBindSelfMessage(
         binding: LayoutSessionMessageImageBinding,
-        item: ImageMessageBean
+        msg: ImageMessageBean
     ) {
-        setImage(binding, item)
-        setMessageTime(binding, item)
+        setImage(binding, msg)
+        setMessageTime(binding, msg)
     }
 
     override fun onBindOtherMessage(
         binding: LayoutSessionMessageImageBinding,
-        item: ImageMessageBean
+        msg: ImageMessageBean
     ) {
-        setImage(binding, item)
-        setMessageTime(binding, item)
+        setImage(binding, msg)
+        setMessageTime(binding, msg)
     }
 
     private fun setImage(
         binding: LayoutSessionMessageImageBinding,
         item: ImageMessageBean
     ) {
-        val imagePath = ChatFileDownloadPresenter.getImagePath(item)
-        binding.ivImg.setImageUrl(imagePath)
+        val imagePath =
+            ChatFileDownloadPresenter.getImagePath(item, V2TIMImageElem.V2TIM_IMAGE_TYPE_ORIGIN)
+        binding.ivImg.setImageURI(imagePath)
     }
 
     private fun setMessageTime(
@@ -42,11 +53,25 @@ class ImageMessageViewBinder() :
     ) {
         binding.vText.tvText.text = formatTimeTo((item.messageTime * 1000), "HH:mm")
     }
+}
+
+class ImageMessageViewBinder(
+    onItemClickListener: OnItemClickListener
+) : BaseMessageViewBinder<ImageMessageBean, LayoutSessionMessageImageBinding, ImageMessageViewHolder>(onItemClickListener) {
+
 
-    override fun onCreateMessageContent(
+    override fun onCreateMessageHolder(
+        rootBinder: LayoutSessionMessageBaseBinding,
         inflater: LayoutInflater,
         parent: ViewGroup
-    ): LayoutSessionMessageImageBinding {
-        return LayoutSessionMessageImageBinding.inflate(inflater, parent, true)
+    ): ImageMessageViewHolder {
+        return ImageMessageViewHolder(
+            rootBinder,
+            LayoutSessionMessageImageBinding.inflate(
+                inflater,
+                parent,
+                true
+            )
+        )
     }
 }

+ 253 - 26
module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/SoundMessageViewBinder.kt

@@ -1,69 +1,296 @@
 package com.adealink.weparty.im.session.adapter.viewbinder
 
+import android.os.Build
 import android.view.LayoutInflater
 import android.view.ViewGroup
+import com.adealink.frame.aab.util.getCompatColor
+import com.adealink.frame.log.Log
 import com.adealink.frame.util.onClick
+import com.adealink.frame.util.runOnUiThread
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.show
+import com.adealink.weparty.commonui.util.formatSecondsTo00
+import com.adealink.weparty.im.R
+import com.adealink.weparty.im.databinding.LayoutSessionMessageBaseBinding
 import com.adealink.weparty.im.databinding.LayoutSessionMessageSoundBinding
+import com.adealink.weparty.module.im.data.TAG_IM_SESSION_SOUND
 import com.adealink.weparty.util.formatTimeTo
+import com.adealink.weparty.util.isSupportSpeedPlay
+import com.tencent.qcloud.tuicore.interfaces.TUIValueCallback
+import com.tencent.qcloud.tuicore.util.ToastUtil
+import com.tencent.qcloud.tuikit.timcommon.interfaces.OnItemClickListener
+import com.tencent.qcloud.tuikit.timcommon.util.DateTimeUtil
+import com.tencent.qcloud.tuikit.timcommon.util.FileUtil
+import com.tencent.qcloud.tuikit.tuichat.TUIChatService
 import com.tencent.qcloud.tuikit.tuichat.bean.message.SoundMessageBean
+import com.tencent.qcloud.tuikit.tuichat.component.audio.AudioPlayer
+import com.tencent.qcloud.tuikit.tuichat.presenter.ChatFileDownloadPresenter
 import java.util.Timer
+import java.util.TimerTask
+import kotlin.math.max
+import kotlin.math.min
 
 /**
  * 对照: SoundMessageHolder
  */
-class SoundMessageViewBinder() :
-    BaseMessageViewBinder<SoundMessageBean, LayoutSessionMessageSoundBinding>() {
+
+class SoundMessageViewHolder(
+    rootBinding: LayoutSessionMessageBaseBinding,
+    binging: LayoutSessionMessageSoundBinding
+) : MessageViewHolder<SoundMessageBean, LayoutSessionMessageSoundBinding>(rootBinding, binging) {
+
+    companion object {
+        val PLAY_SPEED = arrayOf(1f, 2f, 3f)
+    }
 
     private var mTimer: Timer? = null
+    private var times = 0
+    private var playSpeedIndex = 0
+
+    private var downloadSoundCallback: TUIValueCallback<*>? = null
 
     override fun onBindSelfMessage(
         binding: LayoutSessionMessageSoundBinding,
-        item: SoundMessageBean
+        msg: SoundMessageBean
     ) {
-        setSound(binding, item)
-        setMessageTime(binding, item)
+        initView(binding, msg)
+        setMessageTime(binding, msg)
+        //倍数播放
+        binding.btnOtherSpeed.gone()
+        if (isSupportSpeedPlay()) {
+            binding.btnSelfSpeed.show()
+            binding.btnSelfSpeed.onClick {
+                changeClaySpeed(binding, msg)
+            }
+        } else {
+            binding.btnSelfSpeed.gone()
+        }
+        //播放进度颜色
+        binding.vPlayView.setColor(
+            lineColor = getCompatColor(R.color.im_sound_play_self_line_color),
+            lineHintColor = getCompatColor(R.color.im_sound_play_self_line_hint_color),
+            cursorColor = getCompatColor(R.color.im_sound_play_self_cursor_color)
+        )
     }
 
     override fun onBindOtherMessage(
         binding: LayoutSessionMessageSoundBinding,
-        item: SoundMessageBean
+        msg: SoundMessageBean
     ) {
-        setSound(binding, item)
-        setMessageTime(binding, item)
+        initView(binding, msg)
+        setMessageTime(binding, msg)
+        //倍数播放
+        binding.btnSelfSpeed.gone()
+        if (isSupportSpeedPlay()) {
+            binding.btnOtherSpeed.show()
+            binding.btnOtherSpeed.onClick {
+                changeClaySpeed(binding, msg)
+            }
+        } else {
+            binding.btnOtherSpeed.gone()
+        }
+        //播放进度颜色
+        binding.vPlayView.setColor(
+            lineColor = getCompatColor(R.color.im_sound_play_other_line_color),
+            lineHintColor = getCompatColor(R.color.im_sound_play_other_line_hint_color),
+            cursorColor = if (msg.isUnread) {
+                getCompatColor(R.color.im_sound_play_other_cursor_color)
+            } else {
+                getCompatColor(R.color.im_sound_play_other_cursor_read_color)
+            }
+        )
     }
 
-    private fun setSound(
+    private fun initView(
         binding: LayoutSessionMessageSoundBinding,
-        message: SoundMessageBean
+        msg: SoundMessageBean
     ) {
-        val duration = message.getDuration()
-        binding.tvDuration.text = duration.toString()
+        val durationStr = formatSecondsTo00(max(msg.getDuration(), 1)) //音频时间最小展示1s
+        resetTimerStatus(binding, durationStr)
+        binding.root.onClick {
+            onSoundClick(binding, msg, msg.getDuration(), durationStr)
+        }
     }
 
-    private fun setMessageTime(
+    private fun resetTimerStatus(
         binding: LayoutSessionMessageSoundBinding,
-        item: SoundMessageBean
+        timeString: String?
     ) {
-        binding.tvTime.text = formatTimeTo((item.messageTime * 1000), "HH:mm")
+        mTimer?.cancel()
+        mTimer = null
+        times = 0
+        playSpeedIndex = 0
+        binding.tvSelfSpeed.text = "1"
+        binding.tvOtherSpeed.text = "1"
+        binding.vPlayView.stop()
+        binding.tvDuration.text = timeString
     }
 
-    override fun onCreateMessageContent(
-        inflater: LayoutInflater,
-        parent: ViewGroup
-    ): LayoutSessionMessageSoundBinding {
-        return LayoutSessionMessageSoundBinding.inflate(inflater, parent, true).apply {
-            initView(this)
+    private fun isPlaying(message: SoundMessageBean): Boolean {
+        val soundPath = ChatFileDownloadPresenter.getSoundPath(message)
+        return AudioPlayer.getInstance().isPlaying()
+                && AudioPlayer.getInstance().path == soundPath
+    }
+
+    private fun onSoundClick(
+        binding: LayoutSessionMessageSoundBinding,
+        message: SoundMessageBean,
+        soundSecond: Int,
+        soundDurationStr: String?
+    ) {
+        val soundPath = ChatFileDownloadPresenter.getSoundPath(message)
+        if (AudioPlayer.getInstance().isPlaying()) {
+            AudioPlayer.getInstance().stopPlay()
+            resetTimerStatus(binding, soundDurationStr)
+            if (AudioPlayer.getInstance().path == soundPath) {
+                //播放同一个音频
+                return
+            }
+        }
+        if (!FileUtil.isFileExists(soundPath)) {
+            ToastUtil.toastShortMessage(
+                TUIChatService.getAppContext()
+                    .getString(R.string.im_sound_play_tip_for_voice_not_download)
+            )
+            getSound(message)
+            return
+        }
+
+        if (soundSecond > 1) {
+            //音频时间大于1s才启动定时器
+            if (mTimer == null) {
+                mTimer = Timer()
+            }
+            mTimer?.schedule(object : TimerTask() {
+                override fun run() {
+                    runOnUiThread {
+                        if (times < soundSecond) {
+                            times = min(times + PLAY_SPEED[playSpeedIndex].toInt(), soundSecond)
+                            val s = DateTimeUtil.formatSecondsTo00(times)
+                            binding.tvDuration.text = s
+                        } else {
+                            binding.tvDuration.text = soundDurationStr
+                        }
+                    }
+                }
+            }, 0, 1000)
+        }
+
+        val soundPlayButton = binding.btnPlay
+        soundPlayButton.setImageResource(R.drawable.im_session_sound_stop_ic)
+        message.setPlayed()
+        AudioPlayer.getInstance().startPlay(soundPath, object : AudioPlayer.Callback {
+
+            override fun onCompletion(success: Boolean?) {
+                Log.d(
+                    TAG_IM_SESSION_SOUND,
+                    "onSoundClick, onCompletion, message:$message"
+                )
+                soundPlayButton.post {
+                    soundPlayButton.setImageResource(R.drawable.im_session_sound_play_ic)
+                }
+                resetTimerStatus(binding, soundDurationStr)
+            }
+        })
+        startPlayAnimation(
+            binding,
+            soundSecond * 1000L,
+            0,
+            PLAY_SPEED[playSpeedIndex]
+        )
+    }
+
+    private fun startPlayAnimation(
+        binding: LayoutSessionMessageSoundBinding,
+        allDuration: Long, //所有时长(毫秒)
+        duration: Long, //剩余时长(毫秒)
+        speed: Float, //倍速
+    ) {
+        val soundPlayView = binding.vPlayView
+        soundPlayView.start(allDuration, duration, speed)
+    }
+
+
+    private fun getSound(messageBean: SoundMessageBean?) {
+        downloadSoundCallback = object : TUIValueCallback<Any?>() {
+            override fun onProgress(currentSize: Long, totalSize: Long) {
+                Log.d(
+                    TAG_IM_SESSION_SOUND,
+                    "downloadSound progress current: $currentSize, total:$totalSize"
+                )
+            }
+
+            override fun onError(code: Int, desc: String?) {
+                Log.e(TAG_IM_SESSION_SOUND, "getSoundToFile failed code = $code, info = $desc")
+                //ToastUtil.toastLongMessage("getSoundToFile failed code = " + code + ", info = " + desc)
+            }
+
+            override fun onSuccess(obj: Any?) {
+                Log.d(TAG_IM_SESSION_SOUND, "get sound success")
+            }
         }
+        ChatFileDownloadPresenter.downloadSound(messageBean, downloadSoundCallback)
     }
 
-    private fun initView(binding: LayoutSessionMessageSoundBinding) {
-        binding.btnPlay.onClick {
-            binding.vPlayView.start(5_000)
+    private fun changeClaySpeed(
+        binding: LayoutSessionMessageSoundBinding,
+        message: SoundMessageBean
+    ) {
+        if (!isPlaying(message)) {
+            return
         }
+        playSpeedIndex = (playSpeedIndex + 1) % PLAY_SPEED.size
+        val nextSpeed = PLAY_SPEED[playSpeedIndex]
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            AudioPlayer.getInstance().setSpeed(nextSpeed)
+            if (message.isSelf) {
+                binding.tvSelfSpeed.text = nextSpeed.toInt().toString()
+            } else {
+                binding.tvOtherSpeed.text = nextSpeed.toInt().toString()
+            }
+            Log.d(TAG_IM_SESSION_SOUND, "changeClaySpeed, speed: $nextSpeed")
+            Log.d(
+                TAG_IM_SESSION_SOUND,
+                "                      : allTime: ${message.duration * 1000}, currentTim:${AudioPlayer.getInstance().playPosition.toLong()}"
+            )
+
+            startPlayAnimation(
+                binding,
+                message.duration * 1000L,
+                AudioPlayer.getInstance().playPosition.toLong(),
+                nextSpeed
+            )
+        }
+    }
+
+    private fun setMessageTime(
+        binding: LayoutSessionMessageSoundBinding,
+        item: SoundMessageBean
+    ) {
+        binding.tvTime.text = formatTimeTo((item.messageTime * 1000), "HH:mm")
     }
 
-    override fun onViewRecycled(holder: MessageBinding<LayoutSessionMessageSoundBinding>) {
-        super.onViewRecycled(holder)
+}
+
+class SoundMessageViewBinder(
+    onItemClickListener: OnItemClickListener
+) : BaseMessageViewBinder<SoundMessageBean, LayoutSessionMessageSoundBinding, SoundMessageViewHolder>(
+    onItemClickListener
+) {
+
+    override fun onCreateMessageHolder(
+        rootBinder: LayoutSessionMessageBaseBinding,
+        inflater: LayoutInflater,
+        parent: ViewGroup
+    ): SoundMessageViewHolder {
+        return SoundMessageViewHolder(
+            rootBinder,
+            LayoutSessionMessageSoundBinding.inflate(
+                inflater,
+                parent,
+                true
+            )
+        )
     }
 
 }

+ 38 - 18
module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/TextMessageViewBinder.kt

@@ -4,47 +4,51 @@ import android.view.LayoutInflater
 import android.view.ViewGroup
 import com.adealink.frame.aab.util.getCompatString
 import com.adealink.weparty.im.R
+import com.adealink.weparty.im.databinding.LayoutSessionMessageBaseBinding
 import com.adealink.weparty.im.databinding.LayoutSessionMessageTextBinding
 import com.adealink.weparty.util.formatTimeTo
 import com.tencent.qcloud.tuikit.timcommon.component.face.FaceManager
+import com.tencent.qcloud.tuikit.timcommon.interfaces.OnItemClickListener
 import com.tencent.qcloud.tuikit.timcommon.util.TextUtil
 import com.tencent.qcloud.tuikit.tuichat.bean.message.TextMessageBean
 
 /**
  * 对照: TextMessageHolder
  */
-class TextMessageViewBinder() :
-    BaseMessageViewBinder<TextMessageBean, LayoutSessionMessageTextBinding>() {
+class TextMessageViewHolder(
+    rootBinding: LayoutSessionMessageBaseBinding,
+    binding: LayoutSessionMessageTextBinding
+) : MessageViewHolder<TextMessageBean, LayoutSessionMessageTextBinding>(rootBinding, binding) {
     override fun onBindSelfMessage(
         binding: LayoutSessionMessageTextBinding,
-        item: TextMessageBean
+        msg: TextMessageBean
     ) {
-        setText(binding, item)
-        setMessageTime(binding, item)
+        setText(binding, msg)
+        setMessageTime(binding, msg)
     }
 
     override fun onBindOtherMessage(
         binding: LayoutSessionMessageTextBinding,
-        item: TextMessageBean
+        msg: TextMessageBean
     ) {
-        setText(binding, item)
-        setMessageTime(binding, item)
+        setText(binding, msg)
+        setMessageTime(binding, msg)
     }
 
     private fun setText(
         binding: LayoutSessionMessageTextBinding,
-        item: TextMessageBean
+        msg: TextMessageBean
     ) {
-        if (item.text != null) {
+        if (msg.text != null) {
             FaceManager.handlerEmojiText(
                 binding.tvText,
-                item.text,
+                msg.text,
                 false
             )
-        } else if (!item.extra.isNullOrEmpty()) {
+        } else if (!msg.extra.isNullOrEmpty()) {
             FaceManager.handlerEmojiText(
                 binding.tvText,
-                item.extra,
+                msg.extra,
                 false
             )
         } else {
@@ -59,16 +63,32 @@ class TextMessageViewBinder() :
 
     private fun setMessageTime(
         binding: LayoutSessionMessageTextBinding,
-        item: TextMessageBean
+        msg: TextMessageBean
     ) {
-        binding.tvTime.text = formatTimeTo((item.messageTime * 1000), "HH:mm")
+        binding.tvTime.text = formatTimeTo((msg.messageTime * 1000), "HH:mm")
     }
 
-    override fun onCreateMessageContent(
+}
+
+class TextMessageViewBinder(
+    onItemClickListener: OnItemClickListener
+) : BaseMessageViewBinder<TextMessageBean, LayoutSessionMessageTextBinding, TextMessageViewHolder>(
+    onItemClickListener
+) {
+
+    override fun onCreateMessageHolder(
+        rootBinder: LayoutSessionMessageBaseBinding,
         inflater: LayoutInflater,
         parent: ViewGroup
-    ): LayoutSessionMessageTextBinding {
-        return LayoutSessionMessageTextBinding.inflate(inflater, parent, true)
+    ): TextMessageViewHolder {
+        return TextMessageViewHolder(
+            rootBinder,
+            LayoutSessionMessageTextBinding.inflate(
+                inflater,
+                parent,
+                true
+            )
+        )
     }
 
 }

+ 32 - 11
module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/TipsMessageViewBinder.kt

@@ -5,32 +5,37 @@ import android.text.Spanned
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import com.adealink.weparty.im.databinding.LayoutSessionMessageBaseBinding
 import com.adealink.weparty.im.databinding.LayoutSessionMessageTipsBinding
 import com.adealink.weparty.util.formatTimeTo
 import com.tencent.qcloud.tuikit.timcommon.R
 import com.tencent.qcloud.tuikit.timcommon.TIMCommonService
+import com.tencent.qcloud.tuikit.timcommon.interfaces.OnItemClickListener
 import com.tencent.qcloud.tuikit.timcommon.util.TextUtil.ForegroundColorClickableSpan
 import com.tencent.qcloud.tuikit.tuichat.bean.message.TipsMessageBean
 
 /**
  * 对照: TipsMessageHolder
  */
-class TipsMessageViewBinder() :
-    BaseMessageViewBinder<TipsMessageBean, LayoutSessionMessageTipsBinding>() {
+
+class TipsMessageViewHolder(
+    rootBinding: LayoutSessionMessageBaseBinding,
+    binding: LayoutSessionMessageTipsBinding
+) : MessageViewHolder<TipsMessageBean, LayoutSessionMessageTipsBinding>(rootBinding, binding) {
     override fun onBindSelfMessage(
         binding: LayoutSessionMessageTipsBinding,
-        item: TipsMessageBean
+        msg: TipsMessageBean
     ) {
-        setTips(binding, item)
-        setMessageTime(binding, item)
+        setTips(binding, msg)
+        setMessageTime(binding, msg)
     }
 
     override fun onBindOtherMessage(
         binding: LayoutSessionMessageTipsBinding,
-        item: TipsMessageBean
+        msg: TipsMessageBean
     ) {
-        setTips(binding, item)
-        setMessageTime(binding, item)
+        setTips(binding, msg)
+        setMessageTime(binding, msg)
     }
 
     private fun setTips(
@@ -81,11 +86,27 @@ class TipsMessageViewBinder() :
         binding.tvTime.text = formatTimeTo((item.messageTime * 1000), "HH:mm")
     }
 
-    override fun onCreateMessageContent(
+}
+
+class TipsMessageViewBinder(
+    onItemClickListener: OnItemClickListener
+) : BaseMessageViewBinder<TipsMessageBean, LayoutSessionMessageTipsBinding, TipsMessageViewHolder>(
+    onItemClickListener
+) {
+
+    override fun onCreateMessageHolder(
+        rootBinder: LayoutSessionMessageBaseBinding,
         inflater: LayoutInflater,
         parent: ViewGroup
-    ): LayoutSessionMessageTipsBinding {
-        return LayoutSessionMessageTipsBinding.inflate(inflater, parent, true)
+    ): TipsMessageViewHolder {
+        return TipsMessageViewHolder(
+            rootBinder,
+            LayoutSessionMessageTipsBinding.inflate(
+                inflater,
+                parent,
+                true
+            )
+        )
     }
 
 }

+ 34 - 11
module/im/src/main/java/com/adealink/weparty/im/session/adapter/viewbinder/UnSupportMessageViewBinder.kt

@@ -2,42 +2,65 @@ package com.adealink.weparty.im.session.adapter.viewbinder
 
 import android.view.LayoutInflater
 import android.view.ViewGroup
+import com.adealink.weparty.im.databinding.LayoutSessionMessageBaseBinding
 import com.adealink.weparty.im.databinding.LayoutSessionMessageUnsupportBinding
 import com.adealink.weparty.im.session.adapter.data.UnSupportMessageBean
 import com.adealink.weparty.util.formatTimeTo
+import com.tencent.qcloud.tuikit.timcommon.interfaces.OnItemClickListener
 
 /**
  * 对照: UnSupportMessageBean
  */
-class UnSupportMessageViewBinder() :
-    BaseMessageViewBinder<UnSupportMessageBean, LayoutSessionMessageUnsupportBinding>() {
+
+class UnSupportMessageViewHolder(
+    rootBinding: LayoutSessionMessageBaseBinding,
+    binding: LayoutSessionMessageUnsupportBinding
+) : MessageViewHolder<UnSupportMessageBean, LayoutSessionMessageUnsupportBinding>(
+    rootBinding,
+    binding
+) {
     override fun onBindSelfMessage(
         binding: LayoutSessionMessageUnsupportBinding,
-        item: UnSupportMessageBean
+        msg: UnSupportMessageBean
     ) {
-        setMessageTime(binding, item)
+        setMessageTime(binding, msg)
     }
 
     override fun onBindOtherMessage(
         binding: LayoutSessionMessageUnsupportBinding,
-        item: UnSupportMessageBean
+        msg: UnSupportMessageBean
     ) {
-        setMessageTime(binding, item)
+        setMessageTime(binding, msg)
     }
 
     private fun setMessageTime(
         binding: LayoutSessionMessageUnsupportBinding,
-        item: UnSupportMessageBean
+        msg: UnSupportMessageBean
     ) {
-        binding.tvTime.text = formatTimeTo((item.data.messageTime * 1000), "HH:mm")
+        binding.tvTime.text = formatTimeTo((msg.data.messageTime * 1000), "HH:mm")
     }
+}
 
 
-    override fun onCreateMessageContent(
+class UnSupportMessageViewBinder(
+    onItemClickListener: OnItemClickListener
+) : BaseMessageViewBinder<UnSupportMessageBean, LayoutSessionMessageUnsupportBinding, UnSupportMessageViewHolder>(
+    onItemClickListener
+) {
+    override fun onCreateMessageHolder(
+        rootBinder: LayoutSessionMessageBaseBinding,
         inflater: LayoutInflater,
         parent: ViewGroup
-    ): LayoutSessionMessageUnsupportBinding {
-        return LayoutSessionMessageUnsupportBinding.inflate(inflater, parent, true)
+    ): UnSupportMessageViewHolder {
+        return UnSupportMessageViewHolder(
+            rootBinder,
+            LayoutSessionMessageUnsupportBinding.inflate(
+                inflater,
+                parent,
+                true
+            )
+        )
     }
 
+
 }

+ 4 - 0
module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionBottomAudioComp.kt

@@ -20,6 +20,7 @@ import com.tencent.qcloud.tuicore.TUIConstants
 import com.tencent.qcloud.tuicore.util.ToastUtil
 import com.tencent.qcloud.tuikit.tuichat.TUIChatService
 import com.tencent.qcloud.tuikit.tuichat.bean.ChatInfo
+import com.tencent.qcloud.tuikit.tuichat.component.audio.AudioPlayer
 import com.tencent.qcloud.tuikit.tuichat.component.audio.AudioRecorder
 import com.tencent.qcloud.tuikit.tuichat.component.audio.AudioRecorder.AudioRecorderCallback
 import com.tencent.qcloud.tuikit.tuichat.presenter.ChatPresenter
@@ -125,6 +126,9 @@ class SessionBottomAudioComp(
     }
 
     private fun startAudioRecord() {
+        //录音前关闭音频播放
+        AudioPlayer.getInstance().stopPlay()
+
         Log.d(TAG_IM_AUDIO, "startAudioRecord")
         AudioRecorder.startRecord(object : AudioRecorderCallback {
             override fun onStarted() {

+ 1 - 1
module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionBottomInputComp.kt

@@ -54,7 +54,7 @@ class SessionBottomInputComp(
     private val emotionFragment by fastLazy { FaceFragment() }
     private val sessionInputViewModel by activityViewModels<SessionInputViewModel>()
 
-    private var isTextSendEnable = true
+    private var isTextSendEnable = false
     override fun onCreate() {
         super.onCreate()
         initView()

+ 43 - 3
module/im/src/main/java/com/adealink/weparty/im/session/widget/MessageSoundPlayView.kt

@@ -8,6 +8,7 @@ import android.os.Parcelable
 import android.util.AttributeSet
 import android.view.View
 import androidx.core.graphics.toColorInt
+import com.adealink.frame.log.Log
 import com.adealink.weparty.im.R
 
 class MessageSoundPlayView @JvmOverloads constructor(
@@ -21,8 +22,8 @@ class MessageSoundPlayView @JvmOverloads constructor(
             0, 0, 0
         )
         private val WAVE_REPEAT_STYLE = listOf(
-            //0, 20, 30, 60, 100, 60, 100, 80, 60, 30, 20, 0
-            100, 100, 100, 100, 100, 100, 100, 80, 100, 100, 100, 100
+            0, 20, 30, 60, 100, 60, 100, 80, 60, 30, 20, 0
+            //100, 100, 100, 100, 100, 100, 100, 80, 100, 100, 100, 100 //测试用
         )
     }
 
@@ -115,6 +116,13 @@ class MessageSoundPlayView @JvmOverloads constructor(
         }
     }
 
+    fun setColor(lineColor: Int, lineHintColor: Int, cursorColor: Int) {
+        this.lineColor = lineColor
+        this.lineHintColor = lineHintColor
+        this.cursorColor = cursorColor
+        postInvalidate()
+    }
+
 
     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
         super.onLayout(changed, left, top, right, bottom)
@@ -168,6 +176,37 @@ class MessageSoundPlayView @JvmOverloads constructor(
         playAnimator.start()
     }
 
+    /**
+     * @param allTime 总时长
+     * @param duration 剩余时长
+     * @param speed 播放速度
+     */
+    fun start(allTime: Long, duration: Long, speed: Float) {
+        if (isStart) {
+            playAnimator.cancel()
+            playAnimator = null
+        }
+        isStart = true
+        playProgress = duration * 1f / allTime
+        playAnimator = ValueAnimator.ofFloat(playProgress, 1f)
+        playAnimator.duration = ((allTime - duration) / speed).toLong()
+
+        Log.d(
+            "zhangfei",
+            "start() called with: allTime = $allTime, duration = $duration, speed = $speed"
+        )
+        Log.d(
+            "zhangfei",
+            "playProgress: $playProgress, duration:${((allTime - duration) / speed).toLong()}"
+        )
+
+        playAnimator.addUpdateListener {
+            playProgress = it.getAnimatedValue() as Float
+            invalidate()
+        }
+        playAnimator.start()
+    }
+
     override fun onDraw(canvas: Canvas) {
         super.onDraw(canvas)
         drawLine(canvas)
@@ -245,7 +284,7 @@ class MessageSoundPlayView @JvmOverloads constructor(
         return offsetX + cursorWidth / 2f
     }
 
-    private fun cursorEndX(): Float{
+    private fun cursorEndX(): Float {
         val offsetX = (measuredWidth - cursorWidth) * playProgress
         return offsetX + cursorWidth
     }
@@ -258,6 +297,7 @@ class MessageSoundPlayView @JvmOverloads constructor(
         playProgress = 0f
         playAnimator.cancel()
         waveList.clear()
+        postInvalidate()
     }
 
     override fun onSaveInstanceState(): Parcelable? {

BIN
module/im/src/main/res/drawable-xhdpi/session_send_error_ic.png


+ 14 - 1
module/im/src/main/res/layout/layout_session_message_base.xml

@@ -25,6 +25,19 @@
         tools:text="11/20"
         tools:visibility="visible" />
 
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/iv_message_status"
+        android:layout_width="16dp"
+        android:layout_height="16dp"
+        android:layout_marginEnd="4dp"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="@id/message_content"
+        app:layout_constraintEnd_toStartOf="@id/message_content"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@id/message_content"
+        app:srcCompat="@drawable/session_send_error_ic"
+        tools:visibility="visible" />
+
     <androidx.appcompat.widget.LinearLayoutCompat
         android:id="@+id/message_content"
         android:layout_width="0dp"
@@ -33,7 +46,7 @@
         android:layout_marginTop="10dp"
         android:orientation="horizontal"
         app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintStart_toEndOf="@id/iv_message_status"
         app:layout_constraintTop_toBottomOf="@id/tv_time"
         app:layout_goneMarginTop="0dp" />
 

+ 11 - 0
module/im/src/main/res/values/colors.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="im_sound_play_self_line_color">#86909C</color>
+    <color name="im_sound_play_self_line_hint_color">#661D2129</color>
+    <color name="im_sound_play_self_cursor_color">#86909C</color>
+
+    <color name="im_sound_play_other_line_color">#86909C</color>
+    <color name="im_sound_play_other_line_hint_color">#661D2129</color>
+    <color name="im_sound_play_other_cursor_color">#15E5E2</color>
+    <color name="im_sound_play_other_cursor_read_color">#1789FF</color>
+</resources>

+ 1 - 0
module/im/src/main/res/values/strings.xml

@@ -10,4 +10,5 @@
     <string name="im_audio_say_time_short">Message too short</string>
     <string name="im_mic_is_being_used_cant_record">Microphone is being used by another function, unable to record.</string>
     <string name="im_record_audio_failed">Audio recording failed.</string>
+    <string name="im_sound_play_tip_for_voice_not_download">Voice file not downloaded.</string>
 </resources>