1. 简易聊天工具
实现一个简易聊天工具需要一个消息列表,一个消息编辑框和消息发送按钮。我们利用RecyclerView控件来显示控件,并实现RecyclerView下拉来加载以往的消息。
我们用ChartMessage
记录消息内容,ChatMessageAdapter
实现了一个简单的消息界面,一个头像和文本消息。
class ChatMessageAdapter(context: Context) : RecyclerView.Adapter<ChatMessageAdapter.ItemViewHolder>() {
private val VIEW_TYPE_HEADER_VIEW = 100
private val VIEW_TYPE_CONTENT_VIEW = 200
private val mContext: Context = context
private val mContent: MutableList<ChatMessage> = ArrayList()
private val mHeaderViewList: ArrayList<View> = ArrayList()
override fun getItemViewType(position: Int): Int {
return if (position < getHeaderViewSize()) {
VIEW_TYPE_HEADER_VIEW + position
} else {
VIEW_TYPE_CONTENT_VIEW
}
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ItemViewHolder {
return if (viewType == VIEW_TYPE_CONTENT_VIEW) {
ContentViewHolder(LayoutInflater.from(mContext).inflate(R.layout.list_item_chat_message, viewGroup, false))
} else {
HeaderFooterView(mHeaderViewList[viewType - VIEW_TYPE_HEADER_VIEW])
}
}
override fun onBindViewHolder(viewHolder: ItemViewHolder, position: Int) {
if (position >= getHeaderViewSize()) {
viewHolder.bindViewHolder(position - getHeaderViewSize())
}
}
override fun getItemCount(): Int {
return getHeaderViewSize() + mContent.size
}
fun addHeaderView(headerView: View) {
this.mHeaderViewList.add(headerView)
}
open fun getHeaderViewSize(): Int {
return mHeaderViewList.size
}
fun setContent(contentList: MutableList<ChatMessage>?) {
mContent.clear()
if (contentList != null) {
mContent.addAll(contentList)
}
notifyDataSetChanged()
}
open class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
open fun bindViewHolder(position: Int) {}
}
private inner class ContentViewHolder(itemView: View) : ItemViewHolder(itemView) {
private var ivUser: ImageView = itemView.findViewById(R.id.iv_user)
private var tvMessageUser: TextView = itemView.findViewById(R.id.tv_message_user)
private var tvMessageMine: TextView = itemView.findViewById(R.id.tv_message_mine)
private var ivMine: ImageView = itemView.findViewById(R.id.iv_mine)
override fun bindViewHolder(position: Int) {
var message = mContent.getOrNull(position)
if (message?.isMine == true) {
ivUser.visibility = View.INVISIBLE
ivMine.visibility = View.VISIBLE
tvMessageUser.visibility = View.GONE
tvMessageMine.visibility = View.VISIBLE
tvMessageMine.text = message.msg_content
} else {
ivUser.visibility = View.VISIBLE
ivMine.visibility = View.INVISIBLE
tvMessageUser.visibility = View.VISIBLE
tvMessageUser.text = message?.msg_content
tvMessageMine.visibility = View.GONE
}
}
}
private class HeaderFooterView(itemView: View) : ItemViewHolder(itemView)
class ChatMessage {
var id: Int = 0
var msg_content: String? = null
var isMine: Boolean = false
}
}
ChatMessageActivity
实现了简单的界面逻辑,利用Timer
,我们我们每隔一段时间刷线最新消息。requestNewMessage()
和requestMessage()
接口分别用来刷新最新消息和下拉以前的消息。
class ChatMessageActivity : Activity() {
private lateinit var mRecyclerView: CustomRecyclerView
private lateinit var mRefreshViewCreator: RefreshViewCreator
// 第一条信息,下拉时使用
private var mFirstMessage: ChatMessageAdapter.ChatMessage? = null
// 最后一条信息,刷新时使用
private var mLastMessage: ChatMessageAdapter.ChatMessage? = null
private lateinit var mMessageAdapter: ChatMessageAdapter
private var mTimer: Timer = Timer(false)
private var mTimerTask: TimerTask = object : TimerTask() {
override fun run() {
requestNewMessage()
}
}
private val mRefreshListener: RefreshViewCreator.IOnRefreshListener =
object : RefreshViewCreator.IOnRefreshListener {
override fun onRefresh() {
requestMessage()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_application_chat_message)
mRecyclerView = findViewById(R.id.recycler_view)
mMessageAdapter = ChatMessageAdapter(this)
mRecyclerView.adapter = mMessageAdapter
mRefreshViewCreator = CustomRefreshViewCreator(mRefreshListener)
mRecyclerView.setRefreshViewCreator(mRefreshViewCreator)
mMessageAdapter.addHeaderView(mRefreshViewCreator.onCreateRefreshView(this, mRecyclerView)!!)
}
override fun onResume() {
super.onResume()
mTimer.scheduleAtFixedRate(mTimerTask, 0, 10000)
}
override fun onPause() {
super.onPause()
mTimer.cancel()
}
// 刷新最新消息
private fun requestNewMessage() {
}
// 下拉刷新
private fun requestMessage() {
}
}
显示如下
2. 聊天工具优化
简易聊天工具界面已经搭建好了,但是在实际应用中,我们遇到了三个需要优化的点,
- 软键盘弹出时会覆盖掉最新的消息内容。
- 刷新到最新信息时没有及时显示。
- 下拉刷新时显示最新消息,无法定位,体验不佳。
2.1 优化软键盘
你需要监听软键盘显示的事件,详见Android 监听软键盘显示和隐藏,同时我们滚动列表到指定位置,
window.decorView.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
// 获取当前屏幕内容的高度
var screenHeight = window.decorView.height
// 获取View可见区域的bottom
var rect = Rect()
window.decorView.getWindowVisibleDisplayFrame(rect)
// 需要避免底下导航栏的显示和隐藏操作引发的问题
if (screenHeight - rect.bottom > 300) {
if (!mShowSoftKeyBoard) {
mShowSoftKeyBoard = true
if (mRecyclerView.canScrollVertically(1)) {
mRecyclerView.scrollBy(0, screenHeight - rect.bottom)
}
}
} else {
mShowSoftKeyBoard = false
}
}
2.2 优化刷新最新消息
当RecyclerView
有最新消息,如何判断是否需要滚动到最新消息呢,我们通过canScrollVertically(direction)
来判断是否在等待新消息。
private fun addMessageList(messageList: MutableList<ChatMessageAdapter.ChatMessage>) {
if (messageList.isNotEmpty()) {
var scrollToBottom = !mRecyclerView.canScrollVertically(1)
mMessageList.addAll(messageList)
mFirstMessage = mMessageList.first()
mLastMessage = mMessageList.last()
mMessageAdapter.setContent(mMessageList)
if (scrollToBottom) {
mRecyclerView.smoothScrollToPosition(mMessageAdapter.itemCount - 1)
}
}
}
2.3 优化下拉刷新
下拉后,理想的状态是我们保持所有的消息当前所处的位置。所以我们计算获取新消息的高度总和,并进行偏移计算。
private fun insertMessageList(messageList: MutableList<ChatMessageAdapter.ChatMessage>) {
if (messageList.isNotEmpty()) {
mMessageList.addAll(0, messageList)
mFirstMessage = mMessageList.first()
mLastMessage = mMessageList.last()
mMessageAdapter.setContent(mMessageList)
var totalHeight = 0
for (index in 0 until messageList.size) {
totalHeight += getItemHeight(index)
}
mRecyclerView.scrollBy(0, totalHeight)
}
mRefreshViewCreator.refreshFinish()
}
private fun getItemHeight(position: Int): Int {
var viewHolder = mMessageAdapter.onCreateViewHolder(mRecyclerView,
mMessageAdapter.getItemViewType(mMessageAdapter.getHeaderViewSize() + position))
viewHolder.bindViewHolder(position)
viewHolder.itemView.measure(
View.MeasureSpec.makeMeasureSpec(mRecyclerView.width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED))
return viewHolder.itemView.measuredHeight
}
效果如下
当然,我们有时候会显示消息的时间,这样导致数据高度会变化,可以如下优化。
var layoutManager = mRecyclerView.layoutManager as LinearLayoutManager
var itemHeight = layoutManager.findViewByPosition(1)?.height ?: 0
var totalHeight = 0
for (index in 0 until messageList.size + 1) {
totalHeight += getItemHeight(index)
}
mRecyclerView.scrollBy(0, totalHeight - itemHeight)