Search code examples
javaandroidnode.jssocket.ioreal-time

How to create dynamic android Path objects on execution of an Event?


I am trying to Build a real time collaborative drawing board app using Android powered by a Node.js server with Socket.io. I have used the Socket.io Java client for the android side. I am able to draw a path, and send the co-ordinates to the server, and get back a broadcast from the server at close to real time. The whole concept is almost complete, But I am stuck with one Big problem.

I am a complete newbie in Android, and this is in fact my first android app, which I am creating completely for learning purpose. So, the problem is that, on Every new connection with the server, I need a new Path object. What is happening now with my Single Path object is there, When the response comes from the server and I try to draw the path, it is using the same path object and thus, joining the line that I draw, with the line co-ordinates that I receive from the server. This problem creates the need to make a new Path object for every new connection to the server. Please note that, by a new connection I mean, a new device who connected to the server by opening the app. That new guy and you can engage in a collaborative drawing experience, atleast, that's What I am trying to build. What I thought would work was, that, on every new connection, I would create a new Path object and put it in an ArrayList and then reference the respective Path object as required. But, then I found a new problem which is that the paths are initialized when the View is actually created inside the onDraw() overridden method.

So, My final Question is:

In Android, How to create dynamic Path objects on execution of an Event?

Here is custom View

package com.example.thisisppn.drawtogether;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import com.github.nkzawa.emitter.Emitter;
import com.github.nkzawa.socketio.client.Socket;
import com.github.nkzawa.socketio.client.IO;

import org.json.JSONException;
import org.json.JSONObject;
import java.net.URISyntaxException;

public class MainDrawingView extends View {
    private Paint paint = new Paint();
    private Path path = new Path();
    private Socket mSocket;

    public MainDrawingView(Context context, AttributeSet attrs) {

        super(context, attrs);
        paint.setAntiAlias(true);
        paint.setStrokeWidth(5f);
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeJoin(Paint.Join.ROUND);

        try {
            mSocket = IO.socket("http://192.168.0.4:3000");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
        mSocket.on("touch", onTouchEvent);
        mSocket.on("move", onMoveEvent);
        mSocket.on("connected", onConnectEvent);
        mSocket.connect();
    }


    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawPath(path, paint);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // Get the coordinates of the touch event.
        float eventX = event.getX();
        float eventY = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // Set a new starting point
                path.moveTo(eventX, eventY);
//                mSocket.emit("touch", "{"+eventX+", "+eventY+"}");
                mSocket.emit("touch", eventX,eventY);
                return true;
            case MotionEvent.ACTION_MOVE:
                // Connect the points
                path.lineTo(eventX, eventY);
                mSocket.emit("move", eventX,eventY);
                break;
            default:
                return false;
        }

        // Makes our view repaint and call onDraw
        invalidate();
        return true;
    }


    public Emitter.Listener onConnectEvent = new Emitter.Listener() {
        @Override
        public void call(Object... args) {
            ((Activity)getContext()).runOnUiThread(new Runnable() {
                @Override
                public void run() {

                }
            });
        }
    };

    public Emitter.Listener onTouchEvent = new Emitter.Listener() {
        @Override
        public void call(final Object... args) {
            ((Activity)getContext()).runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    //Code for the UiThread
                    JSONObject data = (JSONObject) args[0];
                    String x = null;
                    String y = null;
                    try {
                        x = data.getString("touchX");
                        y = data.getString("touchY");
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                    Log.v("Touch", x+", "+y);
                    float touchX = Float.parseFloat(x);
                    float touchY = Float.parseFloat(y);

                    path.moveTo(touchX + 200, touchY + 200);
                    invalidate();
                }
            });
        }
    };

    public Emitter.Listener onMoveEvent = new Emitter.Listener() {
        @Override
        public void call(final Object... args) {
            ((Activity)getContext()).runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    JSONObject data = (JSONObject) args[0];
                    String x = null;
                    String y = null;
                    try {
                        x = data.getString("touchX");
                        y = data.getString("touchY");
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                    Log.v("Move", x+", "+y);
                    float touchX = Float.parseFloat(x);
                    float touchY = Float.parseFloat(y);
                    path.lineTo(touchX+200, touchY+200);
                    invalidate();
                }
            });
        }
    };


}

The MainActivity.java just has the onCreate method, nothing else.

Following is the very basic Node.js server that I am using to receive and broadcast the touch events. Its basically taking the touch co-ordinates and broadcasting it to all connected clients.

// Setup basic express server
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('socket.io')(server);
var port = process.env.PORT || 3000;

server.listen(port, function () {
    console.log('Server listening at port %d', port);
});

var numUsers = 0;

io.on('connection', function (socket) {
    console.log("Someone connected");

    socket.emit("connected");

    socket.on("touch", function (x, y){
        console.log("Touch event happened somewhere");
        console.log(x+", "+y);
        socket.broadcast.emit("touch", {
            touchX: x,
            touchY: y
        });
    });


    socket.on("move", function (x, y){
        console.log("Move event happened somewhere");
        console.log(x+", "+y);
        socket.emit("move", {
            touchX: x,
            touchY: y 
        });
    });
});

Please let me know if I missed out any required information. Would really appreciate it if someone could help me out with this. I could not find any solution to this on my google searches. Thanks in advance for taking your time.


Solution

  • You should try uniquely identifying any new device connected to server.This can be done assigning some kind of DeviceID for each device connected, server side .Look here for reference. In Android for every new device connected create a new Path object. And also create a data structure to map DeviceID to Path object like

    HashMap<String, Path> map = new HashMap<String, Path>();

    Whenever new device is connected add respective DeviceID and new Path() to map

    public Emitter.Listener onConnectEvent = new Emitter.Listener() {
        @Override
        public void call(Object... args) {
             JSONObject data = (JSONObject) args[0];
             String deviceId = data.getString("DeviceID");
             map.put(deviceId, new Path());
        }
    };
    

    Update respective Path object by the reference of DeviceID in onTouchEvent and onMoveEvent

    public Emitter.Listener onTouchEvent = new Emitter.Listener() {
        @Override
        public void call(final Object... args) {
            ((Activity)getContext()).runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    //Code for the UiThread
                    JSONObject data = (JSONObject) args[0];
                    String x = null;
                    String y = null;
                    String deviceId = data.getString("DeviceID");
                    try {
                        x = data.getString("touchX");
                        y = data.getString("touchY");
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                    Log.v("Touch", x+", "+y);
                    float touchX = Float.parseFloat(x);
                    float touchY = Float.parseFloat(y);
    
                    map.get(deviceId).moveTo(touchX + 200, touchY + 200);
                    invalidate();
                }
            });
        }
    };
    

    Apply similar logic for onMoveEvent. Inside onDraw()method

    @Override
    protected void onDraw(Canvas canvas) {
        for (String key : map.keySet()) {
            Path path = map.get(key);
            canvas.drawPath(path, paint);
        }
    }