Android studio前沿开发(二)---上千行代码,数万字内容,教你做一个简单的微信系统

想做一个类似微信的交流软件吗,今天我们就通过websocket来做一个小微信,样式图如下:
请添加图片描述
请添加图片描述
请添加图片描述

前置学习

在学习这个博客之前,要学习一下网络通信的使用:
Android studio进阶开发(三)–socket通信服务的使用
Android studio前沿开发–利用socket服务器连接AI实现前后端交互(全站首发思路)
Android studio进阶开发(四)–okhttp的网络通信的使用
由于本次篇幅较大,就不在讲述其中的知识与内容了,直接放代码了。

客户端

Java代码:

ViewUtil.java:

package com.example.myapplication;

import android.app.Activity;
import android.content.Context;
import android.view.View;
import android.view.inputmethod.InputMethodManager;

public class ViewUtil {

    public static void hideAllInputMethod(Activity act) {
        // 从系统服务中获取输入法管理器
        InputMethodManager imm = (InputMethodManager) act.getSystemService(Context.INPUT_METHOD_SERVICE);
        if (imm.isActive()) { // 软键盘如果已经打开则关闭之
            imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);
        }
    }

    public static void hideOneInputMethod(Activity act, View v) {
        // 从系统服务中获取输入法管理器
        InputMethodManager imm = (InputMethodManager) act.getSystemService(Context.INPUT_METHOD_SERVICE);
        // 关闭屏幕上的输入法软键盘
        imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
    }

}

Utils.java:

package com.example.myapplication;

import android.content.Context;

public class Utils {
    // 根据手机的分辨率从 dp 的单位 转成为 px(像素)
    public static int dip2px(Context context, float dpValue) {
        // 获取当前手机的像素密度(1个dp对应几个px)
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f); // 四舍五入取整
    }

    // 根据手机的分辨率从 px(像素) 的单位 转成为 dp
    public static int px2dip(Context context, float pxValue) {
        // 获取当前手机的像素密度(1个dp对应几个px)
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f); // 四舍五入取整
    }

    // 获得屏幕的宽度
    public static int getScreenWidth(Context ctx) {
        return ctx.getResources().getDisplayMetrics().widthPixels;
//        int screenWidth;
//        // 从系统服务中获取窗口管理器
//        WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
//        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
//            // 获取当前屏幕的四周边界
//            Rect rect = wm.getCurrentWindowMetrics().getBounds();
//            screenWidth = rect.width();
//        } else {
//            DisplayMetrics dm = new DisplayMetrics();
//            // 从默认显示器中获取显示参数保存到dm对象中
//            wm.getDefaultDisplay().getMetrics(dm);
//            screenWidth = dm.widthPixels;
//        }
//        return screenWidth; // 返回屏幕的宽度数值
    }

    // 获得屏幕的高度
    public static int getScreenHeight(Context ctx) {
        return ctx.getResources().getDisplayMetrics().heightPixels;
//        int screenHeight;
//        // 从系统服务中获取窗口管理器
//        WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
//        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
//            // 获取当前屏幕的四周边界
//            Rect rect = wm.getCurrentWindowMetrics().getBounds();
//            screenHeight = rect.height();
//        } else {
//            DisplayMetrics dm = new DisplayMetrics();
//            // 从默认显示器中获取显示参数保存到dm对象中
//            wm.getDefaultDisplay().getMetrics(dm);
//            screenHeight = dm.heightPixels;
//        }
//        return screenHeight; // 返回屏幕的高度数值
    }

}


SocketUtil.java:

package com.example.myapplication;

import android.app.Activity;
import android.util.Log;
import android.widget.Toast;

import com.google.gson.Gson;

import org.json.JSONObject;

import java.net.InetSocketAddress;
import java.net.SocketAddress;

import io.socket.client.Socket;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;

public class SocketUtil {

    private static final String TAG = "SocketUtil";

    // 发送数据到Socket服务器
    public static void emit(Activity act, Socket socket, String event, Object obj) {
        try {
            if (obj == null) {
                throw new IllegalArgumentException("对象数据不能为null");
            }
            JSONObject json = new JSONObject(new Gson().toJson(obj));
            socket.emit(event, json);
            Log.i(TAG, "数据发送成功: " + json.toString());
        } catch (Exception e) {
            Log.e(TAG, "发送数据失败", e);
            act.runOnUiThread(() -> {
                Toast.makeText(act, "发送数据失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
            });
        }
    }

    // 检查Socket服务器是否可连接
    public static void initSocketConnection(Activity act) {
        String serverUrl = "http://" + NetConst.CHAT_IP + ":" + NetConst.CHAT_PORT;
        SocketManager.init(serverUrl);
    }

    public static void checkServerAvailable(Activity act) {
        SocketManager.init("http://" + NetConst.CHAT_IP + ":" + NetConst.CHAT_PORT);
        SocketManager.mSocket.once(Socket.EVENT_CONNECT, args -> {
            act.runOnUiThread(() ->
                    Toast.makeText(act, "服务器可连接", Toast.LENGTH_SHORT).show());
            SocketManager.disconnect();
        });
        SocketManager.mSocket.once(Socket.EVENT_CONNECT_ERROR, args -> {
            act.runOnUiThread(() ->
                    Toast.makeText(act, "服务器不可达", Toast.LENGTH_SHORT).show());
        });
    }
}

SocketManager.java:

package com.example.myapplication;

import android.util.Log;

import com.google.gson.Gson;

import org.json.JSONException;
import org.json.JSONObject;

import io.socket.client.IO;
import io.socket.client.Socket;
import io.socket.emitter.Emitter;

public class SocketManager {
    private static final String TAG = "SocketManager";
    static Socket mSocket;
    private static String mUserName;

    private static JSONObject safeConvert(Object data) throws JSONException {
        if (data instanceof JSONObject) {
            return (JSONObject) data;
        }
        String jsonStr = new Gson().toJson(data);
        return new JSONObject(jsonStr);
    }

    public static void init(String serverUrl) {
        try {
            IO.Options options = new IO.Options();
            options.transports = new String[]{"websocket"};
            options.reconnection = true;
            mSocket = IO.socket(serverUrl, options);

            // 注册连接监听
            mSocket.on(Socket.EVENT_CONNECT, onConnect);
            mSocket.on(Socket.EVENT_DISCONNECT, onDisconnect);
            mSocket.on(Socket.EVENT_CONNECT_ERROR, onError);

            mSocket.connect();
        } catch (Exception e) {
            Log.e(TAG, "Socket初始化失败", e);
        }
    }

    private static Emitter.Listener onConnect = args -> {
        Log.i(TAG, "Socket连接成功");
        if (mUserName != null) {
            // 发送上线通知
            mSocket.emit("self_online", mUserName);
        }
    };

    private static Emitter.Listener onDisconnect = args -> {
        Log.w(TAG, "Socket断开连接");
        if (mUserName != null) {
            mSocket.emit("self_offline", mUserName);
        }
    };

    private static Emitter.Listener onError = args -> {
        Log.e(TAG, "连接错误: " + args[0].toString());
    };

    public static void setUserName(String name) {
        mUserName = name;
    }

    public static void sendMessage(String event, Object data) {
        if (mSocket == null || !mSocket.connected()) {
            Log.w(TAG, "尝试发送消息时Socket未连接");
            return;
        }

        try {
            // 空数据检查
            if (data == null) {
                throw new IllegalArgumentException("发送数据不能为null");
            }

            // 安全转换
            JSONObject json = safeConvert(data);
            mSocket.emit(event, json);
            Log.i(TAG, "消息发送成功:" + json.toString());
        } catch (JSONException e) {
            Log.e(TAG, "JSON转换失败: " + e.getMessage());
            // 添加错误上报逻辑
            reportError(e);
        } catch (Exception e) {
            Log.e(TAG, "发送消息异常: " + e.getClass().getSimpleName());
        }
    }

    // 示例错误上报方法
    private static void reportError(Throwable e) {
        // 这里可以集成Crashlytics等错误上报SDK
        Log.e(TAG, "需要上报的异常", e);
    }

    public static void disconnect() {
        if (mSocket != null) {
            mSocket.disconnect();
            mSocket.off();
        }
    }
}

RoundDrawable.java:

package com.example.myapplication;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader.TileMode;
import android.graphics.drawable.BitmapDrawable;

import com.example.myapplication.Utils;

public class RoundDrawable extends BitmapDrawable {
    private Paint mPaint = new Paint(); // 声明一个画笔对象
    private int mRoundRadius; // 圆角的半径

    public RoundDrawable(Context ctx, Bitmap bitmap) {
        super(ctx.getResources(), bitmap);
        // 创建一个位图着色器,CLAMP表示边缘拉伸
        BitmapShader shader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP);
        mPaint.setShader(shader); // 设置画笔的着色器对象
        mRoundRadius = Utils.dip2px(ctx, 8);
    }

    @Override
    public void draw(Canvas canvas) {
        RectF rect = new RectF(0, 0, getBitmap().getWidth(), getBitmap().getHeight());
        // 在画布上绘制圆角矩形,也就是只显示圆角矩形内部的图像
        canvas.drawRoundRect(rect, mRoundRadius, mRoundRadius, mPaint);
    }

}

ReceiveFile.java:

package com.example.myapplication;

public class ReceiveFile {
    public String lastFile; // 上次的文件名
    public int receiveCount; // 接收包的数量
    public byte[] receiveData; // 收到的字节数组

    public ReceiveFile() {
    }

    public ReceiveFile(String lastFile, int receiveCount, int partLength) {
        this.lastFile = lastFile;
        this.receiveCount = receiveCount;
        this.receiveData = new byte[partLength];
    }
}

NoScrollListView.java:

package com.example.myapplication;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.ListView;

public class NoScrollListView extends ListView {

    public NoScrollListView(Context context) {
        super(context);
    }

    public NoScrollListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public NoScrollListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    // 重写onMeasure方法,以便自行设定视图的高度
    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 将高度设为最大值,即所有项加起来的总高度
        int expandSpec = MeasureSpec.makeMeasureSpec(
                Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }
}

WeChatAdapter.java:

package com.example.myapplication;

import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;

import com.example.myapplication.FriendListFragment;
import com.example.myapplication.GroupListFragment;
import com.example.myapplication.MyInfoFragment;

