Custom dynamic graph in Android - android

[UPDATE]
To conclude this question, I implemented my graph using the following two methods (see below). drawCurve() receives a Canvas and an array of float. The array is properly filled (timestamps are assumed by the value index in the array) and varies from 0.0 to 1.0. The array is sent to prepareWindowArray() that takes a chunk of the array from position windowStart for windowSize-values, in a circular manner.
The array used by the GraphView and by the data provider (a Bluetooth device) is the same. A Class in the middle ensures that GraphView is not reading data that are being written by the Bluetooth device. Since the GraphView always loop thru the array and redraw it at every iteration, it will update according to the data written by the Bluetooth device, and by forcing the write frequency of the Bluetooth device to the refresh frequency of the Graph, I obtain a smooth animation of my signal.
The GraphView's invalidate() method is called by the Activity, which run a Timer to refresh the graph at every x milliseconds. The frequency at which the graph is refreshed is dynamically set, so that it adapt to the flow of data from the Bluetooth device (which specify the frequency of its signal in the header of its packet).
Find the complete code of my GraphView in the answer I wrote below (in the answer section). If you guys find errors or way to optimize it, please let me know; it would be greatly appreciated!
/**
* Read a buffer array of size greater than "windowSize" and create a window array out of it.
* A curve is then drawn from this array using "windowSize" points, from left
* to right.
* #param canvas is a Canvas object on which the curve will be drawn. Ensure the canvas is the
* later drawn object at its position or you will not see your curve.
* #param data is a float array of length > windowSize. The floats must range between 0.0 and 1.0.
* A value of 0.0 will be drawn at the bottom of the graph, while a value of 1.0 will be drawn at
* the top of the graph. The range is not tested, so you must ensure to pass proper values, or your
* graph will look terrible.
* 0.0 : draw at the bottom of the graph
* 0.5 : draw in the middle of the graph
* 1.0 : draw at the top of the graph
*/
private void drawCurve(Canvas canvas, float[] data){
// Create a reference value to determine the stepping between each points to be drawn
float incrementX = (mRightSide-mLeftSide)/(float) windowSize;
float incrementY = (mBottomSide - mTopSide);
// Prepare the array for the graph
float[] source = prepareWindowArray(data);
// Prepare the curve Path
curve = new Path();
// Move at the first point.
curve.moveTo(mLeftSide, source[0]*incrementY);
// Draw the remaining points of the curve
for(int i = 1; i < windowSize; i++){
curve.lineTo(mLeftSide + (i*incrementX), source[i] * incrementY);
}
canvas.drawPath(curve, curvePaint);
}
The prepareWindowArray() method that implement the circular behavior of the array:
/**
* Extract a window array from the data array, and reposition the windowStart
* index for next iteration
* #param data the array of data from which we get the window
* #return an array of float that represent the window
*/
private float[] prepareWindowArray(float[] data){
// Prepare the source array for the graph.
float[] source = new float[windowSize];
// Copy the window from the data array into the source array
for(int i = 0; i < windowSize; i++){
if(windowStart+i < data.length) // If the windows holds within the data array
source[i] = data[windowStart + i]; // Simply copy the value in the source array
else{ // If the window goes beyond the data array
source[i] = data[(windowStart + 1)%data.length]; // Loop at the beginning of the data array and copy from there
}
}
// Reposition the buffer index
windowStart = windowStart + windowSize;
// If the index is beyond the end of the array
if(windowStart >= data.length){
windowStart = windowStart % data.length;
}
return source;
}
[/UPDATE]
I'm making an app that read data from a Bluetooth device at a fixed rate. Everytime that I have new data, I want them to be plotted on the graph to the right, and to translate the remainder of the graph to the left in realtime. Basically, like an oscilloscope would do.
So I made a custom View, with xy axis, a title and units. To do this, I simply draw those things on the View canvas. Now I want to draw the curve. I manage to draw a static curve from an already filled array using this method:
public void drawCurve(Canvas canvas){
int left = getPaddingLeft();
int bottom = getHeight()-getPaddingTop();
int middle = (bottom-10)/2 - 10;
curvePaint = new Paint();
curvePaint.setColor(Color.GREEN);
curvePaint.setStrokeWidth(1f);
curvePaint.setDither(true);
curvePaint.setStyle(Paint.Style.STROKE);
curvePaint.setStrokeJoin(Paint.Join.ROUND);
curvePaint.setStrokeCap(Paint.Cap.ROUND);
curvePaint.setPathEffect(new CornerPathEffect(10) );
curvePaint.setAntiAlias(true);
mCurve = new Path();
mCurve.moveTo(left, middle);
for(int i = 0; i < mData[0].length; i++)
mCurve.lineTo(left + ((float)mData[0][i] * 5), middle-((float)mData[1][i] * 20));
canvas.drawPath(mCurve, curvePaint);
}
It gives me something like this.
There are still things to fix on my graph (the sub-axis are not properly scaling), but these are details I can fix later.
Now I want to change this static graph (that receives a non-dynamic matrice of values) with something dynamic that would redraw the curve every 40ms, pushing the old data to the left and plotting the new data to the right, so I could visualise in real time the information provided by the Bluetooth device.
I know there are some graphing package that exists already, but I'm kinda noob with these things and I'd like to pratice by implementing this graph myself. Also, most of my GraphView class is done, except for the curve part.
Second question, I'm wondering how I should send the new values to the graph. Should I use something like a FIFO stack, or can I achieve what I want with a simple matrice of doubles?
On a side note, the 4 fields at the bottom are already dynamically updated. Well, they are kind of faking the "dynamic", they loop thru the same double matrice again and again, they don't actually take fresh values.
Thanks for your time! If something's unclear about my question, let me know and I'll update it with more details.

