作者:黑衣侠客
这是我目前写的第二个Android App,但是目前因为知识有限,所以对一些代码不能解释到位,其中有些代码形式参考了网上的一些部分,后期慢慢我会将本篇博客进行修改和完善,此次Android网络聊天室会分成3篇博客进行讲解《Android聊天室(客户端)》,《Android聊天室(服务器)》,《Android聊天室(源码)》。由于目前git使用还不太熟,学习进度又很急,因此这次就以源码形式提供代码。另外,如果电脑配置不太高的话,可以尝试着给电脑安装内存条,这样AndroidStudio和虚拟机的运行都会大大加快,提高了编程效率。
现在介绍一下,当服务器和客户端都写好了之后,如何进行使用。
1.首先利用真机安装好App,运行App出现登录界面。
2.运行服务器(注意:服务器我写到了idea中)
3.在App的登录端输入用户名,然后连接进入聊天界面。
在AndroidMainifest.xml中加入这一行代码:
例如我,加到了这里:
然后,在grade(app)的dependencies中,导入Recyclerview的依赖
这里再次介绍一下grade(app):
<?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"//父布局
android:background="@drawable/picture">//登录界面的背景图片
<androidx.appcompat.widget.Toolbar//启用Toolbar工具(菜单导航栏)
android:id="@+id/toolbar"
android:layout_width="match_parent"//父布局
android:layout_height="?attr/actionBarSize"//引用attr属性值
android:background="#3FA2F8">//背景颜色
<TextView//在Toolbar中显示文本字体
android:id="@+id/tv_room"
android:layout_width="wrap_content"//宽度适应字体
android:layout_height="wrap_content"//高度适应字体
android:layout_gravity="center"//居中排布
android:text="登陆聊天室"
android:textColor="#F3F4F5"
android:textSize="20sp" />//字体大小
</androidx.appcompat.widget.Toolbar>//Toolbar结束使用
<!--<View--> //注意大写,否则闪退
<!--android:id="@+id/ver_view"-->
<!--android:layout_toLeftOf="@+id/text_ip"-->
<!--android:layout_width="0dp"-->
<!--android:layout_height="match_parent"-->
<!--/>-->
<TextView
android:id="@+id/tv_name"
android:text="用户名"
android:textSize="20sp"
android:gravity="center"
android:layout_marginBottom="8dp"//距离底部有8dp的边距(注意这个底部不是父布局的底部,而是IP的顶部,如果不理解可以调节为0dp,进行观察)
android:layout_above="@+id/et_ip"
android:layout_marginLeft="45dp"//这个是左侧边距,是相对于父布局来说的(也就是手机的左侧边缘)
android:layout_width="wrap_content"//适应字体大小
android:layout_height="wrap_content" />
<EditText//用户名文本框
android:id="@+id/et_name"
android:layout_toRightOf="@id/tv_name"//在用户名为tv_name的右侧
android:layout_above="@id/et_ip"//用户名文本框放在IP输入的文本框的上面
android:layout_width="150dp"//文本横线的宽度
android:gravity="center"//布局于中心
android:layout_height="wrap_content" />//适应内容的大小
<TextView//IP的text
android:id="@+id/text_ip"
android:text="IP"
android:textSize="20sp"
android:layout_toLeftOf="@+id/et_ip"
android:layout_marginTop="5dp"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_name"
/>
<EditText//IP的文本框
android:id="@+id/et_ip"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"//水平垂直都居中(父类为RelativeLayout)
android:text="192.168.1.66"//可修改
/>
<TextView//端口的textview
android:id="@+id/tv_port"
android:layout_below="@+id/text_ip"
android:layout_marginLeft="45dp"
android:text="端口"
android:textSize="20sp"
android:layout_marginTop="20dp"//他的顶部是IP的底部
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<EditText//端口的文本框
android:id="@+id/et_port"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="6666"//可修改
android:gravity="center"
android:layout_below="@+id/et_ip"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@id/tv_port"
/>
<Button//button连接按键(用于连接客户端与客户端的控件)
android:id="@+id/btn_cnt"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_below="@id/et_port"
android:layout_marginLeft="130dp"
android:layout_marginTop="30dp"//距离端口的文本框的距离(也就是说,端口文本框的底部是button的顶部)
android:background="#07D0F3"//设置控件颜色
android:textColor="#ffffff"//设置字体颜色
android:text="连接" />
</RelativeLayout>
package com.example.my_chatroom;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import java.io.OutputStream;
import java.net.Socket;
public class MainActivity extends AppCompatActivity implements View.OnClickListener{//承接View.OnClickListener接口
private OutputStream outputStream=null;
private Socket socket=null;
private String ip="192.168.1.66";
private Button btn_cnt;
private EditText et_ip;
private EditText et_name;
private EditText et_port;
private TextView myName;
@Override
protected void onCreate(Bundle savedInstanceState) {//这个方法是系统生成Activity之后自带的,是一个生命周期的开始
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_cnt = (Button)findViewById(R.id.btn_cnt);//获取到布局中id为btn_cnt的button,并赋值给声明为Button类型的变量btn_cnt(连接控件)
et_ip=findViewById(R.id.et_ip);
et_port=findViewById(R.id.et_port);
et_name=findViewById(R.id.et_name);
myName=findViewById(R.id.my_name);
btn_cnt.setOnClickListener(MainActivity.this);//setOnClickListener的参数要求是一个实现了OnClickListener接口的对象实体,它可以是任何类的实例,只要该类实现了OnClickListener.
}
public void onClick(View view){
String name = et_name.getText().toString();
if("".equals(name)){
Toast.makeText(this, "请输入用户名:", Toast.LENGTH_SHORT).show();//如果输入的用户名为空的话,那么下端会出现提示
}else{//反之
Intent intent = new Intent(MainActivity.this,ChatRoom.class);
//intent相当于传播的一种媒介,本行的意思是:使内部代码从MainActivity.this到ChatRoom.class进行转变,但只是设置了目标,还未开始执行
intent.putExtra("name",et_name.getText().toString());//这个意思相当于分类处理,在从Activity到ChatRoom,需要将数据送过去,那么就需要对即将传送的数据进行分类处理,使name,ip,port分别对应指定内容
intent.putExtra("ip",et_ip.toString());
intent.putExtra("port",et_port.toString());
startActivity(intent);//开始执行
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:background="#d8e0e8"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#187C8F">
<Button
android:id="@+id/back"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:background="@drawable/back" />
<TextView
android:id="@+id/text_room"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="聊天室"
android:textColor="#F3F4F5"
android:textSize="20sp" />
</androidx.appcompat.widget.Toolbar>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/msg_recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@drawable/background" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/input_text"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#ffffff"
/>
<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:background="#07D0F3"
android:text="发送"
android:textColor="#ffffff" />
</LinearLayout>
</LinearLayout>
package com.example.my_chatroom;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.annotation.SuppressLint;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class ChatRoom extends AppCompatActivity implements View.OnClickListener{
private List<Msg> msgList = new ArrayList<>();
private EditText inputText;
private Button send;
private Button back;
private RecyclerView msgRecyclerView;
private MsgAdapter adapter;
private Socket socketSend;
private String ip="192.168.1.66";
private String port="6666";
DataInputStream dis;
DataOutputStream dos;
boolean isRunning = false;
private TextView myName;
private String recMsg;
private boolean isSend=false;
private String name;
private Handler handler = new Handler(Looper.myLooper()){//获取当前进程的Looper对象传给handler
@Override
public void handleMessage(@NonNull Message msg){//?
if(!recMsg.isEmpty()){
addNewMessage(recMsg,Msg.TYPE_RECEIVED);//添加新数据
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_room);
Intent intent =getIntent();
name=intent.getStringExtra("name");
inputText = findViewById(R.id.input_text);
send=findViewById(R.id.send);
send.setOnClickListener(this);
back = findViewById(R.id.back);
back.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View view){
AlertDialog.Builder dialog= new AlertDialog.Builder(ChatRoom.this);
dialog.setTitle("退出");
dialog.setMessage("退出登录?");
dialog.setCancelable(false);
dialog.setPositiveButton("是", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
finish();//finish()是在程序执行的过程中使用它来将对象销毁,finish()方法用于结束一个Activity的生命周期
}
});
dialog.setNegativeButton("否", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
}
});
dialog.show();//让返回键开始启动
}
});
runOnUiThread(new Runnable() {
@Override
public void run() {
LinearLayoutManager layoutManager = new LinearLayoutManager(ChatRoom.this);
msgRecyclerView= findViewById(R.id.msg_recycler_view);
msgRecyclerView.setLayoutManager(layoutManager);
adapter = new MsgAdapter(msgList);
msgRecyclerView.setAdapter(adapter);
}
});
new Thread(new Runnable(){
@Override
public void run(){
try{
if((socketSend = new Socket(ip,Integer.parseInt(port)))==null){
Log.d("ttw","发送了一条消息1");
}
else{
isRunning = true;
Log.d("ttw","发送了一条消息2");
dis = new DataInputStream(socketSend.getInputStream());
dos = new DataOutputStream(socketSend.getOutputStream());
new Thread(new receive(),"接收线程").start();
new Thread(new Send(),"发送线程").start();
}
}catch(Exception e){
isRunning = false;
e.printStackTrace();
Looper.prepare();
Toast.makeText(ChatRoom.this, "连接服务器失败!!!", Toast.LENGTH_SHORT).show();
Looper.loop();
try{
socketSend.close();
}catch(IOException e1){
e1.printStackTrace();
}
finish();
}
}
}).start();
}
public void addNewMessage(String msg,int type){
Msg message = new Msg(msg,type);
msgList.add(message);
adapter.notifyItemInserted(msgList.size()-1);
msgRecyclerView.scrollToPosition(msgList.size()-1);
}
class receive implements Runnable{
public void run(){
recMsg = "";
while(isRunning){
try{
recMsg = dis.readUTF();
Log.d("ttw","收到了一条消息"+"recMsg: "+ recMsg);
}catch(Exception e){
e.printStackTrace();
}
if(!TextUtils.isEmpty(recMsg)){
Log.d("ttw","inputStream:"+dis);
Message message = new Message();
message.obj=recMsg;
handler.sendMessage(message);
}
}
}
}
@Override
public void onClick(View view){
String content = inputText.getText().toString();
@SuppressLint("SimpleDateFormat")
String date = new SimpleDateFormat("hh:mm:ss").format(new Date());
StringBuilder sb = new StringBuilder();
sb.append(content).append("\n\n"+date);
content = sb.toString();
if(!"".equals(content)){
Msg msg = new Msg(content,Msg.TYPE_SENT);
msgList.add(msg);
adapter.notifyItemInserted(msgList.size()-1);
msgRecyclerView.scrollToPosition(msgList.size()-1);
isSend = true;
}
sb.delete(0,sb.length());
}
class Send implements Runnable{
@Override
public void run(){
while(isRunning){
String content = inputText.getText().toString();
Log.d("ttw","发了一条消息");
if(!"".equals(content)&&isSend){
@SuppressLint("SimpleDateFormat")
String date = new SimpleDateFormat("hh:mm:ss").format(new Date());
StringBuilder sb = new StringBuilder();
sb.append(content).append("\n\n来自:").append(name).append("\n"+date);
content = sb.toString();
try{
dos.writeUTF(content);
sb.delete(0,sb.length());
Log.d("ttw","发送了一条消息");
}catch(IOException e){
e.printStackTrace();
}
isSend = false;
inputText.setText("");
}
}
}
}
}
package com.example.my_chatroom;
public class Msg {
public static final int TYPE_RECEIVED=0;
public static final int TYPE_SENT =1;
public String getContent(){
return content;
}
public int getType(){
return type;
}
private String content;
private int type;
public Msg(String content,int type){
this.content=content;
this.type=type;
}
}
package com.example.my_chatroom;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class MsgAdapter extends RecyclerView.Adapter<MsgAdapter.ViewHolder> {
private List<Msg> mMsgList;
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,int viewType){
//ViewHolder通常出现在适配器里,为的是listview滚动的时候快速设置值,而不必每次都重新创建很多对象,从而提升性能。
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.msg_item,parent,false);
//LayoutInflat.from()从一个Context中,获得一个布局填充器,这样你就可以使用这个填充器来把xml布局文件转为View对象了。
//LayoutInflater.from(parent.getContext()).inflate(R.layout.msg_item,parent,false);这样的方法来加载布局msg_item.xml
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder,int position){
Msg msg =mMsgList.get(position);
if(msg.getType()==Msg.TYPE_RECEIVED){
holder.leftLayout.setVisibility(View.VISIBLE);
holder.rightLayout.setVisibility(View.GONE);
holder.leftMsg.setText(msg.getContent());
}else if(msg.getType()==Msg.TYPE_SENT){
holder.leftLayout.setVisibility(View.GONE);
holder.rightLayout.setVisibility(View.VISIBLE);
holder.rightMsg.setText(msg.getContent());
}
}
@Override
public int getItemCount(){
return mMsgList.size();
}
static class ViewHolder extends RecyclerView.ViewHolder{
LinearLayout leftLayout;
LinearLayout rightLayout;
TextView leftMsg;
TextView rightMsg;
public ViewHolder(@NonNull View view){
super(view);
leftLayout = view.findViewById(R.id.left_layout);
rightLayout = view.findViewById(R.id.right_layout);
leftMsg = view.findViewById(R.id.left_msg);
rightMsg = view.findViewById(R.id.right_msg);
}
}
public MsgAdapter (List<Msg> msgList){
mMsgList = msgList;
}
}
<?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:layout_height="wrap_content"
android:padding="10dp"
>
<LinearLayout
android:id="@+id/left_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
>
<ImageView
android:id="@+id/iv_head_others"
android:background="@drawable/headofothers"
android:layout_marginTop="20dp"
android:layout_width="60dp"
android:layout_height="60dp" />
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/others_name"
android:layout_gravity="left"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/left_msg"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:background="@drawable/qipao_2"
android:layout_gravity="center"
android:textColor="#B955B9" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/right_layout"
android:layout_gravity="right"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/my_name"
android:layout_gravity="right"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/right_msg"
android:textStyle="bold"
android:layout_gravity="center"
android:background="@drawable/qipao_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<ImageView
android:id="@+id/iv_head_my"
android:background="@drawable/headofmy"
android:layout_marginTop="20dp"
android:layout_width="60dp"
android:layout_height="60dp" />
</LinearLayout>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<solid
android:color="#07D0F3"/>
</selector>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<solid
android:color="#0fffff"/>
</selector>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<corners
android:radius="20dp"/>
<item android:drawable="@drawable/shape"
android:state_enabled="true"
android:state_pressed="true"/>
<item android:drawable="@drawable/previous"
android:state_enabled="true"
android:state_pressed="false"/>
</selector>
然后选几张自己喜欢的图片,放在此项目的drawable文件夹中,然后照着代码重命名一下,特别注意,聊天气泡别忘了设置为.9文件(具体看一下第二版《第一行代码》的3.7)链接
另外我收藏了几篇对于阅读本篇博客有很多帮助的文章
对Activity.runOnUiThread的理解
Android Handler详解
android的MainActivity中setOnClickListener(this)中的this指代
Toolbar中的attr/actionBarSize
onCreate方法
到此为止,所有的客户端代码已全部写入,然后还有些代码的注解还没有写,后期我会对本篇博客进行补充和完善。