想做一个类似微信的交流软件吗,今天我们就通过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多行,实属不易,也是作者的呕心沥血之作,但里面的内容较为困难,要想看懂确实得画一部分精力了。