As mentioned in my question, here's the class that I designed to solve my problems.
/**
* A View implementation that displays a scatter graph with
* automatic unit scaling.
*
* Call the <i>setupGraph()</i> method to modify the graph's
* properties.
* #author Antoine Grondin
*
*/
public class GraphView extends View {
//////////////////////////////////////////////////////////////////
// Configuration
//////////////////////////////////////////////////////////////////
// Set to true to impose the graph properties
private static final boolean TEST = false;
// Scale configuration
private float minX = 0; // When TEST is true, these values are used to
private float maxX = 50; // Draw the graph
private float minY = 0;
private float maxY = 100;
private String titleText = "A Graph...";
private String xUnitText = "s";
private String yUnitText = "Volts";
// Debugging variables
private boolean D = true;
private String TAG = "GraphView";
//////////////////////////////////////////////////////////////////
// Member fields
//////////////////////////////////////////////////////////////////
// Represent the borders of the View
private int mTopSide = 0;
private int mLeftSide = 0;
private int mRightSide = 0;
private int mBottomSide = 0;
private int mMiddleX = 0;
// Size of a DensityIndependentPixel
private float mDips = 0;
// Hold the position of the axis in regard to the range of values
private int positionOfX = 0;
private int positionOfY = 0;
// Index for the graph array window, and size of the window
private int windowStart = 0;
private int windowSize = 128;
private float[] dataSource;
// Painting tools
private Paint xAxisPaint;
private Paint yAxisPaint;
private Paint tickPaint;
private Paint curvePaint;
private Paint backgroundPaint;
private TextPaint unitTextPaint;
private TextPaint titleTextPaint;
// Object to be drawn
private Path curve;
private Bitmap background;
///////////////////////////////////////////////////////////////////////////////
// Constructors
///////////////////////////////////////////////////////////////////////////////
public GraphView(Context context) {
super(context);
init();
}
public GraphView(Context context, AttributeSet attrs){
super(context, attrs);
init();
}
public GraphView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
init();
}
///////////////////////////////////////////////////////////////////////////////
// Configuration methods
///////////////////////////////////////////////////////////////////////////////
public void setupGraph(String title, String nameOfX, float min_X, float max_X, String nameOfY, float min_Y, float max_Y){
if(!TEST){
titleText = title;
xUnitText = nameOfX;
yUnitText = nameOfY;
minX = min_X;
maxX = max_X;
minY = min_Y;
maxY = max_Y;
}
}
/**
* Set the array this GraphView is to work with.
* #param data is a float array of length > windowSize. The floats must range between 0.0 and 1.0.
* A value of 0.0 will be drawn at the bottom of the graph, while a value of 1.0 will be drawn at
* the top of the graph. The range is not tested, so you must ensure to pass proper values, or your
* graph will look terrible.
* 0.0 : draw at the bottom of the graph
* 0.5 : draw in the middle of the graph
* 1.0 : draw at the top of the graph
*/
public void setDataSource(float[] data){
this.dataSource = data;
}
///////////////////////////////////////////////////////////////////////////////
// Initialization methods
///////////////////////////////////////////////////////////////////////////////
private void init(){
initDrawingTools();
}
private void initConstants(){
mDips = getResources().getDisplayMetrics().density;
mTopSide = (int) (getTop() + 10*mDips);
mLeftSide = (int) (getLeft() + 10*mDips);
mRightSide = (int) (getMeasuredWidth() - 10*mDips);
mBottomSide = (int) (getMeasuredHeight() - 10*mDips);
mMiddleX = (mRightSide - mLeftSide)/2 + mLeftSide;
}
private void initWindowSetting() throws IllegalArgumentException {
// Don't do anything if the given values make no sense
if(maxX < minX || maxY < minY ||
maxX == minX || maxY == minY){
throw new IllegalArgumentException("Max and min values make no sense");
}
// Transform the values in scanable items
float[][] maxAndMin = new float[][]{
{minX, maxX},
{minY, maxY}};
int[] positions = new int[]{positionOfY, positionOfX};
// Place the X and Y axis in regard to the given max and min
for(int i = 0; i<2; i++){
if(maxAndMin[i][0] < 0f){
if(maxAndMin[i][1] < 0f){
positions[i] = (int) maxAndMin[i][0];
} else{
positions[i] = 0;
}
} else if (maxAndMin[i][0] > 0f){
positions[i] = (int) maxAndMin[i][0];
} else {
positions[i] = 0;
}
}
// Put the values back in their right place
minX = maxAndMin[0][0];
maxX = maxAndMin[0][1];
minY = maxAndMin[1][0];
maxY = maxAndMin[1][1];
positionOfY = mLeftSide + (int) (((positions[0] - minX)/(maxX-minX))*(mRightSide - mLeftSide));
positionOfX = mBottomSide - (int) (((positions[1] - minY)/(maxY-minY))*(mBottomSide - mTopSide));
}
private void initDrawingTools(){
xAxisPaint = new Paint();
xAxisPaint.setColor(0xff888888);
xAxisPaint.setStrokeWidth(1f*mDips);
xAxisPaint.setAlpha(0xff);
xAxisPaint.setAntiAlias(true);
yAxisPaint = xAxisPaint;
tickPaint = xAxisPaint;
tickPaint.setColor(0xffaaaaaa);
curvePaint = new Paint();
curvePaint.setColor(0xff00ff00);
curvePaint.setStrokeWidth(1f*mDips);
curvePaint.setDither(true);
curvePaint.setStyle(Paint.Style.STROKE);
curvePaint.setStrokeJoin(Paint.Join.ROUND);
curvePaint.setStrokeCap(Paint.Cap.ROUND);
curvePaint.setPathEffect(new CornerPathEffect(10));
curvePaint.setAntiAlias(true);
backgroundPaint = new Paint();
backgroundPaint.setFilterBitmap(true);
titleTextPaint = new TextPaint();
titleTextPaint.setAntiAlias(true);
titleTextPaint.setColor(0xffffffff);
titleTextPaint.setTextAlign(Align.CENTER);
titleTextPaint.setTextSize(20f*mDips);
titleTextPaint.setTypeface(Typeface.MONOSPACE);
unitTextPaint = new TextPaint();
unitTextPaint.setAntiAlias(true);
unitTextPaint.setColor(0xff888888);
unitTextPaint.setTextAlign(Align.CENTER);
unitTextPaint.setTextSize(20f*mDips);
unitTextPaint.setTypeface(Typeface.MONOSPACE);
}
///////////////////////////////////////////////////////////////////////////////
// Overridden methods
///////////////////////////////////////////////////////////////////////////////
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
regenerateBackground();
}
public void onDraw(Canvas canvas){
drawBackground(canvas);
if(dataSource != null)
drawCurve(canvas, dataSource);
}
///////////////////////////////////////////////////////////////////////////////
// Drawing methods
///////////////////////////////////////////////////////////////////////////////
private void drawX(Canvas canvas){
canvas.drawLine(mLeftSide, positionOfX, mRightSide, positionOfX, xAxisPaint);
canvas.drawText(xUnitText, mRightSide - unitTextPaint.measureText(xUnitText)/2, positionOfX - unitTextPaint.getTextSize()/2, unitTextPaint);
}
private void drawY(Canvas canvas){
canvas.drawLine(positionOfY, mTopSide, positionOfY, mBottomSide, yAxisPaint);
canvas.drawText(yUnitText, positionOfY + unitTextPaint.measureText(yUnitText)/2 + 4*mDips, mTopSide + (int) (unitTextPaint.getTextSize()/2), unitTextPaint);
}
private void drawTick(Canvas canvas){
// No tick at this time
// TODO decide how I want to put those ticks, if I want them
}
private void drawTitle(Canvas canvas){
canvas.drawText(titleText, mMiddleX, mTopSide + (int) (titleTextPaint.getTextSize()/2), titleTextPaint);
}
/**
* Read a buffer array of size greater than "windowSize" and create a window array out of it.
* A curve is then drawn from this array using "windowSize" points, from left
* to right.
* #param canvas is a Canvas object on which the curve will be drawn. Ensure the canvas is the
* later drawn object at its position or you will not see your curve.
* #param data is a float array of length > windowSize. The floats must range between 0.0 and 1.0.
* A value of 0.0 will be drawn at the bottom of the graph, while a value of 1.0 will be drawn at
* the top of the graph. The range is not tested, so you must ensure to pass proper values, or your
* graph will look terrible.
* 0.0 : draw at the bottom of the graph
* 0.5 : draw in the middle of the graph
* 1.0 : draw at the top of the graph
*/
private void drawCurve(Canvas canvas, float[] data){
// Create a reference value to determine the stepping between each points to be drawn
float incrementX = (mRightSide-mLeftSide)/(float) windowSize;
float incrementY = mBottomSide - mTopSide;
// Prepare the array for the graph
float[] source = prepareWindowArray(data);
// Prepare the curve Path
curve = new Path();
// Move at the first point.
curve.moveTo(mLeftSide, source[0]*incrementY);
// Draw the remaining points of the curve
for(int i = 1; i < windowSize; i++){
curve.lineTo(mLeftSide + (i*incrementX), source[i] * incrementY);
}
canvas.drawPath(curve, curvePaint);
}
///////////////////////////////////////////////////////////////////////////////
// Intimate methods
///////////////////////////////////////////////////////////////////////////////
/**
* When asked to draw the background, this method will verify if a bitmap of the
* background is available. If not, it will regenerate one. Then, it will draw
* the background using this bitmap. The use of a bitmap to draw the background
* is to avoid unnecessary processing for static parts of the view.
*/
private void drawBackground(Canvas canvas){
if(background == null){
regenerateBackground();
}
canvas.drawBitmap(background, 0, 0, backgroundPaint);
}
/**
* Call this method to force the <i>GraphView</i> to redraw the cache of it's background,
* using new properties if you changed them with <i>setupGraph()</i>.
*/
public void regenerateBackground(){
initConstants();
try{
initWindowSetting();
} catch (IllegalArgumentException e){
Log.e(TAG, "Could not initalize windows.", e);
return;
}
if(background != null){
background.recycle();
}
background = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas backgroundCanvas = new Canvas(background);
drawX(backgroundCanvas);
drawY(backgroundCanvas);
drawTick(backgroundCanvas);
drawTitle(backgroundCanvas);
}
/**
* Extract a window array from the data array, and reposition the windowStart
* index for next iteration
* #param data the array of data from which we get the window
* #return an array of float that represent the window
*/
private float[] prepareWindowArray(float[] data){
// Prepare the source array for the graph.
float[] source = new float[windowSize];
// Copy the window from the data array into the source array
for(int i = 0; i < windowSize; i++){
if(windowStart+i < data.length) // If the windows holds within the data array
source[i] = data[windowStart + i]; // Simply copy the value in the source array
else{ // If the window goes beyond the data array
source[i] = data[(windowStart + 1)%data.length]; // Loop at the beginning of the data array and copy from there
}
}
// Reposition the buffer index
windowStart = windowStart + windowSize;
// If the index is beyond the end of the array
if(windowStart >= data.length){
windowStart = windowStart % data.length;
}
return source;
}
}

