从零开始的聊天室建构(Android Studio)

前言

勿忘IT苦人多,本文会使用JAVA在Android Studio上撰写,Server使用JavaScript在VScode上撰写
需要一些android studio的基础,相关环境方面的问题就不赘述,让我们开始吧
前置作业:
1.安装Java
2.安装Android Studio
3.安装Node.js
4.安装VScode

5.安装Nox,买Android手机(X

1.建立专案

创建一个专案(new project),选择empty Activity -> Next -> Finish,这样就创建了一个空白的专案。
在左边打开Gradle Scripts这个资料夹,选取 build.gradle (Module: app)
我们要在这边引入我们想要使用的功能,在dependencies{}里面新增两行代码

implementation 'androidx.recyclerview:recyclerview:1.0.0'implementation 'com.squareup.okhttp3:okhttp:3.10.0'

然后使用Java8的环境
在android{}里面新增以下代码

compileOptions {        sourceCompatibility '1.8'        targetCompatibility '1.8'    }

这样就修改完成了
http://img2.58codes.com/2024/201271091dqeHaobyB.jpg
文件上方会有一串提示文字,点选" Sync Now ",也可以点选绿色小槌子rebuild

再来去app -> manifests -> AndroidManifest.xml 增加权限

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

INTERNET就是网路,READ_EXTERNAL_STORAGE是我们要传递图片使用的,去读取档案的权限

在"application"内部新增一串代码

android:usesCleartextTraffic="true"

这是用来支持app可以接受未加密的请求
接下来就是使用者界面了

2.使用者界面

主画面,输入自己的名字进入聊天室

<?xml version="1.0" encoding="utf-8"?><RelativeLayout 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">    <EditText        android:id="@+id/editText"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:layout_margin="10dp"        android:layout_marginStart="20dp"        android:layout_marginTop="20dp"        android:layout_marginEnd="20dp"        android:layout_marginBottom="20dp"        android:background="@drawable/edit_text_design"        android:hint="输入您的名字"        android:padding="10dp"        android:textSize="16sp"        tools:ignore="MissingConstraints" />    <Button        android:id="@+id/enterBtn"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:layout_below="@id/editText"        android:layout_marginStart="24dp"        android:layout_marginTop="24dp"        android:layout_marginEnd="24dp"        android:background="@color/colorPrimaryDark"        android:text="进入聊天"        android:textColor="#ffffff" /></RelativeLayout>

文字区域设计 放在drawable里面

<?xml version="1.0" encoding="utf-8"?><shape xmlns:android="http://schemas.android.com/apk/res/android"    android:shape="rectangle">    <solid android:color="#cccccc" />    <stroke android:color="#808080"        android:width="1dp" />    <corners android:radius="5dp"/></shape>

进入MainActivity.java
这里主要是进入房间这个按钮我们希望可以把"名字"这个资讯传入聊天室里面
创建一个Empty Activity 命名为 ChatActivity

package com.example.test0513;import androidx.appcompat.app.AppCompatActivity;import androidx.core.app.ActivityCompat;import android.Manifest;import android.content.Intent;import android.content.pm.PackageManager;import android.os.Bundle;import android.widget.EditText;public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        if(ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)            != PackageManager.PERMISSION_GRANTED){            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 10);        }        EditText editText = findViewById(R.id.editText);        findViewById(R.id.enterBtn)            .setOnClickListener(v -> {                Intent intent = new Intent(this,ChatActivity.class);                intent.putExtra("name",editText.getText().toString());                startActivity(intent);            });    }}

在创建完ChatActivity之后会出现一个activity_chat.xml
建构聊天画面

<?xml version="1.0" encoding="utf-8"?><RelativeLayout 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=".ChatActivity">    <androidx.recyclerview.widget.RecyclerView        android:layout_width="match_parent"        android:layout_height="match_parent"        android:layout_above="@id/messageEdit"        android:id="@+id/recyclerView"/>    <EditText        android:id="@+id/messageEdit"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:layout_alignParentBottom="true"        android:layout_marginStart="16dp"        android:layout_marginBottom="16dp"        android:layout_toStartOf="@id/sendBtn"        android:background="@drawable/edit_text_design"        android:hint="Message..."        android:padding="8dp"        android:textSize="16sp" />    <TextView        android:id="@+id/sendBtn"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_alignParentEnd="true"        android:layout_alignParentBottom="true"        android:layout_marginBottom="16dp"        android:padding="10dp"        android:text="Send"        android:visibility="invisible"        android:textColor="@color/colorPrimary" />    <ImageView        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:src="@drawable/ic_image_black_24dp"        android:tint="@color/colorPrimary"        android:padding="8dp"        android:layout_alignParentBottom="true"        android:layout_toEndOf="@id/messageEdit"        android:layout_marginBottom="16dp"        android:id="@+id/pickImgBtn"        android:layout_alignParentEnd="true" /></RelativeLayout>

接下来都是新增Layout,在Layout资料夹点右键 -> New -> Layout Resource File
新增四个Layout 分别叫

item_received_message.xml

<?xml version="1.0" encoding="utf-8"?><RelativeLayout 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="wrap_content"    tools:context=".MainActivity">    <TextView        android:id="@+id/nameTxt"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_margin="4dp"        android:text="Name"        android:textStyle="bold" />    <TextView        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_below="@+id/nameTxt"        android:background="@color/colorPrimary"        android:padding="8dp"        android:textColor="#ffffff"        android:layout_marginEnd="64dp"        android:layout_marginBottom="4dp"        android:textSize="16sp"        android:id="@+id/receivedTxt"        android:text="Hello my name is yancehn"/></RelativeLayout>

item_received_photo.xml

<?xml version="1.0" encoding="utf-8"?><RelativeLayout 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="wrap_content"    tools:context=".MainActivity">   <TextView       android:layout_width="wrap_content"       android:layout_height="wrap_content"       android:text="Name"       android:id="@+id/nameTxt"       android:layout_margin="4dp"       android:textStyle="bold"/>    <androidx.appcompat.widget.AppCompatImageView        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_marginEnd="64dp"        android:layout_marginStart="4dp"        android:layout_marginBottom="4dp"        android:layout_below="@id/nameTxt"        android:id="@+id/imageView"        android:src="@drawable/ic_image_black_24dp"/></RelativeLayout>

item_send_message.xml

<?xml version="1.0" encoding="utf-8"?><RelativeLayout 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="wrap_content"    tools:context=".MainActivity">    <TextView        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_alignParentEnd="true"        android:layout_marginTop="4dp"        android:layout_marginBottom="4dp"        android:layout_marginStart="64dp"        android:padding="8dp"        android:textSize="16sp"        android:background="#cccccc"        android:text="Hello"        android:id="@+id/sentTxt"/></RelativeLayout>

item_sent_image.xml

<?xml version="1.0" encoding="utf-8"?><RelativeLayout 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="wrap_content"    tools:context=".MainActivity">    <androidx.appcompat.widget.AppCompatImageView        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_alignParentEnd="true"        android:layout_marginTop="4dp"        android:layout_marginBottom="4dp"        android:layout_marginStart="64dp"        android:padding="8dp"        android:id="@+id/imageView"        android:src="@drawable/ic_image_black_24dp"/></RelativeLayout>

这样基本的使用者介面就完成了

3.WebSocket客户端连线

在完成使用者介面之后,接下来我们要来跟伺服器建立连线,我们会使用okhttp3,他是一个网路请求的开源专案
伺服器位址我们可以先用"ws://echo.websocket.org" 这个位址会回传你给他的内容(你跟他说hello 他就跟你说hello),可以用来检视自己发的内容是不是正确的。

打开ChatActivity.java