public class WeChatAdapter extends FragmentPagerAdapter {
    // 碎片页适配器的构造方法,传入碎片管理器
    public WeChatAdapter(FragmentManager fm) {
        super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
    }

    // 获取指定位置的碎片Fragment
    @Override
    public Fragment getItem(int position) {
        if (position == 0) {
            return new FriendListFragment();  // 返回第一个碎片
        } else if (position == 1) {
            return new GroupListFragment();  // 返回第二个碎片
        } else if (position == 2) {
            return new MyInfoFragment();  // 返回第三个碎片
        } else {
            return null;
        }
    }

    // 获取碎片Fragment的个数
    @Override
    public int getCount() {
        return 3;
    }
}

NetConst.java:

package com.example.myapplication;

public class NetConst {
    // HTTP地址的前缀
    public final static String HTTP_PREFIX = "http://192.168.1.7:8080/HttpServer/";
    // WebSocket服务的前缀
    public final static String WEBSOCKET_PREFIX = "ws://192.168.1.7:8080/HttpServer/";
    public final static String BASE_IP = "192.168.43.8"; // 基础Socket服务的ip
    public final static int BASE_PORT = 9010; // 基础Socket服务的端口
    public final static String CHAT_IP = "192.168.43.8"; // 聊天Socket服务的ip(改为你自己电脑的地址)
    public final static int CHAT_PORT = 9011; // 聊天Socket服务的端口
}

MyInfoFragment.java:

package com.example.myapplication;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.fragment.app.Fragment;

import com.example.myapplication.MainApplication;
import com.example.myapplication.R;
import com.example.myapplication.ChatUtil;
import com.example.myapplication.InputDialog;

import io.socket.client.Socket;

public class MyInfoFragment extends Fragment {
    private static final String TAG = "MyInfoFragment";
    protected View mView; // 声明一个视图对象
    protected Context mContext; // 声明一个上下文对象
    private ImageView iv_portrait; // 声明一个图像视图对象
    private TextView tv_nick; // 声明一个文本视图对象
    private Socket mSocket; // 声明一个套接字对象

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mContext = getActivity(); // 获取活动页面的上下文
        mView = inflater.inflate(R.layout.fragment_my_info, container, false);
        initView(); // 初始化视图
        showPortrait(); // 显示用户昵称和用户头像
        mSocket = MainApplication.getInstance().getSocket();
        return mView;
    }

    // 初始化视图
    private void initView() {
        TextView tv_title = mView.findViewById(R.id.tv_title);
        tv_title.setText("个人信息");
        mView.findViewById(R.id.iv_back).setOnClickListener(v -> getActivity().finish());
        iv_portrait = mView.findViewById(R.id.iv_portrait);
        tv_nick = mView.findViewById(R.id.tv_nick);
        mView.findViewById(R.id.ll_nick).setOnClickListener(v -> modifyNickName());
    }

    // 显示用户昵称和用户头像
    private void showPortrait() {
        // 通过 getter 方法获取微信名,并处理空值情况
        String nickName = MainApplication.getInstance().getWechatName();
        if (TextUtils.isEmpty(nickName)) {
            nickName = "默认昵称"; // 或抛出异常提示未登录
            Log.w(TAG, "微信昵称为空,使用默认值");
        }

        tv_nick.setText(nickName);
        Drawable drawable = ChatUtil.getPortraitByName(mContext, nickName);
        iv_portrait.setImageDrawable(drawable);
    }

    // 修改用户昵称
    private void modifyNickName() {
        // 通过 getter 获取当前昵称
        String oldNickName = MainApplication.getInstance().getWechatName();
        if (TextUtils.isEmpty(oldNickName)) {
            Toast.makeText(mContext, "当前昵称未初始化", Toast.LENGTH_SHORT).show();
            return;
        }

        // 弹出昵称填写对话框
        InputDialog dialog = new InputDialog(mContext, oldNickName, 0,
                "请输入新的昵称", (idt, content, seq) -> {
            // 校验新昵称有效性
            if (TextUtils.isEmpty(content)) {
                Toast.makeText(mContext, "昵称不能为空", Toast.LENGTH_SHORT).show();
                return;
            }

            // 通过 setter 更新昵称
            MainApplication.getInstance().setWechatName(content);

            // 更新UI
            showPortrait();

            // 发送改名通知(确保旧昵称和新昵称有效)
            mSocket.emit("self_offline", oldNickName);
            mSocket.emit("self_online", content); // 直接使用 content(已通过校验)
        });
        dialog.show();
    }

}

MessageInfo.java:

package com.example.myapplication;

public class MessageInfo {
    public String from; // 消息的发送者
    public String to; // 消息的接收者
    public String content; // 消息内容

    public MessageInfo(String from, String to, String content) {
        this.from = from;
        this.to = to;
        this.content = content;
    }
}

MD5Util.java:

package com.example.myapplication;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MD5Util {

    public static String encrypt(String raw) {
        String md5Str = raw;
        try {
            MessageDigest md = MessageDigest.getInstance("MD5"); // 创建一个MD5算法对象
            md.update(raw.getBytes()); // 给算法对象加载待加密的原始数据
            byte[] encryContext = md.digest(); // 调用digest方法完成哈希计算
            int i;
            StringBuffer buf = new StringBuffer("");
            for (int offset = 0; offset < encryContext.length; offset++) {
                i = encryContext[offset];
                if (i < 0) {
                    i += 256;
                }
                if (i < 16) {
                    buf.append("0");
                }
                buf.append(Integer.toHexString(i)); // 把字节数组逐位转换为十六进制数
            }
            md5Str = buf.toString(); // 拼装加密字符串
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return md5Str.toUpperCase(); // 输出大写的加密串
    }

}

MainApplication.java:

package com.example.myapplication;

import android.app.Application;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;

import io.socket.client.IO;
import io.socket.client.Socket;

public class MainApplication extends Application {
    private static final String TAG = "MainApplication";
    private static volatile MainApplication instance;
    private Socket mSocket;
    private final AtomicBoolean isConnecting = new AtomicBoolean(false);
    private String wechatName; // 修复变量缺失

    // 定义接口
    public interface SocketConnectionListener {
        void onConnected();
        void onDisconnected();
        void onError(String msg);
    }

    private SocketConnectionListener mConnectionListener;

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
    }

    public synchronized void initSocketConnection() {
        if (isConnecting.get() || (mSocket != null && mSocket.connected())) {
            Log.d(TAG, "已连接或正在连接,跳过重复请求");
            return;
        }

        isConnecting.set(true);
        new Thread(() -> {
            try {
                String uri = "http://" + NetConst.CHAT_IP + ":" + NetConst.CHAT_PORT;
                IO.Options options = new IO.Options();
                options.transports = new String[]{"websocket"};
                options.timeout = 15000;
                options.forceNew = true;
                options.reconnection = false;

                if (mSocket != null) {
                    mSocket.disconnect();
                    mSocket.close();
                }

                mSocket = IO.socket(uri, options);

                mSocket.on(Socket.EVENT_CONNECT, args -> {
                    Log.d(TAG, "Socket连接成功");
                    isConnecting.set(false);
                    runOnUi(() -> {
                        if (mConnectionListener != null) {
                            mConnectionListener.onConnected();
                        }
                    });
                }).on(Socket.EVENT_DISCONNECT, args -> {
                    Log.w(TAG, "Socket断开连接");
                    isConnecting.set(false);
                    runOnUi(() -> {
                        if (mConnectionListener != null) {
                            mConnectionListener.onDisconnected();
                        }
                    });
                }).on(Socket.EVENT_CONNECT_ERROR, args -> {
                    Log.e(TAG, "连接失败: " + Arrays.toString(args));
                    isConnecting.set(false);
                    runOnUi(() -> {
                        if (mConnectionListener != null) {
                            mConnectionListener.onError("连接失败");
                        }
                    });
                });

                mSocket.connect();
            } catch (URISyntaxException e) {
                Log.e(TAG, "URI格式错误", e);
                isConnecting.set(false);
            }
        }).start();
    }

    private void runOnUi(Runnable action) {
        new Handler(Looper.getMainLooper()).post(action);
    }

    @Nullable
    public Socket getSocket() {
        return (mSocket != null && mSocket.connected()) ? mSocket : null;
    }

    public void setConnectionListener(SocketConnectionListener listener) {
        mConnectionListener = listener;
    }

    @NonNull
    public static MainApplication getInstance() {
        if (instance == null) {
            throw new IllegalStateException("MainApplication not initialized!");
        }
        return instance;
    }

    @NonNull
    public String getWechatName() {
        return wechatName != null ? wechatName : "";
    }

    public void setWechatName(@NonNull String name) {
        if (TextUtils.isEmpty(name)) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
        this.wechatName = name;
    }

    public boolean isConnecting() {
        return isConnecting.get();
    }

    public boolean isConnected() {
        return mSocket != null && mSocket.connected();
    }
}

JoinInfo.java:

package com.example.myapplication;

public class JoinInfo {
    public String user_name; // 用户名称
    public String group_name; // 群组名称

    public JoinInfo(String user_name, String group_name) {
        this.user_name = user_name;
        this.group_name = group_name;
    }

}

InputDialog.java:

package com.example.myapplication;

import android.app.Dialog;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.EditText;
import android.widget.TextView;

import com.example.myapplication.R;

public class InputDialog {
    private Dialog mDialog; // 声明一个对话框对象
    private View mView; // 声明一个视图对象
    private String mIdt; // 当前标识
    private int mSeq; // 当前序号
    private String mTitle; // 对话框标题
    private InputCallbacks mCallbacks; // 回调监听器

    public InputDialog(Context context, String idt, int seq, String title, InputCallbacks callbacks) {
        mIdt = idt;
        mSeq = seq;
        mTitle = title;
        mCallbacks = callbacks;
        // 根据布局文件dialog_input.xml生成视图对象
        mView = LayoutInflater.from(context).inflate(R.layout.dialog_input, null);
        // 创建一个指定风格的对话框对象
        mDialog = new Dialog(context, R.style.CustomDialog);
        TextView tv_title = mView.findViewById(R.id.tv_title);
        EditText et_input = mView.findViewById(R.id.et_input);
        tv_title.setText(mTitle);
        mView.findViewById(R.id.tv_cancel).setOnClickListener(v -> dismiss());
        mView.findViewById(R.id.tv_confirm).setOnClickListener(v -> {
            dismiss(); // 关闭对话框
            mCallbacks.onInput(mIdt, et_input.getText().toString(), mSeq);
        });
    }