Well I would start by just trying to redraw it all with the code you have and real dynalic data. Only if that is not quick enough do you need to try anything fancy like scrolling...
If you need fancy I would try somthing like this.
I would draw the dynamic part of the graph into a secondary Bitmap that you keep between frames rather than directly to the canves. I would have the background none dynamic part of the graph in another bitmap that only gets drawen on rescale etc.
In this secondary dynamic bitmap when ploting new data you first need to clear the old data you are replacing you do this by drawing the apropriate slice of the static background bitmap over the top of the stale data, thus clearing it and geting the background nice and fresh again. You then just need to draw your new bit of dynamic data. The trick is that You draw into this second bitmap left to right then just wrap back to the left at the end and start over.
To get from the soncodary bitmap to your cancas draw the bitmap to the canvas in two parts. The older data to the right of what you just added needs to be drawn onto the left part of your final canvas and the new data needs to be drawn imediatly to the right of it.
For sending the data a circular buffer would be the normal thing for this sort of data where once it's off the graph you don't care about it.

Related

How to draw arrow that follows finger from starting actor to another actor Libgdx

I have two tables in my game. When I tap on one the cells of the first table, I want to draw an arrow that follows my finger so that I can move the arrow head to a table cell from the second table. I need to be able to know which was the initial cell and which was the final cell (where the arrow started and where the arrow finished). If the user takes the finger off the screen and it wasn't on a table cell I want nothing to happen.
ArrayList<Actor> myActor = new ArrayList<Actor>();
ArrayList<Actor> anotherActor = new ArrayList<Actor>();
// fill array with my actors
Table firstTable = new Table();
for (int i = 0; i < 7; i++) {
firstTable.add(myActor.get(i));
myActor.get(i).addListener(new ActorGestureListener() {
public void touchDown(InputEvent event, float x, float y, int pointer, int button) {
// draw arrow that follows finger
}
public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
// if event finished and was on a cell from the
// second table get starting actor and finishing actor
// else nothing happens
}
});
}
Table secondTable = new Table();
for (int i = 0; i < 7; i++) {
secondTable.add(anotherActor.get(i));
}
How should I do this?
Hope this helps.
Actor startFromActor = null; // Variable outside of method
public void foo() {
// smt before
ArrayList<Actor> myActor = new ArrayList<Actor>();
ArrayList<Actor> anotherActor = new ArrayList<Actor>();
// fill array with my actors
Table firstTable = new Table();
Table secondTable = new Table();
for (int i = 0; i < 7; i++) {
final Actor curActor = myActor.get(i);
firstTable.add(curActor);
curActor.addListener(new ActorGestureListener() {
public void touchDown(InputEvent event, float x, float y, int pointer, int button) {
if (pointer > 0) return false; // For only one finger
startFromActor = curActor;
// draw arrow that follows finger
}
});
}
for (int i = 0; i < 7; i++) {
final Actor curActor = anotherActor.get(i);
secondTable.add(curActor);
curActor.addListener(new ActorGestureListener() {
public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
if (pointer > 0) return false; // For only one finger
if (startFromActor != null) {
// do something
// startActor - actor from first table
// curActor - actor from second table
startFromActor = null; // do not forget to null-setting
}
}
});
}
}
If you need both directions from table to table, you should set touchUp and touchDown events for every Actor and then check who is owner for start and end actors;
You can draw an arrow using the built in Mesh utilities in libgdx:
Build a Mesh at init
It is suggested to only build one mesh ( usually at init ) and then re-use ( render ) it during runtime as many times as many arrows the application requires. For the urposes of building the mesh, the Meshbuilder and ArrowShapeBuilder can be used. Example:
MeshBuilder meshbuilder = new MeshBuilder();
meshbuilder.begin(VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal, GL20.GL_TRIANGLES);
ArrowShapeBuilder.build(
meshbuilder,
0,0,0 /* Starting xyz */
5,5,0 /* Ending xyz */
0.1f, /* percentage of arrow head */
0.1f, /* percentage of stem thickness */
10 /* divisions, basically level of detail */
);
Mesh debug_arrow = meshbuilder.end();
By the above a renderable mesh is constructed.
Render the arrow to your app
This step is quite simple, provided a Batch is already available :
debug_arrow.render(batch.getShader(), GL20.GL_TRIANGLES);
The position of the arrow is then being set by the stage projection matrix. A Batch should be available, the very least the rendering stage has one through getBatch().
Note: The mesh parameter musn't always be GL20.GL_TRIANGLES, but the parameters must match in meshbuilder.begin and debug_arrow.render.

