Note that there are some explanatory texts on larger screens.

plurals
  1. POAndroid 2D animation drawing: bad performance
    text
    copied!<p>I have an app that draws a grid of dots (let's say 5x5). The user is asked to draw lines on that grid. If the user's finger touches one of the dots in the grid, this dot is being colored to show that this dot is part of a path drawn. In addition a line will be drawn between each two touched dots.</p> <p>The issue - I get very bad performance, which causes few things:</p> <ol> <li>The application gets really slow.</li> <li>Motion events in <code>event.getAction()</code> get bad granularity. I mean<code>enter code here</code> that instead of registering a movement each 10 pixels for example, it registers movements each 100 pixels. This, in turn, will causes the app to NOT redraw some dots the user had touched.</li> <li>Sometimes the motion coordinates are simple wrong: lets say the user is moving her finger from pixel 100 to pixel 500, the reading might show 100...200...150...140...300...400. For some reason the touch location gets messed up in some cases.</li> </ol> <p>Look at the example on how the app "misses out" on dots the user have touched and doesn't draw the green dots:</p> <p><img src="https://i.stack.imgur.com/CHCKl.png" alt="Showing two lines - one is missing a dot. Both lines were drawn from top to bottom, the left one was drawn first"></p> <p>I've tried few thing:</p> <ol> <li>Adding Thread.sleep(100); to <code>else if(event.getAction() == MotionEvent.ACTION_MOVE)</code> inside <code>onTouchEvent(MotionEvent event)</code>, I read that this might give the CPU time to catch up on all those touch events - didn't change a thing</li> <li>Adding <code>this.destroyDrawingCache()</code> to the very end of <code>doDraw()</code> (I use it instead of onDraw, as was suggested by one tutorial I used). I thought this will clear all event/drawing caching which seems to be slowing down the system - didn't change a thing.</li> </ol> <p>I am fairly new to Android animation so I am not sure how to proceed:</p> <ol> <li>I understand I should do as little as possible in <code>doDraw()</code> (my onDraw()) and <code>onTouchEvent()</code>.</li> <li>I read some stuff about <code>invalidate()</code> but not sure how and when to use it. If I understand correctly, my View gets drawn anew each time <code>doDraw()</code> is called. My grid, for instance, is static - how can I avoid redrawing it?</li> </ol> <p>++++++++++++++++++++++++ UPDATE 7th Oct +++++++++++++++++++++</p> <p>I tried using <code>canvas.drawCircle(xPos, yPos, 8, mNodePaint);</code> instead of <code>canvas.drawBitmap(mBitmap, xPos, yPos, null);</code>. I thought that if I DIDN'T use actual bitmaps this might improve performance. As a matter of fact - it didn't! I am a bit confused how such a simple application can pose such a heavy load on the device. I must be doing something really the wrong way.</p> <p>++++++++++++++++++++++++ UPDATE 12th Oct +++++++++++++++++++++</p> <p>I took into account what @LadyWoodi suggested - I've eliminated all variable declarations out of the loops - anyway it is a bad practice and I also got rid of all the "System.Out" lines I use so I can log app behavior to better understand why I get such a lame performance. I am sad to say that if there was a change in performance (I didn't actually measure frame rate change) it is negligible. </p> <p>Any other ideas?</p> <p>++++++++++++++++++++++++ UPDATE 13th Oct +++++++++++++++++++++</p> <ol> <li>As I have a static grid of dots (see hollow black/white dots in screenShot) that never changes during the game I did the following:</li> </ol> <p>-Draw the grid once.</p> <p>-Capture the drawing as bitmap using <code>Bitmap.createBitmap()</code>.</p> <p>-Use <code>canvas.drawBitmap()</code> to draw the bitmap of the static dots grid.</p> <p>-When my thread runs I check to see it the grid of dots is drawn. If it is running I will NOT recreate the static dots grid. I will only render it from my previously rendered bitmap.</p> <p>Surprisingly this changed nothing with my performance! Redrawing the dots grid each time didn't have a true visual effect on app performance.</p> <ol start="2"> <li><p>I decided to use <code>canvas = mHolder.lockCanvas(new Rect(50, 50, 150, 150));</code> inside my drawing thread. It was just for testing purposes to see if I limit the area rendered each time, I can get the performance better. This DID NOT help either. </p></li> <li><p>Then I turned to the DDMS tool in Eclipse to try and profile the app. What it came up with, was that <code>canvas.drawPath(path, mPathPaint);</code> (Canvas.native_drawPath) consumed about 88.5% of CPU time!!!</p></li> </ol> <p>But why??! My path drawing is rather simple, mGraphics contains a collection of Paths and all I do is figure out if each path is inside the boundaries of the game screen and then I draw a path:</p> <pre><code>//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] &gt;= 1 &amp;&amp; mStartCoordinates[1] &gt;= 1) &amp;&amp; (mEndCoordinates[0] &gt;= 1 &amp;&amp; mEndCoordinates[1] &gt;= 1)) { canvas.drawPath(path, mPathPaint); } } </code></pre> <p>Can anyone see any ill programmed lines of code in my examples?</p> <p>++++++++++++++++++++++++ UPDATE 14th Oct +++++++++++++++++++++</p> <p>I've made changes to my <code>doDraw()</code>method. Basically what I do is draw the screen ONLY if something was changed. In all other cases I simply store a cached bitmap of the screen and render it. Please take a look:</p> <pre><code> public void doDraw(Canvas canvas) { synchronized (mViewThread.getSurefaceHolder()) { if(mGraphics.size() &gt; 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] &gt;= 1 &amp;&amp; mStartCoordinates[1] &gt;= 1) &amp;&amp; (mEndCoordinates[0] &gt;= 1 &amp;&amp; mEndCoordinates[1] &gt;= 1)) { canvas.drawPath(path, mPathPaint); } } //nodes that the path goes through, are repainted green //these nodes are building the drawn pattern for (ArrayList&lt;PathPoint&gt; 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); } </code></pre> <p>Now for the results - as long as the device doesn't have to render no paths and simply draws from a bitmap, stuff goes very fast. But the moment I have to rerender the screen using <code>canvas.drawPath()</code> performance becomes as sluggish as a turtle on morphine... The more paths I have (up to 6 and more, which is NOTHING!) the slower the rendering. How odd is this?? - My paths are even not really curvy - the are all straight lines with an occasional turn. What I mean is that the line is not very "complex". </p> <p>I've add more code below - if you have any improvements ideas.</p> <p>Many thanks in advance, D. </p> <p>~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Class "Panel" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~</p> <pre><code>public class Panel extends SurfaceView implements SurfaceHolder.Callback { Bitmap mNodeBitmap; int mNodeBitmapWidthCenter; int mNodeBitmapHeightCenter; Bitmap mDotOK; ViewThread mViewThread; ArrayList&lt;PathPoint&gt; mPathPoints; private ArrayList&lt;Path&gt; mGraphics = new ArrayList&lt;Path&gt;(3); private ArrayList&lt;ArrayList&lt;PathPoint&gt;&gt; mNodesHitPatterns = new ArrayList&lt;ArrayList&lt;PathPoint&gt;&gt;(); private Paint mPathPaint; Path mPath = new Path(); //private ArrayList&lt;Point&gt; mNodeCoordinates = new ArrayList&lt;Point&gt;(); 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&lt;ArrayList&lt;PathPoint&gt;&gt; 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 &lt; mGridNodesCount; i++) { for (int j = 0; j &lt; 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] &gt;= 1 &amp;&amp; mStartCoordinates[1] &gt;= 1) &amp;&amp; (mEndCoordinates[0] &gt;= 1 &amp;&amp; mEndCoordinates[1] &gt;= 1)) { canvas.drawPath(path, mPathPaint); } } //nodes that the path goes through, are repainted green //these nodes are building the drawn pattern for (ArrayList&lt;PathPoint&gt; 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 &lt; mGridNodesCount; i++) { for (int j = 0; j &lt; mGridNodesCount; j++) { //TODO - changed //PathPoint pathPoint = mNodeCoordinates[i][j]; mPathPoint = mNodeCoordinates[i][j]; if((Math.abs((int)event.getX() - mPathPoint.x) &lt;= 35) &amp;&amp; (Math.abs((int)event.getY() - mPathPoint.y) &lt;= 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&lt;PathPoint&gt; newNodesPattern = new ArrayList&lt;PathPoint&gt;(); 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 &lt; mGridNodesCount; i++) { for (int j = 0; j &lt; mGridNodesCount; j++) { //TODO - changed //PathPoint pathPoint = mNodeCoordinates[i][j]; mPathPoint = mNodeCoordinates[i][j]; if((Math.abs((int)event.getX() - mPathPoint.x) &lt;= 35) &amp;&amp; (Math.abs((int)event.getY() - mPathPoint.y) &lt;= 35)) { int lastPatternIndex = mNodesHitPatterns.size()-1; ArrayList&lt;PathPoint&gt; 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 =&gt; try historical { if(historySize &gt; 0) { for (int k = 0; k &lt; historySize; k++) { //System.out.println("Action moveH x: " + event.getHistoricalX(k) + " y: " + event.getHistoricalY(k)); if((Math.abs((int)event.getHistoricalX(k) - mPathPoint.x) &lt;= 35) &amp;&amp; (Math.abs((int)event.getHistoricalY(k) - mPathPoint.y) &lt;= 35)) { int lastPatternIndex = mNodesHitPatterns.size()-1; ArrayList&lt;PathPoint&gt; 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 &lt; mGridSize; i++) { // // for (int j = 0; j &lt; mGridSize; j++) { // // PathPoint pathPoint = mNodeCoordinates[i][j]; // // if((Math.abs((int)event.getX() - pathPoint.x) &lt;= 35) &amp;&amp; (Math.abs((int)event.getY() - pathPoint.y) &lt;= 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&lt;PathPoint&gt; nodePattern : mNodesHitPatterns) { for (int i = 0; i &lt; 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; } } </code></pre> <p>}</p> <p>~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Class "ViewThread" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~</p> <pre><code>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(); //canvas = mHolder.lockCanvas(new Rect(50, 50, 150, 150)); if (canvas != null) { if(!mPanel.getNodesGridDrawn()) { mPanel.drawNodesGrid(canvas); } mPanel.doDraw(canvas); mHolder.unlockCanvasAndPost(canvas); } } } } </code></pre>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload