一、聊天界面实战
本文作为第8篇博客的实战案例,将运用我们上一篇文章所学的知识开发一个较为复杂的聊天界面。
1.1 制作9-Patch图片
9-patch是一种缩放不失真或缩放不变形的图片格式,常用于Android系统的聊天框实现。它两处个特点:
- 文件名以.9.png结尾。
- 图像边缘具有1像素宽度的边框。
那9-Patch图片和普通图片有什么区别呢?例如,我们给LinearLayout设置一个北背景图片,当宽度指定为跟随父类u,将高度设置为50dp,这时会发现图片被拉伸了。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@drawable/message_left"
tools:context=".MainActivity">
</LinearLayout>
图片被拉伸的效果:
这种效果看起来很差,但是通过9-Patch图片就不会出现这种问题。接下来我们来学习一下如何制作9-Patch图片:右键目标图片(以message_left.png为例) ——> Creat 9-Patch file。这里我们就创建了一张9.png的图片。
我们可以通过鼠标在图片的四个边框绘制黑色线条。
- 上边框和左边框的黑色线条表示当图片需要拉伸时,就拉伸黑色线条对应的区域。
- 下边框和右边框的绘制部分表示允许放置内容的区域。
message_left.9.png:
此时我们再来运行一下程序,你会发现图片需要拉伸的时候,就会只拉伸我们指定的区域了。
1.2 编写聊天界面
首先你需要添加RecyclerView的依赖,这里就不再赘述了。然后开始编写主界面的布局文件activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#d8e0e8"
android:orientation="vertical"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/inputText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Type something here"
android:maxLines="2"
android:weight="1"
tools:ignore="Suspicious0dp" />
<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send" />
</LinearLayout>
</LinearLayout>
接下来我们要创建消息实体类Msg,这里需要补充几个知识点。
- 在Msg实体类中我们将TYPE_RECEIVED和TYPE_SENT两个成员声明在companion object中,这样可以让我们直接通过类名直接访问,而不需要创建类的实例。
- 我们还将TYPE_RECEIVED和TYPE_SENT通过const关键字定义为常量。需要注意,const常量关键字只能在单例类、companion object、顶层方法中才能使用!
Kotlin中,顶层方法是指直接定义在文件中的方法,而不是定义在类、接口或对象中。这种方法可以直接使用,无需创建对象。
以下是一个顶层方法的声明:
//顶层方法 直接声明在文件在
fun printMessage(message: String) {
println(message)
}
以下是顶层方法的调用:
//调用顶层方法 直接使用方法名
printMessage("Hello, world!")```
好啦,到这里你应该就能顺利看懂下面的Msg实体类了:
消息内容 消息类型
class Msg(val content: String, val type: Int) {
companion object {
//表明是一条收到的消息
const val TYPE_RECEIVED = 0
//表明是一条发送的消息
const val TYPE_SENT = 1
}
}
创建RecyclerView的子项布局,新建接收消息的布局msg_left_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:background="@drawable/message_left">
<TextView
android:id="@+id/leftMsg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:margin="10dp"
android:textColor="#fff" />
</LinearLayout>
</FrameLayout>
新建发送消息的布局msg_right_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:background="@drawable/message_right">
<TextView
android:id="@+id/rightMsg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:margin="10dp"
android:textColor="#000" />
</LinearLayout>
</FrameLayout>
接下来创建RecyclerView的适配器MsgAdapter。在MsgAdapter中我们声明了两个ViewHolder内部类,分别用来缓存msg_left_item和msg_right_item。然后重写了getItemViewType(),该方法会返回当前position的消息类型。在onCreateViewHolder方法中根据不同的viewType来创建不同的界面。
RecyclerView的ViewHolder用于缓存RecyclerView列表子项的控件
class MsgAdapter(val msgList: List<Msg>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
//内部类ViewHolder用于缓存列表项控件
inner class LeftViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val leftMsg: TextView = view.findViewById(R.id.leftMsg)
}
//发送消息的ViewHolder 缓存msg_right_item布局中的控件
inner class RightViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val rightMsg: TextView = view.findViewById(R.id.rightMsg)
}
//获取当前position的消息类型
override fun getItemViewType(position: Int): Int {
val msg = msgList[position]
return msg.type
}
viewType的值由getItemViewType决定
|
V
//用于创建ViewHolder对象
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
//根据不同的viewType创建不同的界面
return if (viewType == Msg.TYPE_RECEIVED) {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.msg_left_item, parent, false)
LeftViewHolder(view)
} else {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.msg_right_item, parent, false)
RightViewHolder(view)
}
}
//用于对RecyclerView列表项进行赋值
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val msg = msgList[position]
when (holder) {
is LeftViewHolder -> holder.leftMsg.text = msg.content
is RightViewHolder -> holder.rightMsg.text = msg.content
else -> throw IllegalArgumentException()
}
}
//用于告诉RecyclerView一共有多少列表项
override fun getItemCount(): Int {
return msgList.size
}
}
修改MainActivity的代码:
class MainActivity : AppCompatActivity(), View.OnClickListener {
//Msg数据列表
private val msgList = ArrayList<Msg>()
//Msg适配器
private var myAdapter: MsgAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//初始化Msg数据列表
initMsg()
//创建线性布局管理器并设置到RecyclerView上
val myLayoutManager = LinearLayoutManager(this)
myRecyclerView.layoutManager = myLayoutManager
//初始化Msg适配器并设置到RecyclerView上
myAdapter = MsgAdapter(msgList)
myRecyclerView.adapter = myAdapter
//为发送按钮设置点击事件
send_button.setOnClickListener(this)
}
override fun onClick(v: View?) {
when (v) {
send_button -> {
//获取输入框中的内容
val content = inputText.text.toString()
//若输入框中有内容
if (content.isNotEmpty()) {
//将输入框中内容和消息类型添加至消息列表
val msg = Msg(content, Msg.TYPE_SENT)
msgList.add(msg)
//通知RecyclerView列表有新的数据插入
myAdapter?.notifyItemInserted(msgList.size - 1)
//定位到RecyclerView列表的最后一行
myRecyclerView.scrollToPosition(msgList.size - 1)
//清空输入框中的内容
inputText.setText("")
}
}
}
}
private fun initMsg() {
//为msgList的元素添加内容和type字段值
val msg1 = Msg("Hello guy.", Msg.TYPE_RECEIVED)
val msg2 = Msg("Hello.Who's asking?", Msg.TYPE_SENT)
val msg3 = Msg("It's Mike.Nice to meet u !", Msg.TYPE_RECEIVED)
msgList.add(msg1)
msgList.add(msg2)
msgList.add(msg3)
}
}
在MainActivity中我们首先创建了Msg数据列表msgList和Msg适配器myAdapter。在onCreate()方法中我们首先对Msg数据列表进行初始化操作,然后创建了一个布局管理并将其配置到RV上。之后我们让MsgAdapter初始化,并添加到RV上。
在send_button按钮的点击事件中我们先判断EditText是否有内容,若有内容则将内容和消息类型添加到msgList列表中。然后通过Adapter.notifyItemInserted()方法告诉RecyclerView列表有新数据要插入,并通过RecyclerView.scrollToPosition()方法定位到RecyclerView列表的最后一行,保证总是可以看到最后一行消息。
运行效果图:
发送消息: