IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android第二个小项目——聊天室 -> 正文阅读

[移动开发]Android第二个小项目——聊天室

聊天室

前言

这周是我学习Android的第四周,上周写了一个聊天室,这周来总结一下。聊天室分为两个部分,一个部分是客户端,就是我们平时看到的部分,还有一个非常重要的部分,那就是服务端。客户端主要是和用户进行交互,接收用户所传来的指令,在服务端才会将客户端传来的数据进行处理。
在刚开始的时候,我还不知道应该如何去写,没有思路,最后是先做的客户端,将客户端的UI写好之后,再来处理服务端和客户端与服务端的交接的部分。这是我写聊天室的大体思路,我来分享一下。

客户端

首先写的是客户端,因为刚开始,不知道如何去从服务端下手写,所以就先写了客户端的UI。首先,最好写的是布局,我写的聊天室也比较简单,没有多少功能,所以布局也是比较简单的首先来简单介绍一下,我有两个布局,一个是登录的布局,一个是进入之后的聊天布局。

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

    <LinearLayout
        android:layout_marginTop="100dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#000"
            android:text="账户" />

        <EditText
            android:id="@+id/Account_edit"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:maxLines="1"
            android:layout_weight="1" />

    </LinearLayout>

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

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#000"
            android:text="密码" />

        <EditText
            android:id="@+id/password_edit"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:inputType="textPassword"
            android:maxLines="1"
            />

    </LinearLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <Button
            android:id="@+id/login_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="80dp"
            android:background="#FFCC00"
            android:text="登录" />

        <Button
            android:id="@+id/create_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_marginRight="80dp"
            android:background="#FF6600"
            android:text="注册" />

    </RelativeLayout>


  </LinearLayout>

以上是登陆布局,这个布局没什么说的,就是简单的布局和一些简单的控件所组成。
下面是登陆进去的聊天界面。

<?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="match_parent"
        >

    <include layout="@layout/title" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        />

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

        <EditText
            android:id="@+id/ip_edit"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:text="255.255.255.255" />

        <Button
            android:id="@+id/record_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="聊天记录" />

        <Button
            android:id="@+id/clearAll"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="清除聊天记录"
            />

    </LinearLayout>

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

        <EditText
            android:id="@+id/inputText"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:maxLines="1"/>

        <Button
            android:id="@+id/send"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="发送"
            />

        <Button
            android:id="@+id/clear"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="清屏" />

    </LinearLayout>


</LinearLayout>

由于上面使用了RecyclerView,所以就会有每个RecyclerView的小布局。

<?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">


    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <LinearLayout
            android:id="@+id/left_layout"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="left"
            android:background="@drawable/message_left">

            <TextView
                android:id="@+id/left_msg"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_margin="10dp"
                android:textColor="#000" />


        </LinearLayout>

        <TextView
            android:id="@+id/ip_text_left"
            android:layout_width="80dp"
            android:layout_height="wrap_content"

            android:layout_toRightOf="@+id/left_layout"
            android:textColor="#000" />


        <TextView
            android:id="@+id/time_left"
            android:layout_width="80dp"
            android:layout_height="wrap_content"
            android:layout_marginLeft="20dp"
            android:layout_toRightOf="@+id/ip_text_left"
            android:textColor="#000" />

    </RelativeLayout>


    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" >


        <ImageView
            android:id="@+id/my_image"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginTop="20dp"
            android:layout_alignParentRight="true"

            android:background="#000" />

        <LinearLayout
            android:id="@+id/right_layout"
            android:layout_width="wrap_content"
            android:layout_marginTop="10dp"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_gravity="right"
            android:layout_marginRight="60dp"
            android:background="@drawable/message_right">


            <TextView
                android:id="@+id/right_msg"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_margin="10dp"
                android:textColor="#000" />

        </LinearLayout>

        <TextView
            android:id="@+id/ip_text_right"
            android:layout_width="80dp"
            android:layout_height="wrap_content"
            android:layout_toLeftOf="@+id/right_layout"
            android:textColor="#000" />

        <TextView
            android:id="@+id/time_right"
            android:layout_width="80dp"
            android:layout_height="wrap_content"
            android:layout_marginRight="20dp"
            android:layout_toLeftOf="@+id/ip_text_right"
            android:textColor="#000" />


    </RelativeLayout>