package com.example.test0513;import android.os.Bundle;import android.view.View;import android.widget.EditText;import androidx.appcompat.app.AppCompatActivity;import androidx.recyclerview.widget.RecyclerView;import okhttp3.OkHttpClient;import okhttp3.Request;import okhttp3.WebSocket;import okhttp3.WebSocketListener;public class ChatActivity extends AppCompatActivity{    private String name;    private String SERVER_PATH = "ws://echo.websocket.org";    private WebSocket webSocket;    private EditText messageEdit;    private View sendBtn, pickImgBtn;    private RecyclerView recyclerView;        private MessageAdapter messageAdapter;//先别管我    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_chat);        name = getIntent().getStringExtra("name");        initiateSocketConnection();    }    private void initiateSocketConnection() {        OkHttpClient client = new OkHttpClient();        Request request = new Request.Builder().url(SERVER_PATH).build();        webSocket = client.newWebSocket(request, new SocketListener());    }    private class SocketListener extends WebSocketListener{            }}

点在WebSocketListener上按Ctrl+O ,override两个方法,onOpen是当连接成功的时候会调用的方法
onMessage(websocket, string)是当收到string内容的时候会执行的方法,点选这两个方法按下OK

private class SocketListener extends WebSocketListener{        @Override        public void onOpen(WebSocket webSocket, Response response) {            super.onOpen(webSocket, response);        }        @Override        public void onMessage(WebSocket webSocket, String text) {            super.onMessage(webSocket, text);        }    }

现在你的SoketListener会长这样,然后我们希望在连接成功的时候我们可以获得一条讯息
所以在onOpen里面

runOnUiThread(() -> {                Toast.makeText(ChatActivity.this, "Socket Connection Successful",                        Toast.LENGTH_SHORT).show();                initializeView();            });

顺带一提,这里的写法没有Java8是不支援的,然后如果连接成功我们希望初始化界面,新增一个initializeView方法
在那上面按下 Alt+Enter + Enter新增方法(在ChatActivity新增)

4.输入内容

在initializeView这个方法里面我们先把该找的Id找好让他们找到归宿
然后我们希望我们的输入介面是这样的,如果使用者没输入内容的时候我们希望按钮变成发送图片,如果使用者输入文字按钮就变成发送按钮所以在这边新增一个addTextChangedListener,他会在每次使用者打字或是删减时被触发

private void initializeView() {        messageEdit = findViewById(R.id.messageEdit);        sendBtn = findViewById(R.id.sendBtn);        pickImgBtn = findViewById(R.id.pickImgBtn);        recyclerView = findViewById(R.id.recyclerView);        messageEdit.addTextChangedListener(this);        messageAdapter = new MessageAdapter(getLayoutInflater());//先别管我        recyclerView.setAdapter(messageAdapter);//先别管我        recyclerView.setLayoutManager(new LinearLayoutManager(this));//先别管我    }

在this上面按Alt+Enter,选择让ChatActivity实作TextWatcher,按下OK会实作三个方法,在afterTextChanged这里去判断是要显示哪种Button(afterTextChanged是三个的其中一个)

@Override    public void afterTextChanged(Editable s) {        String string = s.toString().trim();        if(string.isEmpty()){            resetMessageEdit();        }else{            sendBtn.setVisibility(View.VISIBLE);            pickImgBtn.setVisibility(View.INVISIBLE);        }    }    private void resetMessageEdit() {        messageEdit.removeTextChangedListener(this);        messageEdit.setText("");        sendBtn.setVisibility(View.INVISIBLE);        pickImgBtn.setVisibility(View.VISIBLE);        messageEdit.addTextChangedListener(this);    }

5.发送按钮

再来我们要赋予按钮功能,回到initializeView这个方法

private void initializeView() {        ...略        sendBtn.setOnClickListener(v -> {            JSONObject jsonObject = new JSONObject();            try {                jsonObject.put("name", name);                jsonObject.put("message", messageEdit.getText().toString());                webSocket.send(jsonObject.toString());                jsonObject.put("isSent", true);                messageAdapter.addItem(jsonObject);//先别管我                resetMessageEdit();            } catch (JSONException e) {                e.printStackTrace();            }        });}

在这里,如果使用者按了发送扭,我们就把他的名字还有他输入的内容发送到伺服器,并把输入的内容清空
接下来就是获取图片的按钮

private void initializeView() {        ...略        pickImgBtn.setOnClickListener(v -> {            Intent intent = new Intent(Intent.ACTION_GET_CONTENT);            intent.setType("image/*");            startActivityForResult(Intent.createChooser(intent, "Pick image"),                    IMAGE_REQUEST_ID);        });    }    @Override    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {        super.onActivityResult(requestCode, resultCode, data);        if (requestCode == IMAGE_REQUEST_ID && resultCode == RESULT_OK) {            try {                InputStream is = getContentResolver().openInputStream(data.getData());                Bitmap image = BitmapFactory.decodeStream(is);                sendImage(image);            } catch (FileNotFoundException | JSONException e) {                e.printStackTrace();            }        }    }    private void sendImage(Bitmap image) throws JSONException {        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();        image.compress(Bitmap.CompressFormat.JPEG, 50, outputStream);        String base64String = android.util.Base64.encodeToString(outputStream.toByteArray(),                Base64.DEFAULT);        JSONObject jsonObject = new JSONObject();        jsonObject.put("name", name);        jsonObject.put("image", base64String);        webSocket.send(jsonObject.toString());        jsonObject.put("isSent", true);        messageAdapter.addItem(jsonObject);//先别管我    }

这里的IMAGE_REQUEST_ID可以设置随意的数字,当使用者点选图片按扭的时候,会跳转到档案选择画面,
这段代码会把使用者选取的点阵图片压缩并转成Base64编码发送,这里我们是用android的Base64类别

6.接收讯息

回到SocketListener的onMessage这里我们谈过,是当接收到消息的时候会执行的方法

@Override        public void onMessage(okhttp3.WebSocket webSocket, String text) {            super.onMessage(webSocket, text);            runOnUiThread(() -> {                try {                    JSONObject jsonObject = new JSONObject(text);                    jsonObject.put("isSent", false);                    messageAdapter.addItem(jsonObject);//先别管我                } catch (JSONException e) {                    e.printStackTrace();                }            });        }

在这里我们把收到的字串内容标记 "isSent" 为false,用来区别这个内容不是自己发出的

7.聊天内容显示

新增一个新的Java class,命名为MessageAdapter,并继承RecyclerView.Adapter,按下Alt+Enter
实作所有方法,我们先前创作了四个layout分别是自己发送的文字内容,自己发送的图片,与别人发的文字和图片
根据不同的情况在使用者介面要放置不同的layout

package com.example.test0513;import android.view.LayoutInflater;import android.view.ViewGroup;import androidx.annotation.NonNull;import androidx.recyclerview.widget.RecyclerView;import org.json.JSONObject;import java.util.ArrayList;import java.util.List;public class MessageAdapter extends RecyclerView.Adapter {    private final int TYPE_MESSAGE_SENT = 0;    private final int TYPE_MESSAGE_RECEIVED = 1;    private final int TYPE_IMAGE_SENT = 2;    private final int TYPE_IMAGE_RECEIVED = 3;    private LayoutInflater inflater;    private List<JSONObject> messages = new ArrayList<>();    public MessageAdapter(LayoutInflater inflater){        this.inflater = inflater;    }    @NonNull    @Override    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {        return null;    }    @Override    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {    }    @Override    public int getItemCount() {        return 0;    }}

所以我们需要用到LayoutInflater,创建一个List来放置所有的聊天内容,
接下来我们要创建四个ViewHolder去控制四个layout的内容

private class SentMessageHolder extends RecyclerView.ViewHolder {        TextView messageTxt;        public SentMessageHolder(@NonNull View itemView) {            super(itemView);            messageTxt = itemView.findViewById(R.id.sentTxt);        }    }    private class SentImageHolder extends RecyclerView.ViewHolder{        ImageView imageView;        public SentImageHolder(@NonNull View itemView) {            super(itemView);            imageView = itemView.findViewById(R.id.imageView);        }    }    private class ReceivedMessageHolder extends RecyclerView.ViewHolder{        TextView nameTxt, messageTxt;        public ReceivedMessageHolder(@NonNull View itemView) {            super(itemView);            nameTxt = itemView.findViewById(R.id.nameTxt);            messageTxt = itemView.findViewById(R.id.receivedTxt);        }    }    private class ReceivedImageHolder extends RecyclerView.ViewHolder{        ImageView imageView;        TextView nameTxt;        public ReceivedImageHolder(@NonNull View itemView) {            super(itemView);            imageView = itemView.findViewById(R.id.imageView);            nameTxt = itemView.findViewById(R.id.nameTxt);        }    }

然后创建一个getItemViewType方法去判断是哪一种情况要用哪一个Layout
如果isSent是true就是自己发送的,如果里面有message就是文字内容

@Override    public int getItemViewType(int position) {        JSONObject message = messages.get(position);        try {            if(message.getBoolean("isSent")){                if(message.has("message")){                    return TYPE_MESSAGE_SENT;                }else{                    return TYPE_IMAGE_SENT;                }            }else{                if(message.has("message")){                    return TYPE_MESSAGE_RECEIVED;                }else{                    return TYPE_IMAGE_RECEIVED;                }            }        } catch (JSONException e) {            e.printStackTrace();        }        return -1;    }

再来修改getItemCount这个方法的内容为

@Override    public int getItemCount() {        return messages.size();    }

来获取有几条讯息

之后让我们看到onCreateViewHolder这个方法,我们要在这里套入要用的Layout
并把这些Layout内容放到我们先前做的ViewHolder里面

@NonNull    @Override    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {        View view;        switch(viewType){            case TYPE_MESSAGE_SENT:                view = inflater.inflate(R.layout.item_send_message, parent, false);                return new SentMessageHolder(view);            case TYPE_MESSAGE_RECEIVED:                view = inflater.inflate(R.layout.item_received_message, parent, false);                return new ReceivedMessageHolder(view);            case TYPE_IMAGE_SENT:                view = inflater.inflate(R.layout.item_sent_image, parent, false);                return new SentImageHolder(view);            case TYPE_IMAGE_RECEIVED:                view = inflater.inflate(R.layout.item_received_photo, parent, false);                return new ReceivedImageHolder(view);        }        return null;    }

最后就是在onBindViewHolder放入我们要这些Layout显示的内容

@Override    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {        JSONObject message = messages.get(position);        try {            if(message.getBoolean("isSent")){                if(message.has("message")){                    SentMessageHolder messageHolder = (SentMessageHolder) holder;                    messageHolder.messageTxt.setText(message.getString("message"));                }else{                    SentImageHolder imageHolder = (SentImageHolder) holder;                    Bitmap bitmap = getBitmapFromString(message.getString("image"));                    imageHolder.imageView.setImageBitmap(bitmap);                }            }else{                if(message.has("message")){                    ReceivedMessageHolder messageHolder = (ReceivedMessageHolder) holder;                    messageHolder.nameTxt.setText(message.getString("name"));                    messageHolder.messageTxt.setText(message.getString("message"));                }else{                    ReceivedImageHolder imageHolder = (ReceivedImageHolder) holder;                    imageHolder.nameTxt.setText(message.getString("name"));                    Bitmap bitmap = getBitmapFromString(message.getString("image"));                    imageHolder.imageView.setImageBitmap(bitmap);                }            }        } catch (JSONException e) {            e.printStackTrace();        }    }    private Bitmap getBitmapFromString(String image) {        byte[] bytes = Base64.decode(image, Base64.DEFAULT);        return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);    }

我们再新增一个方法,在有新的聊天内容的时候调用这个方法,把内容放到我们的messages里面
并告知Adapter内部的内容改变了

public void addItem(JSONObject jsonObject){        messages.add(jsonObject);        notifyDataSetChanged();    }

这样客户端的部分就完成了,开起来做测试
http://img2.58codes.com/2024/20127109vn7cZxcdQ8.png

现在是输入甚么文字就会收到甚么文字,图片也是一样

Server

在搜寻列执行cmd -> npm install websocket
并打上ipconfig/all去查看自己的ip 把SERVER_PATH的内容换成
ws://自己的IP:3000

再把下面的代码放在VScode上运行下
就可以多人在同一个聊天室进行聊天了

const SocketServer = require('websocket').server;const http = require('http');const port = 3000;const server = http.createServer((req, res) => {});server.listen(port, () => {    console.log(`Listening on port ${port}...`);});wsServer = new SocketServer({httpServer:server});const connections = [];wsServer.on('request', (req) => {    const connection = req.accept();    console.log('new connection');    connections.push(connection);    connection.on('message', (mes) => {        connections.forEach(element => {            if(element != connection)                element.sendUTF(mes.utf8Data);        });    });    connection.on('close', (resCode, des) => {        console.log('connection closed');        connections.splice(connections.indexOf(connection), 1);    });});

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章