    // 显示对话框
    public void show() {
        // 设置对话框窗口的内容视图
        mDialog.getWindow().setContentView(mView);
        // 设置对话框窗口的布局参数
        mDialog.getWindow().setLayout(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        mDialog.show(); // 显示对话框
    }

    // 关闭对话框
    public void dismiss() {
        // 如果对话框显示出来了,就关闭它
        if (mDialog != null && mDialog.isShowing()) {
            mDialog.dismiss(); // 关闭对话框
        }
    }

    // 判断对话框是否显示
    public boolean isShowing() {
        if (mDialog != null) {
            return mDialog.isShowing();
        } else {
            return false;
        }
    }

    public interface InputCallbacks {
        void onInput(String idt, String content, int seq);
    }

}

ImagePart.java:

package com.example.myapplication;

public class ImagePart {
    private String name; // 分段名称
    private String data; // 分段数据
    private int seq; // 分段序号
    private int length; // 分段长度

    public ImagePart(String name, String data, int seq, int length) {
        this.name = name;
        this.data = data;
        this.seq = seq;
        this.length = length;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }

    public void setData(String data) {
        this.data = data;
    }

    public String getData() {
        return this.data;
    }

    public void setSeq(int seq) {
        this.seq = seq;
    }

    public int getSeq() {
        return this.seq;
    }

    public void setLength(int length) {
        this.length = length;
    }

    public int getLength() {
        return this.length;
    }

}

ImageMessage.java:

package com.example.myapplication;

public class ImageMessage {
    public String from; // 消息的发送者
    public String to; // 消息的接收者
    private ImagePart part; // 图片片段

    public ImageMessage() {
    }

    public ImageMessage(String from, String to, ImagePart part) {
        this.from = from;
        this.to = to;
        this.part = part;
    }
    
    public void setFrom(String from) {
    	this.from = from;
    }
    
    public String getFrom() {
    	return this.from;
    }

    public void setTo(String to) {
    	this.to = to;
    }
    
    public String getTo() {
    	return this.to;
    }

    public void setPart(ImagePart part) {
    	this.part = part;
    }
    
    public ImagePart getPart() {
    	return this.part;
    }
    
}

ImageDetailActivity.java:

package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.net.Uri;
import android.os.Bundle;
import android.widget.ImageView;

public class ImageDetailActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_image_detail);
        String imagePath = getIntent().getStringExtra("imagePath");
        ImageView iv_photo = findViewById(R.id.iv_photo);
        iv_photo.setImageURI(Uri.parse(imagePath)); // 设置图像视图的路径对象
        iv_photo.setOnClickListener(v -> finish());
    }
}

GroupListFragment.java:

package com.example.myapplication;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.TextView;

import androidx.fragment.app.Fragment;

import com.example.myapplication.GroupChatActivity;
import com.example.myapplication.MainApplication;
import com.example.myapplication.R;
import com.example.myapplication.EntityListAdapter;
import com.example.myapplication.EntityInfo;
import com.example.myapplication.NoScrollListView;

import java.util.ArrayList;
import java.util.List;

public class GroupListFragment extends Fragment implements AdapterView.OnItemClickListener {
    private static final String TAG = "GroupListFragment";
    protected View mView; // 声明一个视图对象
    protected Context mContext; // 声明一个上下文对象
    private NoScrollListView nslv_group; // 声明一个不滚动视图对象
    private List<EntityInfo> mGroupList = new ArrayList<>(); // 群组列表

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 简单起见固定写几个群组,读者可尝试改造为动态创建群组
        mGroupList.add(new EntityInfo("Android开发技术交流群", ""));
        mGroupList.add(new EntityInfo("摄影爱好者", ""));
        mGroupList.add(new EntityInfo("人工智能学习讨论群", ""));
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mContext = getActivity(); // 获取活动页面的上下文
        mView = inflater.inflate(R.layout.fragment_group_list, container, false);
        TextView tv_title = mView.findViewById(R.id.tv_title);
        tv_title.setText(String.format("群聊(%d)", mGroupList.size()));
        mView.findViewById(R.id.iv_back).setOnClickListener(v -> getActivity().finish());
        nslv_group = mView.findViewById(R.id.nslv_group);
        EntityListAdapter adapter = new EntityListAdapter(mContext, mGroupList);
        nslv_group.setAdapter(adapter);
        nslv_group.setOnItemClickListener(this);
        return mView;
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        EntityInfo group = mGroupList.get(position);
        // 以下跳到在指定群组聊天的界面
        Intent intent = new Intent(mContext, GroupChatActivity.class);
        intent.putExtra("self_name", MainApplication.getInstance().getWechatName());
        intent.putExtra("group_name", group.name);
        startActivity(intent);
    }

}

GroupChatActivity.java:

package com.example.myapplication;

import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.example.myapplication.ImageMessage;
import com.example.myapplication.ImagePart;
import com.example.myapplication.JoinInfo;
import com.example.myapplication.MessageInfo;
import com.example.myapplication.ReceiveFile;
import com.example.myapplication.BitmapUtil;
import com.example.myapplication.ChatUtil;
import com.example.myapplication.DateUtil;
import com.example.myapplication.SocketUtil;
import com.example.myapplication.Utils;
import com.example.myapplication.ViewUtil;
import com.google.gson.Gson;

import org.json.JSONObject;

import java.io.ByteArrayOutputStream;
import java.util.HashMap;
import java.util.Map;

import io.socket.client.Socket;

public class GroupChatActivity extends AppCompatActivity {
    private static final String TAG = "GroupChatActivity";
    private TextView tv_title; // 声明一个文本视图对象
    private EditText et_input; // 声明一个编辑框对象
    private ScrollView sv_chat; // 声明一个滚动视图对象
    private LinearLayout ll_show; // 声明一个聊天窗口的线性布局对象
    private int dip_margin; // 每条聊天记录的四周空白距离
    private int CHOOSE_CODE = 3; // 只在相册挑选图片的请求码

