Reputation: 1569
I am trying to make a simple stopwatch using a SurfaceView
. It shows hundredths of a second, so it tries to update as often as possible in order to keep a smooth animation. It works absolutely fine on my tablet running 4.0.3 and my phone running 4.0.4, but when I try it on 4.4.4, there appears to be some tearing I cannot explain.
The bottom image shows what should be happening, and for the most part it works. However, once every few seconds there is a flicker. When I manage to pause at the right time, I see that some of the digits are transposed to the right. They only ever move to the right (a la the top image). This only happens for a fraction of a second, just long enough to cause a noticeable flicker.
Note that the digits are positioned on the canvas using values calculated once (in onMeasure()
) and stored in class fields. They do not change after they are initialized. I tested this by logging the pixel values of each inside onDraw()
.
In the image above it is the minutes values that are transposed, but sometimes it is the other digits. They always seem to be transposed in a set; both the minutes, both the seconds or both the centiseconds.
Further: this only affects drawText()
. I have more complex versions of this stopwatch with lots of paths, circles, etc. The problem only happens with text.
I looked at several questions and comments on SO and elsewhere. Here's what I have determined:
I do not think it is a buffer issue. The behaviour seems similar to descriptions of buffer-based tearing, but in this case I don't think the problem can be old data being posted to the screen, because the digits are never drawn in the positions they are shown.
I read in a comment that it might be related to the function that
updates the values being called more than once between the onDraw()
method calls. I have tried throttling the update, but the problem
persists.
I also tried drawing to a temporary bitmap and using canvas.drawBitmap()
in
the onDraw()
function, again to no avail.
Basically, I want to know:
Upvotes: 0
Views: 779
Reputation: 1569
After reading @fadden's comments, I was able to search a bit more effectively.
It seems that my error was in trying to call onDraw()
in the animation thread. When I changed that one call to postInvalidate()
, it immediately fixed the problem. In short, the inside of the animation loop looks like this:
holder = view.getHolder();
canvas = holder.lockCanvas();
if (canvas != null) {
synchronized (holder) {
if (view != null) {
view.DEBUG_THREADCALLS++;
// NOT view.onDraw(), but:
view.postInvalidate();
}
}
}
As I understand it, the call to onDraw()
causes problems because it results in the app drawing to both the View
and the Surface
.
I am guessing that the reason the problem only showed up in the more modern versions of Android might be because the newer devices have higher refresh rates, and so the problem was simply more visible. I have noticed that the solution also significantly increased the framerate on my oldest device, which is presumably because it is not working so hard drawing to two places at once.
UPDATE
Following further advice from the extremely helpful comments below, I have removed the SurfaceView altogether and am now drawing on a custom View using a separate animation Thread. The result is unbelievably smooth animation even on older devices, far better than the SurfaceView! So to anyone else trying to animate with a SurfaceView, I say: Unless you have a very good reason to, don't!
Upvotes: 1