涉及到的知识点:

1.9.png的使用,这个用来做气泡的

2.RecyclerView滑动组建的使用,貌似要勾选Android 7.0才能使用 之前一直勾8.0折腾了很久

模拟微信聊天系统java 微信模拟聊天工具_微信

3.Socket连网通信

4.线程

5.在子线程中更新UI


在AndroidManifest.xml添加连网权限

<uses-permission android:name="android.permission.INTERNET"/>

准备两个气泡图片

模拟微信聊天系统java 微信模拟聊天工具_聊天_02

模拟微信聊天系统java 微信模拟聊天工具_微信_03

气泡是自己用ps画的,长得比较丑。。。

导入图片后 对图片右键。.9.png

模拟微信聊天系统java 微信模拟聊天工具_群聊_04

把四周黑色的线条拉成这样

模拟微信聊天系统java 微信模拟聊天工具_socket_05

不然拉伸效果会很难看,我们要的是↓这种效果

模拟微信聊天系统java 微信模拟聊天工具_聊天_06

另一个气泡同理

    这里的气泡教程比较详细


布局

先在app\build.gradle里添加如下代码

模拟微信聊天系统java 微信模拟聊天工具_聊天_07

然后开始编辑主Activity布局  activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.shanzhaibanweixin.liziguo.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:background="#eeeeee"
        android:id="@+id/recy"
        android:layout_width="match_parent"
        android:layout_weight="1"
        android:layout_height="0dp"/>

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

        <EditText
            android:id="@+id/text"
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="wrap_content" />
        <Button
            android:id="@+id/bt"
            android:text="发送"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>
</LinearLayout>

大概长这个比样

模拟微信聊天系统java 微信模拟聊天工具_socket_08

新建一个布局命名为xx.xml 下面是每条信息的布局 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:padding="5dp"
    android:layout_height="wrap_content">

    <RelativeLayout
        android:id="@+id/zuo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

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

        <TextView
            android:text="1234567891234568976545678231231231231231231231239231456"
            android:id="@+id/zuotext"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toEndOf="@id/zuoimg"
            android:layout_margin="5dp"
            android:background="@mipmap/qp2"
            android:textSize="25sp" />
    </RelativeLayout>


    <RelativeLayout
        android:id="@+id/you"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:layout_margin="5dp"
            android:id="@+id/youimg"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_alignParentEnd="true"
            android:layout_alignParentTop="true" />
        <TextView
            android:id="@+id/youtext"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toStartOf="@id/youimg"
            android:layout_margin="5dp"
            android:background="@mipmap/qqq"
            android:text="1234567890123413123123123123123568901236547890"
            android:textSize="25sp" />

    </RelativeLayout>


</LinearLayout>

大概长这个比样,正方形的框框是头像

模拟微信聊天系统java 微信模拟聊天工具_微信_09


布局写好了 开始敲代码吧

MainActivity.java的代码

package com.shanzhaibanweixin.liziguo;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private EditText text;
    private Button bt;
    private RecyclerView recy;
    public List list;
    private lianjie lj;
    public Handler hand = new Handler() {//用于在子线程更新UI,收到信息和连接服务器成功时用到
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 1:
                    Toast.makeText(getApplicationContext(), "连接服务器成功", Toast.LENGTH_SHORT).show();
                    break;
                case 2:
                    recy.setAdapter(new adapter(list));//设置适配器
                    recy.scrollToPosition(list.size() - 1);//将屏幕移动到RecyclerView的底部
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        list = new ArrayList<xx>();
        text = findViewById(R.id.text);
        bt = findViewById(R.id.bt);
        recy = findViewById(R.id.recy);
        LinearLayoutManager lin = new LinearLayoutManager(this);
        recy.setLayoutManager(lin);
        lj = new lianjie(this);
        bt.setOnClickListener(new View.OnClickListener() {//给发送按钮设置监听事件
            @Override
            public void onClick(View v) {
                String s = text.getText().toString();
                if (s == null || s.equals("")) {
                    Toast.makeText(getApplicationContext(), "发送消息不能为空", Toast.LENGTH_SHORT).show();
                } else {
                    list.add(new xx(s, R.mipmap.gaara, false));
                    //new一个xx类,第一个参数的信息的内容,第二个参数是头像的图片id,第三个参数表示左右
                    //true为左边,false为右
                    lj.fa(s);//发送消息
                    recy.setAdapter(new adapter(list));//再把list添加到适配器
                    text.setText(null);
                    recy.scrollToPosition(list.size() - 1);//将屏幕移动到RecyclerView的底部
                }
            }
        });
    }
}

