Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Need to make special line follow user's finger smoothly that also has other behaviors

Tags:

android

line

First this is not a duplicate of other "smooth line" questions because I also need to be able to delete parts of my line at will and as such I need a special way to store my line.

I need to make a line follow the user's finger. However I also need to be able to delete the end of this line at will.

Basically I need the behavior of this line to look like the blue line that follows the user's mouse in this game:

http://hakim.se/experiments/html5/coil/

To do this I have some code in my onTouch method that adds a point to an array each time the user's finger moves.

@Override
public boolean onTouch(View v, MotionEvent event) {



    //This for loop is supposed to add all points that were in between this 
    //motion event and the previous motion event to the "linePoints" array.

    for(int i = 0; i < event.getHistorySize(); i++) {
        linePoints[arrayIndex] = new Point((int) event.getHistoricalX(i), (int) event.getHistoricalY(i));
        arrayIndex++;
    }

     //This adds the current location of the user's finger to "linePoints"
    // array
    linePoints[arrayIndex] = new Point((int) event.getX(), (int) event.getY());
    arrayIndex++;

     //This switch statement makes it so that if the user lifts their finger
     // off the screen the line will get deleted.
     switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            screenPressed = true;
            setEventTime(); //Ignore setEventTime(); 
            break;
        case MotionEvent.ACTION_UP:
            screenPressed = false;
            linePoints = new Point[10000]; 
            arrayIndex = 0;
            break;
    }
 return true;


}

Then in the onDraw() method the game draws every point on the line:

 @Override