    private String mSelfName, mGroupName; // 自己名称,群组名称
    private Socket mSocket; // 声明一个套接字对象
    private String mMinute = "00:00"; // 时间提示
    private int mCount = 0; // 群成员数量

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_group_chat);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); // 保持屏幕常亮
        mSelfName = getIntent().getStringExtra("self_name");
        mGroupName = getIntent().getStringExtra("group_name");
        initView(); // 初始化视图
        initSocket(); // 初始化套接字
    }

    // 初始化视图
    private void initView() {
        dip_margin = Utils.dip2px(this, 5);
        tv_title = findViewById(R.id.tv_title);
        et_input = findViewById(R.id.et_input);
        sv_chat = findViewById(R.id.sv_chat);
        ll_show = findViewById(R.id.ll_show);
        findViewById(R.id.iv_back).setOnClickListener(v -> finish());
        findViewById(R.id.ib_img).setOnClickListener(v -> {
            // 创建一个内容获取动作的意图(准备跳到系统相册)
            Intent albumIntent = new Intent(Intent.ACTION_GET_CONTENT);
            albumIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false); // 是否允许多选
            albumIntent.setType("image/*"); // 类型为图像
            startActivityForResult(albumIntent, CHOOSE_CODE); // 打开系统相册
        });
        findViewById(R.id.btn_send).setOnClickListener(v -> sendMessage());
        tv_title.setText(mGroupName);
    }
    
    // 初始化套接字
    private void initSocket() {
        mSocket = MainApplication.getInstance().getSocket();
        // 等待接收群组人数通知
        mSocket.on("person_count", (args) -> {
            int count = (Integer) args[0];
            if (count > mCount) {
                mCount = (Integer) args[0];
                runOnUiThread(() -> tv_title.setText(String.format("%s(%d)", mGroupName, mCount)));
            }
        });
        // 等待接收成员入群通知
        mSocket.on("person_in_group", (args) -> {
            Log.d(TAG, "person_in_group:"+args[0]);
            runOnUiThread(() -> {
                if (!mSelfName.equals(args[0])) {
                    tv_title.setText(String.format("%s(%d)", mGroupName, ++mCount));
                }
                appendHintMsg(String.format("%s 加入了群聊", args[0]));
            });
        });
        // 等待接收成员退群通知
        mSocket.on("person_out_group", (args) -> {
            runOnUiThread(() -> {
                tv_title.setText(String.format("%s(%d)", mGroupName, --mCount));
                appendHintMsg(String.format("%s 退出了群聊", args[0]));
            });
        });
        // 等待接收群消息
        mSocket.on("receive_group_message", (args) -> {
            JSONObject json = (JSONObject) args[0];
            MessageInfo message = new Gson().fromJson(json.toString(), MessageInfo.class);
            // 往聊天窗口添加文本消息
            runOnUiThread(() -> appendChatMsg(message.from, message.content, false));
        });
        // 等待接收群图片
        mSocket.on("receive_group_image", (args) -> receiveImage(args));
        // 下面向Socket服务器发送入群通知
        JoinInfo joinInfo = new JoinInfo(mSelfName, mGroupName);
        SocketUtil.emit(GroupChatActivity.this, mSocket, "join_group", joinInfo);

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 下面向Socket服务器发送退群通知
        JoinInfo joinInfo = new JoinInfo(mSelfName, mGroupName);
        SocketUtil.emit(GroupChatActivity.this, mSocket, "leave_group", joinInfo);
        mSocket.off("person_count"); // 取消接收群组人数通知
        mSocket.off("person_in_group"); // 取消接收成员入群通知
        mSocket.off("person_out_group"); // 取消接收成员退群通知
        mSocket.off("receive_group_message"); // 取消接收群消息
        mSocket.off("receive_group_image"); // 取消接收群图片
    }

    // 发送聊天消息
    private void sendMessage() {
        String content = et_input.getText().toString();
        if (TextUtils.isEmpty(content)) {
            Toast.makeText(this, "请输入聊天消息", Toast.LENGTH_SHORT).show();
            return;
        }
        et_input.setText("");
        ViewUtil.hideOneInputMethod(this, et_input); // 隐藏软键盘
        appendChatMsg(mSelfName, content, true); // 往聊天窗口添加文本消息
        // 下面向Socket服务器发送群消息
        MessageInfo message = new MessageInfo(mSelfName, mGroupName, content);
        SocketUtil.emit(GroupChatActivity.this, mSocket, "send_group_message", message);
    }

    // 往聊天窗口添加聊天消息
    private void appendChatMsg(String name, String content, boolean isSelf) {
        appendNowMinute(); // 往聊天窗口添加当前时间
        // 把群聊消息的线性布局添加到聊天窗口上
        ll_show.addView(ChatUtil.getChatView(this, name, content, isSelf));
        // 延迟100毫秒后启动聊天窗口的滚动任务
        new Handler(Looper.myLooper()).postDelayed(() -> {
            sv_chat.fullScroll(ScrollView.FOCUS_DOWN); // 滚动到底部
        }, 100);
    }

    // 往聊天窗口添加提示消息
    private void appendHintMsg(String hint) {
        appendNowMinute(); // 往聊天窗口添加当前时间
        // 把提示消息的线性布局添加到聊天窗口上
        ll_show.addView(ChatUtil.getHintView(this, hint, dip_margin));
        // 延迟100毫秒后启动聊天窗口的滚动任务
        new Handler(Looper.myLooper()).postDelayed(() -> {
            sv_chat.fullScroll(ScrollView.FOCUS_DOWN); // 滚动到底部
        }, 100);
    }

    // 往聊天窗口添加当前时间
    private void appendNowMinute() {
        String nowMinute = DateUtil.getNowMinute();
        // 分钟数切换时才需要添加当前时间
        if (!mMinute.substring(0, 4).equals(nowMinute.substring(0, 4))) {
            mMinute = nowMinute;
            ll_show.addView(ChatUtil.getHintView(this, nowMinute, dip_margin));
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
        super.onActivityResult(requestCode, resultCode, intent);
        if (resultCode == RESULT_OK && requestCode == CHOOSE_CODE) { // 从相册返回
            if (intent.getData() != null) { // 从相册选择一张照片
                Uri uri = intent.getData(); // 获得已选择照片的路径对象
                String path = uri.toString();
                String imageName = path.substring(path.lastIndexOf("/")+1);
                // 根据指定图片的uri,获得自动缩小后的图片路径
                String imagePath = BitmapUtil.getAutoZoomPath(this, uri);
                // 往聊天窗口添加图片消息
                appendChatImage(mSelfName, imagePath, true);
                sendImage(imageName, imagePath); // 分段传输图片数据
            }
        }
    }

    private int mBlock = 50*1024; // 每段的数据包大小
    // 分段传输图片数据
    private void sendImage(String imageName, String imagePath) {
        Log.d(TAG, "sendImage");
        Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 把位图数据压缩到字节数组输出流
        bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);
        byte[] bytes = baos.toByteArray();
        int count = bytes.length/mBlock + 1;
        Log.d(TAG, "sendImage length="+bytes.length+", count="+count);
        // 下面把图片数据经过BASE64编码后发给Socket服务器
        for (int i=0; i<count; i++) {
            Log.d(TAG, "sendImage i="+i);
            String encodeData = "";
            if (i == count-1) {
                int remain = bytes.length % mBlock;
                byte[] temp = new byte[remain];
                System.arraycopy(bytes, i*mBlock, temp, 0, remain);
                encodeData = Base64.encodeToString(temp, Base64.DEFAULT);
            } else {
                byte[] temp = new byte[mBlock];
                System.arraycopy(bytes, i*mBlock, temp, 0, mBlock);
                encodeData = Base64.encodeToString(temp, Base64.DEFAULT);
            }
            // 往Socket服务器发送本段的图片数据
            ImagePart part = new ImagePart(imageName, encodeData, i, bytes.length);
            ImageMessage message = new ImageMessage(mSelfName, mGroupName, part);
            SocketUtil.emit(GroupChatActivity.this, mSocket,  "send_group_image", message);
        }
    }

    private Map<String, ReceiveFile> mFileMap = new HashMap<>();
    // 接收对方传来的图片数据
    private void receiveImage(Object... args) {
        JSONObject json = (JSONObject) args[0];
        ImageMessage message = new Gson().fromJson(json.toString(), ImageMessage.class);
        ImagePart part = message.getPart();
        if (!mFileMap.containsKey(message.getFrom())) {
            mFileMap.put(message.getFrom(), new ReceiveFile());
        }
        ReceiveFile file = mFileMap.get(message.getFrom());
        if (!part.getName().equals(file.lastFile)) { // 与上次文件名不同,表示开始接收新文件
            file = new ReceiveFile(part.getName(), 0, part.getLength());
            mFileMap.put(message.getFrom(), file);
        }
        file.receiveCount++;
        // 把接收到的图片数据通过BASE64解码为字节数组
        byte[] temp = Base64.decode(part.getData(), Base64.DEFAULT);
        System.arraycopy(temp, 0, file.receiveData, part.getSeq()*mBlock, temp.length);
        // 所有数据包都接收完毕
        if (file.receiveCount >= part.getLength()/mBlock+1) {
            // 从字节数组中解码得到位图对象
            Bitmap bitmap = BitmapFactory.decodeByteArray(file.receiveData, 0, file.receiveData.length);
            String imagePath = String.format("%s/%s.jpg",
                    getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString(),
                    DateUtil.getNowDateTime());
            BitmapUtil.saveImage(imagePath, bitmap);
            // 往聊天窗口添加图片消息
            runOnUiThread(() -> appendChatImage(message.getFrom(), imagePath, false));
        }
    }

    // 往聊天窗口添加图片消息
    private void appendChatImage(String name, String imagePath, boolean isSelf) {
        appendNowMinute(); // 往聊天窗口添加当前时间
        // 把图片消息的线性布局添加到聊天窗口上
        ll_show.addView(ChatUtil.getChatImage(this, name, imagePath, isSelf));
        // 延迟100毫秒后启动聊天窗口的滚动任务
        new Handler(Looper.myLooper()).postDelayed(() -> {
            sv_chat.fullScroll(ScrollView.FOCUS_DOWN); // 滚动到底部
        }, 100);
    }

}

FriendListFragment.java:

package com.example.myapplication;

import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.TextView;

import androidx.fragment.app.Fragment;

import com.example.myapplication.FriendChatActivity;
import com.example.myapplication.MainApplication;
import com.example.myapplication.R;
import com.example.myapplication.EntityListAdapter;
import com.example.myapplication.EntityInfo;
import com.example.myapplication.NoScrollListView;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import io.socket.client.Socket;

public class FriendListFragment extends Fragment implements AdapterView.OnItemClickListener {
    private static final String TAG = "FriendListFragment";
    protected View mView; // 声明一个视图对象
    protected Context mContext; // 声明一个上下文对象
    private TextView tv_title; // 声明一个文本视图对象
    private NoScrollListView nslv_friend; // 声明一个不滚动视图对象
    private Map<String, EntityInfo> mFriendMap = new HashMap<>(); // 好友的名称映射
    private List<EntityInfo> mFriendList = new ArrayList<>(); // 好友列表
    private EntityListAdapter mAdapter; // 好友列表适配器
    private Socket mSocket; // 声明一个套接字对象
    private Handler mHandler = new Handler(Looper.myLooper());
    private ProgressDialog progressDialog;// 声明一个处理器对象



    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mContext = getActivity(); // 获取活动页面的上下文
        mView = inflater.inflate(R.layout.fragment_friend_list, container, false);
        initView(); // 初始化视图
        initSocket(); // 初始化套接字


        return mView;
    }


    // 初始化视图
    private void initView() {
        tv_title = mView.findViewById(R.id.tv_title);
        tv_title.setText(String.format("好友(%d)", mFriendList.size()));
        mView.findViewById(R.id.iv_back).setOnClickListener(v -> getActivity().finish());
        nslv_friend = mView.findViewById(R.id.nslv_friend);
        mAdapter = new EntityListAdapter(mContext, mFriendList);
        nslv_friend.setAdapter(mAdapter);
        nslv_friend.setOnItemClickListener(this);
    }

    // 初始化套接字
    private void initSocket() {
        mSocket = MainApplication.getInstance().getSocket();
        if (mSocket != null && mSocket.connected()) {
            setupSocketListeners();
            notifySelfOnline();
            return; // 已连接则无需操作
        }

        showLoading(); // 显示加载框

        // 设置连接监听器
        MainApplication.getInstance().setConnectionListener(new MainApplication.SocketConnectionListener() {
            // FriendListFragment.java 的 onConnected 回调
            @Override
            public void onConnected() {
                Log.d(TAG, "onConnected 回调触发");
                if (!isAdded()) {
                    Log.e(TAG, "Fragment 已销毁,忽略回调");
                    return;
                }
                mSocket = MainApplication.getInstance().getSocket();
                if (mSocket != null) {
                    setupSocketListeners();
                    notifySelfOnline();
                } else {
                    Log.e(TAG, "Socket 实例为空");
                }
                hideLoading(); // 强制关闭加载框
                MainApplication.getInstance().setConnectionListener(null);
            }

            @Override
            public void onDisconnected() {
                if (!isAdded()) return;
                hideLoading();
                MainApplication.getInstance().setConnectionListener(null);
            }

            @Override
            public void onError(String msg) {
                if (!isAdded()) return;
                Log.e(TAG, "连接错误: " + msg);
                hideLoading();
                MainApplication.getInstance().setConnectionListener(null);
            }
        });

        // 触发连接操作
        MainApplication.getInstance().initSocketConnection();
    }
    private void notifySelfOnline() {
        if (mSocket != null) {
            mSocket.emit("self_online", MainApplication.getInstance().getWechatName());
        }
    }

    // FriendListFragment.java
    private void updateFriendList() {
        mFriendList.clear();
        mFriendList.addAll(mFriendMap.values());
        if (isAdded()) { // 确保 Fragment 存活
            mHandler.post(() -> {
                tv_title.setText(String.format("好友(%d)", mFriendList.size()));
                mAdapter.notifyDataSetChanged();
            });
        }
    }

    private void setupSocketListeners() {
        if (mSocket == null) {
            Log.e(TAG, "Socket is null. Ignore listener setup.");
            return;
        }

        mSocket.on("friend_online", args -> {
            String friendName = (String) args[0];
            if (friendName != null) {
                mFriendMap.put(friendName, new EntityInfo(friendName, "好友"));
                updateFriendList();
            }
        });

        mSocket.on("friend_offline", args -> {
            String friendName = (String) args[0];
            mFriendMap.remove(friendName);
            updateFriendList();
        });
    }



    @Override
    public void onDestroyView() {
        super.onDestroyView();
        hideLoading();
        // 强制释放 Socket 监听
        if (mSocket != null) {
            mSocket.off("friend_online");
            mSocket.off("friend_offline");
            mSocket.disconnect();
        }
        MainApplication.getInstance().setConnectionListener(null);
    }

    private Runnable mRefresh = () -> doRefresh(); // 好友列表的刷新任务
    // 刷新好友列表
    private void doRefresh() {
        mHandler.removeCallbacks(mRefresh); // 防止频繁刷新造成列表视图崩溃
        tv_title.setText(String.format("好友(%d)", mFriendList.size()));
        mAdapter.notifyDataSetChanged();
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        EntityInfo friend = mFriendList.get(position);
        // 以下跳到与指定好友聊天的界面
        Intent intent = new Intent(mContext, FriendChatActivity.class);
        intent.putExtra("self_name", MainApplication.getInstance().getWechatName());
        intent.putExtra("friend_name", friend.name);
        startActivity(intent);
    }
    // FriendListFragment.java
    private void showLoading() {
        if (isAdded() && getActivity() != null) { // 检查 Fragment 是否附加到 Activity
            getActivity().runOnUiThread(() -> {
                if (progressDialog == null) {
                    progressDialog = new ProgressDialog(getActivity());
                    progressDialog.setMessage("Connecting...");
                    progressDialog.setCancelable(false);
                }
                if (!progressDialog.isShowing()) {
                    progressDialog.show();
                }
            });
        }
    }

    private void hideLoading() {
        if (isAdded() && getActivity() != null) { // 检查 Fragment 是否附加到 Activity
            getActivity().runOnUiThread(() -> {
                if (progressDialog != null && progressDialog.isShowing()) {
                    progressDialog.dismiss();
                    progressDialog = null;
                }
            });
        }
    }

