I'm still searching for the right way to draw on canvas multiple times.
Wished result: ImageView set to visible, show first text, then second instead of first, finally third instead of second. Each text is visible for half a second
Idea: paint canvas with setColor(Color.WHITE) after each text, use synchronized block for setting backgroundcolor and text, use invalidate() with custom draw() method
I tryed different ways, this is the current version of my code:
initializeStimulus() - called in onCreate() of my Service
private void initializeStimulus(String stimulus) {
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
stimulusOverlay = new CustomImageView(this);
stimulusOverlay.setBackgroundColor(getResources().getColor(R.color.background_stimulus));
stimulusOverlay.setVisibility(View.VISIBLE);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
);
// Position of stimuli in relation to screen size / 2
Point displaySize = new Point();
windowManager.getDefaultDisplay().getRealSize(displaySize);
int width = displaySize.x;
int height = displaySize.y;
params.x = 0;
params.y = 0;
windowManager.addView(stimulusOverlay, params);
imageBitmap = Bitmap.createBitmap((width/3)*2, height/5, Bitmap.Config.ARGB_8888);
p = new Paint();
Canvas canvas = new Canvas(imageBitmap);
p.setColor(Color.BLACK);
Typeface non_proportional_font = Typeface.createFromAsset(getAssets(), "fonts/luximr.ttf");
p.setTypeface(non_proportional_font);
float scale = getResources().getDisplayMetrics().density;
p.setTextSize(40 * scale);
fillTextPathArray();
stimulusOverlay.setImageBitmap(imageBitmap);
stimulusOverlay.draw(canvas);
}
private void fillTextPathArray(){
mTextPaths = new ArrayList<Path>(VOC_COUNT);
String[] text = new String[VOC_COUNT];
String stimulus = getStimulusFromPref(positionStimulus);
int stimulusLength = stimulus.length();
String masking = String.format("%0" + stimulusLength + "d", 0).replace('0', 'X');
text[0] = masking;
text[1] = stimulus;
text[2] = masking;
for (int i = 0; i < text.length; i++) {
Path path = new Path();
p.getTextPath(text[i], 0, masking.length(), imageBitmap.getWidth()/4, (imageBitmap.getHeight()/3)*2, path);
path.close(); // not required on 2.2/2.3 devices
mTextPaths.add(path);
}
}
This function made an ArrayList of paths, which I need to be displayed in the right order. So at the end, the user will be able to see my canvas changing from first text to second text to third text.
I started with tips like "redraw entire view" or "draw box with backgroundcolor over old text" (Android Canvas Drawing Text and Change Text afterwards), because canvas drawing couldn't be revesed. Didn't work out for me.
The big problem remains: the code runs through and everything will be shown in one canvas, one above the other. AND: Time delays like while loop or Thread.sleep(500) didn't have any impact on the canvas. The result was the same: The code was running through - with the given time delay - and at the end everything was painted, not step by step (first, second, third).
I even tryed to draw each text in separate methods.
So I tryed to implement each given new Canvas, new Paint and even new View - nothing changed. At the end everything was drawn instead of step by step.
Currently I have my custom ImageView (CustomImageView), where I'm working with the invalidate() method while using a synchronized block.
private class CustomImageView extends AppCompatImageView {
private boolean stimulusVisible = false;
private int countStimuli = 0;
public CustomImageView(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
if (!stimulusVisible) {
Object lock = new Object();
synchronized (lock) {
canvas.drawPath(mTextPaths.get(countStimuli), p);
stimulusVisible = true;
countStimuli++;
}
} else {
long startTime = System.currentTimeMillis();
while((System.currentTimeMillis()-startTime) < 500){}
canvas.drawColor(Color.CYAN);
stimulusVisible = false;
}
invalidate();
}
}
The while loop should stop everything for half a sec.
I really hope for your suppoert.
Is there a better way to implement this?
Am I missing something? I though invalidate() would redraw the canvas but it didn't have an impact. I'm trying for 2 days now, please help.
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 :)
As is, 100 pink circles (same bitmap) appear scattered randomly over the phone screen (as is supposed to). When I tap one of the circles, that circle should disappear (change to the background color). I think I have a fundamental misunderstanding of Android and View in general.I think I have a couple obvious errors (that are not so obvious to me, but I've been staring at it so long that I figured I needed some help). Currently, the screen shows the random circles but nothing more. Touching the screen does nothing. Any better ideas to make the circles disappear? It recently reorganized all the bitmaps when you touched it, but I did something recently, and it stopped. The bitmap is 30px by 30px.
public class DrawV extends View {
private Bitmap bit_dot;
private int width;
private int height;
public int[] width_array = new int[100];
public int[] height_array = new int[100];
private View dotV = (View)findViewById(R.id.bigdocpic);//bitmap
Random rand = new Random();
public DrawV(Context context) {
super(context);
bit_dot = BitmapFactory.decodeResource(getResources(), R.drawable.dot_catch);
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
width = metrics.widthPixels;
height = metrics.heightPixels;
}
#Override
//draws 100 randomly placed similar bitmaps
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int height_dimension;
int width_dimension;
for (int i = 0; i < 100; i++){
height_dimension = rand.nextInt(height) + 1;
width_dimension = rand.nextInt(width) + 1;
canvas.drawBitmap(bit_dot, width_dimension, height_dimension, null);
width_array[i] = width_dimension;//
height_array[i] = height_dimension;//
}
}
#Override
public boolean onTouchEvent(MotionEvent event){
Paint p = new Paint();
p.setColor(Color.WHITE);
Path path = new Path();
Canvas c = new Canvas();
for (int i = 0; i < 100; i++){
if ((event.getX() == width_array[i]) && (event.getY() == height_array[i]))
c.drawCircle(width_array[i], height_array[i], 15, p);
}
invalidate();
return false;//false or true?
}
//set visibility of bitmap to invisible
public boolean onTouch(View v, MotionEvent event) {
dotV.setVisibility(View.INVISIBLE);
invalidate();
return false;//false or true? not understanding
}}
Help?
Your onTouchEvent isn't really doing anything important as-is, and you don't have the concept of a circle object.
onDraw should really be drawing these circles from an array/list created earlier - say a List<MyCircles> or MyCircles[]. On touch, you could iterate through all of your circles until you find one that is closest, remove that circle from the array or list, then invalidate.
The reason nothing is happening at all is even though you're drawing those circles again in onTouchEvent, you're redrawing everything yet again in onDraw (invalidate() calls draw/onDraw).
Ideally, create your list of circles in your initializer, draw them in onDraw, and update them in onTouch (That is, delete). There may be a simpler way to do this but this is, at the very least, a more proper approach.
I am new to android and building an app which involves displaying a view for 2 seconds and then change. Here's my onDraw method:
#Override
public void onDraw(Canvas canvas)
{
float level = game.level;
width = getWidth();
tile_length = width/level;
Paint rect = new Paint();
rect.setColor(getResources().getColor(R.color.dark));
canvas.drawRect(0, 0, width, width, rect);
game.numbers.setTextSize( (0.70f * tile_length));
game.numbers.setTextAlign(Paint.Align.CENTER);
grid.setColor(getResources().getColor(R.color.lines));
rect.setColor(getResources().getColor(R.color.tile_on));
int ind = 1;
int tile_num = 1;
FontMetrics fm = game.numbers.getFontMetrics();
float x = tile_length/2;
float y = tile_length/2 - (fm.ascent + fm.descent) / 2;
Log.v(LOG_TAG, "changed = " + game.changed);
for (int i=0; i<width; i+=tile_length)
{
for(int j=0; j<width; j+=tile_length)
{
for(int k = 0; k<level; k++ )
if(tile_num == game.random[k])
{
// Log.v(LOG_TAG, "i = " + i + "j = " + j);
game.set_Coordinates(ind-1, i, j);
String tile = Integer.toString(ind++);
canvas.drawRect(i, j, i+tile_length, j+tile_length, rect);
canvas.drawText(tile, i+x, j+y, game.numbers); //needs to be updated after 2 seconds
break;
}
tile_num++;
}
}
}
I understand i have to use postdelayed method somewhere, but don't know how...Now i just want to ommit the canvas.drawText line after the delay.
do you mean something like this
new Handler().postDelayed(new Runnable(){
public void run(){
// do something here like draw text;
}
}, 2000);
A timer is needed, indeed. What I do, which is very simple, is first to create records of coordinates (and any other data needed) for every point of the drawing -- instead of drawing the points on the spot -- and then reproduce them using a timer (Android handler, preferably, like the one suggested above). This also offers you a lot of possibilities while actual drawing: pause, go faster/slower, go backwards, ...
I don't know if this method can be used for complicated drawings, but it is fine for drawing shapes, curves, surfaces, etc.
Loooong time viewer, finally getting round to signing up here at StackOverflow!
After a very long time searching for a way to do a scrolling background of a ViewGroup in Android, I've developed the following:
public class SlidingDrawable extends Drawable implements Drawable.Callback {
private static final String TAG = "SlidingDraw";
private static float STEP_SIZE = 1.0f;
private BitmapDrawable mBitmap;
private Context mContext;
private float mPosX;
private int mBitmapWidth;
private Runnable mInvalidater;
private Handler mHandler;
public SlidingDrawable(Context c){
mContext = c;
// use this as the callback as we're implementing the interface
setCallback(this);
mHandler = new Handler();
mInvalidater = new Runnable(){
#Override
public void run(){
// decrement the drawables step size
mPosX -= SlidingDrawable.STEP_SIZE;
/*
* Check to see if the current position is at point where it should
* loop. If so, reset back to 0 to restart
*/
if(Math.abs(mPosX) >= mBitmapWidth) mPosX = 0;
// redraw
invalidateDrawable(null);
}
};
}
public static void setStepSize(float newSize){
SlidingDrawable.STEP_SIZE = newSize;
}
public void createBitmap(String path, ViewGroup parent){
// height of the parent container
int height = parent.getHeight();
/* Initialize local variables
* bgBitmap - the resulting bitmap to send into SlidingDrawable instance
* imageStream - raw bitmap data to be decoded into bgBitmap
*/
WindowManager wMgr = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
int mScreenWidth = wMgr.getDefaultDisplay().getWidth();
InputStream imageStream;
Matrix imgMatrix = new Matrix();
Bitmap bitmap = null;
try {
imageStream = mContext.getAssets().open(path);
// create a temporary bitmap object for basic data
Bitmap temp = BitmapFactory.decodeStream(imageStream);
int width = temp.getWidth();
// find the width difference as a percentage to apply to the
// transformation matrix
float widthDifference = ((float)mScreenWidth) / (float)(width / 2);
imgMatrix.postScale(widthDifference, 0, 0f, 0f);
// create a copy of the bitmap, scaled correctly to maintain loop
bitmap = Bitmap.createScaledBitmap(temp, (int)(width * widthDifference), height, true);
// recycle the temp bitmap
temp.recycle();
} catch (IOException e) {
e.printStackTrace();
}
mBitmap = new BitmapDrawable(bitmap);
// required
mBitmapWidth = getIntrinsicWidth() / 2;
Rect bounds = new Rect(0, 0, getIntrinsicWidth(), getIntrinsicHeight());
setBounds(bounds);
}
#Override
public void draw(Canvas canvas) {
canvas.drawBitmap(mBitmap.getBitmap(), mPosX, 0f, null);
scheduleDrawable(this, mInvalidater, SystemClock.uptimeMillis());
}
#Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
#Override
public void scheduleDrawable(Drawable who, Runnable what, long when) {
mHandler.postAtTime(what, who, when);
}
#Override
public void unscheduleDrawable(Drawable who, Runnable what) {
mHandler.removeCallbacks(what, who);
}
#Override
public void invalidateDrawable(Drawable who) {
invalidateSelf();
}
/*
* Methods not directly used or called omitted
*
*/
}
It is used in the Activity like so:
#Override
public void onWindowFocusChanged(boolean focus){
// set the background of the root view of main.xml
SlidingDrawable drawable = new SlidingDrawable(getApplicationContext());
drawable.createBitmap("bgimg/basebg.jpg", mRoot);
mRoot.setBackgroundDrawable(drawable);
}
Long story short, the basebg.jpg image is a tileable image roughly 1600x480. The constructor for SlidingDrawable scales and moves and yaddah yaddah. It works.
Now, the problem is, it seems really inefficient to do it like this. I can't seem to find much information on this sort of implementation, so I'm in the dark on where I can cut CPU cycles, or even if I'm using the method calls correctly.
My questions include:
Is it better to drawBitmap as opposed to using setTranslate() or postTranslate and draw the bitmap using a Matrix?
Is it better to use drawBitmap, or the canvas functions such as translate(), save(), and restore()?
What rate does the draw() method get called at, and is there a way to limit it to, say, 24 FPS o limit redraws?
What the heck is the "when" parameter of these sorts of things? Passing in SystemClock.uptimeMillis() is the only one that worked, and trying to delay it by adding a " + 100" or something to fire every 100ms just made it stutter.
I've researched this as much as I can... I'm leaving it to StackOverflow now :)
After some time with the drawing board, I simplified the functions down. Essentially, it was sending an invalidate() call on every SystemClock.uptimeMillis(), doing one redraw for each step change.
So, I removed the Drawable.Callback interface and passed the invalidateSelf() call directly from the Handler, removing the intermediary interface methods, which didn't seem to do anything special anyway.
There was a slight difference in the CPU usage using
drawBitmap(Bitmap source, int X, int Y, Paint p) vs.
drawBitmap(Bitmap source, Matrix matrix, Paint p), so I opted for the latter to save cycles.
New methods are as follows:
// Runnable
mInvalidater = new Runnable(){
#Override
public void run(){
// decrement the drawables step size
mPosX -= SlidingDrawable.STEP_SIZE;
if(Math.abs(mPosX) >= mBitmapWidth){
mPosX = 0;
mImageMatrix.setTranslate(0, 0);
}
mImageMatrix.postTranslate(-STEP_SIZE, 0);
SlidingDrawable.this.invalidateSelf();
}
};
// Draw method
#Override
public void draw(Canvas canvas) {
canvas.drawBitmap(mBitmap.getBitmap(), mImageMatrix, null);
mHandler.postAtTime(mInvalidater, SystemClock.uptimeMillis() + 64);
}
I tested the battery results by unplugging the phone, running the application for around a minute, then opening the battery usage on my Moto Droid. Before, the battery usage surpassed the Display, now it sits comfortably below.
Angry Birds was also a benchmark, by running the opening screen (where the bg scrolls and the birds fly everywhere) for the same amount of time, in some cases my app sat below Angry Birds, but not always.
CPU usage was checked using the ADB shell command dumpsys cpuinfo as there seems to be a problem viewing CPU info on through the DDMS on devices running 2.2.
I'd still be up to hear other thoughts on this, but for now, it's solved.