public void onDraw(Canvas canvas) {
    super.onDraw(canvas);


    //This code loops through all of linePoints and then draws every point
    // in linePoints to create a line on screen.
    for(Point p : linePoints) {
       if(p == null) {
           break;
       }

        canvas.drawRect(p.x, p.y, p.x+ 2, p.y + 2, black);
        invalidate();

    // I have not added in the deletion behavior yet, because my current
    // method cannot create a smooth line.
    }

The reason I chose to draw points to make a line instead of using Android's Path() class is because I wanted to delete parts of the line at will (by removing points from the array "linePoints").

The problem is if I move my finger too fast then the points spread out and it stops looking like a line.

How can I make sure the line stays smooth but also is stored in such a way I can delete parts of it?

EDIT: Someone asked for more specifics on how the line will be detailed so I will provide.

I want to start deleting the line if the user has been drawing the line for more than "X" seconds. The way I want to delete the line is:

The end of the line will start disappearing until (meanwhile the user is still drawing it) until the line is completely deleted or the user has lifted their finger off the screen.

EDIT 2: I also need to know if the line has intersected itself or created some sort of closed off shape (hence why I chose the point storage system, I thought that if 2 points in the array had the same coordinates then I would know if the line had intersected itself). I currently have no idea how to implement this (because the points are not continuous) but I will provide further edits if I figure something out.

EDIT 3: I have figured out a solution to determine if the line intersects itself (even if the points are spaced out sporadically)! However I still not have solved the problem of creating a smooth line with no gaps.

Solution:

Each time the game adds a new point to the array it will compare it to the previous point it added to the array and model a line segment "A". It will then compare line segment "A" to all the previous line segments made out of 2 points in the array and determine if the compared segments cross. If they do then I know that there is an intersection in the line.

EDIT 4: This is the complete up to date code that I am currently using.

Inside this code I (try) to provide detailed comments and a summary at the top that explains my objectives and what I have done so far.

To preface this large piece of code, my current problem is being able to delete the line at a consistent pace (e.g 10 millimeters per second) if the user has been drawing their line for more than a certain amount of time.

  package com.vroy.trapper;

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


public class GameView extends View implements View.OnTouchListener {

    // I am basically trying to replicate the game: http://hakim.se/experiments/html5/coil/
    // without any of the lighting effects and my version will have slightly
    // different behavior.

    // Right now all I am concerned with is allowing the line to be deleted at a constant pace
    // if the user has been drawing a line for more than "X" seconds.


    /*
    OVERVIEW:

    array of points "linePoints" stores all locations of user touching screen
    that are captured by system.

    Each time a new point is added to "linePoints" I draw a path from the previous point
    to the new point. (Sidenote: While this does make the line look very smooth it can still look odd sometimes)

    The game also checks for intersections in the line to see if the line has made a
    polygon. I do this because this is important for a feature that will be implemented.

    The system then draws the path on screen.

    The system also checks if the user has lifted their finger off the screen,
    if the user has then the system deletes the current line on screen and resets all variables.

    TO BE IMPLEMENTED:

    If the line has formed a polygon then the game will check if that polygon contains certain
    objects that will randomly spawn onscreen.


    PROBLEMS:

    1. Currently I want the line to start deleting itself from the back if the user
    has been drawing the line for more then "X" seconds. However I am not sure how to do this.

    */


    // General variables.
    private int screenWidth;
    private int screenHeight;
    public static boolean screenPressed; //Might not need.
    //    public static float contactLocX;
    //    public static float contactLocY;

        //Time variables.
        private static long startTime; //This variable is used in conjunction with the
                                       //elapsedTime() method to determine if the user
                                       // has been drawing a line for more then "X" seconds.


    //Game variables.
    private static int orbWidth; //Not used currently. This will be the width of the randomly spawned tokens.
    private Point[] linePoints; //The array that holds all captured points.
    private int arrayIndex;
    private Path linePath; //The path that the canvas draws.
    private boolean firstPoint; //If firstPoint is true then that means is 1st point in current line.
                                //I need this for the path.MoveTo() method.

    //Debug values. (Not used currently)
    private int debug;
    private String strdebug;

    //Paints
    Paint black = new Paint();


    public GameView(Context context, AttributeSet attrs) {
        super(context, attrs);


        black.setARGB(255, 0, 0, 0);  //Paint used to draw line.
        black.setStyle(Paint.Style.STROKE);
        black.setStrokeWidth(3);


        linePoints = new Point[10000];

        GameView gameView = (GameView) findViewById(R.id.GameScreen); //Setting up onTouch listener.
        gameView.setOnTouchListener(this);

        arrayIndex = 0;
        linePath = new Path(); //Setting up initial path.
        firstPoint = true;
    }


    //Currently OnSizeChanged is not needed, I only keep it for the future when I implement
    // the random object spawning system.
    @Override
    public void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        screenHeight = getHeight();
        screenWidth = getWidth();
        orbWidth = screenHeight / 20;
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawPath(linePath, black);

        //Currently "1000000000" is a placeholder value (in nano-seconds)
        if(elapsedTime() > 1000000000 ) {

            //Code that evenly deletes the line starting from the back
            //(this is where I most need your assistance).


        }

        invalidate(); //I don't know if this is the best way to refresh the screen

    }



    @Override
    public boolean onTouch(View v, MotionEvent event) {

        //Sets up starting point of path
        if(firstPoint) {
            firstPoint = false;
            linePath.moveTo(event.getX(),event.getY());
            linePoints.add(new TimeStampedPoint((int)event.getX(),              (int)event.getY(),event.getEventTime()));



        }

        //Adds points to path & linePoints that were missed.
        for(int i = 0; i < event.getHistorySize(); i++) {
            linePoints[arrayIndex] = new Point((int) event.getHistoricalX(i), (int) event.getHistoricalY(i));
            linePath.lineTo(linePoints[arrayIndex].x,linePoints[arrayIndex].y);
            if(arrayIndex >= 1) {
                checkForIntersections(linePoints[arrayIndex - 1], linePoints[arrayIndex]);
            }
            arrayIndex++;
        }

        //Adds current point to path & linePath();
        linePoints[arrayIndex] = new Point((int) event.getX(), (int) event.getY());
        if (arrayIndex >= 1) {
            checkForIntersections(linePoints[arrayIndex - 1] ,linePoints[arrayIndex]);
        }
        linePath.lineTo(linePoints[arrayIndex].x,linePoints[arrayIndex].y);
        arrayIndex++;

        //This switch statements creates initial actions for when the finger is pressed/lifted.
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                screenPressed = true;
                setEventTime(); //This starts the timer that will eventually reach "X" seconds.
                break;
            case MotionEvent.ACTION_UP: //The primary purpose of this "switch" is to delete the old line
                                        // & reset variables in preparation for new line
                screenPressed = false;
                linePoints = new Point[10000]; //Possibly filling heap with empty arrays.
                linePath = new Path();
                arrayIndex = 0;
                firstPoint = true;
                break;
        }

        return true;
    }


    private void checkForIntersections(Point p, Point p2) {

        for(int i = arrayIndex - 3; i > 0; i--) {
            if(intersect(p,p2,linePoints[i],linePoints[i-1])) {
                //RETURN POINTS IN THE POLYGON THAT WILL BE USED TO DETERMINE IF "TOKENS"
                // ARE IN POLYGON.
            }
        }
    }

    private void setEventTime() {
    startTime = System.nanoTime();
    }

    //Checks current time since setEventTime
    private long elapsedTime() {
    return  System.nanoTime() - startTime;
    }

    // Things used to determine intersections.

    //Used to determine orientation of <something>
    private static int orientation(Point p, Point q, Point r) {
        double val = (q.y - p.y) * (r.x - q.x)
                - (q.x - p.x) * (r.y - q.y);

        if (val == 0.0)
            return 0; // colinear
        return (val > 0) ? 1 : 2; // clock or counterclock wise
    }

    //Determines intersection of 2 lines (P1,Q1) & (P2,Q2).
    private static boolean intersect(Point p1, Point q1, Point p2, Point q2) {

        int o1 = orientation(p1, q1, p2);
        int o2 = orientation(p1, q1, q2);
        int o3 = orientation(p2, q2, p1);
        int o4 = orientation(p2, q2, q1);

        if (o1 != o2 && o3 != o4)
            return true;

        return false;
    }

    //Will shorten checking process by determining if 2 lines do/don't have the same bounding box.
    //Not yet implemented.
    private static boolean boundBoxCheck(Point p1, Point q1, Point p2, Point q2) {
        return true; //Placeholder code
    }


    }