FriendChatActivity.java:

package com.example.myapplication;

import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.example.myapplication.ImageMessage;
import com.example.myapplication.ImagePart;
import com.example.myapplication.MessageInfo;
import com.example.myapplication.BitmapUtil;
import com.example.myapplication.ChatUtil;
import com.example.myapplication.DateUtil;
import com.example.myapplication.SocketUtil;
import com.example.myapplication.Utils;
import com.example.myapplication.ViewUtil;
import com.google.gson.Gson;

import org.json.JSONObject;

import java.io.ByteArrayOutputStream;

import io.socket.client.Socket;

public class FriendChatActivity extends AppCompatActivity {
    private static final String TAG = "FriendChatActivity";
    private EditText et_input; // 声明一个编辑框对象
    private ScrollView sv_chat; // 声明一个滚动视图对象
    private LinearLayout ll_show; // 声明一个聊天窗口的线性布局对象
    private int dip_margin; // 每条聊天记录的四周空白距离
    private int CHOOSE_CODE = 3; // 只在相册挑选图片的请求码

    private String mSelfName, mFriendName; // 自己名称,好友名称
    private Socket mSocket; // 声明一个套接字对象
    private String mMinute = "00:00"; // 时间提示

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_friend_chat);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); // 保持屏幕常亮
        mSelfName = getIntent().getStringExtra("self_name");
        mFriendName = getIntent().getStringExtra("friend_name");
        initView(); // 初始化视图
        initSocket(); // 初始化套接字
    }

    // 初始化视图
    private void initView() {
        dip_margin = Utils.dip2px(this, 5);
        TextView tv_title = findViewById(R.id.tv_title);
        et_input = findViewById(R.id.et_input);
        sv_chat = findViewById(R.id.sv_chat);
        ll_show = findViewById(R.id.ll_show);
        findViewById(R.id.iv_back).setOnClickListener(v -> finish());
        findViewById(R.id.ib_img).setOnClickListener(v -> {
            // 创建一个内容获取动作的意图(准备跳到系统相册)
            Intent albumIntent = new Intent(Intent.ACTION_GET_CONTENT);
            albumIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false); // 是否允许多选
            albumIntent.setType("image/*"); // 类型为图像
            startActivityForResult(albumIntent, CHOOSE_CODE); // 打开系统相册
        });
        findViewById(R.id.btn_send).setOnClickListener(v -> sendMessage());
        tv_title.setText(mFriendName);
    }

    // 初始化套接字
    private void initSocket() {
        mSocket = MainApplication.getInstance().getSocket();
        // 等待接收好友消息
        mSocket.on("receive_friend_message", (args) -> {
            JSONObject json = (JSONObject) args[0];
            Log.d(TAG, "receive_friend_message:"+json.toString());
            MessageInfo message = new Gson().fromJson(json.toString(), MessageInfo.class);
            // 往聊天窗口添加文本消息
            runOnUiThread(() -> appendChatMsg(message.from, message.content, false));
        });
        // 等待接收好友图片
        mSocket.on("receive_friend_image", (args) -> receiveImage(args));
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mSocket.off("receive_friend_message"); // 取消接收好友消息
        mSocket.off("receive_friend_image"); // 取消接收好友图片
    }

    // 发送聊天消息
    private void sendMessage() {
        String content = et_input.getText().toString();
        if (TextUtils.isEmpty(content)) {
            Toast.makeText(this, "请输入聊天消息", Toast.LENGTH_SHORT).show();
            return;
        }
        et_input.setText("");
        ViewUtil.hideOneInputMethod(this, et_input); // 隐藏软键盘
        appendChatMsg(mSelfName, content, true); // 往聊天窗口添加文本消息
        // 下面向Socket服务器发送聊天消息
        MessageInfo message = new MessageInfo(mSelfName, mFriendName, content);
        SocketUtil.emit(FriendChatActivity.this, mSocket, "send_friend_message", message);
    }

    // 往聊天窗口添加聊天消息
    private void appendChatMsg(String name, String content, boolean isSelf) {
        appendNowMinute(); // 往聊天窗口添加当前时间
        // 把单条消息的线性布局添加到聊天窗口上
        ll_show.addView(ChatUtil.getChatView(this, name, content, isSelf));
        // 延迟100毫秒后启动聊天窗口的滚动任务
        new Handler(Looper.myLooper()).postDelayed(() -> {
            sv_chat.fullScroll(ScrollView.FOCUS_DOWN); // 滚动到底部
        }, 100);
    }

    // 往聊天窗口添加当前时间
    private void appendNowMinute() {
        String nowMinute = DateUtil.getNowMinute();
        // 分钟数切换时才需要添加当前时间
        if (!mMinute.substring(0, 4).equals(nowMinute.substring(0, 4))) {
            mMinute = nowMinute;
            ll_show.addView(ChatUtil.getHintView(this, nowMinute, dip_margin));
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
        super.onActivityResult(requestCode, resultCode, intent);
        if (resultCode == RESULT_OK && requestCode == CHOOSE_CODE) { // 从相册返回
            if (intent.getData() != null) { // 从相册选择一张照片
                Uri uri = intent.getData(); // 获得已选择照片的路径对象
                String path = uri.toString();
                String imageName = path.substring(path.lastIndexOf("/")+1);
                // 根据指定图片的uri,获得自动缩小后的图片路径
                String imagePath = BitmapUtil.getAutoZoomPath(this, uri);
                // 往聊天窗口添加图片消息
                appendChatImage(mSelfName, imagePath, true);
                sendImage(imageName, imagePath); // 分段传输图片数据
            }
        }
    }

    private int mBlock = 50*1024; // 每段的数据包大小
    // 分段传输图片数据
    private void sendImage(String imageName, String imagePath) {
        Log.d(TAG, "sendImage");
        Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 把位图数据压缩到字节数组输出流
        bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);
        byte[] bytes = baos.toByteArray();
        int count = bytes.length/mBlock + 1;
        Log.d(TAG, "sendImage length="+bytes.length+", count="+count);
        // 下面把图片数据经过BASE64编码后发给Socket服务器
        for (int i=0; i<count; i++) {
            Log.d(TAG, "sendImage i="+i);
            String encodeData = "";
            if (i == count-1) {
                int remain = bytes.length % mBlock;
                byte[] temp = new byte[remain];
                System.arraycopy(bytes, i*mBlock, temp, 0, remain);
                encodeData = Base64.encodeToString(temp, Base64.DEFAULT);
            } else {
                byte[] temp = new byte[mBlock];
                System.arraycopy(bytes, i*mBlock, temp, 0, mBlock);
                encodeData = Base64.encodeToString(temp, Base64.DEFAULT);
            }
            // 往Socket服务器发送本段的图片数据
            ImagePart part = new ImagePart(imageName, encodeData, i, bytes.length);
            ImageMessage message = new ImageMessage(mSelfName, mFriendName, part);
            SocketUtil.emit(FriendChatActivity.this, mSocket, "send_friend_message", message);
        }
    }

    private String mLastFile; // 上次的文件名
    private int mReceiveCount; // 接收包的数量
    private byte[] mReceiveData; // 收到的字节数组
    // 接收对方传来的图片数据
    private void receiveImage(Object... args) {
        JSONObject json = (JSONObject) args[0];
        ImageMessage message = new Gson().fromJson(json.toString(), ImageMessage.class);
        ImagePart part = message.getPart();
        if (!part.getName().equals(mLastFile)) { // 与上次文件名不同,表示开始接收新文件
            mLastFile = part.getName();
            mReceiveCount = 0;
            mReceiveData = new byte[part.getLength()];
        }
        mReceiveCount++;
        // 把接收到的图片数据通过BASE64解码为字节数组
        byte[] temp = Base64.decode(part.getData(), Base64.DEFAULT);
        System.arraycopy(temp, 0, mReceiveData, part.getSeq()*mBlock, temp.length);
        // 所有数据包都接收完毕
        if (mReceiveCount >= part.getLength()/mBlock+1) {
            // 从字节数组中解码得到位图对象
            Bitmap bitmap = BitmapFactory.decodeByteArray(mReceiveData, 0, mReceiveData.length);
            String imagePath = String.format("%s/%s.jpg",
                    getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString(),
                    DateUtil.getNowDateTime());
            BitmapUtil.saveImage(imagePath, bitmap);
            // 往聊天窗口添加图片消息
            runOnUiThread(() -> appendChatImage(message.getFrom(), imagePath, false));
        }
    }

    // 往聊天窗口添加图片消息
    private void appendChatImage(String name, String imagePath, boolean isSelf) {
        appendNowMinute(); // 往聊天窗口添加当前时间
        // 把图片消息的线性布局添加到聊天窗口上
        ll_show.addView(ChatUtil.getChatImage(this, name, imagePath, isSelf));
        // 延迟100毫秒后启动聊天窗口的滚动任务
        new Handler(Looper.myLooper()).postDelayed(() -> {
            sv_chat.fullScroll(ScrollView.FOCUS_DOWN); // 滚动到底部
        }, 100);
    }

}