</LinearLayout>

上面就是UI的全部代码了,还有一个标题,我没展示出来,那个标题很简单,就一个TextView协商标题就行了。上面那些UI感觉没什么说的,都是一些非常常见的控件和布局,效果我就不展示了,因为太丑了。
下来就是写每个活动中的内容。首先是登录的活动。

package com.example.chatui;

import androidx.appcompat.app.AppCompatActivity;

import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

import org.litepal.LitePal;

import java.util.List;

public class Account extends AppCompatActivity {

    private EditText account_edit;
    private EditText password_edit;
    private Button login_button;
    private Button create_button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_account);

        account_edit = (EditText) findViewById(R.id.Account_edit);
        password_edit = (EditText) findViewById(R.id.password_edit);
        login_button = (Button) findViewById(R.id.login_button);
        create_button = (Button) findViewById(R.id.create_button);

        create_button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String account = account_edit.getText().toString();
                String password = password_edit.getText().toString();
                if (("".equals(account) || account == null) && ("".equals(password) || password == null)) {
                    Toast.makeText(Account.this, "账号密码都不能为空", Toast.LENGTH_SHORT).show();
                } else if ("".equals(password) || password == null) {
                    Toast.makeText(Account.this, "密码不能为空", Toast.LENGTH_SHORT).show();
                } else if ("".equals(account) || account == null) {
                    Toast.makeText(Account.this, "账号不能为空", Toast.LENGTH_SHORT).show();
                } else {
                    LitePal.getDatabase();
                    Login login = new Login();
                    login.setAccount(account);
                    login.setPassword(password);
                    login.save();
                    account_edit.setText("");
                    password_edit.setText("");
                    Toast.makeText(Account.this, "注册成功,请登录", Toast.LENGTH_SHORT).show();
                }
            }
        });

        login_button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String account = account_edit.getText().toString();
                String password = password_edit.getText().toString();
                List<Login> list = LitePal.findAll(Login.class);


                if (("".equals(account) || account == null) && ("".equals(password) || password == null)) {
                    Toast.makeText(Account.this, "账号密码都不能为空", Toast.LENGTH_SHORT).show();
                } else if ("".equals(password) || password == null) {
                    Toast.makeText(Account.this, "密码不能为空", Toast.LENGTH_SHORT).show();
                } else if ("".equals(account) || account == null) {
                    Toast.makeText(Account.this, "账号不能为空", Toast.LENGTH_SHORT).show();
                }  else {

                    for (Login login : list) {
                        String a = login.getAccount();
                        String p = login.getPassword();

                        if (a.equals(account) && p.equals(password)) {
                            Intent intent = new Intent(Account.this, MainActivity.class);
                            startActivity(intent);
                            finish();
                            return;

                        }
                    }
                    Toast.makeText(Account.this, "账号或密码不正确", Toast.LENGTH_SHORT).show();
                    account_edit.setText("");
                    password_edit.setText("");
                }

            }
        });
    }
}

上面就是为之前的登录UI中的每个Button创建了点击事件。我使用LitePal去操作数据库,保存了账号信息,本来是想用账号去做成那种可以一个账号保存一个信息的,但是最后发现知识储备不够,只能做一个本地数据库,所以账号和密码只是能够进入我们的聊天界面。因为它是本地数据库,当我们换个设备之后就没有另一个设备上的信息,我们就要重新创建我们的信息。
这个完成后,我就开始做我的聊天的活动了,这个是花费了我好长的时间。

package com.example.chatui;