EDIT 5:

I did my implementation of stKent's/Titan's code and my code crashes due to a index out of bounds error.

I will be trying to find the problem and fix it but until I do I will post my code here incase someone else wants to take a hand at fixing it.

   package com.vroy.trapper;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import java.sql.Time;
import java.util.ArrayList;
import java.util.List;


public class GameView extends View implements View.OnTouchListener {

    // I am basically trying to replicate the game: http://hakim.se/experiments/html5/coil/
    // without any of the lighting effects and my version will have slightly
    // different behavior.

    // Right now all I am concerned with is allowing the line to be deleted at a constant pace
    // if the user has been drawing a line for more than "X" seconds.


    /*
    OVERVIEW:

    array of points "linePoints" stores all locations of user touching screen
    that are captured by system.

    Each time a new point is added to "linePoints" I draw a path from the previous point
    to the new point. (Sidenote: While this does make the line look very smooth it can still look odd sometimes)

    The game also checks for intersections in the line to see if the line has made a
    polygon. I do this because this is important for a feature that will be implemented.

    The system then draws the path on screen.

    The system also checks if the user has lifted their finger off the screen,
    if the user has then the system deletes the current line on screen and resets all variables.

    TO BE IMPLEMENTED:

    If the line has formed a polygon then the game will check if that polygon contains certain
    objects that will randomly spawn onscreen.


    PROBLEMS:

    1. Currently I want the line to start deleting itself from the back if the user
    has been drawing the line for more then "X" seconds. However I am not sure how to do this.

    */


    // General variables.
    private int screenWidth;
    private int screenHeight;
    public static boolean screenPressed; //Might not need.
//    public static float contactLocX;
//    public static float contactLocY;

        //Time variables.
        private static long startTime; //This variable is used in conjunction with the
                                       //elapsedTime() method to determine if the user
                                       // has been drawing a line for more then "X" seconds.


