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方法
到此为止,所有的客户端代码已全部写入,然后还有些代码的注解还没有写,后期我会对本篇博客进行补充和完善。

  • 21
    点赞
  • 115
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值