首先环境自己准备,不会的话就参考http://mxblog.mxguanwang.cn/2741.html

文件:MainActivity.java

package com.mxguanwang.mxcar;

import androidx.appcompat.app.AppCompatActivity;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;

import java.util.Timer;
import java.util.TimerTask;


public class MainActivity extends AppCompatActivity {

    private mxcarView mMxcarView = null;
    private TcpClient tcp = new TcpClient();
    long time_send = 0;

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

        setContentView(R.layout.activity_mxcar);
        FrameLayout frame = (FrameLayout) findViewById(R.id.mxcar);
        mMxcarView = new mxcarView(MainActivity.this);
        mMxcarView.SetMainActivity(MainActivity.this);
        mMxcarView.Init();
        mMxcarView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent event) {
                return mMxcarView.OnTouch(event);
            }
        });
        frame.addView(mMxcarView);
        time_send = System.currentTimeMillis();
        tcp.SetMainActivity(this);
        tcp.Run();

        // 起定时器 定时更新延迟
        new Timer().schedule(new TimerTask() {
            @Override
            public void run()
            {
                SendControl();
            }
        }, 0,100);
    }

    void SendControl()
    {
        if (mMxcarView.mIsConnect)
        {
            String strSend = String.format("%04d%04d",mMxcarView.sendx,mMxcarView.sendy);
            tcp.Send(strSend.getBytes());
            time_send = System.currentTimeMillis();
        }
    }

    // 服务器连接
    public void OnConnect()
    {
        mMxcarView.mIsConnect = true;
        byte[] bSend = new byte[1];
        bSend[0] = '2';
        tcp.Send(bSend);
    }

    // 服务器断开
    public void OnDisConnect()
    {
        mMxcarView.mIsConnect = false;
    }
    // 传入数据 和类型
    public void OnRecv(byte[] pData)
    {
        String strRecv = new String(pData);
        int time_car = Integer.parseInt(strRecv.substring(0,4));
        int state_car = Integer.parseInt(strRecv.substring(4,8));
        mMxcarView.time_car = time_car;
        mMxcarView.state_car = state_car;
        mMxcarView.time_input = (int) (System.currentTimeMillis() - time_send);
        mMxcarView.invalidate();
    }
}

文件:mxcarView.java

package com.mxguanwang.mxcar;

import static java.lang.Math.abs;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import androidx.annotation.NonNull;

import java.util.HashMap;
import java.util.Map;

public class mxcarView extends View
{
    public mxcarView(Context context) {
        super(context);
    }

    // 双缓冲实现
    private Bitmap mBitmap;  //缓存绘制的内容
    private Canvas mCanvas;  //内存中创建的Canvas
    // 手指圆形
    private Paint paint_Show = new Paint();
    // 外圈圆形
    private Paint paint_Big = new Paint();
    private Paint paint_bk = new Paint();
    // 是否连接网络
    public boolean mIsConnect = false;
    int cx = 0;
    int cy = 0;
    float radius = 0;

    // 小车的延迟
    int time_car = 0;
    // 小车的状态 0未连接 1已连接
    int state_car = 0;
    // 控制的延迟
    int time_input = 0;

    // 当前手指的位置,用来画圆形
    private static final Point ptCur = new Point();

    public int sendx = 0;
    public int sendy = 0;

    MainActivity mainActivity = null;
    public void SetMainActivity(MainActivity activiy)
    {
        mainActivity = activiy;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        // 初始化bitmap
        mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        mCanvas = new Canvas(mBitmap);
        radius = mBitmap.getWidth()/2-100;
        cx = (int) (radius + 100);
        cy = (int) (mBitmap.getHeight() - radius - 80);
        ptCur.x = cx;
        ptCur.y = cy;
    }

    @SuppressLint("ResourceAsColor")
    public void Init()
    {
        paint_Big.setTextSize(25);
        paint_Big.setStyle(Paint.Style.STROKE);
        paint_Big.setColor(Color.YELLOW);
        paint_Big.setAntiAlias(true);
        paint_Big.setStrokeWidth(5);
        paint_Show.setTextSize(25);
        paint_Show.setStyle(Paint.Style.FILL);
        paint_Show.setColor(Color.GREEN);
        paint_Show.setAntiAlias(true);
        paint_bk.setColor(getResources().getColor(com.google.android.material.R.color.design_default_color_background));
        paint_bk.setTextSize(25);
        paint_bk.setStyle(Paint.Style.FILL);
    }