    //Game variables.
    private static int orbWidth; //Not used currently. This will be the width of the randomly spawned tokens.
    private List<TimeStampedPoint> linePoints; //The array that holds all captured points.
    private int arrayIndex;
    private Path linePath; //The path that the canvas draws.
    private List<TimeStampedPoint> validPoints;
    private boolean firstPoint; //If firstPoint is true then that means is 1st point in current line.
                                //I need this for the path.MoveTo() method.

    //Debug values. (Not used currently)
    private int debugint;
    private String strdebug;

    //Paints
    Paint black = new Paint();


    public GameView(Context context, AttributeSet attrs) {
        super(context, attrs);


        black.setARGB(255, 0, 0, 0);  //Paint used to draw line.
        black.setStyle(Paint.Style.STROKE);
        black.setStrokeWidth(3);


        linePoints = new ArrayList<>();
        validPoints = new ArrayList<>();

        GameView gameView = (GameView) findViewById(R.id.GameScreen); //Setting up onTouch listener.
        gameView.setOnTouchListener(this);

        arrayIndex = 0;
        linePath = new Path(); //Setting up initial path.
        validPoints = new ArrayList<>();
        firstPoint = true;
    }


    //Currently OnSizeChanged is not needed, I only keep it for the future when I implement
    // the random object spawning system.
    @Override
    public void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        screenHeight = getHeight();
        screenWidth = getWidth();
        orbWidth = screenHeight / 20;
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        linePath.rewind();

        validPoints = removeExpiredPoints();
        updatePathUsingPoints(validPoints);

        canvas.drawPath(linePath, black);


        linePoints = validPoints;


