feat: 新增搜索历史消息功能
- 新增搜索历史消息功能 - 修复部分问题
This commit is contained in:
parent
ec9e3a8af4
commit
b8c80e4b00
@ -2,7 +2,19 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="GitCommitMessageStorage">
|
<component name="GitCommitMessageStorage">
|
||||||
<option name="messageStorage">
|
<option name="messageStorage">
|
||||||
<MessageStorage />
|
<MessageStorage>
|
||||||
|
<option name="commitTemplate">
|
||||||
|
<CommitTemplate>
|
||||||
|
<option name="body" value="" />
|
||||||
|
<option name="changes" value="" />
|
||||||
|
<option name="closes" value="" />
|
||||||
|
<option name="scope" value="" />
|
||||||
|
<option name="skipCi" value="" />
|
||||||
|
<option name="subject" value="" />
|
||||||
|
<option name="type" value="feat" />
|
||||||
|
</CommitTemplate>
|
||||||
|
</option>
|
||||||
|
</MessageStorage>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
@ -52,4 +52,13 @@ object LocalDatabase {
|
|||||||
val offset = 0
|
val offset = 0
|
||||||
return query.find(offset.toLong(), limit)
|
return query.find(offset.toLong(), limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAllHistoryMessages(contactId: String, msgLocalId: Long): List<Messages> {
|
||||||
|
val query = messagesBox
|
||||||
|
.query(Messages_.takerId.equal(contactId))
|
||||||
|
.greaterOrEqual(Messages_.msgLocalId, msgLocalId)
|
||||||
|
.order(Messages_.timestamp, QueryBuilder.DESCENDING)
|
||||||
|
.build()
|
||||||
|
return query.find()
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.kaixed.kchat.data.model.search
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: kaixed
|
||||||
|
* @Date: 2024/12/11 16:47
|
||||||
|
*/
|
||||||
|
data class ChatHistoryItem(
|
||||||
|
var msgLocalId: Long,
|
||||||
|
var content: String,
|
||||||
|
var timestamp: Long,
|
||||||
|
var isMine: Boolean
|
||||||
|
)
|
@ -1,6 +1,7 @@
|
|||||||
package com.kaixed.kchat.manager
|
package com.kaixed.kchat.manager
|
||||||
|
|
||||||
import com.kaixed.kchat.data.LocalDatabase
|
import com.kaixed.kchat.data.LocalDatabase
|
||||||
|
import com.kaixed.kchat.data.LocalDatabase.getAllHistoryMessages
|
||||||
import com.kaixed.kchat.data.LocalDatabase.getMessagesWithContact
|
import com.kaixed.kchat.data.LocalDatabase.getMessagesWithContact
|
||||||
import com.kaixed.kchat.data.LocalDatabase.getMoreMessages
|
import com.kaixed.kchat.data.LocalDatabase.getMoreMessages
|
||||||
import com.kaixed.kchat.data.local.entity.Messages
|
import com.kaixed.kchat.data.local.entity.Messages
|
||||||
@ -13,7 +14,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
*/
|
*/
|
||||||
object MessagesManager {
|
object MessagesManager {
|
||||||
|
|
||||||
private const val LIMIT = 10L
|
private const val LIMIT = 20L
|
||||||
|
|
||||||
private val _messages = MutableStateFlow<List<Messages>>(emptyList())
|
private val _messages = MutableStateFlow<List<Messages>>(emptyList())
|
||||||
|
|
||||||
@ -24,6 +25,13 @@ object MessagesManager {
|
|||||||
private var loading = false
|
private var loading = false
|
||||||
private var tempIndex: Long = 0
|
private var tempIndex: Long = 0
|
||||||
|
|
||||||
|
fun queryHistory(msgLocalId: Long): Int {
|
||||||
|
_messages.value = emptyList()
|
||||||
|
val msg = getAllHistoryMessages(contactId, msgLocalId)
|
||||||
|
_messages.value = msg
|
||||||
|
return msg.size
|
||||||
|
}
|
||||||
|
|
||||||
fun setContactId(contactId: String) {
|
fun setContactId(contactId: String) {
|
||||||
this.contactId = contactId
|
this.contactId = contactId
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
package com.kaixed.kchat.processor
|
||||||
|
|
||||||
|
import com.kaixed.kchat.data.local.box.ObjectBox.getBox
|
||||||
|
import com.kaixed.kchat.data.local.entity.Messages
|
||||||
|
import com.kaixed.kchat.data.local.entity.Messages_
|
||||||
|
import io.objectbox.Box
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: kaixed
|
||||||
|
* @Date: 2024/12/12 15:14
|
||||||
|
*/
|
||||||
|
object MessageProcessor {
|
||||||
|
|
||||||
|
private const val INTERVAL = 5
|
||||||
|
|
||||||
|
private val messagesBox: Box<Messages> by lazy { getBox(Messages::class.java) }
|
||||||
|
|
||||||
|
fun processorMsg(messages: Messages): Messages {
|
||||||
|
val curTimestamp = System.currentTimeMillis()
|
||||||
|
val localMsg = messagesBox.query(Messages_.takerId.equal(messages.takerId))
|
||||||
|
.lessOrEqual(Messages_.timestamp, curTimestamp)
|
||||||
|
.orderDesc(Messages_.timestamp).build().findFirst()
|
||||||
|
|
||||||
|
var showTimer = true
|
||||||
|
localMsg?.let {
|
||||||
|
showTimer = curTimestamp - it.timestamp >= INTERVAL * 1L * 60 * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.isShowTimer = showTimer
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
}
|
@ -19,9 +19,11 @@ import com.kaixed.kchat.data.local.entity.Messages_
|
|||||||
import com.kaixed.kchat.network.NetworkInterface.WEBSOCKET
|
import com.kaixed.kchat.network.NetworkInterface.WEBSOCKET
|
||||||
import com.kaixed.kchat.network.NetworkInterface.WEBSOCKET_SERVER_URL
|
import com.kaixed.kchat.network.NetworkInterface.WEBSOCKET_SERVER_URL
|
||||||
import com.kaixed.kchat.network.OkhttpHelper
|
import com.kaixed.kchat.network.OkhttpHelper
|
||||||
|
import com.kaixed.kchat.processor.MessageProcessor
|
||||||
import com.kaixed.kchat.utils.Constants.MMKV_COMMON_DATA
|
import com.kaixed.kchat.utils.Constants.MMKV_COMMON_DATA
|
||||||
import com.kaixed.kchat.utils.ConstantsUtil.getCurrentContactId
|
import com.kaixed.kchat.utils.ConstantsUtil.getCurrentContactId
|
||||||
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
|
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
|
||||||
|
import com.kaixed.kchat.utils.SingleLiveEvent
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import io.objectbox.Box
|
import io.objectbox.Box
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -60,9 +62,9 @@ class WebSocketService : Service() {
|
|||||||
|
|
||||||
private val serviceScope = CoroutineScope(IO + serviceJob)
|
private val serviceScope = CoroutineScope(IO + serviceJob)
|
||||||
|
|
||||||
private val _messagesMutableLiveData = MutableLiveData<Messages?>()
|
private val _messagesMutableLiveData = SingleLiveEvent<Messages?>()
|
||||||
|
|
||||||
val messageLivedata: LiveData<Messages?> get() = _messagesMutableLiveData
|
val messageLivedata: SingleLiveEvent<Messages?> get() = _messagesMutableLiveData
|
||||||
|
|
||||||
private val _conversations = MutableLiveData<List<Conversation>?>()
|
private val _conversations = MutableLiveData<List<Conversation>?>()
|
||||||
|
|
||||||
@ -233,6 +235,7 @@ class WebSocketService : Service() {
|
|||||||
} else {
|
} else {
|
||||||
messages.takerId = messages.senderId
|
messages.takerId = messages.senderId
|
||||||
_messagesMutableLiveData.postValue(messages)
|
_messagesMutableLiveData.postValue(messages)
|
||||||
|
MessageProcessor.processorMsg(messages)
|
||||||
messagesBox.put(messages)
|
messagesBox.put(messages)
|
||||||
updateConversationList(messages)
|
updateConversationList(messages)
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ import com.kaixed.kchat.data.local.entity.Messages
|
|||||||
import com.kaixed.kchat.data.model.FunctionItem
|
import com.kaixed.kchat.data.model.FunctionItem
|
||||||
import com.kaixed.kchat.databinding.ActivityChatBinding
|
import com.kaixed.kchat.databinding.ActivityChatBinding
|
||||||
import com.kaixed.kchat.manager.MessagesManager
|
import com.kaixed.kchat.manager.MessagesManager
|
||||||
|
import com.kaixed.kchat.processor.MessageProcessor
|
||||||
import com.kaixed.kchat.service.WebSocketService
|
import com.kaixed.kchat.service.WebSocketService
|
||||||
import com.kaixed.kchat.service.WebSocketService.LocalBinder
|
import com.kaixed.kchat.service.WebSocketService.LocalBinder
|
||||||
import com.kaixed.kchat.ui.adapter.ChatAdapter
|
import com.kaixed.kchat.ui.adapter.ChatAdapter
|
||||||
@ -45,6 +46,7 @@ import com.kaixed.kchat.ui.widget.LoadingDialogFragment
|
|||||||
import com.kaixed.kchat.utils.Constants.CURRENT_CONTACT_ID
|
import com.kaixed.kchat.utils.Constants.CURRENT_CONTACT_ID
|
||||||
import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT_RATIO
|
import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT_RATIO
|
||||||
import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION
|
import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION
|
||||||
|
import com.kaixed.kchat.utils.ConstantsUtil.getAvatarUrl
|
||||||
import com.kaixed.kchat.utils.ConstantsUtil.getKeyboardHeight
|
import com.kaixed.kchat.utils.ConstantsUtil.getKeyboardHeight
|
||||||
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
|
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
|
||||||
import com.kaixed.kchat.utils.ImageEngines
|
import com.kaixed.kchat.utils.ImageEngines
|
||||||
@ -91,6 +93,10 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
|||||||
|
|
||||||
private val fileViewModel: FileViewModel by viewModels()
|
private val fileViewModel: FileViewModel by viewModels()
|
||||||
|
|
||||||
|
private var isSearchHistory = false
|
||||||
|
|
||||||
|
private var msgLocalId = 0L
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val UNBLOCK_DELAY_TIME = 200L
|
private const val UNBLOCK_DELAY_TIME = 200L
|
||||||
}
|
}
|
||||||
@ -109,6 +115,17 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
|||||||
setPanelChange()
|
setPanelChange()
|
||||||
getKeyBoardVisibility()
|
getKeyBoardVisibility()
|
||||||
observeViewModel()
|
observeViewModel()
|
||||||
|
if (isSearchHistory) {
|
||||||
|
val size = MessagesManager.queryHistory(msgLocalId)
|
||||||
|
binding.recycleChatList.smoothScrollToPosition(size - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (!isSearchHistory) {
|
||||||
|
MessagesManager.firstLoadMessages()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeViewModel() {
|
private fun observeViewModel() {
|
||||||
@ -376,8 +393,11 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun initData() {
|
override fun initData() {
|
||||||
contactId = intent.getStringExtra("contactId").toString()
|
contactId = intent?.getStringExtra("contactId").toString()
|
||||||
contactNickname = intent.getStringExtra("contactNickname")
|
contactNickname = intent?.getStringExtra("contactNickname")
|
||||||
|
isSearchHistory = intent?.getBooleanExtra("isSearchHistory", false) == true
|
||||||
|
msgLocalId = intent.getLongExtra("msgLocalId", 0)
|
||||||
|
|
||||||
binding.ctb.setTitleName(contactNickname!!)
|
binding.ctb.setTitleName(contactNickname!!)
|
||||||
|
|
||||||
softKeyboardHeight = getKeyboardHeight()
|
softKeyboardHeight = getKeyboardHeight()
|
||||||
@ -414,7 +434,6 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
|||||||
|
|
||||||
private fun handleMsg(messages: Messages) {
|
private fun handleMsg(messages: Messages) {
|
||||||
MessagesManager.receiveMessage(messages)
|
MessagesManager.receiveMessage(messages)
|
||||||
// messagesViewModel.receiveMessage(messages)
|
|
||||||
binding.recycleChatList.smoothScrollToPosition(0)
|
binding.recycleChatList.smoothScrollToPosition(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -424,10 +443,11 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
|||||||
timestamp = System.currentTimeMillis(),
|
timestamp = System.currentTimeMillis(),
|
||||||
senderId = username,
|
senderId = username,
|
||||||
takerId = talkerId,
|
takerId = talkerId,
|
||||||
|
avatarUrl = getAvatarUrl(),
|
||||||
type = type,
|
type = type,
|
||||||
)
|
)
|
||||||
|
val msg = MessageProcessor.processorMsg(messages)
|
||||||
val id: Long = messagesBox.put(messages)
|
val id: Long = messagesBox.put(msg)
|
||||||
|
|
||||||
val jsonObject = JSONObject()
|
val jsonObject = JSONObject()
|
||||||
val jsonObject2 = JSONObject()
|
val jsonObject2 = JSONObject()
|
||||||
@ -445,7 +465,6 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
|||||||
binding.etInput.setText("")
|
binding.etInput.setText("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun bindWebSocketService() {
|
private fun bindWebSocketService() {
|
||||||
bindService(
|
bindService(
|
||||||
Intent(
|
Intent(
|
||||||
|
@ -3,7 +3,6 @@ package com.kaixed.kchat.ui.activity
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.kaixed.kchat.data.LocalDatabase.getContactByUsername
|
import com.kaixed.kchat.data.LocalDatabase.getContactByUsername
|
||||||
import com.kaixed.kchat.databinding.ActivityChatDetailBinding
|
import com.kaixed.kchat.databinding.ActivityChatDetailBinding
|
||||||
@ -54,7 +53,11 @@ class ChatDetailActivity : BaseActivity<ActivityChatDetailBinding>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.ciSearchChatHistory.setOnClickListener {
|
binding.ciSearchChatHistory.setOnClickListener {
|
||||||
val intent = Intent(this, SearchChatHistory::class.java)
|
val intent = Intent(this, SearchChatHistory::class.java).apply {
|
||||||
|
putExtra("contactNickname", contact?.remark ?: contact?.nickname)
|
||||||
|
putExtra("contactAvatarUrl", contact?.avatarUrl)
|
||||||
|
putExtra("contactId", contactId)
|
||||||
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,6 @@ class ContactPermissionActivity : BaseActivity<ActivityContactPermissionBinding>
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun initData() {
|
override fun initData() {
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
|
@ -1,16 +1,49 @@
|
|||||||
package com.kaixed.kchat.ui.activity
|
package com.kaixed.kchat.ui.activity
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.core.text.buildSpannedString
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.text.inSpans
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.widget.addTextChangedListener
|
||||||
import com.kaixed.kchat.R
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.kaixed.kchat.data.local.box.ObjectBox.getBox
|
||||||
|
import com.kaixed.kchat.data.local.entity.Messages
|
||||||
|
import com.kaixed.kchat.data.local.entity.Messages_
|
||||||
|
import com.kaixed.kchat.data.model.search.ChatHistoryItem
|
||||||
import com.kaixed.kchat.databinding.ActivitySearchChatHistoryBinding
|
import com.kaixed.kchat.databinding.ActivitySearchChatHistoryBinding
|
||||||
|
import com.kaixed.kchat.ui.adapter.ChatHistoryAdapter
|
||||||
import com.kaixed.kchat.ui.base.BaseActivity
|
import com.kaixed.kchat.ui.base.BaseActivity
|
||||||
|
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
|
||||||
|
import io.objectbox.Box
|
||||||
|
|
||||||
class SearchChatHistory : BaseActivity<ActivitySearchChatHistoryBinding>() {
|
class SearchChatHistory : BaseActivity<ActivitySearchChatHistoryBinding>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val VIEW_CHAT_HISTORY = 0
|
||||||
|
private const val VIEW_SEARCH_FAILED = 1
|
||||||
|
private const val VIEW_SELECT = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
private val messageBox: Box<Messages> by lazy { getBox(Messages::class.java) }
|
||||||
|
|
||||||
|
private val chatHistoryAdapter by lazy {
|
||||||
|
ChatHistoryAdapter(
|
||||||
|
contactAvatarUrl,
|
||||||
|
contactNickname,
|
||||||
|
this
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var contactAvatarUrl: String = ""
|
||||||
|
|
||||||
|
private var contactNickname: String = ""
|
||||||
|
|
||||||
|
private var contactId: String = ""
|
||||||
|
|
||||||
override fun inflateBinding(): ActivitySearchChatHistoryBinding {
|
override fun inflateBinding(): ActivitySearchChatHistoryBinding {
|
||||||
return ActivitySearchChatHistoryBinding.inflate(layoutInflater)
|
return ActivitySearchChatHistoryBinding.inflate(layoutInflater)
|
||||||
}
|
}
|
||||||
@ -18,10 +51,93 @@ class SearchChatHistory : BaseActivity<ActivitySearchChatHistoryBinding>() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContentView(R.layout.activity_search_chat_history)
|
setListener()
|
||||||
|
setupRecyclerView()
|
||||||
|
showView(VIEW_SELECT)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
binding.rvChatHistory.layoutManager = LinearLayoutManager(this)
|
||||||
|
binding.rvChatHistory.adapter = chatHistoryAdapter
|
||||||
|
chatHistoryAdapter.setContactId(contactId)
|
||||||
|
|
||||||
|
chatHistoryAdapter.let { adapter ->
|
||||||
|
val viewHolder = adapter.createViewHolder(binding.rvChatHistory, adapter.getItemViewType(0))
|
||||||
|
val itemView = viewHolder.itemView
|
||||||
|
itemView.measure(
|
||||||
|
View.MeasureSpec.makeMeasureSpec(binding.rvChatHistory.width, View.MeasureSpec.EXACTLY),
|
||||||
|
View.MeasureSpec.UNSPECIFIED
|
||||||
|
)
|
||||||
|
val itemHeight = itemView.measuredHeight
|
||||||
|
Log.d("RecyclerView", "单个 Item 高度: $itemHeight")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setListener() {
|
||||||
|
binding.tvCancel.setOnClickListener { finish() }
|
||||||
|
binding.etSearch.addTextChangedListener(afterTextChanged =
|
||||||
|
{
|
||||||
|
if (it?.length == 0) {
|
||||||
|
chatHistoryAdapter.submitList(null)
|
||||||
|
showView(VIEW_SELECT)
|
||||||
|
} else {
|
||||||
|
getData(it.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showView(viewToShowIndex: Int) {
|
||||||
|
val viewList = listOf(binding.rvChatHistory, binding.tvSearchFailed, binding.clSelect)
|
||||||
|
viewList.forEachIndexed { index, view ->
|
||||||
|
view.visibility = if (index == viewToShowIndex) View.VISIBLE else View.INVISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getData(matchedField: String) {
|
||||||
|
val data = loadDataFromDb(matchedField)
|
||||||
|
|
||||||
|
val items = data.map {
|
||||||
|
ChatHistoryItem(it.msgLocalId, it.content, it.timestamp, it.senderId == getUsername())
|
||||||
|
}
|
||||||
|
|
||||||
|
chatHistoryAdapter.setMatchedField(matchedField)
|
||||||
|
chatHistoryAdapter.submitList(null)
|
||||||
|
chatHistoryAdapter.submitList(items)
|
||||||
|
|
||||||
|
when (items.isEmpty()) {
|
||||||
|
true -> {
|
||||||
|
val text = createEmptyMessage(matchedField)
|
||||||
|
binding.tvSearchFailed.text = text
|
||||||
|
showView(VIEW_SEARCH_FAILED)
|
||||||
|
}
|
||||||
|
|
||||||
|
false -> {
|
||||||
|
showView(VIEW_CHAT_HISTORY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createEmptyMessage(matchedField: String): CharSequence {
|
||||||
|
return buildSpannedString {
|
||||||
|
append("没有找到与")
|
||||||
|
append("“")
|
||||||
|
inSpans(ForegroundColorSpan(Color.parseColor("#07C160"))) {
|
||||||
|
append(matchedField)
|
||||||
|
}
|
||||||
|
append("”")
|
||||||
|
append("相关的聊天记录")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadDataFromDb(content: String): List<Messages> {
|
||||||
|
return messageBox.query(Messages_.content.contains(content)).orderDesc(Messages_.timestamp)
|
||||||
|
.build().find()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initData() {
|
override fun initData() {
|
||||||
|
contactNickname = intent?.getStringExtra("contactNickname") ?: ""
|
||||||
|
contactAvatarUrl = intent?.getStringExtra("contactAvatarUrl") ?: ""
|
||||||
|
contactId = intent?.getStringExtra("contactId") ?: ""
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -21,6 +21,7 @@ import com.kaixed.kchat.databinding.ChatRecycleItemImageNormalBinding
|
|||||||
import com.kaixed.kchat.databinding.ChatRecycleItemTipBinding
|
import com.kaixed.kchat.databinding.ChatRecycleItemTipBinding
|
||||||
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
|
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
|
||||||
import com.kaixed.kchat.utils.PopWindowUtil.showPopupWindow
|
import com.kaixed.kchat.utils.PopWindowUtil.showPopupWindow
|
||||||
|
import com.kaixed.kchat.utils.TextUtil
|
||||||
import com.kaixed.kchat.utils.TextUtil.extractDimensionsAndPrefix
|
import com.kaixed.kchat.utils.TextUtil.extractDimensionsAndPrefix
|
||||||
import com.kaixed.kchat.utils.ViewUtil.changeTimerVisibility
|
import com.kaixed.kchat.utils.ViewUtil.changeTimerVisibility
|
||||||
import com.kaixed.kchat.utils.ViewUtil.setViewVisibility
|
import com.kaixed.kchat.utils.ViewUtil.setViewVisibility
|
||||||
@ -42,25 +43,12 @@ class ChatAdapter(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CUSTOM = 0
|
const val CUSTOM = 0
|
||||||
|
|
||||||
// 提示消息
|
|
||||||
const val TIP = 2
|
const val TIP = 2
|
||||||
|
|
||||||
// 图片消息
|
|
||||||
const val IMAGE = 4
|
const val IMAGE = 4
|
||||||
|
|
||||||
// 语音消息
|
|
||||||
const val VOICE = 6
|
const val VOICE = 6
|
||||||
|
|
||||||
// 位置消息
|
|
||||||
const val LOCATION = 8
|
const val LOCATION = 8
|
||||||
|
|
||||||
// 表情消息
|
|
||||||
const val EMOJI = 10
|
const val EMOJI = 10
|
||||||
|
|
||||||
// 红包消息
|
|
||||||
const val RED_PACKET = 12
|
const val RED_PACKET = 12
|
||||||
|
|
||||||
private const val TAG = "ChatAdapter"
|
private const val TAG = "ChatAdapter"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,25 +56,22 @@ class ChatAdapter(
|
|||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
CUSTOM -> {
|
CUSTOM -> {
|
||||||
CustomViewHolder(
|
CustomViewHolder(
|
||||||
ChatRecycleItemCustomNormalBinding.inflate(
|
ChatRecycleItemCustomNormalBinding
|
||||||
LayoutInflater.from(parent.context), parent, false
|
.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
IMAGE -> {
|
IMAGE -> {
|
||||||
ImageViewHolder(
|
ImageViewHolder(
|
||||||
ChatRecycleItemImageNormalBinding.inflate(
|
ChatRecycleItemImageNormalBinding
|
||||||
LayoutInflater.from(parent.context), parent, false
|
.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
TipViewHolder(
|
TipViewHolder(
|
||||||
ChatRecycleItemTipBinding.inflate(
|
ChatRecycleItemTipBinding
|
||||||
LayoutInflater.from(parent.context), parent, false
|
.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,7 +93,7 @@ class ChatAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
(holder as? HasTimer)?.let {
|
(holder as? HasTimer)?.let {
|
||||||
changeTimerVisibility(position, currentList, it.getTimerView(), singleMessage)
|
changeTimerVisibility(it.getTimerView(), singleMessage)
|
||||||
}
|
}
|
||||||
handleLongClick(holder, position, singleMessage)
|
handleLongClick(holder, position, singleMessage)
|
||||||
}
|
}
|
||||||
@ -176,12 +161,15 @@ class ChatAdapter(
|
|||||||
contentId = binding.tvMsgContent.id,
|
contentId = binding.tvMsgContent.id,
|
||||||
contentMineId = binding.tvMsgContentMine.id
|
contentMineId = binding.tvMsgContentMine.id
|
||||||
)
|
)
|
||||||
|
Glide.with(binding.root.context).load(message.avatarUrl)
|
||||||
|
.into(if (sender) binding.ifvAvatarMine else binding.ifvAvatar)
|
||||||
|
|
||||||
val contentView = if (sender) binding.tvMsgContentMine else binding.tvMsgContent
|
val contentView = if (sender) binding.tvMsgContentMine else binding.tvMsgContent
|
||||||
|
|
||||||
contentView.text = message.content.replaceSpan("[委屈]") {
|
contentView.text = message.content.replaceSpan("[委屈]") {
|
||||||
CenterImageSpan(binding.root.context, R.drawable.emoji).setDrawableSize(55)
|
CenterImageSpan(binding.root.context, R.drawable.emoji).setDrawableSize(55)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTimerView(): TextView = binding.tvTimer
|
override fun getTimerView(): TextView = binding.tvTimer
|
||||||
@ -191,6 +179,8 @@ class ChatAdapter(
|
|||||||
RecyclerView.ViewHolder(binding.root), HasTimer {
|
RecyclerView.ViewHolder(binding.root), HasTimer {
|
||||||
fun bindData(message: Messages) {
|
fun bindData(message: Messages) {
|
||||||
val sender = message.senderId == getUsername()
|
val sender = message.senderId == getUsername()
|
||||||
|
Glide.with(binding.root.context).load(message.avatarUrl)
|
||||||
|
.into(if (sender) binding.ifvAvatarMine else binding.ifvAvatar)
|
||||||
setViewVisibility(
|
setViewVisibility(
|
||||||
parentView = binding.root,
|
parentView = binding.root,
|
||||||
sender = sender,
|
sender = sender,
|
||||||
@ -226,7 +216,6 @@ class ChatAdapter(
|
|||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
return getItem(position).type.toInt()
|
return getItem(position).type.toInt()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateDb(message: Messages) {
|
private fun updateDb(message: Messages) {
|
||||||
|
@ -0,0 +1,87 @@
|
|||||||
|
package com.kaixed.kchat.ui.adapter
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.drake.spannable.replaceSpanFirst
|
||||||
|
import com.drake.spannable.span.HighlightSpan
|
||||||
|
import com.kaixed.kchat.data.model.search.ChatHistoryItem
|
||||||
|
import com.kaixed.kchat.databinding.ChatHistoryRecycleItemBinding
|
||||||
|
import com.kaixed.kchat.ui.activity.ChatActivity
|
||||||
|
import com.kaixed.kchat.utils.ConstantsUtil
|
||||||
|
import com.kaixed.kchat.utils.TextUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: kaixed
|
||||||
|
* @Date: 2024/12/11 16:42
|
||||||
|
*/
|
||||||
|
class ChatHistoryAdapter(
|
||||||
|
private val contactAvatarUrl: String,
|
||||||
|
private val contactNickname: String,
|
||||||
|
private val context: Context
|
||||||
|
) :
|
||||||
|
ListAdapter<ChatHistoryItem, ChatHistoryAdapter.MyViewHolder>(DiffCallback()) {
|
||||||
|
|
||||||
|
private var matchedField: String = ""
|
||||||
|
private val highlightSpan = HighlightSpan("#07C160")
|
||||||
|
private var contactId = ""
|
||||||
|
|
||||||
|
fun setMatchedField(field: String) {
|
||||||
|
matchedField = field
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setContactId(contactId: String) {
|
||||||
|
this.contactId = contactId
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyViewHolder(val binding: ChatHistoryRecycleItemBinding) : ViewHolder(binding.root)
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
|
||||||
|
return MyViewHolder(
|
||||||
|
ChatHistoryRecycleItemBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context), parent, false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
|
||||||
|
val bind = holder.binding
|
||||||
|
val chatHistoryItem = getItem(position)
|
||||||
|
bind.tvTime.text = TextUtil.getTimestampString(chatHistoryItem.timestamp)
|
||||||
|
bind.tvContent.text = chatHistoryItem.content.replaceSpanFirst(matchedField) {
|
||||||
|
highlightSpan
|
||||||
|
}
|
||||||
|
bind.tvNickname.text =
|
||||||
|
if (chatHistoryItem.isMine) ConstantsUtil.getNickName() else contactNickname
|
||||||
|
val avatarUrl =
|
||||||
|
if (chatHistoryItem.isMine) ConstantsUtil.getAvatarUrl() else contactAvatarUrl
|
||||||
|
Glide.with(bind.root.context).load(avatarUrl).into(bind.ifvAvatar)
|
||||||
|
bind.root.postDelayed({
|
||||||
|
val intent = Intent(context, ChatActivity::class.java).apply {
|
||||||
|
putExtra("contactId", contactId)
|
||||||
|
putExtra("contactNickname", contactNickname)
|
||||||
|
putExtra("isSearchHistory", true)
|
||||||
|
putExtra("msgLocalId", chatHistoryItem.msgLocalId)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
}, 200L)
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiffCallback : DiffUtil.ItemCallback<ChatHistoryItem>() {
|
||||||
|
override fun areItemsTheSame(oldItem: ChatHistoryItem, newItem: ChatHistoryItem): Boolean {
|
||||||
|
return oldItem.msgLocalId == newItem.msgLocalId
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(
|
||||||
|
oldItem: ChatHistoryItem,
|
||||||
|
newItem: ChatHistoryItem
|
||||||
|
): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -77,10 +77,7 @@ class FriendListAdapter(var items: MutableList<Contact>, private val context: Co
|
|||||||
if (position == 0) {
|
if (position == 0) {
|
||||||
holder.binding.root.setOnClickListener {
|
holder.binding.root.setOnClickListener {
|
||||||
context.startActivity(
|
context.startActivity(
|
||||||
Intent(
|
Intent(context, ContactRequestListActivity::class.java)
|
||||||
context,
|
|
||||||
ContactRequestListActivity::class.java
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -101,9 +98,9 @@ class FriendListAdapter(var items: MutableList<Contact>, private val context: Co
|
|||||||
} else {
|
} else {
|
||||||
holder.binding.tvLetter.text = item.quanpin?.substring(0, 1)?.uppercase()
|
holder.binding.tvLetter.text = item.quanpin?.substring(0, 1)?.uppercase()
|
||||||
}
|
}
|
||||||
holder.binding.tvLetter.visibility = ViewGroup.VISIBLE
|
holder.binding.tvLetter.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
holder.binding.tvLetter.visibility = ViewGroup.GONE
|
holder.binding.tvLetter.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
if (position <= defaultItems.size - 1) {
|
if (position <= defaultItems.size - 1) {
|
||||||
|
@ -96,8 +96,6 @@ class ContactFragment : BaseFragment<FragmentContactBinding>() {
|
|||||||
contactViewModel.getContactRequestList(getUsername())
|
contactViewModel.getContactRequestList(getUsername())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private fun loadData() {
|
private fun loadData() {
|
||||||
loading = true
|
loading = true
|
||||||
binding.tvLoading.visibility = View.VISIBLE
|
binding.tvLoading.visibility = View.VISIBLE
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
package com.kaixed.kchat.ui.widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.widget.AppCompatEditText
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.kaixed.kchat.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: kaixed
|
||||||
|
* @Date: 2024/12/12 10:09
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
class CustomEditText @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = androidx.appcompat.R.attr.editTextStyle
|
||||||
|
) : AppCompatEditText(context, attrs, defStyleAttr) {
|
||||||
|
init {
|
||||||
|
textCursorDrawable = ContextCompat.getDrawable(context, R.drawable.cursor)
|
||||||
|
}
|
||||||
|
}
|
@ -29,8 +29,10 @@ object ConstantsUtil {
|
|||||||
fun getUsername(): String =
|
fun getUsername(): String =
|
||||||
userSessionMMKV.getString(USERNAME_KEY, "") ?: ""
|
userSessionMMKV.getString(USERNAME_KEY, "") ?: ""
|
||||||
|
|
||||||
fun getAvatarUrl(): String =
|
fun getAvatarUrl(): String {
|
||||||
userSessionMMKV.getString(AVATAR_URL, "") ?: ""
|
val avatarUrl = userSessionMMKV.getString(AVATAR_URL, "") ?: ""
|
||||||
|
return TextUtil.extractDimensionsAndPrefix(avatarUrl)?.first ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
fun getStatusBarHeight(): Int =
|
fun getStatusBarHeight(): Int =
|
||||||
commonDataMMKV.getInt(STATUS_BAR_HEIGHT, STATUS_BAR_DEFAULT_HEIGHT)
|
commonDataMMKV.getInt(STATUS_BAR_HEIGHT, STATUS_BAR_DEFAULT_HEIGHT)
|
||||||
@ -49,5 +51,4 @@ object ConstantsUtil {
|
|||||||
}
|
}
|
||||||
return isFirstLaunch
|
return isFirstLaunch
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -101,14 +101,11 @@ object ViewUtil {
|
|||||||
|
|
||||||
|
|
||||||
fun changeTimerVisibility(
|
fun changeTimerVisibility(
|
||||||
position: Int,
|
|
||||||
messages: List<Messages>,
|
|
||||||
tvTimer: TextView,
|
tvTimer: TextView,
|
||||||
singleMessage: Messages
|
singleMessage: Messages
|
||||||
) {
|
) {
|
||||||
val showTimer: Boolean =
|
val showTimer: Boolean =
|
||||||
if (position == messages.size - 1) true
|
singleMessage.isShowTimer
|
||||||
else singleMessage.timestamp - messages[position + 1].timestamp >= 5 * 1L * 60 * 1000
|
|
||||||
|
|
||||||
if (showTimer) {
|
if (showTimer) {
|
||||||
tvTimer.visibility = View.VISIBLE
|
tvTimer.visibility = View.VISIBLE
|
||||||
|
@ -9,17 +9,22 @@
|
|||||||
android:fitsSystemWindows="true"
|
android:fitsSystemWindows="true"
|
||||||
tools:context=".ui.activity.SearchChatHistory">
|
tools:context=".ui.activity.SearchChatHistory">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/ll_search"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
android:id="@+id/cv_search"
|
android:id="@+id/cv_search"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="30dp"
|
android:layout_height="30dp"
|
||||||
android:layout_margin="15dp"
|
android:layout_margin="10dp"
|
||||||
|
android:layout_weight="1.0"
|
||||||
app:cardCornerRadius="7dp"
|
app:cardCornerRadius="7dp"
|
||||||
app:cardElevation="0dp"
|
app:cardElevation="0dp"
|
||||||
app:cardMaxElevation="0dp"
|
app:cardMaxElevation="0dp">
|
||||||
app:layout_constraintEnd_toStartOf="@id/tv_cancel"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -33,7 +38,7 @@
|
|||||||
android:layout_marginStart="10dp"
|
android:layout_marginStart="10dp"
|
||||||
android:src="@drawable/icon_search" />
|
android:src="@drawable/icon_search" />
|
||||||
|
|
||||||
<EditText
|
<com.kaixed.kchat.ui.widget.CustomEditText
|
||||||
android:id="@+id/et_search"
|
android:id="@+id/et_search"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@ -54,24 +59,195 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_cancel"
|
android:id="@+id/tv_cancel"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="40dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="15dp"
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginEnd="10dp"
|
||||||
|
android:gravity="center"
|
||||||
android:text="取消"
|
android:text="取消"
|
||||||
android:textColor="#576B95"
|
android:textColor="#576B95"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp" />
|
||||||
app:layout_constraintBottom_toBottomOf="@id/cv_search"
|
</LinearLayout>
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/cv_search"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/cv_search" />
|
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/view"
|
android:id="@+id/view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/divider_height"
|
android:layout_height="@dimen/divider_height"
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:background="@color/gray"
|
android:background="@color/gray"
|
||||||
app:layout_constraintTop_toBottomOf="@id/cv_search" />
|
app:layout_constraintTop_toBottomOf="@id/ll_search" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rv_chat_history"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/view" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/cl_select"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/view"
|
||||||
|
app:layout_constraintVertical_bias="0.1">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:text="搜索指定内容"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_date"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1.0"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="日期"
|
||||||
|
android:textColor="@color/normal"
|
||||||
|
android:textSize="13sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/tv_picture"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/tv_picture"
|
||||||
|
app:layout_constraintHorizontal_bias="0.7"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/tv_picture" />
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_picture"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1.0"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="图片与视频"
|
||||||
|
android:textColor="@color/normal"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tv_title" />
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_file"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1.0"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="文件"
|
||||||
|
android:textColor="@color/normal"
|
||||||
|
android:textSize="13sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/tv_picture"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.3"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/tv_picture"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/tv_picture" />
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_link"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1.0"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="链接"
|
||||||
|
android:textColor="@color/normal"
|
||||||
|
android:textSize="13sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/tv_songs"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/tv_songs"
|
||||||
|
app:layout_constraintHorizontal_bias="0.7"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/tv_songs" />
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_songs"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_weight="1.0"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="音乐与音频"
|
||||||
|
android:textColor="@color/normal"
|
||||||
|
android:textSize="13sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tv_picture" />
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_trade"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1.0"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="交易"
|
||||||
|
android:textColor="@color/normal"
|
||||||
|
android:textSize="13sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/tv_songs"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.3"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/tv_songs"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/tv_songs" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/view1"
|
||||||
|
android:layout_width="1dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="@color/gray"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/tv_picture"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/tv_picture"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/tv_date"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/tv_picture" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/view2"
|
||||||
|
android:layout_width="1dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="@color/gray"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/tv_picture"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/tv_file"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/tv_picture"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/tv_picture" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="1dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="@color/gray"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/tv_songs"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/view1"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/view1"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/tv_songs" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="1dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="@color/gray"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/tv_songs"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/view2"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/view2"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/tv_songs" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_search_failed"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/view"
|
||||||
|
app:layout_constraintVertical_bias="0.3" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
61
app/src/main/res/layout/chat_history_recycle_item.xml
Normal file
61
app/src/main/res/layout/chat_history_recycle_item.xml
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/white">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.utils.widget.ImageFilterView
|
||||||
|
android:id="@+id/ifv_avatar"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginVertical="10dp"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:src="@drawable/ic_default_avatar"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:roundPercent="0.3" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_nickname"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:text="糕小菜"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/tv_content"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/ifv_avatar"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/ifv_avatar" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_content"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:text="糕小菜"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/ifv_avatar"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/ifv_avatar"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tv_nickname" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_time"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="10dp"
|
||||||
|
android:text="11月9日"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/ifv_avatar" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="@dimen/divider_height"
|
||||||
|
android:background="@color/gray"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/tv_nickname"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
Loading…
Reference in New Issue
Block a user