前语
在之前写的Socket通讯中,完结了基本的客户端和服务端的通讯,功用比较简单,本文将对功用进行一次升级。完结后效果图如下:
正文
功用升级分为两个环节,页面优化,音讯类型添加。首要来说便是页面的优化,之前悉数写在一个页面里边,虽然可行,可是会显得很臃肿,不利于解读和保护。
一、页面优化
页面优化内容仍是比较多的,首要要做的便是分离页面。在com.llw.socket包下新建一个ui包。
① 分包
这个包下新建一个BaseActivity,代码如下:
open class BaseActivity: AppCompatActivity() {
protected fun getIp() =
intToIp((applicationContext.getSystemService(WIFI_SERVICE) as WifiManager).connectionInfo.ipAddress)
private fun intToIp(ip: Int) =
"${(ip and 0xFF)}.${(ip shr 8 and 0xFF)}.${(ip shr 16 and 0xFF)}.${(ip shr 24 and 0xFF)}"
protected fun showMsg(msg: String) = Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
protected open fun jumpActivity(clazz: Class<*>?) = startActivity(Intent(this, clazz))
}
这儿是作为一个基类,后续咱们写关于Activity的都放在这个下面。
在com.llw.socket包下新建一个adapter包,将MsgAdapter移到adapter包下。
在com.llw.socket包下新建一个bean包,将Message移到bean包下。
② 创立ServerActivity
在创立之前咱们需求改动一下样式,由于之前是运用自带的ActionBar,现在咱们需求去掉,改成NoActionBar,如下图所示:
然后在drawable文件夹下新建一个ic_back_black.xml,作为页面的回来图标,代码如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="#000000"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/white"
android:pathData="M19,11H7.83l4.88,-4.88c0.39,-0.39 0.39,-1.03 0,-1.42l0,0c-0.39,-0.39 -1.02,-0.39 -1.41,0l-6.59,6.59c-0.39,0.39 -0.39,1.02 0,1.41l6.59,6.59c0.39,0.39 1.02,0.39 1.41,0l0,0c0.39,-0.39 0.39,-1.02 0,-1.41L7.83,13H19c0.55,0 1,-0.45 1,-1l0,0C20,11.45 19.55,11 19,11z" />
</vector>
在ui包下新建一个ServerActivity,对应布局是activity_server.xml,布局代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.ServerActivity">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/purple_500"
app:navigationIcon="@drawable/ic_back_black"
app:navigationIconTint="@color/white"
app:subtitleTextColor="@color/white"
app:title="服务端"
app:titleTextColor="@color/white">
<TextView
android:id="@+id/tv_start_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="16dp"
android:text="敞开服务"
android:textColor="@color/white"
android:textSize="14sp" />
</com.google.android.material.appbar.MaterialToolbar>
<LinearLayout
android:id="@+id/lay_client"
android:layout_width="match_parent"
android:layout_height="110dp"
android:orientation="vertical"
android:visibility="gone">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/op_code_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_ip_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:digits="0123456789."
android:hint="衔接Ip地址"
android:inputType="number"
android:lines="1"
android:singleLine="true"
android:text="192.168.0.120" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btn_connect_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="衔接服务"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_start_service" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_msg"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/et_msg"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:background="@drawable/shape_et_bg"
android:gravity="center_vertical"
android:hint="发送给客户端"
android:padding="10dp"
android:textSize="14sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_send_msg"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="发送"
app:cornerRadius="8dp" />
</LinearLayout>
</LinearLayout>
然后咱们再修正一下ServerActivity中的代码,如下所示:
class ServerActivity : BaseActivity(), ServerCallback {
private val TAG = ServerActivity::class.java.simpleName
private lateinit var binding: ActivityServerBinding
//Socket服务是否翻开
private var openSocket = false
//音讯列表
private val messages = ArrayList<Message>()
//音讯适配器
private lateinit var msgAdapter: MsgAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityServerBinding.inflate(layoutInflater)
setContentView(binding.root)
initView()
}
private fun initView() {
binding.toolbar.apply {
subtitle = "IP:${getIp()}"
setNavigationOnClickListener { onBackPressed() }
}
//敞开服务/封闭服务 服务端处理
binding.tvStartService.setOnClickListener {
openSocket = if (openSocket) {
SocketServer.stopServer();false
} else SocketServer.startServer(this)
//显现日志
showMsg(if (openSocket) "敞开服务" else "封闭服务")
//改动按钮文字
binding.tvStartService.text = if (openSocket) "封闭服务" else "敞开服务"
}
//发送音讯给客户端
binding.btnSendMsg.setOnClickListener {
val msg = binding.etMsg.text.toString().trim()
if (msg.isEmpty()) {
showMsg("请输入要发送的信息");return@setOnClickListener
}
//查看是否能发送音讯
val isSend = if (openSocket) openSocket else false
if (!isSend) {
showMsg("当时未敞开服务或衔接服务");return@setOnClickListener
}
SocketServer.sendToClient(msg)
binding.etMsg.setText("")
updateList(1, msg)
}
//初始化列表
msgAdapter = MsgAdapter(messages)
binding.rvMsg.apply {
layoutManager = LinearLayoutManager(this@ServerActivity)
adapter = msgAdapter
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) {
android.R.id.home -> onBackPressed()
}
return super.onOptionsItemSelected(item)
}
/**
* 接收到客户端发的音讯
*/
override fun receiveClientMsg(success: Boolean, msg: String) = updateList(2, msg)
override fun otherMsg(msg: String) {
Log.d(TAG, msg)
}
/**
* 更新列表
*/
private fun updateList(type: Int, msg: String) {
messages.add(Message(type, msg))
runOnUiThread {
(if (messages.size == 0) 0 else messages.size - 1).apply {
msgAdapter.notifyItemChanged(this)
binding.rvMsg.smoothScrollToPosition(this)
}
}
}
}
在这儿我首要承继自BaseActivity,这样能够运用父类的办法,然后实现ServerCallback,就能够收到客户端发送过来的音讯。一起咱们不必再去判别当时是客户端仍是服务端,由于当咱们进入这个页面那便是服务端了。
③ 创立ClientActivity
在ui包下新建一个ClientActivity,对应布局是activity_client.xml,布局代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.ClientActivity">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/purple_500"
app:navigationIcon="@drawable/ic_back_black"
app:navigationIconTint="@color/white"
app:title="客户端"
app:titleTextColor="@color/white">
<TextView
android:id="@+id/tv_connect_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="16dp"
android:text="衔接服务"
android:textColor="@color/white"
android:textSize="14sp" />
</com.google.android.material.appbar.MaterialToolbar>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_msg"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/et_msg"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:background="@drawable/shape_et_bg"
android:gravity="center_vertical"
android:hint="发送给服务端"
android:padding="10dp"
android:textSize="14sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_send_msg"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="发送"
app:cornerRadius="8dp" />
</LinearLayout>
</LinearLayout>
然后咱们再来看ClientActivity的代码,如下所示:
class ClientActivity : BaseActivity(), ClientCallback {
private val TAG = BaseActivity::class.java.simpleName
private lateinit var binding: ActivityClientBinding
//Socket服务是否衔接
private var connectSocket = false
//音讯列表
private val messages = ArrayList<Message>()
//音讯适配器
private lateinit var msgAdapter: MsgAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityClientBinding.inflate(layoutInflater)
setContentView(binding.root)
initView()
}
private fun initView() {
binding.toolbar.setNavigationOnClickListener { onBackPressed() }
//衔接服务/断开衔接 客户端处理
binding.tvConnectService.setOnClickListener {
if (connectSocket) {
SocketClient.closeConnect()
connectSocket = false
showMsg("封闭衔接")
} else {
showEditDialog()
}
binding.tvConnectService.text = if (connectSocket) "封闭衔接" else "衔接服务"
}
//发送音讯给服务端
binding.btnSendMsg.setOnClickListener {
val msg = binding.etMsg.text.toString().trim()
if (msg.isEmpty()) {
showMsg("请输入要发送的信息");return@setOnClickListener
}
//查看是否能发送音讯
val isSend = if (connectSocket) connectSocket else false
if (!isSend) {
showMsg("当时未敞开服务或衔接服务");return@setOnClickListener
}
SocketClient.sendToServer(msg)
binding.etMsg.setText("")
updateList(2, msg)
}
//初始化列表
msgAdapter = MsgAdapter(messages)
binding.rvMsg.apply {
layoutManager = LinearLayoutManager(this@ClientActivity)
adapter = msgAdapter
}
}
private fun showEditDialog() {
val dialogBinding =
DialogEditIpBinding.inflate(LayoutInflater.from(this@ClientActivity), null, false)
AlertDialog.Builder(this@ClientActivity).apply {
setIcon(R.drawable.ic_connect)
setTitle("衔接Ip地址")
setView(dialogBinding.root)
setPositiveButton("确定") { dialog, _ ->
val ip = dialogBinding.etIpAddress.text.toString()
if (ip.isEmpty()) {
showMsg("请输入Ip地址");return@setPositiveButton
}
connectSocket = true
SocketClient.connectServer(ip, this@ClientActivity)
showMsg("衔接服务")
binding.tvConnectService.text = "封闭衔接"
dialog.dismiss()
}
setNegativeButton("取消") { dialog, _ -> dialog.dismiss() }
}.show()
}
/**
* 接收到服务端发的音讯
*/
override fun receiveServerMsg(msg: String) = updateList(1, msg)
override fun otherMsg(msg: String) {
Log.d(TAG, msg)
}
/**
* 更新列表
*/
private fun updateList(type: Int, msg: String) {
messages.add(Message(type, msg))
runOnUiThread {
(if (messages.size == 0) 0 else messages.size - 1).apply {
msgAdapter.notifyItemChanged(this)
binding.rvMsg.smoothScrollToPosition(this)
}
}
}
}
在这儿,咱们相同承继自BaseActivity,不同的是这儿实现了ClientCallback,用于接收服务端发送的音讯。进入这个页面咱们能够专注的处理客户端的事务逻辑代码。
这儿的客户端由于要输入服务端的ip地址,而我又不期望ip地址的输入框占据页面的空间,因而我这儿用了一个弹窗来做ip地址的输入。弹窗中有一个自定义布局,在layout下新建一个dialog_edit_ip.xml,代码如下:
<?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">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_ip_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:digits="0123456789."
android:hint="Ip地址"
android:inputType="number"
android:lines="1"
android:singleLine="true"/>
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
弹窗中用到一个图标,在drawable文件夹下新建ic_connect.xml文件,代码如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/white"
android:pathData="M7,4c0,-1.11 -0.89,-2 -2,-2S3,2.89 3,4s0.89,2 2,2S7,5.11 7,4zM10.19,4.5L10.19,4.5c-0.41,0 -0.76,0.25 -0.92,0.63C8.83,6.23 7.76,7 6.5,7h-3C2.67,7 2,7.67 2,8.5V11h6V8.74c1.43,-0.45 2.58,-1.53 3.12,-2.91C11.38,5.19 10.88,4.5 10.19,4.5zM19,17c1.11,0 2,-0.89 2,-2s-0.89,-2 -2,-2s-2,0.89 -2,2S17.89,17 19,17zM20.5,18h-3c-1.26,0 -2.33,-0.77 -2.77,-1.87c-0.15,-0.38 -0.51,-0.63 -0.92,-0.63h0c-0.69,0 -1.19,0.69 -0.94,1.33c0.55,1.38 1.69,2.46 3.12,2.91V22h6v-2.5C22,18.67 21.33,18 20.5,18zM17.25,11.09c0,0 0,-0.01 0.01,0c-1.06,0.27 -1.9,1.11 -2.17,2.17c0,0 0,-0.01 0,-0.01C14.98,13.68 14.58,14 14.11,14c-0.55,0 -1,-0.45 -1,-1c0,-0.05 0.02,-0.14 0.02,-0.14c0.43,-1.85 1.89,-3.31 3.75,-3.73c0.04,0 0.08,-0.01 0.12,-0.01c0.55,0 1,0.45 1,1C18,10.58 17.68,10.98 17.25,11.09zM18,6.06c0,0.51 -0.37,0.92 -0.86,0.99c0,0 0,0 0,0c-3.19,0.39 -5.7,2.91 -6.09,6.1c0,0 0,0 0,0C10.98,13.63 10.56,14 10.06,14c-0.55,0 -1,-0.45 -1,-1c0,-0.02 0,-0.04 0,-0.06c0,-0.01 0,-0.02 0,-0.03c0.5,-4.12 3.79,-7.38 7.92,-7.85c0,0 0.01,0 0.01,0C17.55,5.06 18,5.51 18,6.06z" />
</vector>
由于服务端和客户端页面的底部都是输入框,因而相同要防止输入框弹出使页面整体向上移动的状况,所以咱们需求改动一下AndroidManifest.xml中的activity标签。
<activity
android:name=".ui.ClientActivity"
android:exported="false"
android:windowSoftInputMode="adjustResize"/>
<activity
android:name=".ui.ServerActivity"
android:exported="false"
android:windowSoftInputMode="adjustResize"/>
实际上首要便是添加这行代码:
android:windowSoftInputMode="adjustResize"
④ 挑选类型
现在服务端和客户端都有了,那么咱们还需求一个进口,用来挑选是服务端仍是客户端。在ui包下新建一个SelectTypeActivity类,对应的布局是activity_select_type.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.SelectTypeActivity">
<com.google.android.material.appbar.MaterialToolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/purple_500"
app:title="挑选类型"
app:titleTextColor="@color/white" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/btn_server"
android:layout_width="240dp"
android:layout_height="120dp"
android:layout_marginBottom="20dp"
android:text="服务端"
android:textSize="18sp" />
<Button
android:id="@+id/btn_client"
android:layout_width="240dp"
android:layout_height="120dp"
android:layout_marginTop="20dp"
android:text="客户端"
android:textSize="18sp" />
</LinearLayout>
</LinearLayout>
然后咱们看一下SelectTypeActivity的代码:
class SelectTypeActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_select_type)
findViewById<Button>(R.id.btn_server).setOnClickListener {
jumpActivity(ServerActivity::class.java)
}
findViewById<Button>(R.id.btn_client).setOnClickListener {
jumpActivity(ClientActivity::class.java)
}
}
}
这儿的代码就很简单,将他作为进口,跳转到ServerActivity和ClientActivity,然后咱们翻开AndroidManifest.xml,一起修正一下MainActivity和SelectTypeActivity所对应的activity标签,修正后的代码如下:
<activity
android:name=".ui.SelectTypeActivity"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.MainActivity"
android:exported="false"/>
这儿便是将SelectTypeActivity作为发动页面,一起MainActivity已经不再运用了,你能够删去也能够留着它,现在运转看看效果。
二、表情音讯
说到表情音讯,实际上便是emoji。基本上每个社交App都会有emoji,国内的基本上用的是自定义的库,国外的便是选用emojipedia中的居多,而在Jetpack组件中就有一个关于emoji的库。
① 依靠Emoji2库
一开始实际上是emoji库,后来emoji2的呈现替换了emoji,由于emoji2的兼容性更强,那么怎样去运用它呢?翻开app下的build.gradle,在dependencies{}闭包下添加如下代码:
def emoji2_version = "1.2.0"
implementation "androidx.emoji2:emoji2:$emoji2_version"
implementation "androidx.emoji2:emoji2-views:$emoji2_version"
implementation "androidx.emoji2:emoji2-views-helper:$emoji2_version"
implementation 'androidx.emoji2:emoji2-bundled:1.0.0-alpha03'
这儿要注意一点,运用最新版的emoji2需求你的appcompat库在1.4.0及以上版本。
也便是这个库,添加好依靠之后点击Sync Now进行下载依靠同步。
② 初始化Emoji2库
运用Emoji2库,有两种办法,一种是运用可下载的,一种是本地绑定的,可下载需求支持Google的GMS服务,本地的不需求,可是会造成apk的巨细添加许多。当然我这个运用首要就用不了GMS,由于是在国内,一起我还不介意运用巨细,所以选用第二种本地绑定的办法。
下面咱们来进行初始化操作,首要咱们在com.llw.socket创立一个SocketApp,代码如下:
class SocketApp : Application() {
private val TAG = SocketApp::class.java.simpleName
companion object {
private var instance: SocketApp by Delegates.notNull()
fun instance() = instance
}
@SuppressLint("RestrictedApi")
override fun onCreate() {
super.onCreate()
instance = this
initEmoji2()
}
/**
* 初始化Emoji2
*/
private fun initEmoji2() = EmojiCompat.init(BundledEmojiCompatConfig(this).apply {
setReplaceAll(true)
registerInitCallback(object : InitCallback() {
override fun onInitialized() {
//初始化成功回调
Log.d(TAG, "onInitialized")
}
override fun onFailed(@Nullable throwable: Throwable?) {
//初始化失败回调
Log.e(TAG, throwable.toString())
}
})
})
}
这儿咱们运用单例,然后初始化Emoji2,BundledEmojiCompatConfig便是绑定本地的办法,你能够挑选生成apk看看里边占内存最大的是什么内容。经过SocketApp中初始化,在页面运用的时分能够就能够无所顾忌了。为了让App在发动的时分调用SocketApp,咱们需求在AndroidManifest.xml中注册。
③ 加载Emoji列表
由于用的是本地的,所以我自己找了几个表情,在main文件夹下新建一个assets文件夹,文件夹下新建一个emoji.txt,里边的内容如下:
仿制进去是这样的,如下图所示:
然后咱们再初始化的时分将这些表情包加载到列表中,相同在SocketApp中完结,回到SocketApp中,新增一个emojiList,代码如下:
val emojiList = arrayListOf<CharSequence>()
然后写一个loadEmoji()函数,代码如下:
private fun loadEmoji() {
val inputStream = assets.open("emoji.txt")
BufferedReader(InputStreamReader(inputStream)).use {
var line: String
while (true) {
line = it.readLine() ?: break
emojiList.add(line)
}
}
}
最终调用,如下图所示:
④ 修正UI
之前服务端和客户端的布局代码中没有表情的进口,现在就需求了,首要咱们预备一个图标,在drawable下新增一个ic_emoji.xml,代码如下:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM15.5,11c0.83,0 1.5,-0.67 1.5,-1.5S16.33,8 15.5,8 14,8.67 14,9.5s0.67,1.5 1.5,1.5zM8.5,11c0.83,0 1.5,-0.67 1.5,-1.5S9.33,8 8.5,8 7,8.67 7,9.5 7.67,11 8.5,11zM12,17.5c2.03,0 3.8,-1.11 4.75,-2.75 0.19,-0.33 -0.05,-0.75 -0.44,-0.75L7.69,14c-0.38,0 -0.63,0.42 -0.44,0.75 0.95,1.64 2.72,2.75 4.75,2.75z" />
</vector>
然后咱们在activity_server.xml和activity_client.xml中的底部布局中添加一个ImageView。
<ImageView
android:id="@+id/iv_emoji"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="8dp"
android:src="@drawable/ic_emoji" />
如图所示:
这儿UI就修正好了,下面咱们挑选点击这个图标的时分呈现一个底部弹窗,弹窗中显现表情列表。
⑤ 表情适配器
由于运用了一个RecyclerView,因而咱们还需求创立适配器的布局,在layout下新建一个item_emoji.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.emoji2.widget.EmojiTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tv_emoji"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="12dp"
tools:ignore="MissingDefaultResource" />
这儿咱们用的便是依靠库中的控件。
下面咱们创立一个适配器,在adapter下新建一个EmojiAdapter,代码如下:
class EmojiAdapter(private val emojis: ArrayList<CharSequence>) :
RecyclerView.Adapter<EmojiAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ViewHolder(ItemEmojiBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val emoji = emojis[position]
holder.mView.tvEmoji.apply {
text = EmojiCompat.get().process(emoji)
setOnClickListener { clickListener?.onItemClick(position) }
}
}
override fun getItemCount() = emojis.size
class ViewHolder(itemView: ItemEmojiBinding) : RecyclerView.ViewHolder(itemView.root) {
var mView: ItemEmojiBinding
init {
mView = itemView
}
}
interface OnClickListener {
fun onItemClick(position: Int)
}
private var clickListener: OnClickListener? = null
fun setOnItemClickListener(listener: OnClickListener) {
clickListener = listener
}
}
这儿还添加了一个点击事件,需求在点击适配器的时分进行处理。
⑥ 表情弹窗
首要创立弹窗的布局,在layout下新建一个dialog_emoji.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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="MissingDefaultResource">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_emoji"
android:layout_width="match_parent"
android:layout_height="300dp"/>
</LinearLayout>
下面咱们就需求写一个显现弹窗的办法了,由于这个办法在服务端和客户端都会用到,因而,我挑选写在BaseActivity中,这儿由于服务端和客户端页面上需求获取点击的表情,因而写一个接口,在ui包下新建一个EmojiCallback接口,代码如下:
interface EmojiCallback {
/**
* 选中Emoji
*/
fun checkedEmoji(charSequence: CharSequence)
}
然后咱们回到BaseActivity中,添加showEmojiDialog()函数,代码如下:
protected fun showEmojiDialog(context: Context, callback: EmojiCallback) {
val emojiBinding = DialogEmojiBinding.inflate(LayoutInflater.from(context), null, false)
val dialog = BottomSheetDialog(this)
dialog.setContentView(emojiBinding.root)
emojiBinding.rvEmoji.apply {
layoutManager = GridLayoutManager(context, 6)
adapter = EmojiAdapter(SocketApp.instance().emojiList).apply {
setOnItemClickListener(object : EmojiAdapter.OnClickListener {
override fun onItemClick(position: Int) {
val charSequence = SocketApp.instance().emojiList[position]
callback.checkedEmoji(charSequence)
dialog.dismiss()
}
})
}
}
dialog.show()
}
这儿咱们先获取布局,然后装备弹窗,再装备弹窗中的列表,再装备列表的适配器,最终再点击适配器时回调接口到页面上。
⑦ 页面运用
一切安排妥当了,下面进入服务端页面ServerActivity,首要实现EmojiCallback回调,在页面中重写checkedEmoji()函数,代码如下:
override fun checkedEmoji(charSequence: CharSequence) {
binding.etMsg.apply {
setText(text.toString() + charSequence)
setSelection(text.toString().length)//光标置于最终
}
}
这儿便是点击表情之后将表情写到输入框中,一起将光标置于最终。
然后咱们需求在点击表情那个ImageView的时分显现底部弹窗,在initView()函数中新增如下代码:
//显现emoji
binding.ivEmoji.setOnClickListener {
//显现底部弹窗
showEmojiDialog(this,this)
}
这个页面运用的代码,ClientActivity和ServerActivity的代码完全一致,就不过多的赘述了。都安排妥当之后咱们运转一下:
三、源码
如果你觉得代码对你有协助的话,无妨Fork或者Star一下~
源码地址:SocketDemo