新建类adapter.java

package com.shanzhaibanweixin.liziguo;

import android.support.v7.widget.RecyclerView;
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 java.util.List;

/**
 * Created by Liziguo on 2018/5/27.
 */

public class adapter extends RecyclerView.Adapter<adapter.ViewHolder> {//适配器
    private List<xx> list;

    static class ViewHolder extends RecyclerView.ViewHolder {
        ImageView zuoimg, youimg;
        TextView zuotext, youtext;
        ViewGroup zuolin, youlin;

        public ViewHolder(View itemView) {
            super(itemView);
            zuolin = itemView.findViewById(R.id.zuo);
            zuoimg = itemView.findViewById(R.id.zuoimg);
            zuotext = itemView.findViewById(R.id.zuotext);
            youlin = itemView.findViewById(R.id.you);
            youimg = itemView.findViewById(R.id.youimg);
            youtext = itemView.findViewById(R.id.youtext);
        }
    }

    public adapter(List l) {
        list = l;
    }

    @Override
    public adapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.xx, parent, false);
        ViewHolder h = new ViewHolder(v);
        Log.d("MainActivity", "onCreate");
        return h;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {//滑动RecyclerView出发的事件
        xx x = list.get(position);
        if (x.zuo) {//判断该信息该信息是显示在左边还是右边,如果要在左边显示则把右边的部分隐藏
            holder.zuolin.setVisibility(View.VISIBLE);
            holder.youlin.setVisibility(View.GONE);//把右边的隐藏
            holder.zuoimg.setImageResource(x.img);
            holder.zuotext.setText(x.text);
        }else{
            holder.youlin.setVisibility(View.VISIBLE);
            holder.zuolin.setVisibility(View.GONE);//把左边的隐藏
            holder.youimg.setImageResource(x.img);
            holder.youtext.setText(x.text);
        }
        Log.d("MainActivity", "onBind");
    }

    @Override
    public int getItemCount() {//这里要重写一下 不然不会显示任何信息
        Log.d("asdasd", "" + list.size());
        return list.size();
    }
}

class xx {//信息类
    public String text;//信息内容
    public int img;//头像的图片id
    public boolean zuo = true;//控制信息显示在左边还有右边,默认左边

    public xx(String s, int i) {
        text = s;
        img = i;
    }

    public xx(String s, int i, boolean b) {
        text = s;
        img = i;
        zuo = b;
    }
}

新建类lianjie.java    连网类使用Socket进行通信

package com.shanzhaibanweixin.liziguo;

import android.os.Message;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;

/**
 * Created by Liziguo on 2018/5/28.
 */

public class lianjie implements Runnable {
    private MainActivity main;
    private Socket so = null;

    public lianjie(MainActivity m) {
        main = m;
        new Thread(this).start();
    }

