Hi I am working on plotting a real time graph of incoming signals using SurfaceView.
The sampling rate is 128Hz and the target graph refresh rate is 50Zh.
Things run pretty smoothly, the points are drawn real-time properly.
I plot the data in segments of a few points using Path()
for each segment I call path.computeBounds() to get a rect that I will use to call holder.lockCanvas(rect) and draw the path. Using a rect prevents flickering and reduces cpu usage
when the graph reaches the end I lock the entire canvas and clear the background, draw the graph frame and then continue on plotting.
the problem is that at the beginning of each new "page" I get a ghost image from the last page:
I believe this is caused by double buffering / use of a dirty area when plotting.
I have looked for solutions to this problem but none seem adequate for this type of application. Any help is most welcome.
Thanks
Jean-Pierre
Code follows:
private void draw() {
Point point = null;
Canvas canvas = null;
Path path = new Path();
ArrayList<Point> pointArray;
float oldX = -1;
boolean setToClear = false;
boolean isNewSegment = false;
if (samplesInQueue == 0) {
return;
}
pointArray = new ArrayList<Point>((int) samplesInQueue);
for (int i = 0; i < samplesInQueue; i++) {
// take a peek at the point without retrieving it from the point
// queue
point = Points.peek();
// check if first point of segment is the start of a page
if (i == 0) {
if (lastSegmentEndPoint != null) {
if (point.x < lastSegmentEndPoint.x) {
// yes then we will need to clear the screen now
isNewSegment = true;
}
} else {
// yes then we will need to clear the screen now
isNewSegment = true;
}
}
if (point != null) {
if (point.x > oldX) {
// put consecutive points in the path point array
point = Points.poll();
samplesInQueue--;
pointArray.add(point);
oldX = point.x;
} else {
// we have a wrap around, stop and indicate we need to clear
// the screen on the next pass
if (!isNewSegment) {
setToClear = true;
}
break;
}
}
}
// no points, return
if (pointArray.size() == 0) {
return;
}
// fill the path
for (int i = 0; i < pointArray.size(); i++) {
Point p = pointArray.get(i);
if (i == 0) {
if (lastSegmentEndPoint != null) {
if (p.x >= lastSegmentEndPoint.x) {
// if we have the end of the last segment, move to it
// and line to the new point
path.moveTo(lastSegmentEndPoint.x, lastSegmentEndPoint.y);
path.lineTo(p.x, p.y);
} else {
// otherwise just line to the new point
path.moveTo(p.x, p.y);
}
} else {
path.moveTo(p.x, p.y);
}
} else {
path.lineTo(p.x, p.y);
}
}
if (clear || isNewSegment) {
if (clear) {
clear = false;
}
// we need to clear, lock the whole canvas
canvas = holder.lockCanvas();
// draw the graph frame / scales
drawGraphFrame = true;
drawGraphFrame(canvas);
} else {
// just draw the path
RectF bounds = new RectF();
Rect dirty = new Rect();
// calculate path bounds
path.computeBounds(bounds, true);
int extra = 0;
dirty.left = (int) java.lang.Math.floor(bounds.left - extra);
dirty.top = (int) java.lang.Math.floor(bounds.top - extra);
dirty.right = (int) java.lang.Math.round(bounds.right + 0.5);
dirty.bottom = (int) java.lang.Math.round(bounds.bottom + 0.5);
// just lock what is needed to plot the path
canvas = holder.lockCanvas(dirty);
}
// draw the path
canvas.drawPath(path, linePaint);
// unlock the canvas
holder.unlockCanvasAndPost(canvas);
// remember last segment end point
lastSegmentEndPoint = pointArray.get(pointArray.size() - 1);
// set clear flag for next pass
if (setToClear) {
clear = true;
}
}
Draw frame / clear graph code
private void drawGraphFrame(Canvas canvas) {
if (!drawGraphFrame) {
return;
}
if (canvas == null) {
Log.e(TAG, "trying to draw on a null canvas");
return;
}
drawGraphFrame = false;
// clear the graph
canvas.drawColor(Color.BLACK, Mode.CLEAR);
// draw the graph frame
canvas.drawLine(leftMargin, topMargin, leftMargin, mCanvasHeight - bottomMargin, framePaint);
canvas.drawLine(leftMargin, mCanvasHeight - bottomMargin, mCanvasWidth - rightMargin, mCanvasHeight
- bottomMargin, framePaint);
// more drawing
}
Your problem is quite straight forward.. your only locking the new portion of the canvas that the new path covers. So the best thing to do is to make your path and dirty rect's private members of your class. Then at the start of your draw method get the path's current bounds (the old bounds) in your dirty rect. Now call path.rewind(); and start modifying your path. After do a union on the dirty rect with the new bounds. Now your dirty rect covers the old and new rect's. So your clear will remove the old path. This also reduces overhead because you don't want to be allocating 100+ objects per second for rect's and path's. Now since your drawing an oscilloscope then you probably want to adjust the old bounds to only be a portion of the width of the view. The same amount your new portion covers.
Hope that's cleared things up.
My simple answer is just using this function clear_holder() wherever you want to clear the canvas. I copy and paste 3 line for 3 times because it need 3 times clear to leave holder blank.
After clearing holder, you should draw any new thing you want!
This link give me this source code!
private void clear_holder(SurfaceHolder holder){
Canvas c = holder.lockCanvas();
c.drawColor( 0, PorterDuff.Mode.CLEAR );
holder.unlockCanvasAndPost(c);
c = holder.lockCanvas();
c.drawColor( 0, PorterDuff.Mode.CLEAR );
holder.unlockCanvasAndPost(c);
c = holder.lockCanvas();
c.drawColor( 0, PorterDuff.Mode.CLEAR );
holder.unlockCanvasAndPost(c);
}
It looks like you are clearing the canvas so, it's not double buffering problem. I think it's related to your path been reused.
Try adding adding the next line when starting new page.
path.reset();
Related
I am using surface view to show some graphics, the problem is that there is a flickering effect when I am moving the figure in the screen, I understand that this is due to double buffering problem, even though I went through many posts, I am unable to fix the problem, please take a look at my code and help me get this fixed.
public class CustomSurfaceView extends SurfaceView implements Runnable{
Thread mThread = null;
SurfaceHolder mSurfaceHolder;
volatile boolean mRunning = false;
Bitmap mBitmap;
boolean mTouched;
float mTouched_x,mTouched_y;
Context mContext;
float mCurrentPosOfRect1x1,mCurrentPosOfRect1y1,mCurrentPosOfRect1x2,mCurrentPosOfRect1y2;
float mCurrentPosOfRect2x1,mCurrentPosOfRect2y1,mCurrentPosOfRect2x2,mCurrentPosOfRect2y2;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
boolean isInitialized = false;
/**
* Constructor..
*/
public CustomSurfaceView(Context context) {
super(context);
mSurfaceHolder = getHolder();
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
mContext = context;
mCurrentPosOfRect1x1 = 100;
mCurrentPosOfRect1y1 = 100;
mCurrentPosOfRect1x2 = 300;
mCurrentPosOfRect1y2 = 300;
mCurrentPosOfRect2x1 = 300;
mCurrentPosOfRect2y1 = 300;
mCurrentPosOfRect2x2 = 500;
mCurrentPosOfRect2y2 = 500;
}
public void onResumeMySurfaceView(){
mRunning = true;
mThread = new Thread(this);
mThread.start();
}
public void onPauseMySurfaceView(){
boolean retry = true;
mRunning = false;
while(retry){
try {
mThread.join();
retry = false;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
#Override
public void run() {
while(mRunning){
if(mSurfaceHolder.getSurface().isValid()){
Canvas canvas = mSurfaceHolder.lockCanvas();
//... actual drawing on canvas
mPaint.setStyle(Paint.Style.STROKE);
if(mTouched){
canvas.drawColor(Color.BLACK);
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth(3);
mPaint.setStrokeWidth(0);
mPaint.setColor(Color.CYAN);
//Left,top
//Right bottom.
if(!isInitialized){
canvas.drawRect(mCurrentPosOfRect1x1, mCurrentPosOfRect1y1,mCurrentPosOfRect1x2, mCurrentPosOfRect1y2,mPaint);
isInitialized = true;
}
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth(3);
mPaint.setStrokeWidth(0);
mPaint.setColor(Color.BLUE);
//Left,top
//Right bottom.
if(!isInitialized){
canvas.drawRect(mCurrentPosOfRect2x1, mCurrentPosOfRect2y1,mCurrentPosOfRect2x2, mCurrentPosOfRect2y2,mPaint);
isInitialized = true;
}
if(isInitialized){
//Check whether the touch points are inside the rectangle & then move...
if((mTouched_x>mCurrentPosOfRect1x1) && (mTouched_x<mCurrentPosOfRect1x2) && (mTouched_y>mCurrentPosOfRect1y1) && (mTouched_y<mCurrentPosOfRect1y2)){
mCurrentPosOfRect1x1 = mTouched_x-100;
mCurrentPosOfRect1x2 = mTouched_x+100;
mCurrentPosOfRect1y1 = mTouched_y-100;
mCurrentPosOfRect1y2 = mTouched_y+100;
}else if((mTouched_x>mCurrentPosOfRect2x1) && (mTouched_x<mCurrentPosOfRect2x2) && (mTouched_y>mCurrentPosOfRect2y1) && (mTouched_y<mCurrentPosOfRect2y2)){
mCurrentPosOfRect2x1 = mTouched_x-100;
mCurrentPosOfRect2x2 = mTouched_x+100;
mCurrentPosOfRect2y1 = mTouched_y-100;
mCurrentPosOfRect2y2 = mTouched_y+100;
}
}
canvas.drawRect(mCurrentPosOfRect1x1, mCurrentPosOfRect1y1,mCurrentPosOfRect1x2, mCurrentPosOfRect1y2, mPaint);
mPaint.setColor(Color.RED);
canvas.drawRect(mCurrentPosOfRect2x1, mCurrentPosOfRect2y1,mCurrentPosOfRect2x2, mCurrentPosOfRect2y2, mPaint);
mPaint.setColor(Color.BLUE);
Paint paint = new Paint() {
{
setStyle(Paint.Style.STROKE);
setStrokeCap(Paint.Cap.ROUND);
setStrokeWidth(3.0f);
setAntiAlias(true);
}
};
paint.setColor(Color.YELLOW);
final Path path = new Path();
final float x1 = mCurrentPosOfRect1x1+ 100;
final float y1 = mCurrentPosOfRect1y1 + 100;
final float x3 = (mCurrentPosOfRect2x1+ 100) ;
final float y3 = (mCurrentPosOfRect2y1 + 100);
final float x2 = (x1 +200);
final float y2 = (y1 -100);
final float x4 = (x3-100);
final float y4 = (y3+200);
path.moveTo(x1, y1);
path.cubicTo(x2,y2,x4,y4,x3,y3);
canvas.drawPath(path, paint);
}
mSurfaceHolder.unlockCanvasAndPost(canvas);
}
}
}
#Override
public boolean onTouchEvent(MotionEvent event){
mTouched_x = event.getX();
mTouched_y = event.getY();
int action = event.getAction();
switch(action){
case MotionEvent.ACTION_DOWN:
mTouched = true;
break;
case MotionEvent.ACTION_MOVE:
mTouched = true;
break;
case MotionEvent.ACTION_UP:
mTouched = false;
break;
case MotionEvent.ACTION_CANCEL:
mTouched = false;
break;
case MotionEvent.ACTION_OUTSIDE:
mTouched = false;
break;
default:
}
return true; //processed
}
}
If you call lockCanvas(), you need to draw on every pixel in the dirty rect. Since you're calling it without a dirty rect, that means updating every pixel on the Canvas.
I believe the problem with your code is that, when mTouched is false, you're not drawing anything at all. Because the Surface is double- or triple-buffered, you're re-displaying the contents of a previous frame, which is going to cause a vibration effect.
I think all you need to do is move the test for mTouched before the lockCanvas() call, so you don't flip the buffers if you're not going to draw anything.
You may want to look through the SurfaceView lifecycle appendix in the graphics architecture doc if you haven't seen it before, as the thread management sometimes yields surprises.
Clear your surfaceviewholder with these lines and make it ready again before playing or drawing any thing on it.
These lines will clear the surface view after using surfaceview at least once
Canvas canvas = mSurfaceHolder.lockCanvas();
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// Draw someting
mSurfaceHolder.unlockCanvasAndPost(canvas);
mSurfaceHolder.setFormat(PixelFormat.TRANSPARENT);
mSurfaceHolder.setFormat(PixelFormat.OPAQUE);
Here this line will make it ready for second time playing the video
mSurfaceHolder.setFormat(PixelFormat.TRANSLUCENT);
flickering is usually a weird issue, so that's my best guess on how to solve on your specific case.
I can see from your code, you're declaring a series of different commands to be applied to the canvas, those are being drawn one at a time in the canvas, in the order of the code, at the moment that your code lockCanvas and the combination of those elements that I believe is the reason for your flickering.
Because:
those draws are not being performed during the system VSYNC (because SurfaceViews)
it's done one at a time in sequence (which takes time and makes the flicker noticeable).
I can think of two solutions for this:
I can see you're only calling drawColor and drawRect on your view. Also, you're not performing any time consuming on it. So I really don't see a reason for the usage of SurfaceView. Refactor the class to a normal extends View and perform them drawing inside onDraw and call invalidate() whenever necessary to re-draw (I believe it will be inside the touch events)
If there's some code you omit for brevity that actually does make the SurfaceView really necessary, you can allocate a temporary canvas with a bitmap using the same size of the screen canvas. Do all the drawing on this temporary canvas and use only the drawBitmap call on your on-screen canvas. A small sample code for this follows.
.
// init your objects inside the `surfaceCreated` callback
Bitmap tempBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas tempCanvas = new Canvas(tempBitmap);
// then on your thread.
while(mRunning){
tempCanvas. // ... do all your drawing operations here
Canvas canvas = mSurfaceHolder.lockCanvas();
canvas.drawBitmap(tempBitmap, 0, 0, null);
mSurfaceHolder.unlockCanvasAndPost(canvas);
}
remember that's just a sample code and not a complete solution, you'll have to do all the normal checks for canvas is valid, etc.
I am using CCDrawNode to create mask type effect (not exactly mask). Everything works well but there is one problem that CCDrawNode only draws square and i want to draw it with custom texture/sprite. Is there any solution to it.
Below is my code of using CCDrawNode
// on "init" you need to initialize your instance
bool HelloWorld::init()
{
//////////////////////////////
// 1. super init first
if ( !CCLayer::init() )
{
return false;
}
CCSize visibleSize = CCDirector::sharedDirector()->getVisibleSize();
CCPoint origin = CCDirector::sharedDirector()->getVisibleOrigin();
CCLayer *layer = CCLayer::create();
CCSprite* pSprite = CCSprite::create("HelloWorld.png");
pSprite->setPosition(ccp(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));
layer->addChild(pSprite, 0);
addChild(layer);
//this is the layer that we want to "cut"
CCLayer* layer1 = CCLayerColor::create(ccc4(122, 144, 0, 255), visibleSize.width, visibleSize.height);
this->setTouchEnabled(true);
//we need to create a ccnode, which will be a stencil for ccclipingnode, draw node is a good choice for that
stencil = CCDrawNode::create();
//CCClipingNode show the intersection of stencil and theirs children
CCClippingNode *cliper = CCClippingNode::create(stencil);
cliper->setInverted(true);
cliper->addChild(layer1);
addChild(cliper);
return true;
}
void HelloWorld::ccTouchesMoved(CCSet* touches, CCEvent* event)
{
CCTouch* touch = (CCTouch*)touches->anyObject();
// get start & end location
CCPoint start = touch->getLocationInView();
CCPoint end = touch->getPreviousLocationInView();
// get corrected location
start = CCDirector::sharedDirector()->convertToGL(start);
end = CCDirector::sharedDirector()->convertToGL(end);
//stencil->drawDot(start, 25, ccc4f(0, 0, 0, 255));
stencil->drawSegment(start, end, 25, ccc4f(0, 0, 0, 255));
}
If you want to draw custom texture you should use CCRenderTexture. In order to draw something you should go smthin like this
myRenderTexture->begin();
mySpriteLoadedWithTexture->visit();
myRenderTexture->end();
Also if you want the drawn lines to be smooth you should draw it in loop so that they are placed in equal distance
float distance = ccpDistance(start, end);
for (int i = 0; i < distance; i++)
{
float difx = end.x - start.x;
float dify = end.y - start.y;
float delta = (float)i / distance;
m_brush->setPosition(CCPoint(start.x + (difx * delta), start.y + (dify * delta)));
m_brush->visit();
}
Hope it helps
I'm making a graphing library in Android and am aware that if I update the canvas by calling invalidate it can cause a heap memory problem in some devices especially when the user is using a Relative Layout and has some views loading or has multiple graphs stacked on each other. So what I want to do is change the graph of the view without android having to redraw all other views on the Relative Layout.
The only solution I have come across is adding a boolean system to the custom view's on draw to check if the same view requested the on draw, otherwise it would keep the same canvas. However, this will only prevent the same view from being redrawn again and will also draw the other views when it is drawn.
Here is the solution mentioned above.
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(cropx != 0){
croppedBmp = Bitmap.createBitmap(drawGraph2, 0, 0, (drawGraph2.getWidth()*cropx/100), drawGraph2.getHeight());
canvas.drawBitmap(croppedBmp, 0, 0, new Paint());
croppedBmp.recycle();
System.gc();
Runtime.getRuntime().gc();
}
status = false;
}
public void plotPoints(ArrayList<Double> yvalue, ArrayList<Double> xpoint){
drawpath2 = new Path();
drawCanvas = new Canvas(drawGraph2);
drawCanvas.scale(1, -1);
boolean first = false;
for(int i = 0; i< xpoint.size(); i++){
if(!Double.isNaN(yvalue.get(i))){
if(first == false){
drawpath2.moveTo(xpoint.get(i).floatValue(),yvalue.get(i).floatValue());
first = true;
}else if(i==xpoint.size()-1){
drawCanvas.drawPath(drawpath2, paint);
drawpath2.reset();
}else{
drawpath2.lineTo(xpoint.get(i).floatValue(), yvalue.get(i).floatValue());
}
}
}
Canvas singleUseCanvas = new Canvas(drawGraph2);
singleUseCanvas.drawPath(drawpath, paint);
status = true;
invalidate();
}
Is it possible to draw a polygon on the mapview of OSMDroid? It should scale easy with the Mapview so I didn't want to use the canvas. Any advice?
I have my own MapOverlay (extends org.osmdroid.views.overlay.Overlay), but can't get my Polygon on it.
If your class is extending org.osmdroid.views.overlay.Overlay you have to modify the draw method so it looks somehow like in the code below:
#Override
protected void draw(final Canvas canvas, final MapView mapView, final boolean shadow) {
if (shadow) {
return;
}
if (this.mPoints.size() < 2) {
//Do nothing
return;
}
final Projection pj = mapView.getProjection();
// precompute new points to the intermediate projection.
final int size = this.mPoints.size();
while (this.mPointsPrecomputed < size) {
final Point pt = this.mPoints.get(this.mPointsPrecomputed);
pj.toMapPixelsProjected(pt.x, pt.y, pt);
this.mPointsPrecomputed++;
}
Point screenPoint0 = null; // points on screen
Point screenPoint1 = null;
Point projectedPoint0; // points from the points list
Point projectedPoint1;
// clipping rectangle in the intermediate projection, to avoid performing projection.
final Rect clipBounds = pj.fromPixelsToProjected(pj.getScreenRect());
mPath.rewind();
mPath.setFillType(Path.FillType.EVEN_ODD);
projectedPoint0 = this.mPoints.get(size - 1);
mLineBounds.set(projectedPoint0.x, projectedPoint0.y, projectedPoint0.x, projectedPoint0.y);
for (int i = size - 2; i >= 0; i--) {
// compute next points
projectedPoint1 = this.mPoints.get(i);
mLineBounds.union(projectedPoint1.x, projectedPoint1.y);
if (!Rect.intersects(clipBounds, mLineBounds)) {
// skip this line, move to next point
projectedPoint0 = projectedPoint1;
screenPoint0 = null;
continue;
}
// the starting point may be not calculated, because previous segment was out of clip
// bounds
if (screenPoint0 == null) {
screenPoint0 = pj.toMapPixelsTranslated(projectedPoint0, this.mTempPoint1);
mPath.moveTo(screenPoint0.x, screenPoint0.y);
}
screenPoint1 = pj.toMapPixelsTranslated(projectedPoint1, this.mTempPoint2);
// skip this point, too close to previous point
if (Math.abs(screenPoint1.x - screenPoint0.x) + Math.abs(screenPoint1.y - screenPoint0.y) <= 1) {
continue;
}
mPath.lineTo(screenPoint1.x, screenPoint1.y);
// update starting point to next position
projectedPoint0 = projectedPoint1;
screenPoint0.x = screenPoint1.x;
screenPoint0.y = screenPoint1.y;
mLineBounds.set(projectedPoint0.x, projectedPoint0.y, projectedPoint0.x, projectedPoint0.y);
}
mPath.close();
canvas.drawPath(mPath, this.mPaint);
}
I have an isometric map which I draw to the canvas. When I try and move the map around, these black lines flicker in between some of the tiles making the whole thing look rather shoddy.
Here is the relevant code from my updating thread
public void run() {
Canvas c;
while (isRunning){
c = null;
try {
c = cellMap.getHolder().lockCanvas(null);
synchronized (cellMap.getHolder()) {
cellMap.onDraw(c);
}
} finally {
if( c != null){
cellMap.getHolder().unlockCanvasAndPost(c);
}
}
}
}
and from my CellMap class (which extends SurfaceView):
public void onDraw(Canvas canvas){
canvas.drawColor(Color.BLACK);
int x = 0;
int y = 0;
for(int i = 0; i < mapSize; i++){
for(int j = 0; j < mapSize; j++){
x = (i-j) * 30 + xOffset;
y = (i+j) * 15 + yOffset;
mapCells[i][j].draw(canvas, paint, x, y);
}
}
mapCells[][] is an array of objects which contain the Bitmap image that needs to be drawn. The draw() function is only 1 line canvas.drawBitmap(bitmapImage, x, y, null)
I have discovered that removing the line Canvas.draw(Color.black) gets rid of the flicker. However, when I move the map the canvas is not cleared so I still see the image from the previous frames. I'd imagine it is because it is taking too long to draw the bitmaps? but the map is only very small at the moment.
I found the problem. When moving the map, the offset values were being changed. This lead to sections of the map being drawn temporarily with different offset values - creating the tearing. duh!
I solved this by copying the xOffset and yOffset values at the start of the onDraw() method, and using those values for the update.