    // 绘制地图
    void DrawMap()
    {
        mCanvas.drawRect(new Rect(0,0,mBitmap.getWidth(),mBitmap.getHeight()),paint_bk);
        int posY = 50;
        // 服务器连接状态
        if (mIsConnect)
        {
            mCanvas.drawText("服务器状态:已连接",10,posY,paint_Show);
        }
        else
        {
            mCanvas.drawText("服务器状态:未连接",10,posY,paint_Show);
            time_input = -1;
        }
        posY += 40;
        // 小车连接状态
        if (state_car == 1)
        {
            mCanvas.drawText("小车状态:已连接",10,posY,paint_Show);
        }
        else
        {
            mCanvas.drawText("小车状态:未连接",10,posY,paint_Show);
            time_car = -1;
        }
        posY += 40;

        // 服务器连接延迟
        mCanvas.drawText(String.format("服务器延迟:%dms",time_input),10,posY,paint_Show);
        posY += 40;

        // 小车连接延迟
        mCanvas.drawText(String.format("小车延迟:%dms",time_car),10,posY,paint_Show);
        posY += 40;

        // 速度
        mCanvas.drawText(String.format("当前速度: %d-%d",sendx,sendy),10,posY,paint_Show);

        mCanvas.drawCircle(cx,cy,radius,paint_Big);
        mCanvas.drawCircle(ptCur.x,ptCur.y,100,paint_Show);
        // Log.d("DrawMap", String.format("DrawMap: off(%d-%d)",ptCur.x,ptCur.y));
    }

    // 重写绘制,按照自己的方式绘制图像
    @Override
    protected void onDraw(Canvas canvas) {
        //super.onDraw(canvas);
        DrawMap();
        canvas.drawBitmap(mBitmap,0,0,null);
    }

    void CalcPoint(int x,int y)
    {
        float conX = 0;
        float conY = 0;
        conX = x - cx;
        conY = y - cy;
        conX = conX / radius;
        conY = conY / radius;
        if (conX < -1)
        {
            conX = -1;
        }

        if (conX > 1)
        {
            conX = 1;
        }

        if (conY < -1)
        {
            conY = -1;
        }

        if (conY > 1)
        {
            conY = 1;
        }

        sendx = (int) (1023*conX);
        sendy = (int) (1023*conY);

        if (sendx < 0)
        {
            sendx = 5000 - sendx;
        }

        if (sendy < 0)
        {
            sendy = 5000 - sendy;
        }

        Log.d("OnTouch", String.format("百分比: off(%.2f%%-%.2f%%) - send(%d,%d)",
                conX*100,conY*100,sendx,sendy));
        ptCur.x = x;
        ptCur.y = y;
        if (mainActivity != null)
        {
            mainActivity.SendControl();
        }
        invalidate();
    }

    public final boolean OnTouch(@NonNull MotionEvent event)
    {
        int x = (int)event.getX();
        int y = (int)event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
            {
                x = cx;
                y = cy;
            }
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
            {
                CalcPoint(x,y);
                break;
            }
        }
        return true;
    }
}

文件:TcpClient.java

package com.mxguanwang.mxcar;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.sql.Array;
import java.util.Date;
import java.util.LinkedList;
import java.util.Timer;
import java.util.TimerTask;

// 操作网络数据的类
public class TcpClient {
    boolean Connected = false;
    Socket socket = null; // 定义socket
    private OutputStream outputStream = null; // 定义输出流(发送)
    private InputStream inputStream = null; // 定义输入流(接收)
    static String strIp = "你的IP";
    static int nPort = 6668;// 你的端口
    long lastTick = 0;
    // 是否已经接收到回复了,没有回复就抛弃当前消息。
    boolean bIsRecv = false;

    MainActivity mainActivity = null;

    // 是否已经连接
    boolean bIsConnect = false;
    // 保存需要发送的数据
    LinkedList<byte[]> listSend = new LinkedList<byte[]>();

    public void SetMainActivity(MainActivity activiy)
    {
        mainActivity = activiy;
    }

