feat(chat-room-ui): ChatMessageAdapter 구현
This commit is contained in:
		@@ -0,0 +1,390 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * 보이스온 - 채팅방 메시지 어댑터 (4.2 ViewHolder 구현)
 | 
				
			||||||
 | 
					 * - 4가지 ViewType 지원
 | 
				
			||||||
 | 
					 * - ViewHolder 바인딩 로직 구현: 시간 포맷팅, 상태 표시, 이미지 로딩, 그룹화 처리, 타이핑 애니메이션
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					package kr.co.vividnext.sodalive.chat.talk.room
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.annotation.SuppressLint
 | 
				
			||||||
 | 
					import android.text.TextUtils
 | 
				
			||||||
 | 
					import android.view.LayoutInflater
 | 
				
			||||||
 | 
					import android.view.View
 | 
				
			||||||
 | 
					import android.view.ViewGroup
 | 
				
			||||||
 | 
					import android.view.animation.Animation
 | 
				
			||||||
 | 
					import android.view.animation.AnimationUtils
 | 
				
			||||||
 | 
					import android.widget.TextView
 | 
				
			||||||
 | 
					import androidx.annotation.LayoutRes
 | 
				
			||||||
 | 
					import androidx.core.view.isVisible
 | 
				
			||||||
 | 
					import androidx.recyclerview.widget.RecyclerView
 | 
				
			||||||
 | 
					import coil.load
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.R
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.databinding.ItemChatAiMessageBinding
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.databinding.ItemChatTypingIndicatorBinding
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.databinding.ItemChatUserMessageBinding
 | 
				
			||||||
 | 
					import java.text.SimpleDateFormat
 | 
				
			||||||
 | 
					import java.util.Date
 | 
				
			||||||
 | 
					import java.util.Locale
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 채팅 리스트 아이템 타입 정의 (UI 렌더링을 위한 래퍼)
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					sealed class ChatListItem {
 | 
				
			||||||
 | 
					    data class UserMessage(val data: ChatMessage) : ChatListItem()
 | 
				
			||||||
 | 
					    data class AiMessage(val data: ChatMessage, val displayName: String? = null) : ChatListItem()
 | 
				
			||||||
 | 
					    data class Notice(val text: String) : ChatListItem()
 | 
				
			||||||
 | 
					    object TypingIndicator : ChatListItem()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    companion object {
 | 
				
			||||||
 | 
					        const val VIEW_TYPE_USER_MESSAGE = 1
 | 
				
			||||||
 | 
					        const val VIEW_TYPE_AI_MESSAGE = 2
 | 
				
			||||||
 | 
					        const val VIEW_TYPE_NOTICE = 3
 | 
				
			||||||
 | 
					        const val VIEW_TYPE_TYPING_INDICATOR = 4
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val items: MutableList<ChatListItem> = mutableListOf()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Notice 접기 상태 (간단 전역 토글)
 | 
				
			||||||
 | 
					    private var isNoticeCollapsed: Boolean = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 타이핑 인디케이터 표시 상태
 | 
				
			||||||
 | 
					    private var isTypingVisible: Boolean = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun findTypingIndicatorIndex(): Int {
 | 
				
			||||||
 | 
					        return items.indexOfLast { it is ChatListItem.TypingIndicator }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 타이핑 인디케이터를 목록 하단에 표시한다.
 | 
				
			||||||
 | 
					     * 이미 표시 중이면 중복 삽입하지 않는다.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    fun showTypingIndicator() {
 | 
				
			||||||
 | 
					        if (isTypingVisible) return
 | 
				
			||||||
 | 
					        // 목록의 마지막에 추가
 | 
				
			||||||
 | 
					        items.add(ChatListItem.TypingIndicator)
 | 
				
			||||||
 | 
					        isTypingVisible = true
 | 
				
			||||||
 | 
					        notifyItemInserted(items.lastIndex)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 타이핑 인디케이터를 숨긴다(제거).
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    fun hideTypingIndicator() {
 | 
				
			||||||
 | 
					        val index = findTypingIndicatorIndex()
 | 
				
			||||||
 | 
					        if (index >= 0) {
 | 
				
			||||||
 | 
					            items.removeAt(index)
 | 
				
			||||||
 | 
					            isTypingVisible = false
 | 
				
			||||||
 | 
					            notifyItemRemoved(index)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @SuppressLint("NotifyDataSetChanged")
 | 
				
			||||||
 | 
					    fun setItems(newItems: List<ChatListItem>) {
 | 
				
			||||||
 | 
					        items.clear()
 | 
				
			||||||
 | 
					        items.addAll(newItems)
 | 
				
			||||||
 | 
					        notifyDataSetChanged()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun addItem(item: ChatListItem) {
 | 
				
			||||||
 | 
					        items.add(item)
 | 
				
			||||||
 | 
					        notifyItemInserted(items.lastIndex)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun getItemCount(): Int = items.size
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun getItemViewType(position: Int): Int {
 | 
				
			||||||
 | 
					        return when (items[position]) {
 | 
				
			||||||
 | 
					            is ChatListItem.UserMessage -> VIEW_TYPE_USER_MESSAGE
 | 
				
			||||||
 | 
					            is ChatListItem.AiMessage -> VIEW_TYPE_AI_MESSAGE
 | 
				
			||||||
 | 
					            is ChatListItem.Notice -> VIEW_TYPE_NOTICE
 | 
				
			||||||
 | 
					            is ChatListItem.TypingIndicator -> VIEW_TYPE_TYPING_INDICATOR
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
 | 
				
			||||||
 | 
					        val inflater = LayoutInflater.from(parent.context)
 | 
				
			||||||
 | 
					        return when (viewType) {
 | 
				
			||||||
 | 
					            VIEW_TYPE_USER_MESSAGE ->
 | 
				
			||||||
 | 
					                UserMessageViewHolder(
 | 
				
			||||||
 | 
					                    ItemChatUserMessageBinding.inflate(
 | 
				
			||||||
 | 
					                        inflater,
 | 
				
			||||||
 | 
					                        parent,
 | 
				
			||||||
 | 
					                        false
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            VIEW_TYPE_AI_MESSAGE ->
 | 
				
			||||||
 | 
					                AiMessageViewHolder(
 | 
				
			||||||
 | 
					                    ItemChatAiMessageBinding.inflate(
 | 
				
			||||||
 | 
					                        inflater,
 | 
				
			||||||
 | 
					                        parent,
 | 
				
			||||||
 | 
					                        false
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            VIEW_TYPE_TYPING_INDICATOR ->
 | 
				
			||||||
 | 
					                TypingIndicatorViewHolder(
 | 
				
			||||||
 | 
					                    ItemChatTypingIndicatorBinding.inflate(
 | 
				
			||||||
 | 
					                        inflater,
 | 
				
			||||||
 | 
					                        parent,
 | 
				
			||||||
 | 
					                        false
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            VIEW_TYPE_NOTICE -> {
 | 
				
			||||||
 | 
					                // 전용 레이아웃이 아직 없으므로 기본 텍스트 아이템으로 구현하고 접기 기능 지원
 | 
				
			||||||
 | 
					                NoticeMessageViewHolder(inflateAndroidSimpleItem(parent))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            else -> throw IllegalArgumentException("Unknown viewType: $viewType")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
 | 
				
			||||||
 | 
					        // 그룹화 판정: 같은 발신자 연속 여부와 그룹의 마지막 여부 계산
 | 
				
			||||||
 | 
					        fun isSameSender(prev: ChatListItem?, curr: ChatListItem?): Boolean {
 | 
				
			||||||
 | 
					            if (prev == null || curr == null) return false
 | 
				
			||||||
 | 
					            val p = when (prev) {
 | 
				
			||||||
 | 
					                is ChatListItem.UserMessage -> prev.data.mine
 | 
				
			||||||
 | 
					                is ChatListItem.AiMessage -> prev.data.mine
 | 
				
			||||||
 | 
					                else -> return false
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            val c = when (curr) {
 | 
				
			||||||
 | 
					                is ChatListItem.UserMessage -> curr.data.mine
 | 
				
			||||||
 | 
					                is ChatListItem.AiMessage -> curr.data.mine
 | 
				
			||||||
 | 
					                else -> return false
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return p == c
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val currItem = items[position]
 | 
				
			||||||
 | 
					        val prevItem = if (position > 0) items[position - 1] else null
 | 
				
			||||||
 | 
					        val nextItem = if (position < items.lastIndex) items[position + 1] else null
 | 
				
			||||||
 | 
					        val grouped = isSameSender(prevItem, currItem)
 | 
				
			||||||
 | 
					        val isLastInGroup = !isSameSender(currItem, nextItem)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        when (holder) {
 | 
				
			||||||
 | 
					            is UserMessageViewHolder -> {
 | 
				
			||||||
 | 
					                val item = currItem as ChatListItem.UserMessage
 | 
				
			||||||
 | 
					                holder.bind(item.data, showTime = isLastInGroup, isGrouped = grouped)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            is AiMessageViewHolder -> {
 | 
				
			||||||
 | 
					                val item = currItem as ChatListItem.AiMessage
 | 
				
			||||||
 | 
					                holder.bind(
 | 
				
			||||||
 | 
					                    item.data,
 | 
				
			||||||
 | 
					                    item.displayName,
 | 
				
			||||||
 | 
					                    isGrouped = grouped,
 | 
				
			||||||
 | 
					                    showTime = isLastInGroup
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            is NoticeMessageViewHolder -> {
 | 
				
			||||||
 | 
					                val item = currItem as ChatListItem.Notice
 | 
				
			||||||
 | 
					                holder.bind(item.text, isNoticeCollapsed) {
 | 
				
			||||||
 | 
					                    isNoticeCollapsed = !isNoticeCollapsed
 | 
				
			||||||
 | 
					                    notifyItemChanged(position)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            is TypingIndicatorViewHolder -> {
 | 
				
			||||||
 | 
					                holder.bind()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // region ViewHolders
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** 사용자 메시지 뷰홀더: 시간 포맷팅, 상태(투명도) 표시 */
 | 
				
			||||||
 | 
					    class UserMessageViewHolder(
 | 
				
			||||||
 | 
					        private val binding: ItemChatUserMessageBinding
 | 
				
			||||||
 | 
					    ) : RecyclerView.ViewHolder(binding.root) {
 | 
				
			||||||
 | 
					        fun bind(data: ChatMessage, showTime: Boolean, isGrouped: Boolean) {
 | 
				
			||||||
 | 
					            binding.tvMessage.text = data.message
 | 
				
			||||||
 | 
					            binding.tvTime.text = formatMessageTime(data.createdAt)
 | 
				
			||||||
 | 
					            binding.tvTime.visibility = if (showTime) View.VISIBLE else View.INVISIBLE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 상태에 따른 시각적 피드백: 전송중은 약간 투명, 실패는 더 투명
 | 
				
			||||||
 | 
					            val alpha = when (data.status) {
 | 
				
			||||||
 | 
					                MessageStatus.SENDING -> 0.6f
 | 
				
			||||||
 | 
					                MessageStatus.FAILED -> 0.4f
 | 
				
			||||||
 | 
					                MessageStatus.SENT -> 1.0f
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            binding.messageContainer.alpha = alpha
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 그룹 내부 간격 최소화 (상단 패딩 축소)
 | 
				
			||||||
 | 
					            adjustTopPadding(isGrouped)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 접근성: 상태 포함 설명
 | 
				
			||||||
 | 
					            val statusDesc = when (data.status) {
 | 
				
			||||||
 | 
					                MessageStatus.SENDING -> "전송 중"
 | 
				
			||||||
 | 
					                MessageStatus.FAILED -> "전송 실패"
 | 
				
			||||||
 | 
					                MessageStatus.SENT -> "전송 완료"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            val timeDesc = if (showTime) {
 | 
				
			||||||
 | 
					                binding.tvTime.text
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                ""
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            binding.root.contentDescription =
 | 
				
			||||||
 | 
					                "내 메시지 ${binding.tvMessage.text}, $statusDesc, $timeDesc"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private fun adjustTopPadding(isGrouped: Boolean) {
 | 
				
			||||||
 | 
					            val density = binding.root.resources.displayMetrics.density
 | 
				
			||||||
 | 
					            val top = if (isGrouped) (2 * density).toInt() else (6 * density).toInt()
 | 
				
			||||||
 | 
					            val bottom = (6 * density).toInt()
 | 
				
			||||||
 | 
					            val start = binding.root.paddingStart
 | 
				
			||||||
 | 
					            val end = binding.root.paddingEnd
 | 
				
			||||||
 | 
					            binding.root.setPaddingRelative(start, top, end, bottom)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** AI 메시지 뷰홀더: 이미지 로딩, 그룹화 처리, 시간 포맷팅 */
 | 
				
			||||||
 | 
					    class AiMessageViewHolder(
 | 
				
			||||||
 | 
					        private val binding: ItemChatAiMessageBinding
 | 
				
			||||||
 | 
					    ) : RecyclerView.ViewHolder(binding.root) {
 | 
				
			||||||
 | 
					        fun bind(data: ChatMessage, displayName: String?, isGrouped: Boolean, showTime: Boolean) {
 | 
				
			||||||
 | 
					            binding.tvMessage.text = data.message
 | 
				
			||||||
 | 
					            binding.tvTime.text = formatMessageTime(data.createdAt)
 | 
				
			||||||
 | 
					            binding.tvTime.visibility = if (showTime) View.VISIBLE else View.INVISIBLE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 그룹화: isGrouped가 true면 프로필/이름 숨김
 | 
				
			||||||
 | 
					            binding.ivProfile.visibility = if (isGrouped) View.INVISIBLE else View.VISIBLE
 | 
				
			||||||
 | 
					            binding.tvName.visibility = if (isGrouped) View.GONE else View.VISIBLE
 | 
				
			||||||
 | 
					            if (!isGrouped) {
 | 
				
			||||||
 | 
					                binding.tvName.text = displayName ?: ""
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 프로필 이미지 로딩 (Coil)
 | 
				
			||||||
 | 
					            if (binding.ivProfile.isVisible) {
 | 
				
			||||||
 | 
					                binding.ivProfile.load(data.profileImageUrl) {
 | 
				
			||||||
 | 
					                    placeholder(R.drawable.ic_placeholder_profile)
 | 
				
			||||||
 | 
					                    error(R.drawable.ic_placeholder_profile)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 그룹 내부 간격 최소화 (상단 패딩 축소)
 | 
				
			||||||
 | 
					            adjustTopPadding(isGrouped)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 접근성
 | 
				
			||||||
 | 
					            itemView.contentDescription = buildString {
 | 
				
			||||||
 | 
					                if (!isGrouped && !binding.tvName.text.isNullOrEmpty()) {
 | 
				
			||||||
 | 
					                    append(binding.tvName.text)
 | 
				
			||||||
 | 
					                    append(" ")
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                append("메시지 ")
 | 
				
			||||||
 | 
					                append(binding.tvMessage.text)
 | 
				
			||||||
 | 
					                if (showTime) {
 | 
				
			||||||
 | 
					                    append(", ")
 | 
				
			||||||
 | 
					                    append(binding.tvTime.text)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private fun adjustTopPadding(isGrouped: Boolean) {
 | 
				
			||||||
 | 
					            val density = itemView.resources.displayMetrics.density
 | 
				
			||||||
 | 
					            val top = if (isGrouped) (2 * density).toInt() else (6 * density).toInt()
 | 
				
			||||||
 | 
					            val bottom = (6 * density).toInt()
 | 
				
			||||||
 | 
					            val start = itemView.paddingStart
 | 
				
			||||||
 | 
					            val end = itemView.paddingEnd
 | 
				
			||||||
 | 
					            itemView.setPaddingRelative(start, top, end, bottom)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** 안내 메시지 뷰홀더: 텍스트 표시 + 간단한 접기/펼치기 */
 | 
				
			||||||
 | 
					    class NoticeMessageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
 | 
				
			||||||
 | 
					        private val textView: TextView = itemView.findViewById(android.R.id.text1)
 | 
				
			||||||
 | 
					        fun bind(text: String, collapsed: Boolean, onToggle: () -> Unit) {
 | 
				
			||||||
 | 
					            textView.text = text
 | 
				
			||||||
 | 
					            // 접힘 상태: 한 줄 + 말줄임표, 펼침 상태: 여러 줄
 | 
				
			||||||
 | 
					            if (collapsed) {
 | 
				
			||||||
 | 
					                textView.maxLines = 1
 | 
				
			||||||
 | 
					                textView.ellipsize = TextUtils.TruncateAt.END
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                textView.maxLines = 10_000
 | 
				
			||||||
 | 
					                textView.ellipsize = null
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            itemView.setOnClickListener { onToggle() }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** 타이핑 인디케이터 뷰홀더: 점 애니메이션 시작/정지 */
 | 
				
			||||||
 | 
					    class TypingIndicatorViewHolder(
 | 
				
			||||||
 | 
					        private val binding: ItemChatTypingIndicatorBinding
 | 
				
			||||||
 | 
					    ) : RecyclerView.ViewHolder(binding.root) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fun bind() {
 | 
				
			||||||
 | 
					            startTypingAnimation()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private fun startTypingAnimation() {
 | 
				
			||||||
 | 
					            val context = binding.root.context
 | 
				
			||||||
 | 
					            val anim1: Animation = AnimationUtils.loadAnimation(
 | 
				
			||||||
 | 
					                context,
 | 
				
			||||||
 | 
					                R.anim.typing_dots_animation
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            val anim2: Animation = AnimationUtils.loadAnimation(
 | 
				
			||||||
 | 
					                context,
 | 
				
			||||||
 | 
					                R.anim.typing_dots_animation
 | 
				
			||||||
 | 
					            ).apply { startOffset = 200 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            val anim3: Animation = AnimationUtils.loadAnimation(
 | 
				
			||||||
 | 
					                context,
 | 
				
			||||||
 | 
					                R.anim.typing_dots_animation
 | 
				
			||||||
 | 
					            ).apply { startOffset = 400 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            binding.dot1.startAnimation(anim1)
 | 
				
			||||||
 | 
					            binding.dot2.startAnimation(anim2)
 | 
				
			||||||
 | 
					            binding.dot3.startAnimation(anim3)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fun stopTypingAnimation() {
 | 
				
			||||||
 | 
					            binding.dot1.clearAnimation()
 | 
				
			||||||
 | 
					            binding.dot2.clearAnimation()
 | 
				
			||||||
 | 
					            binding.dot3.clearAnimation()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // region Util
 | 
				
			||||||
 | 
					    private fun inflateAndroidSimpleItem(
 | 
				
			||||||
 | 
					        parent: ViewGroup,
 | 
				
			||||||
 | 
					        @LayoutRes layout: Int = android.R.layout.simple_list_item_1
 | 
				
			||||||
 | 
					    ): View {
 | 
				
			||||||
 | 
					        return LayoutInflater.from(parent.context)
 | 
				
			||||||
 | 
					            .inflate(layout, parent, false)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
 | 
				
			||||||
 | 
					        super.onViewAttachedToWindow(holder)
 | 
				
			||||||
 | 
					        if (holder is TypingIndicatorViewHolder) {
 | 
				
			||||||
 | 
					            holder.bind()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
 | 
				
			||||||
 | 
					        if (holder is TypingIndicatorViewHolder) {
 | 
				
			||||||
 | 
					            holder.stopTypingAnimation()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        super.onViewDetachedFromWindow(holder)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// region file-level util
 | 
				
			||||||
 | 
					private fun formatMessageTime(timestamp: Long): String {
 | 
				
			||||||
 | 
					    return try {
 | 
				
			||||||
 | 
					        val df = SimpleDateFormat("a h:mm", Locale.getDefault())
 | 
				
			||||||
 | 
					        df.format(Date(timestamp))
 | 
				
			||||||
 | 
					    } catch (_: Exception) {
 | 
				
			||||||
 | 
					        ""
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					// endregion
 | 
				
			||||||
		Reference in New Issue
	
	Block a user