import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import org.litepal.LitePal;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    private List<Msg> msgList = new ArrayList<>();

    private String ip = "192.168.1.135";
    private int port = 12345;
    private Socket socket;
    private Button send;
    private EditText inputText;
    private static RecyclerView msgRecyclerView;
    private static MsgAdapter adapter;
    private Button clear;
    private Button record;
    private EditText ip_edit;
    private Button clearAll;
    private BufferedReader br;
    private PrintStream ps;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        LitePal.getDatabase();

        inputText = (EditText) findViewById(R.id.inputText);
        send = (Button) findViewById(R.id.send);
        clear = (Button) findViewById(R.id.clear);
        record = (Button) findViewById(R.id.record_button);
        ip_edit = (EditText) findViewById(R.id.ip_edit);
        clearAll = (Button) findViewById(R.id.clearAll);


        msgRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        msgRecyclerView.setLayoutManager(layoutManager);
        adapter = new MsgAdapter(msgList);
        msgRecyclerView.setAdapter(adapter);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.hide();
        }

        new Thread() {
            @Override
            public void run() {
                try {
                    socket = new Socket(ip, port);
                    br = new BufferedReader(new InputStreamReader(socket.getInputStream(),"GBK"));
                    ps = new PrintStream(socket.getOutputStream(), true, "GBK");
                    new Thread(new Receive()).start();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();

        send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String content = inputText.getText().toString();
                if (!"".equals(content) && socket != null ) {
                    try {
                        String ip = null;
                        ip = ip_edit.getText().toString();
                        String time = getCurrentTime();
                        int i = Msg.TYPE_SEND;
                        Msg msg = new Msg(content, ip, i, time);
                        msgList.add(msg);
                        Record record = new Record();
                        record.setContent(content);
                        record.setIp(ip);
                        record.setTime(time);
                        record.setType(i);
                        record.save();
                        new Thread() {
                            @Override
                            public void run() {
                                ps.println(msg.getContent());
                                ps.println(msg.getIp());
                                ps.println(msg.getTime());
                            }
                        }.start();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    adapter.notifyItemInserted(msgList.size()-1);
                    msgRecyclerView.scrollToPosition(msgList.size()-1);
                    inputText.setText("");
                } else if (socket == null) {
                    Toast.makeText(MainActivity.this, "没有连接到服务器,请检查是否连接服务器!", Toast.LENGTH_SHORT).show();
                }
            }
        });

        clear.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                msgList.clear();
                adapter.notifyDataSetChanged();
            }
        });

        record.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                msgList.clear();
                adapter.notifyDataSetChanged();

                List<Record> list = LitePal.findAll(Record.class);

                for (Record record : list) {
                    Msg msg = new Msg(record.getContent(),
                            record.getIp(), record.getType(),record.getTime());
                    msgList.add(msg);
                    adapter.notifyItemInserted(msgList.size()-1);
                    msgRecyclerView.scrollToPosition(msgList.size()-1);
                }

            }
        });

        clearAll.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this);
                dialog.setTitle("警告!!!");
                dialog.setMessage("你确定要删除所有的聊天记录吗?此操作不能撤回!");
                dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        LitePal.deleteAll(Record.class);
                        msgList.clear();
                        adapter.notifyDataSetChanged();
                    }
                });
                dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                    }
                });
                dialog.show();
            }
        });

    }


    private static String getCurrentTime() {
        Date d = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");
        return sdf.format(d);
    }


    private class Receive implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    String content = br.readLine();
                    String time = br.readLine();
                    String IP = br.readLine();
                    Log.d("receive", content + time);
                    if (content == null || content == "") {
                        continue;
                    } else {
                        Msg msg = new Msg(content, IP, Msg.TYPE_RECEIVED, time);
                        msgList.add(msg);
                        Record record = new Record();
                        record.setContent(msg.getContent());
                        record.setTime(msg.getTime());
                        record.setType(Msg.TYPE_RECEIVED);
                        record.setIp(msg.getIp());
                        record.save();
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                adapter.notifyItemInserted(msgList.size()-1);
                                msgRecyclerView.scrollToPosition(msgList.size()-1);
                            }
                        });
                    }

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }
}

其实这个也没多少内容,但这个确实占用了我做这个项目的大部分时间。首先,要做每个Button的点击事件,这是最基本的,接下来我觉得可能是这个项目最重要的部分了,那就是连接服务器了,我先是在网上找了如何查找自己IP的代码,但是获取到的IP是127.0.0.1,由于要在局域网内进行聊天,肯定不能是一个设备,所以这样不能获取到服务器的IP,最后实在没办法了,就只能使用最笨的办法了,那就是将我的电脑IP直接写进去,这样就直接获取到了我的服务器IP,这样虽然可以,但是没换一个局域网,IP就会发生改变,就需要去重新指定IP,我现在也没有什么好的办法。
还有需要注意的就是操作一些耗时的操作时,要重新开启一条线程,不然就会抛出异常。因为我们发送消息时是按下发送键才会发送消息,这样就有一个依据去什么时候调用发送的指令,但是接受消息没有这样具体的操作,没有人知道什么时候要接受消息,所以我们要一直接收消息,接收消息是一个无限循环,这样不管你什么时候发来的消息,就都可以接收。
还有就是和之前账户一样,存储的聊天记录也是在本地的,还是和之前一样,还是使用LitePal去存储的。我们存储聊天记录时,只需要在两个地方去存储,一个是发送时,另一个就是在接收时。删除聊天记录的操作也很简单,只需要清空数据库中的数据就将聊天记录全部删除了。