        invalidate(); //I don't know if this is the best way to refresh the screen

    }



    @Override
    public boolean onTouch(View v, MotionEvent event) {
        debugint = arrayIndex;
        strdebug = Integer.toString(debugint);

        Log.i("ARRAY INDEX: ",strdebug);

        debugint = linePoints.size();
        strdebug = Integer.toString(debugint);

        Log.i("LIST SIZE: ",strdebug);



        //Sets up starting point of path
        if(firstPoint) {
            firstPoint = false;
            linePath.moveTo(event.getX(),event.getY());
            linePoints.add(new TimeStampedPoint((int)event.getX(),(int)event.getY(),event.getEventTime()));
        }

        //Adds points to path & linePoints that were missed.
        for(int i = 0; i < event.getHistorySize(); i++) {
            linePoints.add(new TimeStampedPoint((int) event.getHistoricalX(i), (int) event.getHistoricalY(i), event.getHistoricalEventTime(i)));
            linePath.lineTo(linePoints.get(arrayIndex).x,linePoints.get(arrayIndex).y);
            if(arrayIndex >= 1) {
                checkForIntersections(linePoints.get(arrayIndex), linePoints.get(arrayIndex));
            }
            arrayIndex++;
        }

        //Adds current point to path & linePath();

        debugint = linePoints.size();
        strdebug = Integer.toString(debugint);
        Log.i("Before" , strdebug);



        linePoints.add(new TimeStampedPoint((int) event.getX(), (int) event.getY(),event.getEventTime()));

        debugint = linePoints.size();
        strdebug = Integer.toString(debugint);
        Log.i("After:", strdebug);


        if (arrayIndex >= 1) {
            checkForIntersections(linePoints.get(arrayIndex - 1) ,linePoints.get(arrayIndex));
        }
        linePath.lineTo(linePoints.get(arrayIndex).x,linePoints.get(arrayIndex).y);
        arrayIndex++;

        //This switch statements creates initial actions for when the finger is pressed/lifted.
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                screenPressed = true;
                setEventTime(); //This starts the timer that will eventually reach "X" seconds.
                break;
            case MotionEvent.ACTION_UP: //The primary purpose of this "switch" is to delete the old line
                                        // & reset variables in preparation for new line
                screenPressed = false;
                linePoints.clear();
                linePath = new Path();
                arrayIndex = 0;
                firstPoint = true;
                break;
        }

        return true;
    }


    private void checkForIntersections(TimeStampedPoint p, TimeStampedPoint p2) {

        for(int i = arrayIndex - 3; i > 0; i--) {
            if(intersect(p,p2,linePoints.get(i),linePoints.get(i-1))) {
                //RETURN POINTS IN THE POLYGON THAT WILL BE USED TO DETERMINE IF "TOKENS"
                // ARE IN POLYGON.
            }
        }
    }

    private void setEventTime() {
    startTime = System.nanoTime();
    }

    //Checks current time since setEventTime
    private long elapsedTime() {
    return  System.nanoTime() - startTime;
    }

    // Things used to determine intersections.

    //Used to determine orientation of <something>
    private static int orientation(Point p, Point q, Point r) {
        double val = (q.y - p.y) * (r.x - q.x)
                - (q.x - p.x) * (r.y - q.y);

        if (val == 0.0)
            return 0; // colinear
        return (val > 0) ? 1 : 2; // clock or counterclock wise
    }

    //Determines intersection of 2 lines (P1,Q1) & (P2,Q2).
    private static boolean intersect(TimeStampedPoint p1, TimeStampedPoint q1, TimeStampedPoint p2, TimeStampedPoint q2) {

        int o1 = orientation(p1, q1, p2);
        int o2 = orientation(p1, q1, q2);
        int o3 = orientation(p2, q2, p1);
        int o4 = orientation(p2, q2, q1);

        if (o1 != o2 && o3 != o4)
            return true;

        return false;
    }

    //Will shorten checking process by determining if 2 lines do/don't have the same bounding box.
    //Not yet implemented.
    private static boolean boundBoxCheck(Point p1, Point q1, Point p2, Point q2) {
        return true; //Placeholder code
    }


    //Point class that also stores time of creation
    @SuppressLint("ParcelCreator")
    private static class TimeStampedPoint extends Point {

        private final long timeStamp;

        private TimeStampedPoint(final int x, final int y, final long timeStamp) {
            super(x, y);
            this.timeStamp = timeStamp;
        }
    }


    private List<TimeStampedPoint> removeExpiredPoints() {
        final List<TimeStampedPoint> result = new ArrayList<>();

        for (final TimeStampedPoint point: linePoints) {
            if (System.currentTimeMillis() - point.timeStamp <= 10000) {
                // We only include points in the result if they are not expired.
                result.add(point);
            }
        }

        return result;
    }

    private void updatePathUsingPoints(final List<TimeStampedPoint> validPoints) {
        if (validPoints.size() < 2) {
            return; // Return the empty path here; nothing to draw.
        }

        linePath.moveTo(validPoints.get(0).x,validPoints.get(0).y);

        for (int i = 1; i < validPoints.size(); i++) {
            final Point targetPoint = validPoints.get(i);
            linePath.lineTo(targetPoint.x, targetPoint.y);
        }


    }

}

There is also something else that is very very important that I must note. I believe it is my fault for not noting this until edit 4 but while I want the line to be deleted from the end I would also like it to be deleted evenly, I think the current code provided by stkent and Titan deletes the points in the line at a consistent pace however that does not actually mean the line itself will be deleted at a consistent pace (Because the points are spread out unevenly).

Much thanks to everyone for sticking with me through the numerous edits until now I hope a solution can be found that also allows the line to be deleted at a consistent pace.

like image 567
Roymunson Avatar asked Jan 03 '16 14:01

Roymunson


2 Answers

I suggest using an ArrayList instead of a static array, as you may not always need to store 10000 Points. I also suggest making a subclass of Point, and have it store a timestamp upon instantiation. Consider:

public class TimedPoint extends Point {
    private static final int KEEP_ALIVE_TIME_MS = 200; //tweak this value to your needs
    private final long time;

    public TimedPoint(int x, int y) {
        super(x, y);
        time = System.currentTimeMillis();
    }

    public TimedPoint(int x, int y, long time) {
        super(x, y);
        this.time = time;
    }