    void OnDisConnect()
    {
        if (!socket.isClosed())
        {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (bIsConnect)
        {
            bIsConnect = false;
            if (mainActivity != null)
            {
                mainActivity.OnDisConnect();
            }
        }
    }

    // 初始化加运行
    public boolean Run()
    {
        Connect_Thread connect_Thread = new Connect_Thread();
        connect_Thread.start();
        Receive_Thread receive_Thread = new Receive_Thread();
        receive_Thread.start();
        Send_Thread send_Thread = new Send_Thread();
        send_Thread.start();
        return true;
    }

    // 发送数据
    public boolean Send(byte[] pData)
    {
        if (socket == null || !socket.isConnected())
        {
            return false;
        }
        listSend.addLast(pData);
        return true;
    }

    // 连接线程
    class Connect_Thread extends Thread
    {
        // 重写run方法
        public void run()
        {
            System.out.println("连接线程已经启动");
            if (socket != null)
            {
                System.out.println("连接线程 socket != null");
                return;
            }

            InetAddress ipAddress = null;
            try {
                ipAddress = InetAddress.getByName(strIp);
            } catch (UnknownHostException e) {
                e.printStackTrace();
            }
            SocketAddress addr = new InetSocketAddress(ipAddress,nPort);
            socket = new Socket();

            while (true)
            {
                try{ Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }
                try
                {
                    if (bIsConnect)
                    {
                        continue;
                    }
                    else
                    {
                        if (inputStream != null)
                        {
                            inputStream.close();
                        }

                        if (outputStream != null)
                        {
                            outputStream.close();
                        }
                    }
                    // 被关闭后无法连接,重新new一个
                    if (socket.isClosed())
                    {
                        socket = new Socket();
                    }
                    socket.connect(addr,1000);
                    if (socket.isConnected())
                    {
                        socket.setSoTimeout(2000);
                        outputStream = socket.getOutputStream();
                        inputStream = socket.getInputStream();
                        bIsConnect = true;
                        lastTick = System.currentTimeMillis();
                        System.out.println("服务器连接成功");
                        mainActivity.OnConnect();
                    }
                    else
                    {
                        System.out.println("服务器连接失败");
                    }
                }catch (Exception e)
                {
                    if (e.getMessage().indexOf("timed out") == -1)
                    {
                        // TODO Auto-generated catch block
                        System.out.println("err :"+ e.getMessage());
                        e.printStackTrace();
                        OnDisConnect();
                    }
                }
            }
        }
    }

    // 接收线程
    class Receive_Thread extends Thread
    {
        public void run()// 重写run方法
        {
            System.out.println("接收线程已经启动");
            final int HeadLen = 28;
            // 专门用来接收数据头
            final byte[] bufferHead = new byte[HeadLen];// 创建接收缓冲区

            // 数据头接收的数据
            int iRecvHeadLen = 0;

            // 数据接收缓冲区
            final byte[] bufferRecv = new byte[8*1024];
            // 当前已经接收的数据
            int iRecvLen = 0;
            // 数据总长度,需要从head中读取
            int iDataLen = 8;
            // 缓冲区长度,暂时不考虑。后续再说。
            int iBuffSize = 8*1024;
            while (true)
            {
                try
                {
                    if (!bIsConnect)
                    {
                        System.out.println("接收线程 socket未连接");
                        // 如果断开就重置并丢弃所有数据。
                        iRecvHeadLen = 0;
                        iRecvLen = 0;
                        iDataLen = 8;
                        try{ Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }
                        continue;
                    }

                    // 按需读取,保证不会接收过多。
                    int iNeed = iDataLen - iRecvLen;
                    int iRecv = inputStream.read(bufferRecv,iRecvLen,iNeed);
                    if (iRecv <= 0)
                    {
                        continue;
                    }
                    iRecvLen += iRecv;
                    if (iRecvLen < iDataLen)
                    {
                        continue;
                    }

                    // 接收到正确的数据包则更新时间
                    // lastTick = System.currentTimeMillis();
                    // 这里肯定已经接收完毕,并且数据量刚刚好
                    if (mainActivity != null)
                    {
                        mainActivity.OnRecv(bufferRecv);
                    }

                    // 对数据进行初始化
                    iRecvHeadLen = 0;
                    iRecvLen = 0;
                    iDataLen = 8;
                }
                catch (IOException e)
                {
                    // TODO Auto-generated catch block
                    // System.out.println("recv err :"+ e.getMessage());
                    // 除超时之外的直接关闭
                    if (e.getMessage().indexOf("timed out") == -1)
                    {
                        e.printStackTrace();
                        OnDisConnect();
                        continue;
                    }
                }
            }
        }
    }

    // 发送线程,安卓不允许主线程发送数据
    class Send_Thread extends Thread
    {
        public void run()// 重写run方法
        {
            System.out.println("发送线程已经启动");
            while (true)
            {
                try
                {
                    if (listSend.isEmpty())
                    {
                        try{ Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
                        continue;
                    }

                    if (!bIsConnect)
                    {
                        System.out.println("发送线程 socket未连接");
                        // 如果断开就重置并丢弃所有数据。
                        try{ Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }
                        continue;
                    }

                    outputStream.write(listSend.getFirst());
                    listSend.removeFirst();
                } catch (IOException e)
                {
                    // TODO Auto-generated catch block
                    System.out.println("send err :"+ e.getMessage());
                    e.printStackTrace();
                    // 发送出现异常就直接关闭并重连
                    OnDisConnect();
                }
            }
        }
    }
}

注意事项:

1.我不懂安卓,也不懂java,代码都是东拼西凑出来的。

2.好像有bug,出现过多次软件闪退的问题,用来测试没问题,但是尽量不要上路,如果找到bug的话请告诉我,多谢了。

3.如果上面代码不全,那没关系,我还提供了整个项目的包。

4.截图如下:

如果失效了可以用百度网盘的https://pan.baidu.com/link/zhihu/7ph3zauRhriTShl0R3XidxZ2RtVLd0bQQ5RH==

代码请改名为zip文件再解压。


一沙一世界,一花一天堂。君掌盛无边,刹那成永恒。