服务端

服务端也是非常重要的,首先来看一下服务端的代码:


import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;


public class Server1 {

    private static final int port = 12345;
    private static HashMap<String,Socket> hm;
    private static ServerSocket server;

    public static void main(String[] args) throws IOException {
        hm = new HashMap<>();
        startServer();
    }

    public static void startServer() throws IOException {
        new Thread() {
            @Override
            public void run() {
                try {
                    server = new ServerSocket(port);
                    while (true) {
                        Socket socket = server.accept();
                        System.out.println(1);
                        StringBuilder sb = new StringBuilder(socket.getInetAddress().toString());
                        sb.deleteCharAt(0);
                        hm.put(sb.toString(), socket);
                        System.out.println(sb.toString());
                        handle(socket);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }.start();

    }

    private static void handle(Socket socket) throws IOException {

        new Thread() {
            @Override
            public void run() {
                try {
                    while (true) {
                        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                        String content = br.readLine();
                        String ip = br.readLine();
                        String time = br.readLine();

                        if ("255.255.255.255".equals(ip)) {
                            System.out.println("发给全部人");
                            String str = null;
                            for (String sendIP : hm.keySet()) {
                                try {
                                    StringBuilder sb = new StringBuilder(socket.getInetAddress().toString());
                                    sb.deleteCharAt(0);
                                    str = sb.toString();
                                    if (str.equals(sendIP)) {
                                        continue;
                                    }
                                    Socket s = hm.get(sendIP);
                                    PrintStream ps = new PrintStream(s.getOutputStream(), true, "GBK");
                                    ps.println(content);
                                    System.out.println(content);
                                    ps.println(time);
                                    ps.println(str);
                                } catch (Exception e) {
                                    hm.remove(str);
                                }

                            }
                            System.out.println("发完了");
                        } else {
                            Socket s = null;
                            for (String sendIP : hm.keySet()) {
                                try {
                                    if (ip.equals(sendIP)) {
                                        s = hm.get(sendIP);
                                        break;
                                    }
                                } catch (Exception e) {
                                    hm.remove(ip);
                                }
                            }
                            if (s != null) {
                                StringBuilder sb = new StringBuilder(socket.getInetAddress().toString());
                                String str = sb.deleteCharAt(0).toString();
                                PrintStream ps = new PrintStream(s.getOutputStream(), true, "GBK");
                                ps.println(content);
                                System.out.println(content);
                                ps.println(time);
                                ps.println(str);

                                System.out.println("发送给" + socket.getInetAddress());

                            }
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();


    }


}

这是服务端的代码,服务端只需要指定端口号,如何获取服务端的IP并且连接服务端这就不是服务端干的事了,这是客户端应该关心的,服务端只需要想象已经连接到客户端之后该如何操作。我们要判断一下我们客户端时要发送给谁的,就是发送过来的IP,如果是“255.255.255.255”,那就是发送给全部人,然后就将传入的消息除了自己,给连接在服务端上的每个客户端都发送出去。如果是发给某个IP的话,
就找到那个客户端,如果已连接,就将消息发送出去,如果没有连接服务器,就不会将消息发送出去。

还有需要注意的就是编码问题,因为在虚拟机上只能发送字母,没有汉字,发送的消息和接收的消息是一样的,但是如果在真机上测试,我们默认的汉字是“GBK”码表,但是发送后转换成了“utf-8”码表,所以我们要指定码表发送,指定码表接收。

以下是我的客户端全部代码,服务端的代码在上面就是全部了。

客户端代码

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-12 16:42:49  更:2021-08-12 16:45:02 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/18 23:32:08-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码