    public boolean hasExpired(long time) {
        return (time-this.time>KEEP_ALIVE_TIME_MS);
    }
}

public class GameView extends View ... {
    ArrayList<TimedPoint> linePoints = new ArrayList<>(); //Lists can grow and shrink to demand
    //this implementation is backed by an array.
    ...

    public void addPoint(int x, int y) {
        linePoints.add(new TimedPoint(x, y);
    }

    public void removeOldPoints() {
        long time = System.currentTimeMillis();
        Iterator<TimedPoint> i = linePoints.iterator();
        while(i.hasNext()) {
            TimedPoint point = i.next();
            if(point.hasExpired(time))
                i.remove();
        }
    }
}

removeOldPoints() will remove any points from linePoints whose time difference is greater than the threshold defined in TimedPoint. This assumes you can call removeOldPoints() regularly. Hint hint, calling in onDraw() would be great.

If removeOldPoints() is called in onDraw before the line is drawn, you can guarantee that any point that's held in linePoints should be drawn. At that point it's as simple as iterating over the list and drawing the points as a line, and the "tail" will start to disappear as you draw.


You could also pass linePoints to TimedPoint and set a Timer upon construction, and schedule() each TimedPoint to remove itself at a certain time in the future. This does not assume you can call removeOldPoints() regularly. Consider:

public class TimedPoint extends Point {
    private static final long KEEP_ALIVE_TIME_MS = 200; //tweak this value to your needs
    //we don't need a timestamp, because every point disposes of itself. We do need a timer, though.
    private final Timer lifetime = new Timer();

    public TimedPoint(final List<TimedPoint> linePoints, int x, int y) {
        super(x, y);
        lifetime.schedule(new TimerTask() {
            @Override
            public void run() {
                linePoints.remove(TimedPoint.this);
            }
        }, KEEP_ALIVE_TIME_MS);
    }
}

 public class GameView extends View ... {
    List<TimedPoint> linePoints = Collections.synchronizedList(new ArrayList<>()); //Lists can grow and shrink to demand
    //this implementation is backed by an array.
    //and is thread safe for Timer
    ...

    public void addPoint(int x, int y) {
        linePoints.add(new TimedPoint(x, y);
    }
   //notice removeOldPoints() is gone! Each point now disposes of itself, no calls needed.
}

There are a couple of things you could tweak with this approach as well. For instance, points start to "die" as soon as they're "born". We can change that to only when added to the list, if that's more appropriate.

Also, there is probably room for optimization as well, as I think this may spawn a new Thread per point. This should actually improve performance(if removeOldPoints() was the bottleneck), up until your cpu is crippled by context switches. If you're feeling pedantic, or performance becomes an issue; you could use a threadpool and a queue.

Here is the documentation for ArrayList to help you get acclimated to the new class.

Happy coding :)


EDIT it seems you're still having trouble. Try this and let me know what it does for you.

public class GameView ... {
    ArrayList<TimedPoint> linePoints = new ArrayList<>(); //Lists can grow and shrink to demand
    //this implementation is backed by an array.
    ...

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        removeOldPoints();
        Path path = linePointsToPath(); //I'm not sure if you need to store path, let's generate it.
        if(path != null)
            canvas.drawPath(path, black);
    }

    public void addPoint(int x, int y) {
        linePoints.add(new TimedPoint(x, y);
        invalidate();
    }

    public void removeOldPoints() {
        int oldLen = linePoints.size();
        long time = System.currentTimeMillis();
        Iterator<TimedPoint> i = linePoints.iterator();
        while(i.hasNext()) {
            TimedPoint point = i.next();
            if(point.hasExpired(time))
                i.remove();
        }
        int newLen = linePoints.size();
        if(newLen != oldLen) //if we removed items from list
            invalidate();
    }

    //small tweaks to stKents method
    private Path linePointsToPath() {
        if(linePoints.size() < 2)
            return null;
        Path path = new Path();
        Point p = points.get(0);
        Path.moveTo(p.x, p.y);
        for(Point point : linePoints) {
            if(p != point)
                path.lineTo(point.x, point.y); //skip first point, because of moveTo
        }
        return path;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        ...
        addPoint(...);
    }
}
like image 112
MeetTitan Avatar answered Oct 23 '22 08:10

MeetTitan


Based on your most recent code, here's what I'd try first. I'm making the following assumptions in this answer:

