Android聊天室(客户端)
Android聊天室(客户端)
作者:黑衣侠客
一.前言:
这是我目前写的第二个Android App,但是目前因为知识有限,所以对一些代码不能解释到位,其中有些代码形式参考了网上的一些部分,后期慢慢我会将本篇博客进行修改和完善,此次Android网络聊天室会分成3篇博客进行讲解《Android聊天室(客户端)》,《Android聊天室(服务器)》,《Android聊天室(源码)》。由于目前git使用还不太熟,学习进度又很急,因此这次就以源码形式提供代码。另外,如果电脑配置不太高的话,可以尝试着给电脑安装内存条,这样AndroidStudio和虚拟机的运行都会大大加快,提高了编程效率。
二.操作方法:
现在介绍一下,当服务器和客户端都写好了之后,如何进行使用。
1.首先利用真机安装好App,运行App出现登录界面。
2.运行服务器(注意:服务器我写到了idea中)
3.在App的登录端输入用户名,然后连接进入聊天界面。
三.准备:
1.编译器:AndroidStudio(有很多功能待我们发现)
2.所需知识点:
- Java网络编程(很重要)
- 《Android第一行代码》第三章(UI的基础)
剩下的所用知识点还有很多,我会在下面依次介绍。
(另外,不懂的知识点需要用百度/谷歌之类的搜索)
四.代码部分:
首先加入网络权限:
在AndroidMainifest.xml中加入这一行代码:
例如我,加到了这里:
然后,在grade(app)的dependencies中,导入Recyclerview的依赖
这里再次介绍一下grade(app):
activity_main.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"//父布局 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>
MainActivity
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);//开始执行 } }}
activity_chat_room
<?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>
ChatRoom
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(""); } } } }}
Msg(类)
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; }}
MsgAdapter(类)
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; }}
msg_item.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: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>
一些特效代码:
shape.xml
<?xml version="1.0" encoding="utf-8"?><selector xmlns:android="http://schemas.android.com/apk/res/android"> <solid android:color="#07D0F3"/></selector>
previous.xml
<?xml version="1.0" encoding="utf-8"?><selector xmlns:android="http://schemas.android.com/apk/res/android"> <solid android:color="#0fffff"/></selector>
click.xml
<?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方法
到此为止,所有的客户端代码已全部写入,然后还有些代码的注解还没有写,后期我会对本篇博客进行补充和完善。
更多相关文章
- android:组件化方案
- 在代码中实现android:tint效果
- android单元测试
- Android(安卓)要想美化就用Shape
- Android与js交互实例
- Android中的Binder详解
- Android与js交互实例
- Android(安卓)Wi-Fi工作原理
- Android(安卓)全屏