Related
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.
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(...);
}
}
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 :)
I have a small problem with ploting my graph. On a picture below is what I have already done.
The graph should represent the actual signal strength of available Wi-Fi network(s). It's a simple XYPlot here data are represented with SimpleXYSeries (values are dynamically created).
Here is a little snippet of code (only for example):
plot = (XYPlot) findViewById(R.id.simplexyPlot);
series1 = new SimpleXYSeries(Arrays.asList(series1Numbers),
SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Link 1");
f1 = new LineAndPointFormatter(color.getColor(), null,
Color.argb(60, color.getRed(), color.getGreen(), color.getBlue()), null);
plot.addSeries(series1, f1);
The example in the picture is a dynamic simulation of dB changes. Everything works, I guess, correctly, but what I want to achieve is to have line with "rounded" corners (see the picture to see what I mean).
I already tried to customize LineFormatter:
f1.getFillPaint().setStrokeJoin(Join.ROUND);
f1.getFillPaint().setStrokeWidth(8);
But this didn't work as expected.
Note: The Wifi Analyzer application has a similar graph and its graph has the rounded corners I want. It looks like this:
You can use Path.cubicTo() method. It draws a line using cubic spline algorithm which results in the smoothing effect you want.
Checkout the answer to a similar question here, where a guy is talking about cubic splines. There is a short algorithm showing how to calculate input parameters for Path.cubicTo() method. You can play with divider values to achieve required smoothness. For example, in the picture below I divided by 5 instead of 3. Hope this helps.
I have spent some time and implemented a SplineLineAndPointFormatter class, which does the stuff you need in androidplot library. It uses same technics. Here is how androidplot example applications looks like. You just need to use it instead of LineAndPointFormatter.
Here is code example and the class I wrote.
f1 = new SplineLineAndPointFormatter(color.getColor(), null,
Color.argb(60, color.getRed(), color.getGreen(), color.getBlue()), null);
plot.addSeries(series1, f1);
Here is the class doing the magic. It is based on version 0.6.1 of androidplot library.
package com.androidplot.xy;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import com.androidplot.ui.SeriesRenderer;
import com.androidplot.util.ValPixConverter;
public class SplineLineAndPointFormatter extends LineAndPointFormatter {
public SplineLineAndPointFormatter() { }
public SplineLineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor) {
super(lineColor, vertexColor, fillColor, null);
}
public SplineLineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor, FillDirection fillDir) {
super(lineColor, vertexColor, fillColor, null, fillDir);
}
#Override
public Class<? extends SeriesRenderer> getRendererClass() {
return SplineLineAndPointRenderer.class;
}
#Override
public SeriesRenderer getRendererInstance(XYPlot plot) {
return new SplineLineAndPointRenderer(plot);
}
public static class SplineLineAndPointRenderer extends LineAndPointRenderer<BezierLineAndPointFormatter> {
static class Point {
public float x, y, dx, dy;
public Point(PointF pf) { x = pf.x; y = pf.y; }
}
private Point prev, point, next;
private int pointsCounter;
public SplineLineAndPointRenderer(XYPlot plot) {
super(plot);
}
#Override
protected void appendToPath(Path path, final PointF thisPoint, PointF lastPoint) {
pointsCounter--;
if (point == null) {
point = new Point(thisPoint);
point.dx = ((point.x - prev.x) / 5);
point.dy = ((point.y - prev.y) / 5);
return;
} else if (next == null) {
next = new Point(thisPoint);
} else {
prev = point;
point = next;
next = new Point(thisPoint);
}
point.dx = ((next.x - prev.x) / 5);
point.dy = ((next.y - prev.y) / 5);
path.cubicTo(prev.x + prev.dx, prev.y + prev.dy, point.x - point.dx, point.y - point.dy, point.x, point.y);
if (pointsCounter == 1) { // last point
next.dx = ((next.x - point.x) / 5);
next.dy = ((next.y - point.y) / 5);
path.cubicTo(point.x + point.dx, point.y + point.dy, next.x - next.dx, next.y - next.dy, next.x, next.y);
}
}
#Override
protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAndPointFormatter formatter) {
Number y = series.getY(0);
Number x = series.getX(0);
if (x == null || y == null) throw new IllegalArgumentException("no null values in xyseries permitted");
XYPlot p = getPlot();
PointF thisPoint = ValPixConverter.valToPix(x, y, plotArea,
p.getCalculatedMinX(), p.getCalculatedMaxX(), p.getCalculatedMinY(), p.getCalculatedMaxY());
prev = new Point(thisPoint);
point = next = null;
pointsCounter = series.size();
super.drawSeries(canvas, plotArea, series, formatter);
}
}
}
1- I guess that you only use a few points to draw graphs of signals. All graph/chart applications try to connect points with direct lines and then your chart will be shown. So if you only use three points, your graph will looks like a triangle! If you want your graph to be curved, you have to add more points. Then it comes out like a curve.
2- Or you can find any library that can draw sin graph, for example GraphView Library. Then try to draw this function:
So it looks like to this:
Then translate it to (a,0), so result seems like what you want.
3- And another way, you can use built in Math.sin in Java:
Chose for example 1000 point in range a to b and compute value of above function for each point and finally create a path and show them in a canvas.
You can use quadTo (float x1, float y1, float x2, float y2) that simplify drawing quad curves for you. The documentation says:
Add a quadratic bezier from the last point, approaching control point
(x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
this contour, the first point is automatically set to (0,0).
Parameters
x1 The x-coordinate of the control point on a quadratic curve
y1 The y-coordinate of the control point on a quadratic curve
x2 The x-coordinate of the end point on a quadratic curve
y2 The y-coordinate of the end point on a quadratic curve
Finally, I add a simple class that extends View and can draw a curve that looks like what you want:
public class SinWave extends View {
private float first_X = 50;
private float first_Y = 230;
private float end_X = 100;
private float end_Y = 230;
private float Max = 50;
public SinWave(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint() {
{
setStyle(Paint.Style.STROKE);
setStrokeCap(Paint.Cap.ROUND);
setStrokeWidth(0.7f);
setAntiAlias(true);
setColor(0xFFFF00FF);
}
};
final Path path = new Path();
path.moveTo(first_X, first_Y);
path.quadTo((first_X + end_X)/2, Max, end_X, end_Y);
canvas.drawPath(path, paint);
}
}
The result must look like this:
You can add more methods to the class and change it to increase performance!
There's always been a smooth line renderer in Androidplot: BezierLineAndPointRenderer, which like the implementations above uses Android's built in Bezier drawing routines cubicTo(...) & quadTo(...). The problem is that using Beziers to draw smooth lines in this way creates a false line that overshoots the actual control points by varying amounts, which you can see happening if you look closely at the image above.
The solution is to use the Catmull-Rom spline interpolation, which is now finally supported by Androidplot. Details here: http://androidplot.com/smooth-curves-and-androidplot/
Just use ChartFactory.getCubeLineChartView instead of ChartFactory.getLineChartView using achart engine
In some simple cases, this could help:
mPaint.pathEffect = CornerPathEffect(radius)
even in combination with
path.lineTo(x,y)
try this:
symbol = new Path();
paint = new Paint();
paint.setAntiAlias(true);
paint.setStrokeWidth(2);
paint.setColor(-7829368);
paint.setStrokeJoin(Paint.Join.ROUND); // set the join to round you want
paint.setStrokeCap(Paint.Cap.ROUND); // set the paint cap to round too
paint.setPathEffect(new CornerPathEffect(10) );
paint.setStyle(Paint.Style.STROKE);
symbol.moveTo(50.0F, 230.0F);
symbol.lineTo(75.0F, 100.0F);
symbol.lineTo(100.0F, 230.0F);
most of the info found here
I am trying to check collisions between lines and a circle, if the circle hits the line, it would work change a boolean to true and once its not touched, it would change it back to false. I am using canvas and surface view.
Here is my code for checking collision which didn't work and ended up in error:
#Override
public void run() {
while(runnable)
{
if(!holder.getSurface().isValid()){
continue;
}
Canvas mCanvas = holder.lockCanvas();
update(mCanvas);
values = new Values(mCanvas);
createPaints();
drawBackground(mCanvas);
drawObjects(mCanvas);
holder.unlockCanvasAndPost(mCanvas);
}
}
Now the collision is processed in the update:
private void update(Canvas c) {
ball.update(c, checkLinesCollision(values.level1, ball.getX(), ball.getY()));
//takes a canvas, and a boolean
}
boolean checkLinesCollision(float[] f,float x,float y){
int c = 0;
for(int i = 0; i < f.length; i+=4){
float x1 = f[i];
float y1 = f[i+1];
float x2 = f[i+2];
float y2 = f[i+3];
if (x> x1 && x<x2 && y>y1 && y>y2){
c++;
}
}
if(c>0){return true;}else{return false;}
}
the values for the level
float yLow = c.getHeight()-c.getHeight()/4;
level1 = new float[]{0,yLow,c.getWidth(),yLow,
40,c.getHeight()/2,300,c.getHeight()/2};
ball update function:
public void update(Canvas c, boolean b) {
if(b){
dy=-dy;
b = false;
}
y -= dy;
dy--;
}
Now according to the logcat the problem is in the main update function.
I think that I am using the wrong function, what can I do to fix it?
Thanks!
I have found the solution. Apparently the problem was with the float array; in my app, i declared the float array in a constructor. After a long time of researching on the web and rechecking my logcat, i found that you cannot declare an array in the constructor. all i did was moving the float array outside of the constructor and everything was alright, even though i found the solution, i am not sure why you cannot declare it in the constructor..
I want to make a small app. You will touch the screen and draw something and it will list points you pass and draw small green 3x3 rectangles for each fifth point. I use onTouchEvent for listing points using TextView and send it to setContentView. However, I have problem in drawing. I checked examples for drawing (onDraw) but I am not able to get it working for both printing point plus drawing green dots. Any help would be great, thanks.
Here you are, a quick sample of drawing on SurfaceView.
public class FunPanel extends SurfaceView {
class Point {
int X;
int Y;
public Point() {
X = Y = -1;
}
}
private ArrayList<Point> mPoints = new ArrayList<Point>();
private Point mCurPoint = new Point();
private Bitmap mBitmap = ....// your desired image
#Override
public void doDraw(Canvas canvas) {
if( !(mPoints.size() % 5) ) {
canvas.drawBitmap(mBitmap, mCurPoint.X, mCurPoint.Y, null);
}
}
#Override
public boolean onTouchEvent(MotionEvent event) {
mCurPoint.X = (int) event.getX() - mBitmap.getWidth() / 2;
mCurPoint.Y = (int) event.getY() - mBitmap.getHeight() / 2;
mPoints.add(mCurPoint);
return super.onTouchEvent(event);
}
}
It's not entirely clear what you're trying to do, but have a look at this It should get you started in the right direction. Basically extend a View and override the onDraw(Canvas) to draw the Rectangles and override the onTouchEvent(MotionEvent) to grab the touch points from the screen.
Possible duplicate How to make custom brush for canvas in android?
Hello friends,
I am too stuck to create this type of brush for paint application, but didn't find anything related to this.
I am new to paint/canvas so I don't have knowledge about this for the basic I have completed but for the effect like creating brush I didn't have anything like how to create/implement it. Does anybody have example of or code for this?
I need this type of brush for my application simple one example need for understanding:
Thank you.
I guess there is no easy way. I found this discussion and particularly the following post is interesting:
Professional Computer Graphics is never easy. That's why there are so
few people really tackling it. To make things worse, professional
techniques are rarely published. I don't know how much effort you
desire to make to get it, but I will give you some light. So, if you
want, you can study, develop and get it the best way. If it seem too
hard for you, let it here as a curiosity.
The professional way to make calligraphic brushes nowadays is like
that:
The master curve is smooth because it's drawn based on spline(s). To
get the more professional result, construct two splines: one using the
points you got (for example, from mouse events) lying over the spline
and another using the points like the spline control points. So the
curve you draw is the curve generated from the interpolation of these
two splines. This way, you have a "master curve" to draw.
You should also have a "master thickness" on which a variation must be
applied. This thickness variation is calculated according to the
result you want. The more common kind of calligraphic brush is just
like in the image you linked: the curved regions usually are thinner
than the straight ones. It's the more usual type because most
designers get this kind of result when drawing with a tablet, so
programs emulate this behavior. This effect in particular is usually
calculated using a function based on the second derivate of the master
spline. The thickness variation amplitude can be a configurable value.
The thin and sharp curve tips are made in a extra calculation.
Sometimes it can be a good idea smoothing even the thickness
variations with splines or some kind of "ceil function".
If you made everything right, you have a thick (and of course closed)
curve in your hands. Draw it using the best filling algorithm you can
develop. Use anti-aliasing if you are able to.
All these techniques can be calculated in real time while the user
moves the mouse. The more points you get, the more calculations you
make, but it works well because most calculations you already made are
still valid. Usually you just need to reconstruct a small (last) part.
One last suggestion: never make 2D smoothing using function regression
methods, unless your points really represent a function (so you need
to keep the "math meaning" of the points as much as possible). I can
not imagine a slower way to smooth points that have no special
semantics. The only exception is when you have very very sparse points
and the input order doesn't matter, but it's not the case when
somebody is drawing with brushes.
You can achieved this effect by drawing bitmap texture on a canvas. I cropped a little texture from image you shared and used that as texture in canvas :-
Texture image :-
Here is my view class :-
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.view.MotionEvent;
import android.view.View;
import com.serveroverload.dali.R;
public class CanvasBrushDrawing extends View {
private Bitmap mBitmapBrush;
private Vector2 mBitmapBrushDimensions;
private List<Vector2> mPositions = new ArrayList<Vector2>(100);
private static final class Vector2 {
public Vector2(float x, float y) {
this.x = x;
this.y = y;
}
public final float x;
public final float y;
}
public CanvasBrushDrawing(Context context) {
super(context);
// load your brush here
mBitmapBrush = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_launcher);
mBitmapBrushDimensions = new Vector2(mBitmapBrush.getWidth(), mBitmapBrush.getHeight());
setBackgroundColor(0xffffffff);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (Vector2 pos : mPositions) {
canvas.drawBitmap(mBitmapBrush, pos.x, pos.y, null);
}
}
#Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_MOVE:
final float posX = event.getX();
final float posY = event.getY();
mPositions.add(new Vector2(posX - mBitmapBrushDimensions.x / 2, posY - mBitmapBrushDimensions.y / 2));
invalidate();
}
return true;
}
}
You can use this view in your activity like this :-
setContentView(new CanvasBrushDrawing(MainActivity.this));
Now You just need better texture files from your designer. Hope it helped
You can see complete source code on Git repo https://github.com/hiteshsahu/Dali-PaintBox
Though it is too late i want to share something. This might help someone. Various brush techniques are discussed in the following link with JavaScript code for HTML canvas. All you have to do is convert JavaScript code to your expected one. It is pretty simple to covert JavaScript Canvas code to Android Canvas code.
Exploring canvas drawing techniques
I have converted "Multiple lines" technique to Java code for android; You can check the following android view code.
public class MultipleLines extends View {
private Bitmap bitmap;
private Canvas canvas;
private Paint mPaint;
public MultipleLines(Context context) {
super(context);
init();
}
private void init(){
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setColor(0xFFFF0000);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(1);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
canvas = new Canvas(bitmap);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touch_start(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
touch_move(x, y);
invalidate();
break;
case MotionEvent.ACTION_UP:
touch_up();
invalidate();
break;
}
return true;
}
private boolean isDrawing;
private List<PointF> points = new ArrayList<>();
private void touch_start(float touchX, float touchY) {
isDrawing = true;
points.add(new PointF(touchX, touchY));
canvas.save();
}
private void touch_move(float touchX, float touchY) {
if (!isDrawing) return;
canvas.drawColor(Color.TRANSPARENT);
points.add(new PointF(touchX, touchY));
stroke(offsetPoints(-10));
stroke(offsetPoints(-5));
stroke(points);
stroke(offsetPoints(5));
stroke(offsetPoints(10));
}
private void touch_up() {
isDrawing = false;
points.clear();
canvas.restore();
}
private List<PointF> offsetPoints(float val) {
List<PointF> offsetPoints = new ArrayList<>();
for (int i = 0; i < points.size(); i++) {
PointF point = points.get(i);
offsetPoints.add(new PointF(point.x + val, point.y + val));
}
return offsetPoints;
}
private void stroke(List<PointF> points) {
PointF p1 = points.get(0);
PointF p2 = points.get(1);
Path path = new Path();
path.moveTo(p1.x, p1.y);
for (int i = 1; i < points.size(); i++) {
// we pick the point between pi+1 & pi+2 as the
// end point and p1 as our control point
PointF midPoint = midPointBtw(p1, p2);
path.quadTo(p1.x, p1.y, midPoint.x, midPoint.y);
p1 = points.get(i);
if(i+1 < points.size()) p2 = points.get(i+1);
}
// Draw last line as a straight line while
// we wait for the next point to be able to calculate
// the bezier control point
path.lineTo(p1.x, p1.y);
canvas.drawPath(path,mPaint);
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.WHITE);
canvas.drawBitmap(bitmap, 0, 0, null);
}
private PointF midPointBtw(PointF p1, PointF p2) {
return new PointF(p1.x + (p2.x - p1.x) / 2.0f, p1.y + (p2.y - p1.y) / 2.0f);
}
}