  • You will only be drawing one line/path at any given time (if not, you'll need to perform the procedure outlined below for each path, by iterating over some collection of Paths)

Create a wrapper around the Point class that adds a timestamp:

private static class TimeStampedPoint extends Point {

    private final long timeStamp;

    private TimeStampedPoint(final int x, final int y, final long timeStamp) {
        super(x, y);
        this.timeStamp = timeStamp;
    }
}

Then update your point storage to the following:

List<TimeStampedPoint> linePoints = new ArrayList<>();

(You'll need to make a bunch of changes to the code as a result of this. In particular, you can use the List method add to append new points to the end of this list, rather than tracking the arrayIndex explicitly.)

In your onTouchEvent method, replace this block of code:

for(int i = 0; i < event.getHistorySize(); i++) {
    linePoints[arrayIndex] = new Point((int) event.getHistoricalX(i), (int) event.getHistoricalY(i));
    linePath.lineTo(linePoints[arrayIndex].x,linePoints[arrayIndex].y);
    if(arrayIndex >= 1) {
        checkForIntersections(linePoints[arrayIndex - 1], linePoints[arrayIndex]);
    }
    arrayIndex++;
}

with something that looks like this:

for(int i = 0; i < event.getHistorySize(); i++) {
    TimeStampedPoint point = new TimeStampedPoint((int) event.getHistoricalX(i), (int) event.getHistoricalY(i), event.getHistoricalEventTime(i));

    linePoints.add(point);
    linePath.lineTo(point.x, point.y);

    int numberOfPoints = linePoints.size();

    if(numberOfPoints >= 2) {
        checkForIntersections(linePoints.get(numberOfPoints - 2), linePoints.get(numberOfPoints - 1));
    }
}

Make a similar adjustment everywhere else you add values to the linePoints array. Note also that we are no longer creating the Path incrementally during this loop. That's because we'll perform some sanitization (i.e., removing expired points) before constructing the Path. To do this, clear the linePath each time you prepare to draw (you might be able to move this method somewhere else if performance is poor; I'm just suggesting it happen in onDraw to make the suggested lifecycle clear). Your onDraw method would then look something like this:

@Override
public void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // Reset the Path.
    linePath.rewind();

    validPoints = removeExpiredPoints();
    updatePathUsingPoints(validPoints);
    canvas.drawPath(linePath, black);

    linePoints = validPoints;

    invalidate(); //I don't know if this is the best way to refresh the screen
}

where validPoints is another field of type List<TimeStampedPoint>s. [In general, calling invalidate from inside onDraw is probably not the best idea, but that is outside the scope of this question.]

Two new methods have been introduced here:

private List<TimeStampedPoint> removeExpiredPoints() {
    final List<TimeStampedPoint> result = new ArrayList<>();

    for (final TimeStampedPoint point: linePoints) {
        if (System.uptimeMillis() - point.getTimeStamp <= 10000) {
            // We only include points in the result if they are not expired.
            result.add(point);
        }
    }

    return result;
}

and

private void updatePathUsingPoints(final List<TimeStampedPoint> validPoints) {
    if (validPoints.size() < 2) {
        return linePath; // Return the empty path here; nothing to draw.
    }

    linePath.moveTo(validPoints.get(0));

    for (int i = 1; i < validPoints.size(); i++) {
        final Point targetPoint = validPoints.get(i);
        linePath.lineTo(targetPoint.x, targetPoint.y);
    }
}

Hopefully this gives you enough of a framework to get started. If you notice the disappearing end of the line is jerky, I have ideas that can help, but it's a bunch more typing - so let's not prematurely optimize :)

like image 2
stkent Avatar answered Oct 23 '22 08:10

stkent