Identify the grid of a plotted point in AndroidPlot

I have an ecg graph plotting application and the graph looks like this.
What I need to know is,is it possible to know the subgrid in which the point is plotted... say in a format like (row_index,column_index) or something like this. Actually I don't know whether it is a possible scenario. So if there is no way to do this please let me know.
Given below is my graph configuring method.
private void configureGraph() {
/**
* ecgPlot corresponds to XYPlot
*/
XYGraphWidget graph = ecgPlot.getGraph();
/**
* Paint to denote line color
*/
Paint paint = new Paint();
paint.setColor(Color.WHITE);
paint.setStrokeWidth(3.0f);
/**
* Setting graph x and y boundary values
*/
ecgPlot.setRangeBoundaries(-40, 40, BoundaryMode.FIXED);
ecgPlot.setDomainBoundaries(0, 1500, BoundaryMode.FIXED);
ecgPlot.setPlotPadding(-10, 0, 0, 0);
/**
* Removes default bkg - ie; black
*/
ecgPlot.setBackgroundPaint(null);
graph.setBackgroundPaint(null);
graph.setGridBackgroundPaint(null);
/**
* Adjusting grid line width
*/
graph.getDomainGridLinePaint().setStrokeWidth(4.0f);
graph.getRangeGridLinePaint().setStrokeWidth(4.0f);
graph.getDomainSubGridLinePaint().setStrokeWidth(1.0f);
graph.getRangeSubGridLinePaint().setStrokeWidth(1.0f);
/**
* Removes border
*/
ecgPlot.setBorderPaint(null);
/**
* Setting grid color
*/
graph.getDomainGridLinePaint().setColor(getResources().getColor(R.color.colorECGGrid));
graph.getRangeGridLinePaint().setColor(getResources().getColor(R.color.colorECGGrid));
graph.getRangeSubGridLinePaint().setColor(getResources().getColor(R.color.colorECGGrid));
graph.getDomainSubGridLinePaint().setColor(getResources().getColor(R.color.colorECGGrid));
/**
* Setting number of sub grid lines per grid
*/
graph.setLinesPerDomainLabel(5);
graph.setLinesPerRangeLabel(5);
ecgPlot.setRangeStep(StepMode.INCREMENT_BY_VAL, 1);
ecgPlot.setDomainStepValue(75);
ecgPlot.setLinesPerDomainLabel(5);
ecgPlot.setDomainLabel(null);
ecgPlot.setRangeLabel(null);
Paint paintTest = new Paint();
paintTest.setColor(Color.TRANSPARENT);
paintTest.setStrokeWidth(3.0f);
ecgLinePointFormatter.setLegendIconEnabled(false);
// PointLabelFormatter pointLabelFormatter = new PointLabelFormatter();
// pointLabelFormatter.setTextPaint(paint);
}
Thanks in advance
Unfortunately I'm not at a place where I can actually test whether this code works, but here's a general idea on how you can convert from real XY values into subgrid coords:
double subX(Number x) {
// calculate the value each subgrid represents:
double stepVal = (plot.getBounds().getWidth().doubleValue() / 75) * 5;
// find the value of x relative to the left edge of the screen
double xOff = x.doubleValue() - plot.getBounds().getMinX().doubleValue();
return xOff / stepVal;
}
double subY(Number y) {
double stepVal = plot.getBounds().getHeight().doubleValue() / 5;
double yOff = y.doubleValue() - plot.getBounds().getMinY().doubleValue();
return yOff / stepVal;
}
Then, given Number x and Number y that you want to convert into subgrid coords:
Number x = subX(realX);
Number y = subY(realY);
If you need pixel values instead of real values, you can use the XYPlot.seriesToScreenX/Y(Number) and XYPlot.screenToSeriesX/Y(Number) methods to convert back and forth.

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

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 :)