EntityListAdapter.java:

package com.example.myapplication;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.myapplication.R;
import com.example.myapplication.EntityInfo;
import com.example.myapplication.ChatUtil;

import java.util.List;

public class EntityListAdapter extends BaseAdapter {
    private Context mContext; // 声明一个上下文对象
    private List<EntityInfo> mUserList; // 声明一个用户信息列表

    // 用户适配器的构造方法,传入上下文与用户列表
    public EntityListAdapter(Context context, List<EntityInfo> user_list) {
        mContext = context;
        mUserList = user_list;
    }

    // 获取列表项的个数
    @Override
    public int getCount() {
        return mUserList.size();
    }

    // 获取列表项的数据
    @Override
    public Object getItem(int arg0) {
        return mUserList.get(arg0);
    }

    // 获取列表项的编号
    @Override
    public long getItemId(int arg0) {
        return arg0;
    }

    // 获取指定位置的列表项视图
    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null) { // 转换视图为空
            holder = new ViewHolder(); // 创建一个新的视图持有者
            // 根据布局文件item_user.xml生成转换视图对象
            convertView = LayoutInflater.from(mContext).inflate(R.layout.item_user, null);
            holder.iv_portrait = convertView.findViewById(R.id.iv_portrait);
            holder.tv_name = convertView.findViewById(R.id.tv_name);
            holder.tv_relation = convertView.findViewById(R.id.tv_relation);
            convertView.setTag(holder); // 将视图持有者保存到转换视图当中
        } else { // 转换视图非空
            // 从转换视图中获取之前保存的视图持有者
            holder = (ViewHolder) convertView.getTag();
        }
        EntityInfo user = mUserList.get(position);
        holder.tv_name.setText(user.name); // 显示用户的名称
        holder.tv_relation.setText(user.relation); // 显示用户的描述
        Drawable drawable = ChatUtil.getPortraitByName(mContext, user.name); // 获取用户的头像
        holder.iv_portrait.setImageDrawable(drawable); // 显示用户的头像
        return convertView;
    }

    // 定义一个视图持有者,以便重用列表项的视图资源
    public final class ViewHolder {
        public ImageView iv_portrait; // 声明用户头像的图像视图对象
        public TextView tv_name; // 声明用户名称的文本视图对象
        public TextView tv_relation; // 声明用户关系的文本视图对象
    }

}

EntityInfo.java:

package com.example.myapplication;

public class EntityInfo {
    public String name; // 实体名称
    public String relation; // 实体关系

    public EntityInfo(String name, String relation) {
        this.name = name;
        this.relation = relation;
    }
}

DateUtil.java:

package com.example.myapplication;

import android.annotation.SuppressLint;

import java.text.SimpleDateFormat;
import java.util.Date;

@SuppressLint("SimpleDateFormat")
public class DateUtil {
    // 获取当前的日期时间
    public static String getNowDateTime() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        return sdf.format(new Date());
    }

    // 获取当前的时间
    public static String getNowTime() {
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        return sdf.format(new Date());
    }

    // 获取当前的分钟
    public static String getNowMinute() {
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
        return sdf.format(new Date());
    }

    // 获取当前的时间(精确到毫秒)
    public static String getNowTimeDetail() {
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
        return sdf.format(new Date());
    }

    // 将长整型的时间数值格式化为日期时间字符串
    public static String formatDate(long time) {
        Date date = new Date(time);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(date);
    }

}

ChatUtil.java:

package com.example.myapplication;

import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.media.MediaDrm;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.example.myapplication.ImageDetailActivity;
import com.example.myapplication.R;
import com.example.myapplication.RoundDrawable;

public class ChatUtil {
    private static final String TAG = "ChatUtil";
    // 头像图片的资源数组
    public final static int[] mPortraitArray = {
            R.drawable.portrait01, R.drawable.portrait02, R.drawable.portrait03, R.drawable.portrait04,
            R.drawable.portrait05, R.drawable.portrait06, R.drawable.portrait07, R.drawable.portrait08,
            R.drawable.portrait09, R.drawable.portrait10, R.drawable.portrait11, R.drawable.portrait12,
            R.drawable.portrait13, R.drawable.portrait14, R.drawable.portrait15, R.drawable.portrait16
    };

    // 根据昵称获取对应的头像
    public static Drawable getPortraitByName(Context ctx, String name) {

        String md5 = MD5Util.encrypt(name);
        char lastChar = md5.charAt(md5.length()-1);
        int pos = lastChar>='A' ? lastChar-'A'+10 : lastChar-'0';
        Bitmap bitmap = BitmapFactory.decodeResource(ctx.getResources(), mPortraitArray[pos]);
        RoundDrawable drawable = new RoundDrawable(ctx, bitmap);
        return drawable;
    }

    // 获得一个消息内容的视图模板
    public static View getChatView(Context ctx, String name, String content, boolean isSelf) {
        int layoutId = isSelf ? R.layout.chat_me : R.layout.chat_other;
        View view = LayoutInflater.from(ctx).inflate(layoutId, null);
        ImageView iv_portrait = view.findViewById(R.id.iv_portrait);
        TextView tv_content = view.findViewById(R.id.tv_content);
        iv_portrait.setImageDrawable(getPortraitByName(ctx, name));
        tv_content.setText(content);
        LinearLayout.LayoutParams ll_params = new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        ll_params.gravity = isSelf ? Gravity.RIGHT : Gravity.LEFT;
        view.setLayoutParams(ll_params);
        return view;
    }

    // 获得一个提示内容的文本视图模板
    public static TextView getHintView(Context ctx, String content, int margin) {
        TextView tv = new TextView(ctx);
        tv.setText(content);
        tv.setTextSize(12);
        tv.setTextColor(Color.GRAY);
        LinearLayout.LayoutParams tv_params = new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        tv_params.setMargins(margin, margin, margin, margin);
        tv_params.gravity = Gravity.CENTER;
        tv.setLayoutParams(tv_params);
        return tv;
    }

    // 获得一个消息图片的视图模板
    public static View getChatImage(Context ctx, String name, String imagePath, boolean isSelf) {
        int layoutId = isSelf ? R.layout.chat_me : R.layout.chat_other;
        View view = LayoutInflater.from(ctx).inflate(layoutId, null);
        ImageView iv_portrait = view.findViewById(R.id.iv_portrait);
        view.findViewById(R.id.tv_content).setVisibility(View.GONE);
        ImageView iv_content = view.findViewById(R.id.iv_content);
        iv_portrait.setImageDrawable(getPortraitByName(ctx, name));
        iv_content.setVisibility(View.VISIBLE);
        LinearLayout.LayoutParams iv_params = (LinearLayout.LayoutParams) iv_content.getLayoutParams();
        Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
        RoundDrawable drawable = new RoundDrawable(ctx, bitmap);
        iv_content.setImageDrawable(drawable);
        iv_params.height = Utils.dip2px(ctx, 240 / (1.0f * bitmap.getWidth() / bitmap.getHeight() + 1));
        iv_params.width = Utils.dip2px(ctx, 240) - iv_params.height;
        Log.d(TAG, "iv_params.width="+iv_params.width+", iv_params.height="+iv_params.height);
        iv_content.setLayoutParams(iv_params);
        iv_content.setOnClickListener(v -> {
            Intent intent = new Intent(ctx, ImageDetailActivity.class);
            intent.putExtra("imagePath", imagePath);
            ctx.startActivity(intent);
        });
        LinearLayout.LayoutParams ll_params = new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        ll_params.gravity = isSelf ? Gravity.RIGHT : Gravity.LEFT;
        ll_params.gravity = ll_params.gravity | Gravity.TOP;
        view.setLayoutParams(ll_params);
        return view;
    }

}

BitmapUtil.java:

package com.example.myapplication;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;

import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.ByteBuffer;

public class BitmapUtil {
    private final static String TAG = "BitmapUtil";

