演示地址: https://www.bilibili.com/video/BV1Wz4y127HX/
代码
权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
java部分
MainActivity.java
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.provider.MediaStore;
import android.util.Base64;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.sql.Date;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Handler handler;
private LayoutInflater inflater;
private View layout;
private AlertDialog.Builder builder;
private TextView titleview;
private ListView showmsg;
private EditText sendmsgtext;
private Button startserver;
private Button continueserver;
private Button sendmsgbt;
private Button sendimg;
private int StartPort;
private boolean isContinue = true,isServer = false;
private String message = "",userSendMsg = "",titletext = "";
private String[] ContinueServerData = new String[2];// 0.ipv4 1.端口号
private Long mID = 0L;
private List<MessageInfor> datas = new ArrayList<MessageInfor>();
private SimpleDateFormat simpleDateFormat;
private MessageAdapte messageAdapte;
private static Socket socket = null;//用于与服务端通信的Socket
private static ServerSocket server;
private static List<PrintWriter> allOut; //存放所有客户端的输出流的集合,用于广播
private static final int IMAGE = 1;//调用系统相册-选择图片
private static String[] PERMISSIONS_STORAGE = {
//依次权限申请
Manifest.permission.INTERNET,
Manifest.permission.READ_EXTERNAL_STORAGE
};
/**
* 作者 LinOwl
* 2021.02.17
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getSupportActionBar().hide();//隐藏标题栏
applypermission();
InitView();
handler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
if(msg.what == 1){
titleview.setText(titletext);
}else if(msg.what == 2){
titleview.setText("当前在线人数["+(allOut.size()+1)+"]");
}
super.handleMessage(msg);
}
};
}
/**
* 初始化控件
*/
private void InitView() {
titleview = (TextView) findViewById(R.id.titleview);
showmsg = (ListView) findViewById(R.id.showmsg);
sendmsgtext = (EditText) findViewById(R.id.sendmsgtext);
startserver = (Button) findViewById(R.id.startserver);
continueserver = (Button) findViewById(R.id.continueserver);
sendmsgbt = (Button) findViewById(R.id.sendmsgbt);
sendimg = (Button) findViewById(R.id.sendimg);
simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
messageAdapte = new MessageAdapte();
showmsg.setAdapter(messageAdapte);
startserver.setOnClickListener(this);
continueserver.setOnClickListener(this);
sendmsgbt.setOnClickListener(this);
sendimg.setOnClickListener(this);
}
//定义判断权限申请的函数,在onCreat中调用就行
public void applypermission(){
if(Build.VERSION.SDK_INT>=23){
boolean needapply=false;
for(int i=0;i<PERMISSIONS_STORAGE.length;i++){
int chechpermission= ContextCompat.checkSelfPermission(getApplicationContext(),
PERMISSIONS_STORAGE[i]);
if(chechpermission!= PackageManager.PERMISSION_GRANTED){
needapply=true;
}
}
if(needapply){
ActivityCompat.requestPermissions(MainActivity.this,PERMISSIONS_STORAGE,1);
}
}
}
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.startserver:
//加载布局
inflater = LayoutInflater.from(this);
layout = inflater.inflate(R.layout.start_server,null);
//通过对 AlertDialog.Builder 对象调用 setView()
builder = new AlertDialog.Builder(MainActivity.this);
builder.setView(R.layout.start_server);
builder.setCancelable(false);//是否为可取消
//加载控件
EditText editprot = (EditText) layout.findViewById(R.id.editprot);
new AlertDialog.Builder(MainActivity.this)
.setView(layout) //设置显示内容
.setPositiveButton("开启", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
StartPort = Integer.valueOf(editprot.getText().toString());
mID = System.currentTimeMillis();
ServerInit();
}
})
.setNegativeButton("取消", null)
.setCancelable(false) //按回退键不可取消该对话框
.show();
break;
case R.id.continueserver:
//加载布局
inflater = LayoutInflater.from(this);
layout = inflater.inflate(R.layout.continue_server,null);
//通过对 AlertDialog.Builder 对象调用 setView()
builder = new AlertDialog.Builder(MainActivity.this);
builder.setView(R.layout.continue_server);
builder.setCancelable(false);//是否为可取消
//加载控件
EditText editipv4text = (EditText) layout.findViewById(R.id.editipv4text);
EditText editprottext = (EditText) layout.findViewById(R.id.editprottext);
new AlertDialog.Builder(MainActivity.this)
.setView(layout) //设置显示内容
.setPositiveButton("连接", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ContinueServerData[0] = editipv4text.getText().toString();
ContinueServerData[1] = editprottext.getText().toString();
ContinueSever();
}
})
.setNegativeButton("取消", null)
.setCancelable(false) //按回退键不可取消该对话框
.show();
break;
case R.id.sendmsgbt://发送消息
if(isServer){//服务器
message = sendmsgtext.getText().toString();
if(message==null||"".equals(message)){
Toast.makeText(MainActivity.this,"发送消息不能为空",Toast.LENGTH_LONG).show();
return ;
}
long Ltimes = System.currentTimeMillis();
message = sendmsgtext.getText().toString();
datas.add(new MessageInfor(message,Ltimes,mID,"1"));
sendMessage("{\"isimg\":\"1\",\"msg\":\""+message+"\",\"times\":\""+Ltimes+"\",\"id\":\""+mID+"\",\"peoplen\":\""+"当前在线人数["+(allOut.size()+1)+"]"+"\"}");
sendmsgtext.setText("");
}else {//客户端
sendMsgText();
}
break;
case R.id.sendimg:
//调用相册
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, IMAGE);
break;
}
}
/**
* 服务器端
* @param out
*/
//将给定的输出流放入集合
private synchronized void addOut(PrintWriter out){
allOut.add(out);
}
//将给定的输出流移出集合
private synchronized void removeOut(PrintWriter out){
allOut.remove(out);
}
//将给定的消息发给客户端
private void sendMessage(String message) {
Thread sendmsg = new Thread(new Runnable() {
@Override
public void run() {
for(PrintWriter out:allOut) {
out.println(message);
}
}
});
sendmsg.start();
}
//服务器初始化
public void ServerInit() {
try {
server = new ServerSocket(StartPort);
allOut = new ArrayList<PrintWriter>();
isServer = true;
} catch (Exception e) {
e.printStackTrace();
}
new Thread(
new Runnable() {
@Override
public void run() {
while(true) {
Socket socket1 = null;
try {
socket1 = server.accept();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ClientHandler hander = new ClientHandler(socket1);
Thread t = new Thread(hander);
t.start();
}
}
}).start();
}
//该线程类是与指定的客户端进行交互工作
class ClientHandler implements Runnable{
//当前线程客户端的Socket
private Socket socket;
//该客户端的地址
private String host;
public ClientHandler(Socket socket) {
this.socket=socket;
InetAddress address = socket.getInetAddress();
//获取ip地址
host = address.getHostAddress();
}
@Override
public void run() {
PrintWriter pw = null;
try {
//有用户加入
sendMessage("["+host+"]加入聊天!");
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");
pw = new PrintWriter(osw,true);
//将该客户的输出流存入共享集合,以便消息可以广播给该客户端
addOut(pw);
handler.sendEmptyMessage(2);
//处理来自客户端的数据
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,"utf-8");
BufferedReader br = new BufferedReader(isr);
String message = null;
while((message = br.readLine())!=null) {
try {
JSONObject json = new JSONObject(message);
if(json.getString("isimg").equals("1")){//不为图片
datas.add(new MessageInfor(json.getString("msg"),Long.valueOf(json.getString("times")),Long.valueOf(json.getString("id")),"1"));
}else if(json.getString("isimg").equals("0")){//为图片
datas.add(new MessageInfor(json.getString("msg"),Long.valueOf(json.getString("times")),Long.valueOf(json.getString("id")),"0"));
}
titletext = json.getString("peoplen");
handler.sendEmptyMessage(1);
//messageAdapte.notifyDataSetChanged();//通知数据源发生变化
}catch (JSONException e){
e.printStackTrace();
}
sendMessage(message);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
//将该客户端的输出流从共享集合中删除
removeOut(pw);
//有用户退出
sendMessage("["+host+"]退出聊天!");
handler.sendEmptyMessage(2);
try {
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
/**
* 客户端
* @return
*/
public boolean ContinueSever(){
Thread continuethread = new Thread(
new Runnable() {
@Override
public void run() {
try {
//localhost 127.0.0.1
socket = new Socket(ContinueServerData[0],Integer.valueOf(ContinueServerData[1]));
mID = System.currentTimeMillis();
} catch (Exception e) {
isContinue = false;
isServer = false;
e.printStackTrace();
}
}
}
);
continuethread.start();
while(isContinue){
if(socket != null){
break;
}
}
if(isContinue) {
new Thread(
new Runnable() {
@Override
public void run() {
/*
* 客户端开始工作的方法
*/
try {
//启动用于读取服务端发送消息的线程
ServerHandler handler = new ServerHandler();
//ServerHandler是自己写的类,实现Runnable接口,有多线程功能
Thread t = new Thread(handler);
t.start();
//将数据发送到服务端
OutputStream out = socket.getOutputStream();//获取输出流对象
OutputStreamWriter osw = new OutputStreamWriter(out,"utf-8");//转化成utf-8格式
PrintWriter pw = new PrintWriter(osw,true);
while(true) {
if(userSendMsg != "" && userSendMsg!=null){
pw.println(userSendMsg);//把信息输出到服务端
userSendMsg = "";
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
return isContinue;
}
class ServerHandler implements Runnable{
/**
* 读取服务端发送过来的消息
*/
@Override
public void run() {
try {
InputStream in = socket.getInputStream();//输入流
InputStreamReader isr = new InputStreamReader(in,"UTF-8");//以utf-8读
BufferedReader br = new BufferedReader(isr);
String message1=br.readLine();
while(message1!=null) {
Log.i("测试4",message1);
try {
JSONObject json = new JSONObject(message1);
if(json.getLong("id") != mID){
if(json.getString("isimg").equals("1")){//不为图片
datas.add(new MessageInfor(json.getString("msg"),Long.valueOf(json.getString("times")),Long.valueOf(json.getString("id")),"1"));
}else if(json.getString("isimg").equals("0")){//为图片
datas.add(new MessageInfor(json.getString("msg"),Long.valueOf(json.getString("times")),Long.valueOf(json.getString("id")),"0"));
}
}
titletext = json.getString("peoplen");
handler.sendEmptyMessage(1);
//messageAdapte.notifyDataSetChanged();//通知数据源发生变化
}catch (JSONException e){
e.printStackTrace();
}
message1=br.readLine();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 发送消息
*/
private void sendMsgText(){
message = sendmsgtext.getText().toString();
if(message==null||"".equals(message)){
Toast.makeText(MainActivity.this,"发送消息不能为空",Toast.LENGTH_LONG).show();
return ;
}
long Ltimes = System.currentTimeMillis();
MessageInfor m = new MessageInfor(message,Ltimes,mID,"1");//消息 时间戳 id
userSendMsg = "{\"isimg\":\"1\",\"msg\":\""+sendmsgtext.getText().toString()+"\",\"times\":\""+Ltimes+"\",\"id\":\""+mID+"\"}";
datas.add(m);
messageAdapte.notifyDataSetChanged();//通知数据源发生变化
sendmsgtext.setText("");
}
class MessageAdapte extends BaseAdapter {
@Override
public int getCount() {
return datas.size();
}
@Override
public MessageInfor getItem(int i) {
return datas.get(i);
}
@Override
public long getItemId(int i) {
Long id = datas.get(i).getUserID();
return id==null?0:id;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
MessageHolder holder = null;
if(view == null){
view = LayoutInflater.from(MainActivity.this).inflate(R.layout.chart_item,null);
holder = new MessageHolder();
holder.left = (TextView) view.findViewById(R.id.itemleft);
holder.right = (TextView) view.findViewById(R.id.itemright);
holder.lefttime = (TextView) view.findViewById(R.id.itemtimeleft);
holder.righttime = (TextView) view.findViewById(R.id.itemtimeright);
holder.rightimgtime = (TextView) view.findViewById(R.id.rightimgtime);
holder.leftimgtime = (TextView) view.findViewById(R.id.leftimgtime);
holder.rightimg = (ImageView) view.findViewById(R.id.rightimg);
holder.leftimg = (ImageView) view.findViewById(R.id.leftimg);
view.setTag(holder);
}else {
holder = (MessageHolder) view.getTag();
}
MessageInfor mi = getItem(i);
//显示
if (mi.getUserID() == mID){//id相等
if(mi.getType().equals("0")){//图片
holder.leftimg.setVisibility(View.GONE);
holder.leftimgtime.setVisibility(View.GONE);
holder.rightimg.setVisibility(View.VISIBLE);
holder.rightimgtime.setVisibility(View.VISIBLE);
holder.rightimg.setImageBitmap(convertStringToIcon(mi.getMsg()));
holder.rightimgtime.setText(simpleDateFormat.format(new Date(mi.getTime())));
holder.left.setVisibility(View.GONE);
holder.lefttime.setVisibility(View.GONE);
holder.right.setVisibility(View.GONE);
holder.righttime.setVisibility(View.GONE);
}else if(mi.getType().equals("1")){//消息
holder.leftimg.setVisibility(View.GONE);
holder.leftimgtime.setVisibility(View.GONE);
holder.rightimg.setVisibility(View.GONE);
holder.rightimgtime.setVisibility(View.GONE);
holder.left.setVisibility(View.GONE);
holder.lefttime.setVisibility(View.GONE);
holder.right.setVisibility(View.VISIBLE);
holder.righttime.setVisibility(View.VISIBLE);
holder.right.setText(mi.getMsg());
holder.righttime.setText(simpleDateFormat.format(new Date(mi.getTime())));
}
}else {
if(mi.getType().equals("0")){//图片
holder.leftimg.setVisibility(View.VISIBLE);
holder.leftimgtime.setVisibility(View.VISIBLE);
holder.rightimg.setVisibility(View.GONE);
holder.rightimgtime.setVisibility(View.GONE);
holder.leftimg.setImageBitmap(convertStringToIcon(mi.getMsg()));
holder.leftimgtime.setText(simpleDateFormat.format(new Date(mi.getTime())));
holder.left.setVisibility(View.GONE);
holder.lefttime.setVisibility(View.GONE);
holder.right.setVisibility(View.GONE);
holder.righttime.setVisibility(View.GONE);
}else if(mi.getType().equals("1")){//消息
holder.leftimg.setVisibility(View.GONE);
holder.leftimgtime.setVisibility(View.GONE);
holder.rightimg.setVisibility(View.GONE);
holder.rightimgtime.setVisibility(View.GONE);
holder.left.setVisibility(View.VISIBLE);
holder.lefttime.setVisibility(View.VISIBLE);
holder.right.setVisibility(View.GONE);
holder.righttime.setVisibility(View.GONE);
holder.left.setText(mi.getMsg());
holder.lefttime.setText(simpleDateFormat.format(new Date(mi.getTime())));
}
}
return view;
}
}
class MessageHolder{
public TextView left;
public TextView right;
public TextView lefttime;
public TextView righttime;
private TextView rightimgtime;
private TextView leftimgtime;
private ImageView rightimg;
private ImageView leftimg;
}
public void onActivityResult(int requestCode, int resultCode, final Intent data) {
//获取图片路径
if (requestCode == IMAGE && resultCode == Activity.RESULT_OK && data != null) {
Uri selectedImage = data.getData();
String[] filePathColumns = {MediaStore.Images.Media.DATA};
Cursor c = getContentResolver().query(selectedImage, filePathColumns, null, null, null);
c.moveToFirst();
int columnIndex = c.getColumnIndex(filePathColumns[0]);
String imagePath = c.getString(columnIndex);
activityImage(imagePath);
c.close();
}
super.onActivityResult(requestCode, resultCode, data);
}
/**
* 处理图片发送
* @param imaePath 图片路径
*/
private void activityImage(String imaePath){
Bitmap bm = BitmapFactory.decodeFile(imaePath);
bm = resizeBitmap(bm,400,400,true);
long Ltimes = System.currentTimeMillis();
String imgString = convertIconToString(bm);
imgString = imgString.replace("\n","");
datas.add(new MessageInfor(imgString,Ltimes,mID,"0"));
if(isServer){//服务器
sendMessage("{\"isimg\":\"0\",\"msg\":\""+imgString+"\",\"times\":\""+Ltimes+"\",\"id\":\""+mID+"\"}");
}else {//客户端
userSendMsg = "{\"isimg\":\"0\",\"msg\":\""+imgString+"\",\"times\":\""+Ltimes+"\",\"id\":\""+mID+"\"}";
}
}
/**
* 图片转成string
* @param bitmap
* @return
*/
private String convertIconToString(Bitmap bitmap){
ByteArrayOutputStream baos = new ByteArrayOutputStream();// outputstream
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
byte[] appicon = baos.toByteArray();// 转为byte数组
return Base64.encodeToString(appicon, Base64.DEFAULT);
}
/**
* string转成bitmap
* @param st
* @return
*/
private Bitmap convertStringToIcon(String st){
Bitmap bitmap = null;
try
{
byte[] bitmapArray;
bitmapArray = Base64.decode(st, Base64.DEFAULT);
bitmap = BitmapFactory.decodeByteArray(bitmapArray, 0,bitmapArray.length);
return bitmap;
}
catch (Exception e)
{
return null;
}
}
/**
* 处理图片
* @param bitmap 图片bitmap
* @param MaxWidth 最大长
* @param MaxHeight 最大宽
* @param filter 是否过滤
* @return 处理后的bitmap
*/
private Bitmap resizeBitmap(Bitmap bitmap,int MaxWidth,int MaxHeight,boolean filter){
Float ScalingNumber;
Bitmap reBitmap;
Matrix matrix = new Matrix();
ScalingNumber = Float.valueOf(scalingNumber(bitmap.getWidth(),bitmap.getHeight(),MaxWidth,MaxHeight));
matrix.setScale(1/ScalingNumber, 1/ScalingNumber);
reBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),bitmap.getHeight(), matrix, filter);
return reBitmap;
}
/**
* 计算缩放比例
* @param oldWidth 原长
* @param oldHeight 原宽
* @param MaxWidth 最大长
* @param MaxHeight 最大宽
* @return 缩放比系数
*/
private int scalingNumber(int oldWidth,int oldHeight,int MaxWidth,int MaxHeight){
int scalingN = 1;
if(oldWidth > MaxWidth || oldHeight > MaxHeight){
scalingN = 2;
while((oldWidth/scalingN > MaxWidth) || (oldHeight/scalingN > MaxHeight)){
scalingN*=2;
}
}
return scalingN;
}
}
MessageInfor.java
package com.example.socketlw;
public class MessageInfor {
private int ID;
private String msg;
private Long time;
private Long userID;
/**
* 作者 LinOwl
* 2021.02.17
*/
public MessageInfor(String msg, Long time, Long userID, String type) {
this.msg = msg;
this.time = time;
this.userID = userID;
this.type = type;
}
public int getID() {
return ID;
}
public void setID(int ID) {
this.ID = ID;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Long getTime() {
return time;
}
public void setTime(Long time) {
this.time = time;
}
public Long getUserID() {
return userID;
}
public void setUserID(Long userID) {
this.userID = userID;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
private String type;
}
drawable部分
button_broder.xml
<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<layer-list>
<item android:width="56dp" android:height="56dp">
<shape android:shape="oval">
<gradient android:endColor="@color/tv_style_color_seconde" android:gradientRadius="28dp" android:startColor="@color/tv_style_color_main" android:type="radial" />
</shape>
</item>
<item android:width="56dp" android:height="56dp" android:left="40dp">
<shape android:shape="oval">
<gradient android:endColor="@color/tv_style_color_seconde" android:gradientRadius="28dp" android:startColor="@color/tv_style_color_main" android:type="radial" />
</shape>
</item>
<item android:width="40dp" android:height="28dp" android:left="28dp">
<shape android:shape="rectangle">
<gradient android:angle="90" android:endColor="#FAF3FF" android:startColor="@color/tv_style_color_main" android:type="linear" />
</shape>
</item>
<item android:width="40dp" android:height="28dp" android:left="28dp" android:top="28dp">
<shape android:shape="rectangle">
<gradient android:angle="270" android:endColor="@color/tv_style_color_seconde" android:startColor="@color/tv_style_color_main" android:type="linear" />
</shape>
</item>
<item android:width="88dp" android:height="44dp" android:left="4dp" android:top="6dp">
<shape android:shape="rectangle">
<corners android:radius="24dp" />
<solid android:color="#8A73F5" />
</shape>
</item>
</layer-list>
</item>
<item android:state_pressed="false">
<layer-list>
<item android:width="56dp" android:height="56dp">
<shape android:shape="oval">
<gradient android:endColor="@color/tv_style_color_seconde" android:gradientRadius="28dp" android:startColor="@color/tv_style_color_main" android:type="radial" />
</shape>
</item>
<item android:width="56dp" android:height="56dp" android:left="40dp">
<shape android:shape="oval">
<gradient android:endColor="@color/tv_style_color_seconde" android:gradientRadius="28dp" android:startColor="@color/tv_style_color_main" android:type="radial" />
</shape>
</item>
<item android:width="40dp" android:height="28dp" android:left="28dp">
<shape android:shape="rectangle">
<gradient android:angle="90" android:endColor="#FAF3FF" android:startColor="@color/tv_style_color_main" android:type="linear" />
</shape>
</item>
<item android:width="40dp" android:height="28dp" android:left="28dp" android:top="28dp">
<shape android:shape="rectangle">
<gradient android:angle="270" android:endColor="@color/tv_style_color_seconde" android:startColor="@color/tv_style_color_main" android:type="linear" />
</shape>
</item>
<item android:width="88dp" android:height="44dp" android:left="4dp" android:top="6dp">
<shape android:shape="rectangle">
<corners android:radius="24dp" />
<solid android:color="#ffffff" />
</shape>
</item>
</layer-list>
</item>
</selector>
button_broder_bk.xml
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#62BBF6">
<item>
<shape android:shape="rectangle">
<solid android:color="@null" />
<corners android:radius="4dp" />
</shape>
</item>
<item android:drawable="@drawable/button_broder" />
</ripple>
item_left.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<layer-list>
<item>
<shape>
<corners android:bottomLeftRadius="5dp" android:bottomRightRadius="5dp" android:topRightRadius="5dp" />
<solid android:color="#9EDEF1" />
<padding android:left="5dp" android:right="5dp" android:bottom="5dp" android:top="5dp"/>
</shape>
</item>
</layer-list>
</item>
</selector>
item_right.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<layer-list>
<item>
<shape>
<corners android:bottomLeftRadius="5dp" android:bottomRightRadius="5dp" android:topRightRadius="5dp" />
<solid android:color="#F3E8DB" />
<padding android:left="5dp" android:right="5dp" android:bottom="5dp" android:top="5dp"/>
</shape>
</item>
</layer-list>
</item>
</selector>
layout部分
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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/titleview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="NULL"
android:gravity="center"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/startserver"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="5dp"
android:background="@drawable/button_broder_bk"
android:text="开启服务器"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/continueserver"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="20dp"
android:layout_marginTop="5dp"
android:background="@drawable/button_broder_bk"
android:text="连接服务器"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@+id/startserver"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<EditText
android:id="@+id/sendmsgtext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="12"
android:inputType="textPersonName" />
<Button
android:id="@+id/sendmsgbt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="发送" />
<Button
android:id="@+id/sendimg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="发送图片" />
</LinearLayout>
<ListView
android:id="@+id/showmsg"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="客户端"
app:layout_constraintBottom_toTopOf="@+id/linearLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleview"
android:transcriptMode="alwaysScroll"
android:divider="#00000000"/>
</androidx.constraintlayout.widget.ConstraintLayout>
chart_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/itemleft"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:background="@drawable/item_left"
android:gravity="center" />
<TextView
android:id="@+id/itemtimeleft"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/itemleft"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:gravity="center"
android:textColor="#A7A9AA"/>
<TextView
android:id="@+id/itemright"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
android:background="@drawable/item_right" />
<TextView
android:id="@+id/itemtimeright"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/itemright"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:gravity="center"
android:textColor="#A7A9AA"/>
<ImageView
android:id="@+id/rightimg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"/>
<ImageView
android:id="@+id/leftimg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_marginTop="10dp"
android:layout_marginStart="10dp"/>
<TextView
android:id="@+id/rightimgtime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_below="@+id/rightimg"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:textColor="#A7A9AA"/>
<TextView
android:id="@+id/leftimgtime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_below="@+id/leftimg"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:textColor="#A7A9AA"/>
</RelativeLayout>
continue_server.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="#F3F3F6">
<TextView
android:layout_width="match_parent"
android:layout_height="64dp"
android:background="#FFFFBB33"
android:text="连接服务器"
android:textColor="#090909"
android:gravity="center"
android:textSize="24dp"
android:textStyle="bold"
android:scaleType="center" />
<EditText
android:id="@+id/editipv4text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="ipv4"
android:gravity="center"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_marginLeft="50dp"
android:layout_marginRight="50dp"/>
<EditText
android:id="@+id/editprottext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="50dp"
android:layout_marginTop="10dp"
android:layout_marginRight="50dp"
android:layout_marginBottom="10dp"
android:gravity="center"
android:hint="端口号" />
</LinearLayout>
start_server.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="#F3F3F6">
<TextView
android:layout_width="match_parent"
android:layout_height="64dp"
android:background="#FFFFBB33"
android:text="开启服务器"
android:textColor="#090909"
android:gravity="center"
android:textSize="24dp"
android:textStyle="bold"
android:scaleType="center" />
<EditText
android:id="@+id/editprot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="端口号"
android:gravity="center"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_marginLeft="50dp"
android:layout_marginRight="50dp"/>
</LinearLayout>
values部分
colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="funtion_bt_false">#DCD7D7</color>
<color name="funtion_bt_true">#C5C5C5</color>
<color name="colorPress">#ffffff</color>
<color name="colorNormal">#8A73F5</color>
<color name="tv_style_color_main">#7A63E5</color>
<color name="tv_style_color_seconde">#FFFFFF</color>
</resources>
themes.xml

源代码分享
链接:https://pan.baidu.com/s/159nd330OR55lKEQiAvHetw
提取码:oz7v
PS:部分机型存在差异,有些功能无法实现,以自己实际验证效果为准。