    @Override
    public void run() {
        try {
            so = new Socket("xxx.xxx.xxx.xxx", 61666);//第一个参数是你服务器的ip,第二个参数是端口号
            //服务器连接成功的话则发一个Message给UI线程 跳到MainActivity.java的第24行
            Message msg = new Message();
            msg.what = 1;
            main.hand.sendMessage(msg);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

        //连接服务器成功之后开始接受消息
        BufferedReader in = null;
        try {
            in = new BufferedReader(new InputStreamReader(so.getInputStream(),"UTF-8"));
            while (true) {
                String s = in.readLine();
                if (s == null || s.equals("")) break;
                //收到消息之后便new一个xx类,第一个参数是信息内容,第二个参数是头像ID
                //第三个参数显示在左边还是右边,没有第三个参数的话默认显示在左边
                //我事先准备了两张头像
                //最后将他添加到list里
                main.list.add(new xx(s, R.mipmap.sz));
                //收到消息后就要更新RecyclerView将他们显示出来,跳到MainActivity.java的第24行
                Message msg = new Message();
                msg.what = 2;
                main.hand.sendMessage(msg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void fa(final String s) {//发送消息
        new Thread(new Runnable() {
            @Override
            public void run() {//我发现有的手机在UI线程发消息会崩溃,而有的不会
                // 这里就在一个新的线程发消息
                BufferedWriter out = null;
                try {
                    out = new BufferedWriter(new OutputStreamWriter(so.getOutputStream(),"UTF-8"));
                    out.write(s + "\r\n");
                    out.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}



好的,现在已经完成一大半了,接下来就剩服务器端了。服务器端我是用eclipse写的

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class FWQ微信 {//2017.12.3
	public static ArrayList<Socket> list = new ArrayList<Socket>();
	public static Executor ex = Executors.newCachedThreadPool();

	public static void main(String[] args) {
		ServerSocket soo = null;
		Socket so = null;
		try {
			soo = new ServerSocket(61666);
			String chuangjian;
			chuangjian = "创建服务器成功...";
			System.out.println(chuangjian);

			while (true) {
				so = soo.accept();
				list.add(so);
				ex.execute(new Jie(so));
			}
		} catch (IOException e) {
			System.out.println("创建服务器失败...");
			try {
				soo.close();
				so.close();
			} catch (IOException e1) {
			}

		}
	}
}

class Jie implements Runnable {// 用于接收消息
	private Socket so;

	Jie(Socket so) {
		this.so = so;
		String lianjie;
		lianjie = "已连接 用户IP:" + so.getInetAddress().getHostAddress() + "当前连接数:" + FWQ微信.list.size();
		System.out.println(lianjie);
	}

	public void run() {
		BufferedReader in = null;
		try {
			in = new BufferedReader(new InputStreamReader(so.getInputStream(), "UTF-8"));
			String s;
			while ((s = in.readLine()) != null) {
				new Thread(new Fa(s, so)).start();// 收到消息之后 就把收到的消息发送给除了发送者之外在所有人
			}
			FWQ微信.list.remove(so);
			String tuichu;
			tuichu = "已退出 用户IP:" + so.getInetAddress().getHostAddress() + "当前连接数:" + FWQ微信.list.size();
			System.out.println(tuichu);
			in.close();
			so.close();
		} catch (IOException e) {
			FWQ微信.list.remove(so);
			String tuichu;
			tuichu = "已退出 用户IP:" + so.getInetAddress().getHostAddress() + "当前连接数:" + FWQ微信.list.size();
			System.out.println(tuichu);
			try {
				in.close();
				so.close();
			} catch (IOException e1) {
			}
		}
	}
}

class Fa implements Runnable {// 发送消息
	private String s;
	private Socket ss;

	Fa(String s, Socket so) {
		this.s = s;
		ss = so;
	}

	public void run() {
		Socket so;

		try {
			for (int i = 0; i < FWQ微信.list.size(); i++) {
				so = FWQ微信.list.get(i);
				if (so == ss)
					continue;// 收到消息之后 就把收到的消息发送给除了发送者之外在所有人。ss为发送者
				BufferedWriter out = new BufferedWriter(new OutputStreamWriter(so.getOutputStream(), "UTF-8"));
				out.write(s + "\r\n");
				out.flush();
			}
		} catch (IOException e) {
			System.out.println("群发异常");

		}

	}

}

接下来我们运行一下试试

先是服务器端,这里我已经把服务器打包成jar文件上传到服务器了

只接在eclipse上运行也是可以的 lianjie.java的ip改成你电脑的ip就行了

不过手机跟电脑要在同一个局域网

再打开我们的山寨版微信

模拟微信聊天系统java 微信模拟聊天工具_群聊_10

模拟微信聊天系统java 微信模拟聊天工具_微信_11

大功告成!