I have an application that draws a grid of points (let them say 5x5). The user is prompted to draw lines on this grid. If the user's finger touches one of the points in the grid, this point is painted to show that this point is part of the path. In addition, a line will be drawn between every two touching points.
Problem - I get very poor performance that causes a few things:
- The application is getting very slow.
- Motion events in
event.getAction() get poor granularity. I mean enter code here , that instead of detecting movement every 10 pixels, for example, it records movements every 100 pixels. This, in turn, will cause the application to NOT redraw some points that the user has touched. - Sometimes the coordinates of the movement are simple: we can say that the user moves his finger from pixel 100 to pixel 500, the reading can show 100 ... 200 ... 150 ... 140 ... 300 ... 400. For some reason in some cases, the location of the touch is messed up.
See an example of how the application โmissesโ the points that the user touched and did not draw green dots:

I tried a little:
- Adding Thread.sleep (100); before
else if(event.getAction() == MotionEvent.ACTION_MOVE) inside onTouchEvent(MotionEvent event) , I read that this can give CPU time to catch up with all these touch events - nothing has changed. - Adding
this.destroyDrawingCache() to the very end of doDraw() (I use it instead of onDraw, as suggested in one tutorial that I used). I thought this would clear all event / drawing caching, which seems to slow down the system - nothing has changed.
I'm new to Android animation, so I'm not sure how to do this:
- As far as I understand, I should do as little as possible in
doDraw() (my onDraw ()) and onTouchEvent() . - I read something about
invalidate() , but I don't know how and when to use it. If I understand correctly, my view is re-received every time doDraw() called. My grid, for example, is static - how can I avoid redrawing it?
++++++++++++++++++++++++++ UPDATE October 7th October +++++++++++++++++++++
I tried using canvas.drawCircle(xPos, yPos, 8, mNodePaint); instead of canvas.drawBitmap(mBitmap, xPos, yPos, null); . I thought that if I had NOT used the actual bitmaps, this could improve performance. Not really! I'm a little confused how such a simple application can create such a heavy load on the device. I have to do something really wrong.
++++++++++++++++++++++++++ UPDATE October 12th October +++++++++++++++++++++
I took into account what @LadyWoodi suggested - I removed all variable declarations from the loops - in any case, this is bad practice, and I also got rid of all the "System.Out" lines that I use, so I can register application behavior to better understand why I get such lame performance. I am sad to say that if there was a change in performance (in fact, I did not measure the change in frame rate), this is insignificant.
Any other ideas?
++++++++++++++++++++++++++ UPDATE October 13th October +++++++++++++++++++++
- Since I have a static grid of dots (see hollow black / white dots in screenShot) that never change during the game, I did the following:
-Turn the grid once.
-Capture drawing as a bitmap using Bitmap.createBitmap() .
-Use canvas.drawBitmap() to draw a bitmap image of a grid of static points.
-When my thread starts, I check to see how the grid of points is drawn. If it is running, I DO NOT recreate the grid of static points. I will display it only from a previously made bitmap.
Surprisingly, this has not changed with my work! Redrawing the grid of points each time did not have a true visual impact on application performance.
I decided to use canvas = mHolder.lockCanvas(new Rect(50, 50, 150, 150)); inside my drawing. It was just for testing, to see if every time I limit the area that I get, I can improve performance. This CANNOT help.
Then I turned to the DDMS tool in Eclipse to try to profile the application. What he came up with was that canvas.drawPath(path, mPathPaint); (Canvas.native_drawPath) consumed about 88.5% of the processor time !!!
But why?! My path drawing is pretty simple, mGraphics contains a collection of Paths, and all I do is find out if each path is inside the borders of the game screen, and then I draw a path:
//draw path user is creating with her finger on screen for (Path path : mGraphics) { //get path values mPm = new PathMeasure(path, true); mPm.getPosTan(0f, mStartCoordinates, null); //System.out.println("aStartCoordinates X:" + aStartCoordinates[0] + " aStartCoordinates Y:" + aStartCoordinates[1]); mPm.getPosTan(mPm.getLength(), mEndCoordinates, null); //System.out.println("aEndCoordinates X:" + aEndCoordinates[0] + " aEndCoordinates Y:" + aEndCoordinates[1]); //coordinates are within game board boundaries if((mStartCoordinates[0] >= 1 && mStartCoordinates[1] >= 1) && (mEndCoordinates[0] >= 1 && mEndCoordinates[1] >= 1)) { canvas.drawPath(path, mPathPaint); } }
Can anyone see any programmed lines of code in my examples?
+++++++++++++++++++++++++ UPDATE October 14th October +++++++++++++++++++++
I made changes to my doDraw() method. Basically what I do is draw the screen ONLY if something has changed. In all other cases, I simply store the cached screen bitmap and render it. Please see:
public void doDraw(Canvas canvas) { synchronized (mViewThread.getSurefaceHolder()) { if(mGraphics.size() > mPathsCount) { mPathsCount = mGraphics.size(); //draw path user is creating with her finger on screen for (Path path : mGraphics) { //get path values mPm = new PathMeasure(path, true); mPm.getPosTan(0f, mStartCoordinates, null); //System.out.println("aStartCoordinates X:" + aStartCoordinates[0] + " aStartCoordinates Y:" + aStartCoordinates[1]); mPm.getPosTan(mPm.getLength(), mEndCoordinates, null); //System.out.println("aEndCoordinates X:" + aEndCoordinates[0] + " aEndCoordinates Y:" + aEndCoordinates[1]); //coordinates are within game board boundaries if((mStartCoordinates[0] >= 1 && mStartCoordinates[1] >= 1) && (mEndCoordinates[0] >= 1 && mEndCoordinates[1] >= 1)) { canvas.drawPath(path, mPathPaint); } } //nodes that the path goes through, are repainted green //these nodes are building the drawn pattern for (ArrayList<PathPoint> nodePattern : mNodesHitPatterns) { for (PathPoint nodeHit : nodePattern) { canvas.drawBitmap(mDotOK, nodeHit.x - ((mDotOK.getWidth()/2) - (mNodeBitmap.getWidth()/2)), nodeHit.y - ((mDotOK.getHeight()/2) - (mNodeBitmap.getHeight()/2)), null); } } mGameField = Bitmap.createBitmap(mGridNodesCount * mNodeGap, mGridNodesCount * mNodeGap, Bitmap.Config.ARGB_8888); } else { canvas.drawBitmap(mGameField, 0f, 0f, null); }
Now for the results - as long as the device does not need to display paths and just draw from a bitmap, everything goes very quickly. But at that moment when I need to reload the screen using canvas.drawPath() , the performance becomes as slow as a turtle on a morphine ... The more I have the path (up to 6 or more, NOTHING!), The slower the rendering . How strange is that? - My paths are not even very seductive - all straight lines with a random turn. I mean, the line is not very "complicated."
I will add the following code below - if you have any ideas for improvement.
Thanks a lot in advance, D.
~~~~~~~~~~~~~~~~~~~~~~~~~ Panel class ~~~~~~~~~~~~ ~~~~~~~~~~~ ~~~~~~~~
public class Panel extends SurfaceView implements SurfaceHolder.Callback { Bitmap mNodeBitmap; int mNodeBitmapWidthCenter; int mNodeBitmapHeightCenter; Bitmap mDotOK; ViewThread mViewThread; ArrayList<PathPoint> mPathPoints; private ArrayList<Path> mGraphics = new ArrayList<Path>(3); private ArrayList<ArrayList<PathPoint>> mNodesHitPatterns = new ArrayList<ArrayList<PathPoint>>(); private Paint mPathPaint; Path mPath = new Path(); //private ArrayList<Point> mNodeCoordinates = new ArrayList<Point>(); private int mGridNodesCount = 5; private int mNodeGap = 100; PathPoint mNodeCoordinates[][] = new PathPoint[mGridNodesCount][mGridNodesCount]; PathMeasure mPm; float mStartCoordinates[] = {0f, 0f}; float mEndCoordinates[] = {0f, 0f}; PathPoint mPathPoint; Boolean mNodesGridDrawn = false; Bitmap mGameField = null; public Boolean getNodesGridDrawn() { return mNodesGridDrawn; } public Panel(Context context) { super(context); mNodeBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dot); mNodeBitmapWidthCenter = mNodeBitmap.getWidth()/2; mNodeBitmapHeightCenter = mNodeBitmap.getHeight()/2; mDotOK = BitmapFactory.decodeResource(getResources(), R.drawable.dot_ok); getHolder().addCallback(this); mViewThread = new ViewThread(this); mPathPaint = new Paint(); mPathPaint.setAntiAlias(true); mPathPaint.setDither(true); //for better color mPathPaint.setColor(0xFFFFFF00); mPathPaint.setStyle(Paint.Style.STROKE); mPathPaint.setStrokeJoin(Paint.Join.ROUND); mPathPaint.setStrokeCap(Paint.Cap.ROUND); mPathPaint.setStrokeWidth(5); } public ArrayList<ArrayList<PathPoint>> getNodesHitPatterns() { return this.mNodesHitPatterns; } public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } public void surfaceCreated(SurfaceHolder holder) { //setPadding(100, 100, 0, 0); if (!mViewThread.isAlive()) { mViewThread = new ViewThread(this); mViewThread.setRunning(true); mViewThread.start(); } } public void surfaceDestroyed(SurfaceHolder holder) { if (mViewThread.isAlive()) { mViewThread.setRunning(false); } } //draw the basic nodes grid that the user will use to draw the lines on //store as bitmap public void drawNodesGrid(Canvas canvas) { canvas.drawColor(Color.WHITE); for (int i = 0; i < mGridNodesCount; i++) { for (int j = 0; j < mGridNodesCount; j++) { int xPos = j * mNodeGap; int yPos = i * mNodeGap; try { //TODO - changed mNodeCoordinates[i][j] = new PathPoint(xPos, yPos, null); } catch (Exception e) { e.printStackTrace(); } canvas.drawBitmap(mNodeBitmap, xPos, yPos, null); } } mNodesGridDrawn = true; mGameField = Bitmap.createBitmap(mGridNodesCount * mNodeGap, mGridNodesCount * mNodeGap, Bitmap.Config.ARGB_8888); } public void doDraw(Canvas canvas) { canvas.drawBitmap(mGameField, 0f, 0f, null); synchronized (mViewThread.getSurefaceHolder()) { //draw path user is creating with her finger on screen for (Path path : mGraphics) { //get path values mPm = new PathMeasure(path, true); mPm.getPosTan(0f, mStartCoordinates, null); //System.out.println("aStartCoordinates X:" + aStartCoordinates[0] + " aStartCoordinates Y:" + aStartCoordinates[1]); mPm.getPosTan(mPm.getLength(), mEndCoordinates, null); //System.out.println("aEndCoordinates X:" + aEndCoordinates[0] + " aEndCoordinates Y:" + aEndCoordinates[1]); //coordinates are within game board boundaries if((mStartCoordinates[0] >= 1 && mStartCoordinates[1] >= 1) && (mEndCoordinates[0] >= 1 && mEndCoordinates[1] >= 1)) { canvas.drawPath(path, mPathPaint); } } //nodes that the path goes through, are repainted green //these nodes are building the drawn pattern for (ArrayList<PathPoint> nodePattern : mNodesHitPatterns) { for (PathPoint nodeHit : nodePattern) { canvas.drawBitmap(mDotOK, nodeHit.x - ((mDotOK.getWidth()/2) - (mNodeBitmap.getWidth()/2)), nodeHit.y - ((mDotOK.getHeight()/2) - (mNodeBitmap.getHeight()/2)), null); } } this.destroyDrawingCache(); } } @Override public boolean onTouchEvent(MotionEvent event) { synchronized (mViewThread.getSurefaceHolder()) { if(event.getAction() == MotionEvent.ACTION_DOWN) { //System.out.println("Action downE x: " + event.getX() + " y: " + event.getY()); for (int i = 0; i < mGridNodesCount; i++) { for (int j = 0; j < mGridNodesCount; j++) { //TODO - changed //PathPoint pathPoint = mNodeCoordinates[i][j]; mPathPoint = mNodeCoordinates[i][j]; if((Math.abs((int)event.getX() - mPathPoint.x) <= 35) && (Math.abs((int)event.getY() - mPathPoint.y) <= 35)) { //mPath.moveTo(pathPoint.x + mBitmap.getWidth() / 2, pathPoint.y + mBitmap.getHeight() / 2); //System.out.println("Action down x: " + pathPoint.x + " y: " + pathPoint.y); ArrayList<PathPoint> newNodesPattern = new ArrayList<PathPoint>(); mNodesHitPatterns.add(newNodesPattern); //mNodesHitPatterns.add(nh); //pathPoint.setAction("down"); break; } } } } else if(event.getAction() == MotionEvent.ACTION_MOVE) { final int historySize = event.getHistorySize(); //System.out.println("historySize: " + historySize); //System.out.println("Action moveE x: " + event.getX() + " y: " + event.getY()); coordinateFound: for (int i = 0; i < mGridNodesCount; i++) { for (int j = 0; j < mGridNodesCount; j++) { //TODO - changed //PathPoint pathPoint = mNodeCoordinates[i][j]; mPathPoint = mNodeCoordinates[i][j]; if((Math.abs((int)event.getX() - mPathPoint.x) <= 35) && (Math.abs((int)event.getY() - mPathPoint.y) <= 35)) { int lastPatternIndex = mNodesHitPatterns.size()-1; ArrayList<PathPoint> lastPattern = mNodesHitPatterns.get(lastPatternIndex); int lastPatternLastNode = lastPattern.size()-1; if(lastPatternLastNode != -1) { if(!mPathPoint.equals(lastPattern.get(lastPatternLastNode).x, lastPattern.get(lastPatternLastNode).y)) { lastPattern.add(mPathPoint); //System.out.println("Action moveC [add point] x: " + pathPoint.x + " y: " + pathPoint.y); } } else { lastPattern.add(mPathPoint); //System.out.println("Action moveC [add point] x: " + pathPoint.x + " y: " + pathPoint.y); } break coordinateFound; } else //no current match => try historical { if(historySize > 0) { for (int k = 0; k < historySize; k++) { //System.out.println("Action moveH x: " + event.getHistoricalX(k) + " y: " + event.getHistoricalY(k)); if((Math.abs((int)event.getHistoricalX(k) - mPathPoint.x) <= 35) && (Math.abs((int)event.getHistoricalY(k) - mPathPoint.y) <= 35)) { int lastPatternIndex = mNodesHitPatterns.size()-1; ArrayList<PathPoint> lastPattern = mNodesHitPatterns.get(lastPatternIndex); int lastPatternLastNode = lastPattern.size()-1; if(lastPatternLastNode != -1) { if(!mPathPoint.equals(lastPattern.get(lastPatternLastNode).x, lastPattern.get(lastPatternLastNode).y)) { lastPattern.add(mPathPoint); //System.out.println("Action moveH [add point] x: " + pathPoint.x + " y: " + pathPoint.y); } } else { lastPattern.add(mPathPoint); //System.out.println("Action moveH [add point] x: " + pathPoint.x + " y: " + pathPoint.y); } break coordinateFound; } } } } } } } else if(event.getAction() == MotionEvent.ACTION_UP) { // for (int i = 0; i < mGridSize; i++) { // // for (int j = 0; j < mGridSize; j++) { // // PathPoint pathPoint = mNodeCoordinates[i][j]; // // if((Math.abs((int)event.getX() - pathPoint.x) <= 35) && (Math.abs((int)event.getY() - pathPoint.y) <= 35)) // { // //the location of the node // //mPath.lineTo(pathPoint.x + mBitmap.getWidth() / 2, pathPoint.y + mBitmap.getHeight() / 2); // // //System.out.println("Action up x: " + pathPoint.x + " y: " + pathPoint.y); // // //mGraphics.add(mPath); // // mNodesHit.add(pathPoint); // // pathPoint.setAction("up"); // break; // } // } // } } //System.out.println(mNodesHitPatterns.toString()); //create mPath for (ArrayList<PathPoint> nodePattern : mNodesHitPatterns) { for (int i = 0; i < nodePattern.size(); i++) { if(i == 0) //first node in pattern { mPath.moveTo(nodePattern.get(i).x + mNodeBitmapWidthCenter, nodePattern.get(i).y + mNodeBitmapHeightCenter); } else { mPath.lineTo(nodePattern.get(i).x + mNodeBitmapWidthCenter, nodePattern.get(i).y + mNodeBitmapWidthCenter); } //mGraphics.add(mPath); } } mGraphics.add(mPath); return true; } }
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ViewThread class ~~~~~~~~~ ~~~ ~~~~~~~~~~~~~~~~~~~~
public class ViewThread extends Thread { private Panel mPanel; private SurfaceHolder mHolder; private boolean mRun = false; public ViewThread(Panel panel) { mPanel = panel; mHolder = mPanel.getHolder(); } public void setRunning(boolean run) { mRun = run; } public SurfaceHolder getSurefaceHolder() { return mHolder; } @Override public void run() { Canvas canvas = null; while (mRun) { canvas = mHolder.lockCanvas();