    // 把位图数据保存到指定路径的图片文件
    public static void saveImage(String path, Bitmap bitmap) {
        // 根据指定的文件路径构建文件输出流对象
        try (FileOutputStream fos = new FileOutputStream(path)) {
            // 把位图数据压缩到文件输出流中
            bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 把位图数据保存到指定路径的图片文件
    public static void saveImage(String path, ByteBuffer buffer) {
        try (FileOutputStream fos = new FileOutputStream(path)) {
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            fos.write(data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 获得旋转角度之后的位图对象
    public static Bitmap getRotateBitmap(Bitmap bitmap, float rotateDegree) {
        Matrix matrix = new Matrix(); // 创建操作图片用的矩阵对象
        matrix.postRotate(rotateDegree); // 执行图片的旋转动作
        // 创建并返回旋转后的位图对象
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
                bitmap.getHeight(), matrix, false);
    }

    // 获得比例缩放之后的位图对象
    public static Bitmap getScaleBitmap(Bitmap bitmap, double scaleRatio) {
        Matrix matrix = new Matrix(); // 创建操作图片用的矩阵对象
        matrix.postScale((float)scaleRatio, (float)scaleRatio);
        // 创建并返回缩放后的位图对象
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
                bitmap.getHeight(), matrix, false);
    }

    // 获得自动缩小后的位图对象
    public static Bitmap getAutoZoomImage(Context ctx, Uri uri) {
        Log.d(TAG, "getAutoZoomImage uri="+uri.toString());
        Bitmap zoomBitmap = null;
        // 打开指定uri获得输入流对象
        try (InputStream is = ctx.getContentResolver().openInputStream(uri)) {
            // 从输入流解码得到原始的位图对象
            Bitmap originBitmap = BitmapFactory.decodeStream(is);
            int ratio = originBitmap.getWidth()/2000+1;
            // 获得比例缩放之后的位图对象
            zoomBitmap = getScaleBitmap(originBitmap, 1.0/ratio);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return zoomBitmap;
    }

    // 获得自动缩小后的位图对象
    public static Bitmap getAutoZoomImage(Bitmap origin) {
        int ratio = origin.getWidth()/2000+1;
        // 获得并返回比例缩放之后的位图对象
        return getScaleBitmap(origin, 1.0/ratio);
    }

    // 获得自动缩小后的图片路径
    public static String getAutoZoomPath(Context ctx, Uri uri) {
        Log.d(TAG, "getAutoZoomPath uri="+uri.toString());
        String imagePath = String.format("%s/%s.jpg",
                ctx.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString(),
                DateUtil.getNowDateTime());
        // 打开指定uri获得输入流对象
        try (InputStream is = ctx.getContentResolver().openInputStream(uri)) {
            // 从输入流解码得到原始的位图对象
            Bitmap originBitmap = BitmapFactory.decodeStream(is);
            int ratio = originBitmap.getWidth()/1000+1;
            // 获得比例缩放之后的位图对象
            Bitmap zoomBitmap = getScaleBitmap(originBitmap, 1.0/ratio);
            saveImage(imagePath, zoomBitmap);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return imagePath;
    }

}

XML页面代码:

activity_friend_chat.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ededed"
    android:focusable="true"
    android:focusableInTouchMode="true"
    android:orientation="vertical">

    <include layout="@layout/title_chat" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_margin="5dp"
        android:layout_weight="1"
        android:gravity="bottom"
        android:orientation="vertical">

        <ScrollView
            android:id="@+id/sv_chat"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <LinearLayout
                android:id="@+id/ll_show"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="bottom"
                android:orientation="vertical">

            </LinearLayout>
        </ScrollView>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="5dp"
        android:orientation="horizontal">

        <EditText
            android:id="@+id/et_input"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@drawable/editext_selector"
            android:hint="请输入聊天内容"
            android:textColor="@color/black"
            android:textSize="15sp" />

        <ImageButton
            android:id="@+id/ib_img"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_marginLeft="5dp"
            android:background="@drawable/add_img" />
    </LinearLayout>

    <Button
        android:id="@+id/btn_send"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:layout_margin="5dp"
        android:background="@drawable/shape_green"
        android:text="发 送"
        android:textColor="@color/white"
        android:textSize="17sp" />
</LinearLayout>

activity_group_chat.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ededed"
    android:focusable="true"
    android:focusableInTouchMode="true"
    android:orientation="vertical">

    <include layout="@layout/title_chat" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_margin="5dp"
        android:layout_weight="1"
        android:gravity="bottom"
        android:orientation="vertical">

        <ScrollView
            android:id="@+id/sv_chat"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <LinearLayout
                android:id="@+id/ll_show"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="bottom"
                android:orientation="vertical">

            </LinearLayout>
        </ScrollView>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="5dp"
        android:orientation="horizontal">

        <EditText
            android:id="@+id/et_input"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@drawable/editext_selector"
            android:hint="请输入聊天内容"
            android:textColor="@color/black"
            android:textSize="15sp" />

        <ImageButton
            android:id="@+id/ib_img"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_marginLeft="5dp"
            android:background="@drawable/add_img" />
    </LinearLayout>

    <Button
        android:id="@+id/btn_send"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:layout_margin="5dp"
        android:background="@drawable/shape_green"
        android:text="发 送"
        android:textColor="@color/white"
        android:textSize="17sp" />
</LinearLayout>

activity_image_detail.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/iv_photo"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="@color/black" />
</LinearLayout>

activity_main.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"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button_jump"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button_jump"
        tools:layout_editor_absoluteX="317dp"
        tools:layout_editor_absoluteY="679dp" />

</androidx.constraintlayout.widget.ConstraintLayout>

actvity_we_chat.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/vp_content"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <RadioGroup
        android:id="@+id/rg_tabbar"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:orientation="horizontal">

        <RadioButton
            android:id="@+id/rb_friend"
            style="@style/TabButton"
            android:checked="true"
            android:text="好友"
            android:drawableTop="@drawable/tab_first_selector" />

        <RadioButton
            android:id="@+id/rb_group"
            style="@style/TabButton"
            android:text="群聊"
            android:drawableTop="@drawable/tab_second_selector" />

        <RadioButton
            android:id="@+id/rb_my"
            style="@style/TabButton"
            android:text="我的"
            android:drawableTop="@drawable/tab_third_selector" />
    </RadioGroup>

</LinearLayout>

activity_we_login.xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:orientation="vertical">

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="150dp"
            android:src="@drawable/wechat" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:padding="25dp"
            android:text="请输入您的昵称"
            android:textColor="@color/black"
            android:textSize="17sp" />

        <EditText
            android:id="@+id/et_name"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_margin="5dp"
            android:background="@drawable/editext_selector"
            android:hint="请输入昵称"
            android:textColor="@color/black"
            android:textSize="17sp" />

        <Button
            android:id="@+id/btn_login"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:background="@drawable/shape_green"
            android:text="登 录"
            android:textColor="@color/white"
            android:textSize="17sp" />

    </LinearLayout>

</RelativeLayout>

chat_me.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="right"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_content"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/chat_me_bg"
            android:textColor="@color/black"
            android:textSize="15sp" />

    </LinearLayout>

    <ImageView
        android:id="@+id/iv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginRight="5dp"
        android:layout_marginBottom="5dp"
        android:visibility="gone" />

    <ImageView
        android:id="@+id/iv_portrait"
        android:layout_width="35dp"
        android:layout_height="35dp" />

</LinearLayout>

chat_other.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/iv_portrait"
        android:layout_width="35dp"
        android:layout_height="35dp" />

    <ImageView
        android:id="@+id/iv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="5dp"
        android:layout_marginBottom="5dp"
        android:visibility="gone" />

    <TextView
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/chat_other_bg"
        android:textColor="@color/black"
        android:textSize="15sp" />

</LinearLayout>

dialog_input.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/transparent"
    android:gravity="center"
    android:orientation="vertical"
    android:paddingLeft="40dp"
    android:paddingRight="40dp">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:background="@color/white"
        android:padding="15dp"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="match_parent"
            android:layout_height="30dp"
            android:layout_marginBottom="10dp"
            android:textColor="@color/black"
            android:textSize="17sp" />

        <EditText
            android:id="@+id/et_input"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:background="@drawable/editext_selector"
            android:hint="请输入"
            android:textColor="@color/black"
            android:textSize="15sp" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="25dp"
            android:orientation="horizontal"
            android:layout_marginTop="25dp">

            <View
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1" />

            <TextView
                android:id="@+id/tv_cancel"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="45dp"
                android:text="取 消"
                android:textColor="@color/red"
                android:textSize="15sp" />

            <TextView
                android:id="@+id/tv_confirm"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="45dp"
                android:text="确 定"
                android:textColor="@color/red"
                android:textSize="15sp" />

        </LinearLayout>

    </LinearLayout>
</LinearLayout>

fragment_friend_list.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <include layout="@layout/title_chat" />

    <com.example.myapplication.NoScrollListView

        android:id="@+id/nslv_friend"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:paddingLeft="5dp"
        android:paddingRight="5dp" />

</LinearLayout>

fragment_group_list.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <include layout="@layout/title_chat" />

    <com.example.myapplication.NoScrollListView
        android:id="@+id/nslv_group"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:paddingLeft="5dp"
        android:paddingRight="5dp" />

</LinearLayout>

fragment_my_info.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <include layout="@layout/title_chat" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:paddingLeft="20dp"
        android:paddingRight="20dp"
        android:orientation="vertical" >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal" >

            <TextView
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:gravity="center_vertical"
                android:text="头像"
                android:textColor="@color/black"
                android:textSize="17sp" />

            <ImageView
                android:id="@+id/iv_portrait"
                android:layout_width="60dp"
                android:layout_height="60dp"
                android:layout_margin="10dp" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center_vertical"
                android:text=">"
                android:textColor="@color/gray"
                android:textSize="20sp" />

        </LinearLayout>

        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#ededed" />

        <LinearLayout
            android:id="@+id/ll_nick"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:orientation="horizontal" >

            <TextView
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:gravity="center_vertical"
                android:text="昵称"
                android:textColor="@color/black"
                android:textSize="17sp" />

            <TextView
                android:id="@+id/tv_nick"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="10dp"
                android:gravity="center_vertical"
                android:textColor="@color/gray"
                android:textSize="17sp" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center_vertical"
                android:text=">"
                android:textColor="@color/gray"
                android:textSize="20sp" />

        </LinearLayout>

    </LinearLayout>

</LinearLayout>

item_user.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/iv_portrait"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_margin="10dp" />

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3"
        android:layout_margin="5dp"
        android:gravity="left|center_vertical"
        android:textColor="@color/black"
        android:textSize="17sp" />

    <TextView
        android:id="@+id/tv_relation"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:layout_margin="5dp"
        android:gravity="right|center_vertical"
        android:textColor="@color/black"
        android:textSize="17sp" />
</LinearLayout>

title_chat.xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="40dp"
    android:background="#ededed" >

    <ImageView
        android:id="@+id/iv_back"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_alignParentLeft="true"
        android:padding="10dp"
        android:src="@drawable/icon_back" />

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_toRightOf="@+id/iv_back"
        android:gravity="center"
        android:textColor="@color/black"
        android:textSize="18sp" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1px"
        android:layout_alignParentBottom="true"
        android:background="#dddddd" />

</RelativeLayout>

有关颜色的代码:

background:

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <path
        android:fillColor="#3DDC84"
        android:pathData="M0,0h108v108h-108z" />
    <path
        android:fillColor="#00000000"
        android:pathData="M9,0L9,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,0L19,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M29,0L29,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M39,0L39,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M49,0L49,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M59,0L59,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M69,0L69,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M79,0L79,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M89,0L89,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M99,0L99,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,9L108,9"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,19L108,19"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,29L108,29"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,39L108,39"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,49L108,49"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,59L108,59"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,69L108,69"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,79L108,79"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,89L108,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,99L108,99"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,29L89,29"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,39L89,39"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,49L89,49"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,59L89,59"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,69L89,69"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,79L89,79"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M29,19L29,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M39,19L39,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M49,19L49,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M59,19L59,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M69,19L69,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M79,19L79,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
</vector>

foreground:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
        <aapt:attr name="android:fillColor">
            <gradient
                android:endX="85.84757"
                android:endY="92.4963"
                android:startX="42.9492"
                android:startY="49.59793"
                android:type="linear">
                <item
                    android:color="#44000000"
                    android:offset="0.0" />
                <item
                    android:color="#00000000"
                    android:offset="1.0" />
            </gradient>
        </aapt:attr>
    </path>
    <path
        android:fillColor="#FFFFFF"
        android:fillType="nonZero"
        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
        android:strokeWidth="1"
        android:strokeColor="#00000000" />
</vector>

gradle配置:

plugins {
    alias(libs.plugins.android.application)
}

android {
    namespace = "com.example.myapplication"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.example.myapplication"
        minSdk = 24
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
}

dependencies {

    implementation(libs.appcompat)
    implementation(libs.material)
    implementation(libs.activity)
    implementation(libs.constraintlayout)
    testImplementation(libs.junit)
    androidTestImplementation(libs.ext.junit)
    androidTestImplementation(libs.espresso.core)
    implementation("androidx.work:work-runtime:2.7.1")

    // gson库
    implementation("com.google.code.gson:gson:2.9.0")

    // okhttp库
    implementation("com.squareup.okhttp3:okhttp:4.9.3")

    // glide库
    implementation("com.github.bumptech.glide:glide:4.13.1")

    // recyclerview
    implementation("androidx.recyclerview:recyclerview:1.2.0")

    // swiperefreshlayout
    implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")

    // socketio库
    implementation("io.socket:socket.io-client:1.0.1")

}

服务端代码:

package com.socketio.server;

import com.alibaba.fastjson.JSONObject;
import com.corundumstudio.socketio.Configuration;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIOServer;
import com.socketio.bean.ImageMessage;
import com.socketio.bean.JoinInfo;
import com.socketio.bean.MessageInfo;

import java.util.HashMap;
import java.util.Map;

public class WeChatServer {

    // 客户端映射
    private static Map<String, SocketIOClient> clientMap = new HashMap<>();
    // 人员名字映射
    private static Map<String, String> nameMap = new HashMap<>();
    // 群名称与群成员映射
    private static Map<String, Map<String, String>> groupMap = new HashMap<>();
    
    public static void main(String[] args) {
        Configuration config = new Configuration();
        // 如果调用了setHostname方法,就只能通过主机名访问,不能通过IP访问
        //config.setHostname("localhost");
        config.setPort(9011); // 设置监听端口
        final SocketIOServer server = new SocketIOServer(config);
        // 添加连接连通的监听事件
        server.addConnectListener(client -> {
            String sessionId = client.getSessionId().toString();
            System.out.println("getRemoteAddress "+client.getRemoteAddress().toString());
            System.out.println(sessionId+"已连接");
            System.out.println(client.getSessionId().toString()+"已连接");
            clientMap.put(sessionId, client);
        });
        // 添加连接断开的监听事件
        server.addDisconnectListener(client -> {
            String sessionId = client.getSessionId().toString();
            System.out.println(sessionId+"已断开");
            System.out.println(client.getSessionId().toString()+"已断开");
            for (Map.Entry<String, SocketIOClient> item : clientMap.entrySet()) {
                if (!sessionId.equals(item.getKey())) {
                    item.getValue().sendEvent("friend_offline", nameMap.get(sessionId));
                }
            }
            for (Map.Entry<String, SocketIOClient> item : clientMap.entrySet()) {
                if (sessionId.equals(item.getKey())) {
                    clientMap.remove(item.getKey());
                    break;
                }
            }
            for (Map.Entry<String, Map<String, String>> group : groupMap.entrySet()) {
                group.getValue().remove(sessionId);
            }
            nameMap.remove(sessionId);
        });
        // 添加我已上线的监听事件
        server.addEventListener("self_online", String.class, (client, name, ackSender) -> {
            String sessionId = client.getSessionId().toString();
            System.out.println(sessionId+"已上线:"+name);
            for (Map.Entry<String, SocketIOClient> item : clientMap.entrySet()) {
                item.getValue().sendEvent("friend_online", name);
                client.sendEvent("friend_online", nameMap.get(item.getKey()));
            }
            nameMap.put(sessionId, name);
        });
        // 添加我已下线的监听事件
        server.addEventListener("self_offline", String.class, (client, name, ackSender) -> {
            String sessionId = client.getSessionId().toString();
            System.out.println(sessionId+"已下线:"+name);
            for (Map.Entry<String, SocketIOClient> item : clientMap.entrySet()) {
                if (!sessionId.equals(item.getKey())) {
                    item.getValue().sendEvent("friend_offline", name);
                }
            }
            nameMap.remove(sessionId);
        });
        // 添加文本发送的事件监听器
        server.addEventListener("send_friend_message", JSONObject.class, (client, json, ackSender) -> {
            String sessionId = client.getSessionId().toString();
            System.out.println(sessionId+"发送消息:"+json.toString());
            MessageInfo message = (MessageInfo) JSONObject.toJavaObject(json, MessageInfo.class);
            for (Map.Entry<String, String> item : nameMap.entrySet()) {
                if (message.getTo().equals(item.getValue())) {
                    clientMap.get(item.getKey()).sendEvent("receive_friend_message", message);
                    break;
                }
            }
        });
        // 添加图像发送的事件监听器
        server.addEventListener("send_friend_image", JSONObject.class, (client, json, ackSender) -> {
            String sessionId = client.getSessionId().toString();
            System.out.println(sessionId+"发送图片:"+json.toString());
            ImageMessage message = (ImageMessage) JSONObject.toJavaObject(json, ImageMessage.class);
            System.out.println("getFrom="+message.getFrom()+",getTo="+message.getTo()+",getName="+message.getPart().getName());
            for (Map.Entry<String, String> item : nameMap.entrySet()) {
                if (message.getTo().equals(item.getValue())) {
                    System.out.println(item.getKey()+"receive_friend_image");
                    clientMap.get(item.getKey()).sendEvent("receive_friend_image", message);
                    break;
                }
            }
        });
        // 添加入群的事件监听器
        server.addEventListener("join_group", JSONObject.class, (client, json, ackSender) -> {
            String sessionId = client.getSessionId().toString();
            System.out.println(sessionId+"已入群:"+json.toString());
            JoinInfo info = (JoinInfo) JSONObject.toJavaObject(json, JoinInfo.class);
            if (!groupMap.containsKey(info.getGroup_name())) {
                System.out.println("groupMap.put "+info.getGroup_name());
                groupMap.put(info.getGroup_name(), new HashMap<String, String>());
            }
            for (Map.Entry<String, Map<String, String>> group : groupMap.entrySet()) {
                System.out.println("群名称为"+group.getKey());
                if (info.getGroup_name().equals(group.getKey())) {
                    group.getValue().put(sessionId, info.getUser_name());
                    for (Map.Entry<String, String> user : group.getValue().entrySet()) {
                        System.out.println("群成员为"+user.getKey());
                        clientMap.get(user.getKey()).sendEvent("person_in_group", info.getUser_name());
                        System.out.println(user.getKey()+" person_in_group");
                    }
                    System.out.println("person_count="+group.getValue().size());
                    client.sendEvent("person_count", group.getValue().size());
                }
            }
        });
        // 添加退群的事件监听器
        server.addEventListener("leave_group", JSONObject.class, (client, json, ackSender) -> {
            String sessionId = client.getSessionId().toString();
            System.out.println(sessionId+"已退群:"+json.toString());
            JoinInfo info = (JoinInfo) JSONObject.toJavaObject(json, JoinInfo.class);
            for (Map.Entry<String, Map<String, String>> group : groupMap.entrySet()) {
                if (info.getGroup_name().equals(group.getKey())) {
                    group.getValue().remove(sessionId);
                    for (Map.Entry<String, String> user : group.getValue().entrySet()) {
                        clientMap.get(user.getKey()).sendEvent("person_out_group", info.getUser_name());
                        System.out.println(user.getKey()+" person_out_group");
                    }
                }
            }
        });
        // 添加群消息发送的事件监听器
        server.addEventListener("send_group_message", JSONObject.class, (client, json, ackSender) -> {
            String sessionId = client.getSessionId().toString();
            System.out.println(sessionId+"发送消息:"+json.toString());
            MessageInfo message = (MessageInfo) JSONObject.toJavaObject(json, MessageInfo.class);
            for (Map.Entry<String, Map<String, String>> group : groupMap.entrySet()) {
                if (message.getTo().equals(group.getKey())) {
                    for (Map.Entry<String, String> user : group.getValue().entrySet()) {
                        if (!user.getValue().equals(message.getFrom())) {
                            clientMap.get(user.getKey()).sendEvent("receive_group_message", message);
                            System.out.println("receive_group_message 接收方为"+user.getKey());
                        }
                    }
                    break;
                }
            }
        });
        // 添加群图片发送的事件监听器
        server.addEventListener("send_group_image", JSONObject.class, (client, json, ackSender) -> {
            String sessionId = client.getSessionId().toString();
            System.out.println(sessionId+"发送图片:"+json.toString());
            ImageMessage message = (ImageMessage) JSONObject.toJavaObject(json, ImageMessage.class);
            for (Map.Entry<String, Map<String, String>> group : groupMap.entrySet()) {
                if (message.getTo().equals(group.getKey())) {
                    for (Map.Entry<String, String> user : group.getValue().entrySet()) {
                        if (!user.getValue().equals(message.getFrom())) {
                            clientMap.get(user.getKey()).sendEvent("receive_group_image", message);
                            System.out.println("receive_group_image 接收方为"+user.getKey());
                        }
                    }
                    break;
                }
            }
        });

        server.start();
        System.out.println("SocketIO 服务器已启动,监听端口:9011");// 启动Socket服务
    }

}

效果图:

请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述

尾言

本次的项目统计下来也有8万多字,3000多行,实属不易,也是作者的呕心沥血之作,但里面的内容较为困难,要想看懂确实得画一部分精力了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

隐-梵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值