Ok, I am developing a sidescrolling game and my problem is on how to properly draw and update the screen. I am drawing on a SurfaceView and I use Path to make the contourns, currently the algorithm only draws this:
And I am sidescrolling by using Path.offSet() and then canvas.drawPath(), later on I update the last X position on the path by using Path.addRect() (and thats basically how I am drawing everything: using Path.addRect())
So here is the thread that updates the screen:
#Override
public void run() {
int x = LibraryLoader.getTerrainSizeX();
int y = LibraryLoader.getTerrainSizeY();
int count = 0;
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
Path path = new Path();
makePath(path, x, y, 0, LibraryLoader.getTerrainThickness());
Path path2 = new Path();
makePath(path2, x, y, LibraryLoader.getTerrainThickness(), y);
while (run) {
Canvas c = null;
try {
c = surfaceHolder.lockCanvas(null);
synchronized (surfaceHolder) {
fps = fps();
drawMyData(c, path, path2, paint, fps);
LibraryLoader.updateOffSet();
updatePaths(path, path2, x, y);
if ((count++) == (x / 2) - 1) {
LibraryLoader.updateOffSetArray();
count = 0;
}
}
} finally {
if (c != null) {surfaceHolder.unlockCanvasAndPost(c);}
}
}
and its respective methods:
public void updatePaths(Path path, Path path2, int x, int y) {
path.offset(-1f, 0);
path.addRect(x-3, topValue, x-2, bottomValue, Path.Direction.CW);
path2.offset(-1f, 0);
path2.addRect(x-3, topValue, x-2, y, Path.Direction.CW);
}
So, in my phone it works perfectly at 60fps, the problem is I tested in a lower end device and it begins at 40fps then drops every update until it gets below 10fps...(and keeps dropping). I guess I need to clean the state of the path, or I shouldn't even be using the Path class to begin with. So my question is how should I update the screen with the best performance? Obs: The canvas is not hardware accelerated.
Well folks I figured out that I was wrong about everything I did. The answer is simple: If your android application updates the whole screen every frame, use Opengl. Canvas is for app design for what I've seen, hope I am not mistaken. For example, if you want to make a custom animation for a LOGO or a button, so you use canvas, I guess. If anyone stumbles in this post do watch the videos Morrison Chang mentioned, they are very helpful to put you on the right track. Cheers.
Related
I have been experimenting with squeezing as much performance out of SurfaceView as possible. Currently, I'm subclassing it and implementing a runnable interface on it instead of a callback. I understand there is no hardware acceleration on it.
Still, if I either draw a canvas primitive vertical line scrolling across the screen or a bitmap vertical line, both run slower and slower after each pass. This felt to me like a memory leak, or is it just Android itself? Is OpenGL or another library really my last resort?
I've drawn plenty of scrolling backgrounds before at decent speeds (I think around 5 pixels per tick, this I'm aiming around 20-50 pixels a tick which if anything would be less stops along the way to render).
EDIT: Here is the SurfaceView extended, the thread it makes, the drawing method, and the initialization of it. Basically, this is in a slightly bigger class that just holds this screen's data. The drawXYZ() methods simply use the canvas primitives or a bitmap to paint mainly as the background, which is a solid background color with some vertical and horizontal lines on it like a music staff, little calculating is involved.
The drawCursor is what makes the scrolling vertical line and when I just let it loop the scrolling from left to right, it eventually lags much slower than the first scroll.
public class MySurfaceView extends SurfaceView implements Runnable
{
Thread renderThread = null;
SurfaceHolder holder;
volatile boolean running = false;
public MySurfaceView() {
super(mainActivity);
this.holder = getHolder();
holder.setFixedSize(screenW, screenH);
}
public void resume() {
running = true;
renderThread = new Thread(this);
renderThread.start();
}
#Override
public void run() {
while (running) {
if (!holder.getSurface().isValid()) {
continue;
}
Canvas canvas = holder.lockCanvas();
if(canvas != null) {
doDraw(canvas);
holder.unlockCanvasAndPost(canvas);
}
}
}
public void pause() {
running = false;
while (true) {
try {
renderThread.join();
break;
} catch (InterruptedException e) {
// retry
}
}
}
protected void doDraw(Canvas canvas)
{
canvas.drawColor(Color.rgb(56, 56, 62));
lastNotePlayed = OptionsContainer.getNotePlaying();
//Draw contours (rows).
paint.setColor(Color.rgb(0, 255, 255));
paint.setStrokeWidth(3);
paint.setTextSize(35);
drawContours(canvas, paint);
//Beats per measure (BPM).
paint.setColor(Color.rgb(233, 232, 232));
paint.setStrokeWidth(1);
paint.setStyle(Paint.Style.STROKE);
paint.setPathEffect(bpmLines);
drawBPM(canvas, paint);
paint.setPathEffect(null);
//Draw measures.
paint.setStrokeWidth(5);
drawMeasures(canvas, paint);
//Draw note node inputs.
paint.setColor(Color.rgb(76, 255, 0));
for (int i = 0; i < OptionsContainer.noteList.length; i++) {
if (OptionsContainer.noteList[i].getContour() != 0) {
if (OptionsContainer.noteList[i].getContour() > (OptionsContainer.contour / 2)) {
//Staff on left side, below note.
canvas.drawBitmap(lowerStaffBmp, OptionsContainer.noteList[i].getX(), OptionsContainer.noteList[i].getY(), null);
} else {
canvas.drawBitmap(higherStaffBmp, OptionsContainer.noteList[i].getX(), OptionsContainer.noteList[i].getY() - 40, null);
}
}
}
//Draw cursor.
paint.setStrokeWidth(2);
paint.setColor(Color.WHITE);
drawCursor(canvas, paint);
if (OptionsContainer.isRest)
canvas.drawBitmap(restBmp, (OptionsContainer.screenWidth / 2), (screenHeight - 100) / 2, null);
}
}
#Override
public void init() {
surfaceView = new MySurfaceView();
surfaceView.setLayoutParams(layoutParams);
surfaceView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
// Normalize x,y between 0 and 1
float x = event.getX();
float y = event.getY();
if (x < (OptionsContainer.screenWidth) && y < screenH) {
NoteNode note = new NoteNode(x, y, MainActivity.options);
if (note.getContour() == OptionsContainer.noteList[note.getBeat() - 1].getContour()) {
OptionsContainer.noteList[note.getBeat() - 1] = new NoteNode(x, screenHeight + 200, MainActivity.options);
} else {
OptionsContainer.noteList[note.getBeat() - 1] = note;
}
}
}
return true;
}
});
mainActivity.addContentView(surfaceView, layoutParams);
surfaceView.resume();
}
EDIT #2: Final Answer
Add Path.reset() after the path is drawn in drawBPM(). I'd imagine that stops a memory leak of that path which is trying to keep track of ALL the paths it has been writing and overwriting, little to our knowledge just looking at the lines on the screen. There was a similar Stack Overflow question but fadden's debugging tips below were very helpful for initially trying to figure out what and where it was going wrong.
"Squeezing performance" and Canvas-rendering don't really go together on a SurfaceView, but you can do okay on many devices.
Grafika's "multi-surface test" Activity features a bouncing circle, rendered in software. I haven't noticed it get slower over time, so I suspect something is wrong in your code. Note Grafika does not subclass SurfaceView, and I generally recommend against doing so -- it's too easy to do the wrong thing. The only valid reason to subclass SurfaceView is if you want to draw on both the Surface and the View, e.g. for some sort of mask effect.
You didn't show any code, so there's not much more we can tell you.
I don't see anything blatantly wrong in the code; seems pretty straightforward. I'd check to make sure OptionsContainer.noteList.length isn't growing without bound. Next step would be to use traceview to figure out which part of the rendering is slow, or just spread System.nanoTime() calls around to identify which part is getting progressively slower. If everything in the method shown is executing at a consistent speed except drawCursor(), move the time-check calls into there, narrowing it down until you find what's draining your performance.
If something is consuming memory quickly enough to cause heap issues, you should see a great deal of GC activity in the logcat output. The DDMS allocation tracker tool can help with that.
Background
I'm developing an app for Android that plots data as a line graph using AndroidPlot. Because of the nature of the data, it's important that it be pannable and zoomable. I'm using AndroidPlot's sample code on bitbucket for panning and zooming, modified to allow panning and zooming in both X and Y directions.
Everything works as desired except that there are no X and Y axis lines. It is very disorienting to look at the data without them. The grid helps, but there's no guarantee that grid lines will actually fall on the axis.
To remedy this I have tried adding two series, one that falls on just the X axis and the other on the Y. The problem with this is that if one zooms out too far the axis simply end, and it becomes apparent that I have applied a 'hack'.
Question
Is it possible to add X and Y axis lines to AndroidPlot? Or will my sad hack have to do?
EDIT
Added tags
I figured it out. It wasn't trivial, took a joint effort with a collaborator, and sucked up many hours of our time.
Starting with the sample mentioned in my question, I had to extend XYPlot (which I called GraphView) and override the onPreInit method. Note that I have two PointF's, minXY and maxXY, that are defined in my overridden XYPlot and manipulated when I zoom or scroll.
#Override
protected void onPreInit() {
super.onPreInit();
final Paint axisPaint = new Paint();
axisPaint.setColor(getResources().getColor(R.color.MY_AXIS_COLOR));
axisPaint.setStrokeWidth(3); //or whatever stroke width you want
XYGraphWidget oldWidget = getGraphWidget();
XYGraphWidget widget = new XYGraphWidget(getLayoutManager(),
this,
new SizeMetrics(
oldWidget.getHeightMetric(),
oldWidget.getWidthMetric())) {
//We now override XYGraphWidget methods
RectF mGridRect;
#Override
protected void doOnDraw(Canvas canvas, RectF widgetRect)
throws PlotRenderException {
//In order to draw the x axis, we must obtain gridRect. I believe this is the only
//way to do so as the more convenient routes have private rather than protected access.
mGridRect = new RectF(widgetRect.left + ((isRangeAxisLeft())?getRangeLabelWidth():1),
widgetRect.top + ((isDomainAxisBottom())?1:getDomainLabelWidth()),
widgetRect.right - ((isRangeAxisLeft())?1:getRangeLabelWidth()),
widgetRect.bottom - ((isDomainAxisBottom())?getDomainLabelWidth():1));
super.doOnDraw(canvas, widgetRect);
}
#Override
protected void drawGrid(Canvas canvas) {
super.drawGrid(canvas);
if(mGridRect == null) return;
//minXY and maxXY are PointF's defined elsewhere. See my comment in the answer.
if(minXY.y <= 0 && maxXY.y >= 0) { //Draw the x axis
RectF paddedGridRect = getGridRect();
//Note: GraphView.this is the extended XYPlot instance.
XYStep rangeStep = XYStepCalculator.getStep(GraphView.this, XYAxisType.RANGE,
paddedGridRect, getCalculatedMinY().doubleValue(),
getCalculatedMaxY().doubleValue());
double rangeOriginF = paddedGridRect.bottom;
float yPix = (float) (rangeOriginF + getRangeOrigin().doubleValue() * rangeStep.getStepPix() /
rangeStep.getStepVal());
//Keep things consistent with drawing y axis even though drawRangeTick is public
//drawRangeTick(canvas, yPix, 0, getRangeLabelPaint(), axisPaint, true);
canvas.drawLine(mGridRect.left, yPix, mGridRect.right, yPix, axisPaint);
}
if(minXY.x <= 0 && maxXY.x >= 0) { //Draw the y axis
RectF paddedGridRect = getGridRect();
XYStep domianStep = XYStepCalculator.getStep(GraphView.this, XYAxisType.DOMAIN,
paddedGridRect, getCalculatedMinX().doubleValue(),
getCalculatedMaxX().doubleValue());
double domainOriginF = paddedGridRect.left;
float xPix = (float) (domainOriginF - getDomainOrigin().doubleValue() * domianStep.getStepPix() /
domianStep.getStepVal());
//Unfortunately, drawDomainTick has private access in XYGraphWidget
canvas.drawLine(xPix, mGridRect.top, xPix, mGridRect.bottom, axisPaint);
}
}
};
widget.setBackgroundPaint(oldWidget.getBackgroundPaint());
widget.setMarginTop(oldWidget.getMarginTop());
widget.setMarginRight(oldWidget.getMarginRight());
widget.setPositionMetrics(oldWidget.getPositionMetrics());
getLayoutManager().remove(oldWidget);
getLayoutManager().addToTop(widget);
setGraphWidget(widget);
//More customizations can go here
}
And that was that. I sure wish this was built into AndroidPlot; it'll be nasty trying to fix this when it breaks in an AndroidPlot update...
I am new to android programming and have run into a small problem. If you could help, that would be greatly appreciated.
I am making rectangles for a live wallpaper I am making using:
void drawFrame() {
final SurfaceHolder holder = getSurfaceHolder();
Canvas c = null;
try {
c = holder.lockCanvas();
if (c != null) {
Paint paint = new Paint();
paint.setColor(Color.CYAN);
Paint ypaint = new Paint();
ypaint.setColor(Color.RED);
Paint ppaint = new Paint();
ppaint.setColor(Color.GREEN);
drawVerStripes(c, ppaint, 0,20);
drawVerStripes(c, paint, 50,20);
drawVerStripes(c, ypaint,0,10);
}
} finally {
if (c != null)
holder.unlockCanvasAndPost(c);
}
The function:
Params:
width: width of the rectangle
space: the difference between last rect and this rect
int y=0;
int oldY=0;
private void drawVerStripes(Canvas c, Paint paint, int space, int width) {
y=oldY+space;
c.drawRect(y, 0, y+width,c.getHeight(), paint);
oldY=y;
}
The result is the rectangles moving to one side of screen really fast. I want them to stay on screen and not move.
In other words, is there a way that drawVerStripes is executed only once and not every frame.
Your drawFrame method is called with each frame. If your devices refresh rate is 70, then your method is called 70 times a second. If you increase x or y coordinate by 1, then you move 70 pixels in either x or y coordinates. You should do refresh rate independent movement.
1) Calculate delta time which is the time that passed since the last screen update.
2) Determine how many pixels you want your rectangle to move every second.
Let's say you want you rectangle to move 50 pixels in x.
Then do the following. x += deltaTime * 50.
long startTime = System.nanoTime(); // Execute before the first time your drawFrame executes
float deltaTime = (System.nanoTime()-startTime) / 1000000000.0f; // In drawFrame method calculate
long startTime = System.nanoTime(); // Execute after calculating delta time.
I believe that you need to set y and oldY to 0 again at the beginning of drawFrame.
I'm trying to draw a simple rect on the screen in a class and using it as an actor.
But whatever I do, it seems that there is no option to draw it transparent. Does anyone know how to do this?
Thanks in advance!
public class AreaColorRect extends Actor {
public float opacity = 0.0f;
private Color shapeFillColor = new Color();
public Rectangle area;
public ShapeRenderer shapeRen;
public AreaColorRect(float x, float y, float w, float h) {
shapeRen = new ShapeRenderer();
this.area = new Rectangle(x, y, w, h);
}
#Override
public void draw(SpriteBatch batch, float parentAlpha) {
shapeRen.begin(ShapeType.Filled);
shapeRen.setColor(new Color(shapeFillColor.r, shapeFillColor.g, shapeFillColor.b,
0.0f));
shapeRen.rect(area.x, area.y, area.width, area.height);
shapeRen.end();
}
public void setShapeFillColor(float r, float g, float b) {
this.shapeFillColor = new Color(r, g, b, 1);
}
}
You're mixing contexts. End your SpriteBatch before starting the ShapeRenderer. See libgdx - ShapeRenderer in Group.draw renders in wrong colour. This might not be the problem, though.
You also need to turn on blending. You can just do this once globally, or enable it as necessary (and disable it). It should be enabled by the SpriteBatch context, but I don't think its enabled for the ShapeRenderer.
Gdx.graphics.getGL10().glEnable(GL10.GL_BLEND); // Or GL20
The order you render your background and Actors also makes a difference for transparency.
Finally, you've set the opacity to 0, so the object will be completely invisible. That generally doesn't do anything at all. (I assume you're just trying to get a change from the current 100% visible?)
Sorry for digging this up. Since you are tagging Android, though, I thought I would just chip in my experiences.
I recently spent almost 4 hours on a problem, where alpha blending just wasn't working, as it isn't for you.
Turned out that libgdx' AndroidApplicationConfiguration uses 0 bit for alpha channel as default.
If this is the case for you, too, it might be worth changing that to something more sensible before you initialize() your app.
I just decided to use Scene2d for my game and reached the same question: how to use a rectangle as an actor and draw it on the screen?
Then i found this piece of documentation on the libgdx wiki
(By default the Scene2d has a SpriteBatch that handles drawing of the actors)
If an actor needs to perform drawing differently, such as with a
ShapeRenderer, the Batch should be ended and then begun again at the
end of the method. Of course, this causes the batch to be flushed, so
should be used judiciously. The transformation and projection matrices
from the Batch can be used:
private ShapeRenderer renderer = new ShapeRenderer();
public void draw (Batch batch, float parentAlpha) {
batch.end();
renderer.setProjectionMatrix(batch.getProjectionMatrix());
renderer.setTransformMatrix(batch.getTransformMatrix());
renderer.translate(getX(), getY(), 0);
renderer.begin(ShapeType.Filled);
renderer.setColor(Color.BLUE);
renderer.rect(0, 0, getWidth(), getHeight());
renderer.end();
batch.begin();
}
I'm working on a simple Android app for plotting routes on a map. All is going well, but I have an issue when zooming in on my Samsung Galaxy S2. It works fine on a Galaxy S3, so I'm wondering whether it's related to memory management on the lower specced device. It also works fine on the emulator.
Here is equivalent code located in the overlays onDraw method, just condensed for posting here:
Point current = new Point();
Path path = new Path();
Projection projection = mapView.getProjection();
Iterator<GeoPoint> iterator = pointList.iterator();
if (iterator.hasNext()) {
projection.toPixels(iterator.next(), current);
path.moveTo((float) current.x, (float) current.y);
} else return path;
while(iterator.hasNext()) {
projection.toPixels(iterator.next(), current);
path.lineTo((float) current.x, (float) current.y);
}
Paint roadPaint = new Paint();
roadPaint.setAntiAlias(true);
roadPaint.setStrokeWidth(8.0f);
roadPaint.setColor(Color.BLACK);
roadPaint.setStyle(Paint.Style.STROKE);
canvas.drawPath(path, roadPaint);
It's not too dissimilar to most of the sample code floating around for doing this. I'm just wondering if anyone can confirm my suspicions and advise if there is anything I can do in terms of configuration or tweaks that I can do to force drawing at all zoom levels?
Thanks in advance.
Cheers,
Nathan
The problem is that you are painting the overlay yourself for a very specific state of the mapview. You should use OverlayItem instead.
The OverlayItem is added to the MapView overlays collection, and the MapView handles all the re-drawing depending on it's own state ( zoom, location, etc )
#Override
public void draw( Canvas canvas, MapView mapView, boolean shadow )
{
super.draw( canvas, mapView, shadow );
int x1 = -1;
int y1 = -1;
int x2 = -1;
int y2 = -1;
Paint paint = new Paint();
paint.setStyle( Paint.Style.STROKE );
paint.setColor( GeoLocation.ROUTE_COLOR );
paint.setStrokeWidth( STROKE_WIDTH );
for ( int i = 0; i < mRouteGeoPoints.size(); i++ )
{
Point point = new Point();
mapView.getProjection().toPixels( geoPoints.get( i ), point );
x2 = point.x;
y2 = point.y;
if ( i > 0 )
{
canvas.drawLine( x1, y1, x2, y2, paint );
}
x1 = x2;
y1 = y2;
}
}
You said that code above was an equivalent (not the real code you are running) and that's clear because you are returning a Path object in a onDraw() which you couldn't.
The "compressed form" of code you show should work as well as using the drawLine(). So the problem should come from something else (may the original code).
Anyway, I'll give you a couple of hints:
When the top and bottom of object you are drawing to a canvas are both out of screen, the object is ignored and not drawn. Check if this is not whats happening with your path. See my answer in this post Android Map Overlay Disappears on Zoom
You don't need to rebuild the path object every time. You are probably already doing it, and that's why you made the short version above. See my answer in this post with some suggestions to improve path drawing: Overlay behavior when zooming
If for some reason you really want to use the slower approach of drawLine(), you can use the follwing to make the line look better:
paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setColor(...);
paint.setAlpha(...);
paint.setStrokeWidth(...);
Finally, if the issue remains, update your question with more relevant code and let me know. Maybe I can help further.
Regards.