Identify when the surfaceview is fully transparent

I'm writing a scratch card like app, and I use a SurfaceView for that.
I fill it with some kind of color and I draw some Path on it with PorterDuff.Mode.CLEAR PorterDuffXfermode. I have to identify when the user fully scratched it (the SurfaceView's canvas is fully transparent). Can anybody give me some advice, how to identify it?
I tried it with saving the coordinates of the paths, but because of the drawing stroke width I can't calculate the covered area well.
I tried to get a Bitmap from the SurfaceView's getDrawingCache method and iterate on its pixels and use the getPixel method. It doesn't work and i think it would be not an efficient way to examine the canvas.
Assuming the canvas will not be large or scalable to an arbitrary size I think looping over the pixels would be effective.
Given a canvas of large or arbitrary size I would create an array representation of the canvas and mark pixels as you go, keeping a count of how many the user has hit at least once. Then test that number against a threshold value that determines how much of the ticket must be scratched for it to be considered "scratched off". Incoming pseudo-code
const int count = size_x * size_y; // pixel count
const int threshhold = 0.8 * count // user must hit 80% of the pixels to uncover
const int finger_radius = 2; // the radias of our finger in pixels
int scratched_pixels = 0;
bit [size_x][size_y] pixels_hit; // array of pixels all initialized to 0
void OnMouseDown(int pos_x, int pos_y)
{
// calculates the mouse position in the canvas
int canvas_pos_x, canvas_pos_y = MousePosToCanvasPos(pos_x, pos_y);
for(int x = canvas_pos_x - finger_rad; x < canvas_pos_x + brush_rad; ++x)
{
for(int y = canvas_pos_y - finger_rad; y < canvas_pos_y + brush_rad; ++y)
{
int dist_x = x - canvas_pos_x;
int dist_y = y - canvas_pos_y;
if((dist_x * dist_x + dist_y * dist_y) <= brush_rad * brush_rad
&& pixels_hit[x][y] == 0)
{
++scratched_pixels;
pixels_hit[x][y] = 1;
}
}
}
}
bool IsScratched()
{
if(scratched_pixels > threshhold)
return true;
else
return false;
}

Android: How to do this framing paint?

I Have Some static images like below:
Now, I want is, when i touch on the face or hand, then the selected color should be fill on that skin portion.
See below image of result:
So how to get the result like above ??
Redo and Undo Functionality Should be also there.
I have try with the FloodFill color but doing that i can only able to do color in to the perticular portion. as FloodFill only fill the color till the same pixwl color comes. If the touch place pixel color get change the it will not fill color on it.
So Usinf FloodFill i got the result like below image, If i press on the hand, then only hand portion will fill with color, instead of it i want to fill color to the other hand and face also.
So Please help me in this case.
EDITED
After some reply i got the solution like this one.
But still there is a memory issue. It consume lots of memory to draw the color. So please can anyone help me for it ?
You can have a complete image colored the actual way and when you fill a certain region with a color, it will replace all the regions that is specified by that color to be filled in.
Layman's terms:
User will click on the hand of the OUTLINE
That click location will be checked with another image with perfectly color coded regions. Lets call it a MASK for this case. All the skin regions will have the same color. The shirt areas will be another color.
Wherever the user clicks, the selected color by the user will be applied to every pixel that has that similar color in the MASK, but instead of painting directly on the MASK, you paint onto the pixels of the the OUTLINE.
I hope this helps.
Feel free to comment if you want an example and then I can update the answer with that, but I think you can get it from here.
EDIT:
Basically start off with a simple image like this. This we can call as OUTLINE
Then as the developer, you have to do some work. Here, you color code the OUTLINE. The result we call a MASK. To make this we, color code the regions with the same color that you want. This can be done on paint or whatever. I used Photoshop to be cool lol :D.
Then there is the ALGORITHM to get it working on the phone. Before you read the code, look at this variable.
int ANTILAISING_TOLERANCE = 70; //Larger better coloring, reduced sensing
If you zoom up on the image specifically noting the black regions of the border, you can actually see that sometimes, the computer blends the colors a little bit. In order to account for that change, we use this tolerance value.
COLORINGANDROIDACTIVITY.JAVA
package mk.coloring;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.view.View.OnTouchListener;
public class ColoringAndroidActivity extends Activity implements OnTouchListener{
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
findViewById(R.id.imageView1).setOnTouchListener(this);
}
int ANTILAISING_TOLERANCE = 70;
public boolean onTouch(View arg0, MotionEvent arg1) {
Bitmap mask = BitmapFactory.decodeResource(getResources(), R.drawable.mask);
int selectedColor = mask.getPixel((int)arg1.getX(),(int)arg1.getY());
int sG = (selectedColor & 0x0000FF00) >> 8;
int sR = (selectedColor & 0x00FF0000) >> 16;
int sB = (selectedColor & 0x000000FF);
Bitmap original = BitmapFactory.decodeResource(getResources(), R.drawable.empty);
Bitmap colored = Bitmap.createBitmap(mask.getWidth(), mask.getHeight(), Config.ARGB_8888);
Canvas cv = new Canvas(colored);
cv.drawBitmap(original, 0,0, null);
for(int x = 0; x<mask.getWidth();x++){
for(int y = 0; y<mask.getHeight();y++){
int g = (mask.getPixel(x,y) & 0x0000FF00) >> 8;
int r = (mask.getPixel(x,y) & 0x00FF0000) >> 16;
int b = (mask.getPixel(x,y) & 0x000000FF);
if(Math.abs(sR - r) < ANTILAISING_TOLERANCE && Math.abs(sG - g) < ANTILAISING_TOLERANCE && Math.abs(sB - b) < ANTILAISING_TOLERANCE)
colored.setPixel(x, y, (colored.getPixel(x, y) & 0xFF000000) | 0x00458414);
}
}
((ImageView)findViewById(R.id.imageView1)).setImageBitmap(colored);
return true;
}
}
This code doesn't provide the user with much of color choices. Instead, if the user touches a region, it will look at the MASK and paint the OUTLINE accordingly. But, you can make really interesting and interactive.
RESULT
When I touched the man's hair, it not only colored the hair, but colored his shirt and hand with the same color. Compare it with the MASK to get a good idea of what happened.
This is just a basic idea. I have created multiple Bitmaps but there is not really a need for that. I had used it for testing purposes and takes up unnecessary memory. And you don't need to recreate the mask on every click, etc.
I hope this helps you :D
Good luck
Use a FloodFill Algorithm. Fill the complete canvas but keep the bound fill area as it is like circle, rectangle. You can also check this link. Android: How to fill color to the specific part of the Image only?. The general idea get the x and y co-ordinates on click.
final Point p1 = new Point();
p1.x=(int) x; p1.y=(int) y; X and y are co-ordinates when user clicks on the screen
final int sourceColor= mBitmap.getPixel((int)x,(int) y);
final int targetColor =mPaint.getColor();
new TheTask(mDrawingManager.mDrawingUtilities.mBitmap, p1, sourceColor, targetColor).execute(); //Use AsyncTask and do floodfillin the doinBackground().
Check the above links for floodfill algorithmin android. This should help you achieve what you want. Android FingerPaint Undo/Redo implementation. This should help you modify according to your needs regarding undo and redo.
Edit:
A post on stackoverflow led me to a efficient way of using flood fill algorithm without delay and OOM.
Picking from the SO Post
Filling a small closed area works fine with the above flood fill algorithm. However for large area the algorithm works slow and consumes lot of memory. Recently i came across a post which uses QueueLinear Flood Fill which is way faster that the above.
Source :
http://www.codeproject.com/Articles/16405/Queue-Linear-Flood-Fill-A-Fast-Flood-Fill-Algorith
Code :
public class QueueLinearFloodFiller {
protected Bitmap image = null;
protected int[] tolerance = new int[] { 0, 0, 0 };
protected int width = 0;
protected int height = 0;
protected int[] pixels = null;
protected int fillColor = 0;
protected int[] startColor = new int[] { 0, 0, 0 };
protected boolean[] pixelsChecked;
protected Queue<FloodFillRange> ranges;
// Construct using an image and a copy will be made to fill into,
// Construct with BufferedImage and flood fill will write directly to
// provided BufferedImage
public QueueLinearFloodFiller(Bitmap img) {
copyImage(img);
}
public QueueLinearFloodFiller(Bitmap img, int targetColor, int newColor) {
useImage(img);
setFillColor(newColor);
setTargetColor(targetColor);
}
public void setTargetColor(int targetColor) {
startColor[0] = Color.red(targetColor);
startColor[1] = Color.green(targetColor);
startColor[2] = Color.blue(targetColor);
}
public int getFillColor() {
return fillColor;
}
public void setFillColor(int value) {
fillColor = value;
}
public int[] getTolerance() {
return tolerance;
}
public void setTolerance(int[] value) {
tolerance = value;
}
public void setTolerance(int value) {
tolerance = new int[] { value, value, value };
}
public Bitmap getImage() {
return image;
}
public void copyImage(Bitmap img) {
// Copy data from provided Image to a BufferedImage to write flood fill
// to, use getImage to retrieve
// cache data in member variables to decrease overhead of property calls
width = img.getWidth();
height = img.getHeight();
image = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(image);
canvas.drawBitmap(img, 0, 0, null);
pixels = new int[width * height];
image.getPixels(pixels, 0, width, 1, 1, width - 1, height - 1);
}
public void useImage(Bitmap img) {
// Use a pre-existing provided BufferedImage and write directly to it
// cache data in member variables to decrease overhead of property calls
width = img.getWidth();
height = img.getHeight();
image = img;
pixels = new int[width * height];
image.getPixels(pixels, 0, width, 1, 1, width - 1, height - 1);
}
protected void prepare() {
// Called before starting flood-fill
pixelsChecked = new boolean[pixels.length];
ranges = new LinkedList<FloodFillRange>();
}
// Fills the specified point on the bitmap with the currently selected fill
// color.
// int x, int y: The starting coords for the fill
public void floodFill(int x, int y) {
// Setup
prepare();
if (startColor[0] == 0) {
// ***Get starting color.
int startPixel = pixels[(width * y) + x];
startColor[0] = (startPixel >> 16) & 0xff;
startColor[1] = (startPixel >> 8) & 0xff;
startColor[2] = startPixel & 0xff;
}
// ***Do first call to floodfill.
LinearFill(x, y);
// ***Call floodfill routine while floodfill ranges still exist on the
// queue
FloodFillRange range;
while (ranges.size() > 0) {
// **Get Next Range Off the Queue
range = ranges.remove();
// **Check Above and Below Each Pixel in the Floodfill Range
int downPxIdx = (width * (range.Y + 1)) + range.startX;
int upPxIdx = (width * (range.Y - 1)) + range.startX;
int upY = range.Y - 1;// so we can pass the y coord by ref
int downY = range.Y + 1;
for (int i = range.startX; i <= range.endX; i++) {
// *Start Fill Upwards
// if we're not above the top of the bitmap and the pixel above
// this one is within the color tolerance
if (range.Y > 0 && (!pixelsChecked[upPxIdx])
&& CheckPixel(upPxIdx))
LinearFill(i, upY);
// *Start Fill Downwards
// if we're not below the bottom of the bitmap and the pixel
// below this one is within the color tolerance
if (range.Y < (height - 1) && (!pixelsChecked[downPxIdx])
&& CheckPixel(downPxIdx))
LinearFill(i, downY);
downPxIdx++;
upPxIdx++;
}
}
image.setPixels(pixels, 0, width, 1, 1, width - 1, height - 1);
}
// Finds the furthermost left and right boundaries of the fill area
// on a given y coordinate, starting from a given x coordinate, filling as
// it goes.
// Adds the resulting horizontal range to the queue of floodfill ranges,
// to be processed in the main loop.
// int x, int y: The starting coords
protected void LinearFill(int x, int y) {
// ***Find Left Edge of Color Area
int lFillLoc = x; // the location to check/fill on the left
int pxIdx = (width * y) + x;
while (true) {
// **fill with the color
pixels[pxIdx] = fillColor;
// **indicate that this pixel has already been checked and filled
pixelsChecked[pxIdx] = true;
// **de-increment
lFillLoc--; // de-increment counter
pxIdx--; // de-increment pixel index
// **exit loop if we're at edge of bitmap or color area
if (lFillLoc < 0 || (pixelsChecked[pxIdx]) || !CheckPixel(pxIdx)) {
break;
}
}
lFillLoc++;
// ***Find Right Edge of Color Area
int rFillLoc = x; // the location to check/fill on the left
pxIdx = (width * y) + x;
while (true) {
// **fill with the color
pixels[pxIdx] = fillColor;
// **indicate that this pixel has already been checked and filled
pixelsChecked[pxIdx] = true;
// **increment
rFillLoc++; // increment counter
pxIdx++; // increment pixel index
// **exit loop if we're at edge of bitmap or color area
if (rFillLoc >= width || pixelsChecked[pxIdx] || !CheckPixel(pxIdx)) {
break;
}
}
rFillLoc--;
// add range to queue
FloodFillRange r = new FloodFillRange(lFillLoc, rFillLoc, y);
ranges.offer(r);
}
// Sees if a pixel is within the color tolerance range.
protected boolean CheckPixel(int px) {
int red = (pixels[px] >>> 16) & 0xff;
int green = (pixels[px] >>> 8) & 0xff;
int blue = pixels[px] & 0xff;
return (red >= (startColor[0] - tolerance[0])
&& red <= (startColor[0] + tolerance[0])
&& green >= (startColor[1] - tolerance[1])
&& green <= (startColor[1] + tolerance[1])
&& blue >= (startColor[2] - tolerance[2]) && blue <= (startColor[2] + tolerance[2]));
}
// Represents a linear range to be filled and branched from.
protected class FloodFillRange {
public int startX;
public int endX;
public int Y;
public FloodFillRange(int startX, int endX, int y) {
this.startX = startX;
this.endX = endX;
this.Y = y;
}
}
}
One basic way would be something like the floodfill algorythm.
The Wikipedia article describes the algorythm and its variations pretty well.
Here you can find a implementation on SO. But depending on your specific needs this one has to be modified.

Categories

Resources