前言
本文将对Socket通讯进行进一步的优化,并增加新的功能,具体改变了那些,一起来看。效果如下图所示:
正文
本文的优化,从逻辑、UI和功能三个方向上进行,之前的代码实际上是有一些逻辑问题。
一、增加线程池
之前在使用的过程中,每一次发送一条消息就会新建一个线程,这无疑是不可取的,而现在我们通过一个线程池来管理,对多个线程进行统一地管理,避免资源竞争中出现的问题,对线程进行复用,线程在执行完任务后不会立刻销毁,而会等待另外的任务,这样就不会频繁地创建、销毁线程和调用GC。
那么问题又来了,既然线程池有这么多的好处,为什么作者一开始不用呢?
emm… 我一开始没想那么多,没有想过这个Socket会去写系列文章,现在写也不晚嘛!嗯,就是这样!
① 增加服务端线程池
打开SocketServer,在里面声明线程池,代码如下:
private var serverThreadPool: ExecutorService? = null
在发送消息到客户端的时候对这个线程池进行初始化,并且执行子线程,修改sendToClient()函数,代码如下:
fun sendToClient(msg: String) { if (serverThreadPool == null) { serverThreadPool = Executors.newCachedThreadPool() } serverThreadPool?.execute { if (socket == null) { mCallback.otherMsg("客户端还未连接") return@execute } if (socket!!.isClosed) { mCallback.otherMsg("Socket已关闭") return@execute } outputStream = socket!!.getOutputStream() try { outputStream.write(msg.toByteArray()) outputStream.flush() } catch (e: IOException) { e.printStackTrace() mCallback.otherMsg("向客户端发送消息: $msg 失败") } } }
在发送消息之前,先检查socket 是否为null,因为有可能你在还没有客户端连接的时候就给客户端发送消息,不做处理的话,会导致空指针异常,程序闪退。同时将异常消息通过otherMsg()回调到页面上,页面上可以使用showMsg()函数告知用户。
而当我们停止服务的时候也需要关闭线程池,修改stopServer()函数,代码如下:
fun stopServer() { socket?.apply { shutdownInput() shutdownOutput() close() } serverSocket?.close() //关闭线程池 serverThreadPool?.shutdownNow() serverThreadPool = null }
② 增加客户端线程池
打开SocketClient,在里面声明线程池,代码如下:
private var clientThreadPool: ExecutorService? = null
在发送消息到服务端的时候对这个线程池进行初始化,并且执行子线程,修改sendToServer()函数,代码如下:
fun sendToServer(msg: String) { if (clientThreadPool == null) { clientThreadPool = Executors.newSingleThreadExecutor() } clientThreadPool?.execute { if (socket == null) { mCallback.otherMsg("客户端还未连接") return@execute } if (socket!!.isClosed) { mCallback.otherMsg("Socket已关闭") return@execute } outputStream = socket?.getOutputStream() try { outputStream?.write(msg.toByteArray()) outputStream?.flush() } catch (e: IOException) { e.printStackTrace() mCallback.otherMsg("向服务端发送消息: $msg 失败") } } }
在发送消息之前,先检查socket 是否为null,因为有可能你在客户端没连接到服务端的时候就给服务端发送消息,不做处理的话,会导致空指针异常,程序闪退。同时将异常消息通过otherMsg()回调到页面上,页面上可以使用showMsg()函数告知用户,这里和服务端的处理类似。
而当我们关闭客户端连接的时候也需要关闭线程池,修改closeConnect()函数,代码如下:
fun closeConnect() { inputStreamReader?.close() outputStream?.close() socket?.close() //关闭线程池 clientThreadPool?.shutdownNow() clientThreadPool = null }
写完这些,建议你运行一下,说不定就会报错,运行之后效果和之前是一样的,但是我们避免了一些问题的出现,虽然你感觉不到,但是这很有必要。
二、修改表情出现布局
在修改之前,我们先来看看之前的是什么效果,点击表情的时候出现了底部弹窗,弹窗覆盖了布局布局,同时页面上有阴影,如下图所示:
我们再来看看QQ的:
QQ的会将输入框布局顶上去,我们现在是覆盖了,那么我们怎么做到顶上去呢?
① BottomSheet使用
Android中的布局可以实现这样的功能,因为底部是一样的,所以可以写在一起,目前我们先这么来写,后续可能会有改动。在layout下新建一个bottom_sheet_edit.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" android:id="@+id/bottom_sheet" android:layout_width="match_parent" android:layout_height="300dp" android:background="@color/white" android:orientation="vertical" app:behavior_hideable="true" app:behavior_peekHeight="50dp" app:layout_behavior="@string/bottom_sheet_behavior"> <!--底部显示的内容--> <LinearLayout android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center_vertical" android:paddingStart="8dp" android:paddingEnd="8dp"> <androidx.appcompat.widget.AppCompatImageView android:id="@+id/iv_emoji" android:layout_width="36dp" android:layout_height="36dp" android:layout_marginEnd="8dp" android:src="@drawable/ic_emoji" /> <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> <!--底部弹出的内容--> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_emoji" android:overScrollMode="never" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
这个布局是这样的效果。
这里的50dp是指底部显示的高度,底部的列表就用来装载表情。然后我们需要使用CoordinatorLayout(协调布局)来进行配置。
② CoordinatorLayout使用
在修改之前,先在colors.xml中增加一个颜色,代码如下:
<color name="bg_color">#F8F8F8</color>
这个颜色作为页面的背景色,然后我们修改activity_server.xml布局,代码如下:
<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout 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:background="@color/bg_color" tools:context=".ui.ServerActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginBottom="50dp" android:orientation="vertical"> <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:padding="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="match_parent" /> </LinearLayout> <include android:id="@+id/lay_bottom_sheet_edit" layout="@layout/bottom_sheet_edit" /> </androidx.coordinatorlayout.widget.CoordinatorLayout>
再来修改activity_client.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout 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:background="@color/bg_color" tools:context=".ui.ClientActivity"> <LinearLayout app:layout_behavior="@string/appbar_scrolling_view_behavior" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginBottom="50dp" android:orientation="vertical"> <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:padding="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="match_parent" /> </LinearLayout> <include android:id="@+id/lay_bottom_sheet_edit" layout="@layout/bottom_sheet_edit" /> </androidx.coordinatorlayout.widget.CoordinatorLayout>
底部插入的这个布局就是我们刚才写的bottom_sheet_edit.xml,50dp此时可以在页面上显示出来。其余的部分我们需要在点击表情的使用再显示出来。
③ Activity中修改
因为布局有修改,那么对应的ServerActivity和ClientActivity也会有修改,下面这个函数在两个Activity中都需要调用,代码如下:
//是否显示表情 private var isShowEmoji = false private var bottomSheetBehavior: BottomSheetBehavior<LinearLayout>? = null private fun initBottomSheet() { //Emoji布局 bottomSheetBehavior = BottomSheetBehavior.from(binding.layBottomSheetEdit.bottomSheet).apply { state = BottomSheetBehavior.STATE_HIDDEN isHideable = false isDraggable = false } binding.layBottomSheetEdit.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] checkedEmoji(charSequence) } }) } } //显示emoji binding.layBottomSheetEdit.ivEmoji.setOnClickListener { bottomSheetBehavior!!.state = if (isShowEmoji) BottomSheetBehavior.STATE_COLLAPSED else BottomSheetBehavior.STATE_EXPANDED } bottomSheetBehavior!!.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { when (newState) { BottomSheetBehavior.STATE_EXPANDED -> {//显示 isShowEmoji = true binding.layBottomSheetEdit.ivEmoji.setImageDrawable( ContextCompat.getDrawable( this@ServerActivity, R.drawable.ic_emoji_checked ) ) } BottomSheetBehavior.STATE_COLLAPSED -> {//隐藏 isShowEmoji = false binding.layBottomSheetEdit.ivEmoji.setImageDrawable( ContextCompat.getDrawable( this@ServerActivity, R.drawable.ic_emoji ) ) } else -> isShowEmoji = false } Log.e(TAG, "onStateChanged: $newState") } override fun onSlide(bottomSheet: View, slideOffset: Float) { } }) }
这里的代码需要说明一下,我们从上往下进行说明:
//Emoji布局 bottomSheetBehavior = BottomSheetBehavior.from(binding.layBottomSheetEdit.bottomSheet).apply { state = BottomSheetBehavior.STATE_HIDDEN isHideable = false isDraggable = false }
这段代码就是在配置BottomSheetBehavior,这里我们通过from()函数获取与布局所绑定的BottomSheetBehavior,然后配置BottomSheetBehavior,设置默认状态为隐藏,当前不显示,不可上下拖动。
binding.layBottomSheetEdit.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] checkedEmoji(charSequence) } }) } }
这段代码你之前应该见过,就是那个表情弹窗中的列表适配器部分代码,这里我把它抽离到这个函数中。
//显示emoji binding.layBottomSheetEdit.ivEmoji.setOnClickListener { bottomSheetBehavior!!.state = if (isShowEmoji) BottomSheetBehavior.STATE_COLLAPSED else BottomSheetBehavior.STATE_EXPANDED }
这里就是在点击那个表情图标的时候,根据isShowEmoji的状态,来判断当前是显示还是隐藏。
//BottomSheet显示隐藏的相关处理 bottomSheetBehavior!!.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { when (newState) { BottomSheetBehavior.STATE_EXPANDED -> {//显示 isShowEmoji = true binding.layBottomSheetEdit.ivEmoji.setImageDrawable( ContextCompat.getDrawable( this@ServerActivity, R.drawable.ic_emoji_checked ) ) } BottomSheetBehavior.STATE_COLLAPSED -> {//隐藏 isShowEmoji = false binding.layBottomSheetEdit.ivEmoji.setImageDrawable( ContextCompat.getDrawable( this@ServerActivity, R.drawable.ic_emoji ) ) } else -> isShowEmoji = false } Log.e(TAG, "onStateChanged: $newState") } override fun onSlide(bottomSheet: View, slideOffset: Float) { } })
这里我们给bottomSheetBehavior添加了一个回调,主要是实现onStateChanged的方法,另一个方法适用于滑动的,用不上,这里我们在状态改变的时候修改isShowEmoji 的值,然后切换图标,这里的ic_emoji_checked图标需要补充一下,在drawable文件夹下新建一个ic_emoji_checked.xml,里面的代码如下:
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="36dp" android:height="36dp" android:tint="@color/purple_500" android:viewportWidth="24" android:viewportHeight="24"> <path android:fillColor="@color/purple_500" 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>
最后记得在initView()中调用,ServerActivity和ClientActivity一样改动。
之前在initView()中所写的如下代码:
//显示emoji binding.ivEmoji.setOnClickListener { //显示底部弹窗 showEmojiDialog(this,this) }
可以删掉了,最后将
binding.btnSendMsg
改成
binding.layBottomSheetEdit.btnSendMsg •
binding.etMsg •
改成
binding.layBottomSheetEdit.etMsg
确保当前的Activity没有报错,然后运行。下面我们通过一个GIF来看看效果。
三、业务层优化
通过上面的修改,你有没有觉得很麻烦呢?ServerActivitty和ClientActivity中写了很多一样的代码,改动也一样,一些布局也一样,那么可不可以写到一起,然后又有区分呢?
这其实的编程的思想不断进步有关系,第一篇文章,我们就是服务端和客户端写到一起的,然后在第二篇的时候觉得可以分开写,各自做各自的事情,但是会产生一些重复的代码和布局。到了现在我们看到这一点越来越明显了,那么我们就需要进一步细分,所以一些设计模式还是很有效果的,这些设计模式就是为了解决开发中实际的问题而出现的。
下面开始我们的优化之路,为了减少重复的布局,我们可以写一个基类,然后我们的客户端和服务端都继承自这个基类,各自的类里面再去根据自己的差异进行设计,这样会更合理一些,类似顶层接口设计。
① 创建基类Activity
在ui包下新建一个BaseSocketActivity,对应的布局为activity_base_socket.xml,布局代码如下:
<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout 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:background="@color/bg_color" tools:context=".ui.BaseSocketActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginBottom="50dp" android:orientation="vertical"> <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_func" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end" android:padding="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="match_parent" /> </LinearLayout> <include android:id="@+id/lay_bottom_sheet_edit" layout="@layout/bottom_sheet_edit" /> </androidx.coordinatorlayout.widget.CoordinatorLayout>
然后我们修改BaseSocketActivity中的代码:
open class BaseSocketActivity : BaseActivity() { lateinit var binding: ActivityBaseSocketBinding private val TAG = BaseSocketActivity::class.java.simpleName lateinit var etMsg: EditText lateinit var btnSendMsg: Button lateinit var ivMore: ImageView //Socket服务是否打开 var openSocket = false //Socket服务是否连接 var connectSocket = false //消息列表 private val messages = ArrayList<Message>() //消息适配器 private lateinit var msgAdapter: MsgAdapter //是否显示表情 private var isShowEmoji = false private var bottomSheetBehavior: BottomSheetBehavior<LinearLayout>? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityBaseSocketBinding.inflate(layoutInflater) setContentView(binding.root) } }
② 标题设置
这是基本的页面,创建了一些变量,下面我们对这个基类进行丰富,首先是设置标题,在BaseSocketActivity 中新增setTitle()函数,代码如下:
private fun setTitle( mTitle: String, mSubtitle: String = "", funcTitle: String, click: View.OnClickListener) { binding.toolbar.apply { title = mTitle subtitle = mSubtitle setNavigationOnClickListener { onBackPressed() } } binding.tvFunc.text = funcTitle binding.tvFunc.setOnClickListener(click) }
这里就是设置Toolbar的标题和子标题,已经Navigation图标点击事件处理,最后设置标题栏右侧的点击事件,我们可以再写两个函数分别处理客户端和服务端的标题,代码如下:
fun setServerTitle(startService: View.OnClickListener) = setTitle("服务端", "IP:${getIp()}", "开启服务", startService) fun setClientTitle(connectService: View.OnClickListener) = setTitle(mTitle = "客户端", funcTitle = "连接服务", click = connectService)
③ 开启服务和停止服务
下面我们再写一些函数,开始服务,代码如下:
fun startServer() { openSocket = true SocketServer.startServer(this) showMsg("开启服务") binding.tvFunc.text = "关闭服务" }
停止服务,代码如下:
fun stopServer() { openSocket = false SocketServer.stopServer() showMsg("关闭服务") binding.tvFunc.text = "开启服务" }
④ 连接服务和关闭连接
连接服务,代码如下:
fun connectServer(ipAddress: String) { connectSocket = true SocketClient.connectServer(ipAddress, this) showMsg("连接服务") binding.tvFunc.text = "关闭连接" }
关闭连接,代码如下:
fun closeConnect() { connectSocket = false SocketClient.closeConnect() showMsg("关闭连接") binding.tvFunc.text = "连接服务" }
⑤ 实现接口回调
在实现接口回调之前,我需要先修改一下ServerCallback,代码如下:
interface ServerCallback { //接收客户端的消息 fun receiveClientMsg(ipAddress: String, msg: String) //其他消息 fun otherMsg(msg: String) }
修改一下ClientCallback,代码如下:
interface ClientCallback { //接收服务端的消息 fun receiveServerMsg(ipAddress: String, msg: String) //其他消息 fun otherMsg(msg: String) }
这里主要就是在接收消息的时候将IP地址也传递过来,下面我们修改SocketClient中的ClientThread类中的代码:
class ClientThread(private val socket: Socket, private val callback: ClientCallback) : Thread() { override fun run() { val inputStream: InputStream? try { inputStream = socket.getInputStream() val buffer = ByteArray(1024) var len: Int var receiveStr = "" if (inputStream.available() == 0) { Log.e(TAG, "inputStream.available() == 0") } while (inputStream.read(buffer).also { len = it } != -1) { receiveStr += String(buffer, 0, len, Charsets.UTF_8) if (len < 1024) { socket.inetAddress.hostAddress?.let { callback.receiveServerMsg(it, receiveStr) } receiveStr = "" } } } catch (e: IOException) { e.printStackTrace() e.message?.let { Log.e("socket error", it) } } } }
实际上主要是这一行代码:
socket.inetAddress.hostAddress?.let { callback.receiveServerMsg(it, receiveStr) }
然后修改SocketServer中的ServerThread类中的代码:
class ServerThread(private val socket: Socket, private val callback: ServerCallback) :Thread() { override fun run() { val inputStream: InputStream? try { inputStream = socket.getInputStream() val buffer = ByteArray(1024) var len: Int var receiveStr = "" if (inputStream.available() == 0) { Log.e(TAG, "inputStream.available() == 0") } while (inputStream.read(buffer).also { len = it } != -1) { receiveStr += String(buffer, 0, len, Charsets.UTF_8) if (len < 1024) { socket.inetAddress.hostAddress?.let { callback.receiveClientMsg(it, receiveStr) } receiveStr = "" } } } catch (e: IOException) { e.printStackTrace() e.message?.let { Log.e("socket error", it) } } } }
主要是这一行代码:
socket.inetAddress.hostAddress?.let { callback.receiveClientMsg(it, receiveStr) }
然后BaseSocketActivity实现三个接口,ServerCallback、ClientCallback和
实现里面的回调,代码如下:
override fun receiveClientMsg(ipAddress: String, msg: String) { } override fun receiveServerMsg(ipAddress: String, msg: String) { } override fun otherMsg(msg: String) { } override fun checkedEmoji(charSequence: CharSequence) { }
这几个函数,需要在里面再写代码,稍等一会儿。
⑥ 消息处理
之前的消息是分服务端和客户端的,而实际上是需要,发送消息的一方在右边,收到的消息在左边,这里我们首先修改一下Message类,代码如下:
data class Message(val isMyself: Boolean, val msg: String)
然后我们修改一下item_rv_msg.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" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="8dp" android:paddingBottom="8dp" android:orientation="vertical"> <RelativeLayout android:id="@+id/lay_other" android:layout_width="match_parent" android:layout_height="wrap_content"> <com.google.android.material.imageview.ShapeableImageView android:id="@+id/iv_other" android:layout_width="60dp" android:layout_height="60dp" android:layout_marginStart="16dp" android:src="@drawable/icon_server" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:shapeAppearanceOverlay="@style/circleImageStyle" /> <TextView android:id="@+id/tv_other_msg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:background="@drawable/shape_left_msg_bg" android:text="123" android:textColor="@color/black" android:layout_toEndOf="@id/iv_other" /> </RelativeLayout> <RelativeLayout android:id="@+id/lay_myself" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/tv_myself_msg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:layout_toStartOf="@id/iv_myself" android:background="@drawable/shape_right_msg_bg" android:text="123" android:textColor="@color/white" app:layout_constraintEnd_toStartOf="@+id/iv_myself" app:layout_constraintTop_toTopOf="@+id/iv_myself" /> <com.google.android.material.imageview.ShapeableImageView android:id="@+id/iv_myself" android:layout_width="60dp" android:layout_alignParentEnd="true" android:layout_marginEnd="16dp" android:layout_height="60dp" android:src="@drawable/icon_client" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:shapeAppearanceOverlay="@style/circleImageStyle" /> </RelativeLayout> </LinearLayout>
修改适配器MsgAdapter,代码如下:
class MsgAdapter(private val messages: ArrayList<Message>) : RecyclerView.Adapter<MsgAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(ItemRvMsgBinding.inflate(LayoutInflater.from(parent.context), parent, false)) override fun onBindViewHolder(holder: ViewHolder, position: Int) { val message = messages[position] if (message.isMyself) { holder.mView.tvMyselfMsg.text = message.msg } else { holder.mView.tvOtherMsg.text = message.msg } holder.mView.layOther.visibility = if (message.isMyself) View.GONE else View.VISIBLE holder.mView.layMyself.visibility = if (message.isMyself) View.VISIBLE else View.GONE } override fun getItemCount() = messages.size class ViewHolder(itemView: ItemRvMsgBinding) : RecyclerView.ViewHolder(itemView.root) { var mView: ItemRvMsgBinding init { mView = itemView } } }
然后在BaseSocketActivity中进行消息适配器的处理,新增initView(),代码如下:
private fun initView() { etMsg = binding.layBottomSheetEdit.etMsg btnSendMsg = binding.layBottomSheetEdit.btnSendMsg ivMore = binding.layBottomSheetEdit.ivMore //初始化BottomSheet initBottomSheet() //初始化列表 msgAdapter = MsgAdapter(messages) binding.rvMsg.apply { layoutManager = LinearLayoutManager(this@BaseSocketActivity) adapter = msgAdapter } }
新增initBottomSheet()函数,代码如下:
private fun initBottomSheet() { //Emoji布局 bottomSheetBehavior = BottomSheetBehavior.from(binding.layBottomSheetEdit.bottomSheet).apply { state = BottomSheetBehavior.STATE_HIDDEN isHideable = false isDraggable = false } //表情列表适配器 binding.layBottomSheetEdit.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] checkedEmoji(charSequence) } }) } } //显示emoji binding.layBottomSheetEdit.ivEmoji.setOnClickListener { bottomSheetBehavior!!.state = if (isShowEmoji) BottomSheetBehavior.STATE_COLLAPSED else BottomSheetBehavior.STATE_EXPANDED } //BottomSheet显示隐藏的相关处理 bottomSheetBehavior!!.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { isShowEmoji = when (newState) { BottomSheetBehavior.STATE_EXPANDED -> {//显示 binding.layBottomSheetEdit.ivEmoji.setImageDrawable( ContextCompat.getDrawable(this@BaseSocketActivity, R.drawable.ic_emoji_checked) ) true } BottomSheetBehavior.STATE_COLLAPSED -> {//隐藏 binding.layBottomSheetEdit.ivEmoji.setImageDrawable( ContextCompat.getDrawable(this@BaseSocketActivity, R.drawable.ic_emoji) ) false } else -> false } } override fun onSlide(bottomSheet: View, slideOffset: Float){} }) }
消息列表代码,新增updateList()函数,代码如下:
private fun updateList(isMyself: Boolean, msg: String) { messages.add(Message(isMyself, msg)) runOnUiThread { (if (messages.size == 0) 0 else messages.size - 1).apply { msgAdapter.notifyItemChanged(this) binding.rvMsg.smoothScrollToPosition(this) } } }
最后我们修改一下之前的接口函数,代码如下:
override fun receiveClientMsg(ipAddress: String, msg: String) = updateList(false, msg) override fun receiveServerMsg(ipAddress: String, msg: String) = updateList(false, msg) override fun otherMsg(msg: String) { Log.d(TAG, "otherMsg: $msg") } override fun checkedEmoji(charSequence: CharSequence) { etMsg.apply { setText(text.toString() + charSequence) setSelection(text.toString().length)//光标置于最后 } }
这里直接在处理消息的时候,只要是接收到的消息,就是其他的消息,这样好区分一些。
⑦ 发送消息
发送消息有两种,发给服务端和客户端,在BaseSocketActivity,新增代码如下:
fun sendToClient(msg: String) { SocketServer.sendToClient(msg) etMsg.setText("") updateList(true, msg) } fun sendToServer(msg: String) { SocketClient.sendToServer(msg) etMsg.setText("") updateList(true, msg) }
现在底部的代码就写完了,下面我们修改上层的代码。
四、上层优化
不出意外的话,目前你的ServerActivity和ClientActivity肯定是报错的,我们可以不管他,你也可以把它们删除掉,对应的布局也删掉。
① ServerPlusActivity
下面我们在ui包下创建ServerPlusActivity,代码如下:
class ServerPlusActivity : BaseSocketActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //开启服务/停止服务 setServerTitle { if (openSocket) stopServer() else startServer() } //发送消息给服务端 btnSendMsg.setOnClickListener { val msg = etMsg.text.toString().trim() if (msg.isEmpty()) { showMsg("请输入要发送的信息");return@setOnClickListener } //检查是否能发送消息 val isSend = if (openSocket) openSocket else false if (!isSend) { showMsg("当前未开启服务或连接服务");return@setOnClickListener } sendToClient(msg) } } }
这里就很简单了,通过继承BaseSocketActivity(),然后调用之前写好的方法就可以了,相比之前就简化了很多,也去掉了很多重复代码。
② ClientPlusActivity
然后再来看客户端的代码,在ui包下新建一个ClientPlusActivity,代码如下:
class ClientPlusActivity: BaseSocketActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //连接服务/关闭服务 setClientTitle { if (connectSocket) closeConnect() else showEditDialog() } //发送消息给服务端 btnSendMsg.setOnClickListener { val msg = etMsg.text.toString().trim() if (msg.isEmpty()) { showMsg("请输入要发送的信息");return@setOnClickListener } //检查是否能发送消息 val isSend = if (connectSocket) connectSocket else false if (!isSend) { showMsg("当前未开启服务或连接服务");return@setOnClickListener } sendToServer(msg) } } private fun showEditDialog() { val dialogBinding = DialogEditIpBinding.inflate(LayoutInflater.from(this@ClientPlusActivity), null, false) AlertDialog.Builder(this@ClientPlusActivity).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 } connectServer(ip) dialog.dismiss() } setNegativeButton("取消") { dialog, _ -> dialog.dismiss() } }.show() } }
相比于服务端,客户端代码稍微多一点,不过也比较简单。最后我们修改一下AndroidManifest.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.llw.socket"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <application android:name=".SocketApp" android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SocketDemo" tools:targetApi="31"> <activity android:name=".ui.ServerPlusActivity" android:exported="false" /> <activity android:name=".ui.ClientPlusActivity" android:exported="false" /> <activity android:name=".ui.BaseSocketActivity" android:exported="false" android:windowSoftInputMode="adjustResize"/> <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> </application> </manifest>
最后修改一下SelectTypeActivity中的代码,点击时跳转到我们刚才所写的ServerPlusActivity和ClientPlusActivity,代码如下:
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(ServerPlusActivity::class.java) } findViewById<Button>(R.id.btn_client).setOnClickListener { jumpActivity(ClientPlusActivity::class.java) } } }
经过这么长时间的代码编写,我们也该来看看效果了。
五、源码
如果你觉得代码对你有帮助的话,不妨Fork或者Star一下~