Working with SurfaceView on Android

Hello, Khabravchans!
When working with 2D graphics in Android, rendering can be done using Canvas. The easiest way to do this is with your own class inherited from View. You just need to describe the onDraw () method, and use the canvas provided as the parameter to perform all the necessary actions. However, this approach has its drawbacks. The onDraw () method is called by the system. You can use the invalidate () method manually, telling the system about the need for redrawing. But calling invalidate () does not guarantee that the onDraw () method is called immediately. Therefore, if we need to constantly draw (for example, for any game), the above method is hardly worth considering.

There is another approach - using the SurfaceView class. After reading the official guide and studying several examples, I decided to write a short article in Russian, which will probably help someone get used to this rendering method faster. The article is intended for beginners. No complicated and tricky techniques are described here.

SurfaceView Class


A feature of the SurfaceView class is that it provides a separate area for drawing, the actions of which should be taken out in a separate application stream. Thus, the application does not need to wait until the system is ready to render the entire hierarchy of view elements. The auxiliary stream can use the canvas of our SurfaceView to render at the speed that is needed.

The whole implementation comes down to two main points:
  1. Creating a class inherited from SurfaceView that implements the SurfaceHolder.Callback interface
  2. Create a stream that will control rendering.

Class creation


As mentioned above, we need our own class that extends SurfaceView and implements the SurfaceHolder.Callback interface. This interface offers to implement three methods: surfaceCreated (), surfaceChanged () and surfaceDestroyed (), called when creating an area for painting, changing it and destroying it, respectively.
Working with the canvas for painting is carried out not directly through the class we created, but using the SurfaceHolder object. You can get it by calling the getHolder () method. It is this object that will provide us with canvas for rendering.
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
    public MySurfaceView(Context context) {
        super(context);
        getHolder().addCallback(this);
    }
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
        int height) {	
    }
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
    }
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }
}

In the constructor of the class, we get the SurfaceHolder object and using the addCallback () method indicate that we want to receive the corresponding callbacks.
In the surfaceCreated () method, as a rule, you need to start rendering, and in surfaceDestroyed (), on the contrary, finish it. Let us leave the bodies of these methods empty and implement the flow responsible for rendering.

Implementation of the rendering stream


Create a class inherited from Thread. In the constructor, it will take two parameters: SurfaceHolder and Resources to load the image that we will draw on the screen. Also in the class we will need a flag variable indicating that a drawing is being performed and a method for setting this variable. Well, of course, you will need to override the run () method.
As a result, we get the following class:

class DrawThread extends Thread{
    private boolean runFlag = false;
    private SurfaceHolder surfaceHolder;
    private Bitmap picture;
    private Matrix matrix;
    private long prevTime;
    public DrawThread(SurfaceHolder surfaceHolder, Resources resources){
        this.surfaceHolder = surfaceHolder;
        // загружаем картинку, которую будем отрисовывать
        picture = BitmapFactory.decodeResource(resources, R.drawable.icon);
        // формируем матрицу преобразований для картинки
        matrix = new Matrix();
        matrix.postScale(3.0f, 3.0f);
        matrix.postTranslate(100.0f, 100.0f);
        // сохраняем текущее время
        prevTime = System.currentTimeMillis();
    }
    public void setRunning(boolean run) {
        runFlag = run;
    }
    @Override
    public void run() {
        Canvas canvas;
        while (runFlag) {
            // получаем текущее время и вычисляем разницу с предыдущим 
            // сохраненным моментом времени
            long now = System.currentTimeMillis();
            long elapsedTime = now - prevTime;
            if (elapsedTime > 30){
                // если прошло больше 30 миллисекунд - сохраним текущее время
                // и повернем картинку на 2 градуса.
                // точка вращения - центр картинки
                prevTime = now;
                matrix.preRotate(2.0f, picture.getWidth() / 2, picture.getHeight() / 2);
            }
            canvas = null;
            try {
                // получаем объект Canvas и выполняем отрисовку
                canvas = surfaceHolder.lockCanvas(null);
                synchronized (surfaceHolder) {
                    canvas.drawColor(Color.BLACK);
                    canvas.drawBitmap(picture, matrix, null);
                }
            } 
            finally {
                if (canvas != null) {
                    // отрисовка выполнена. выводим результат на экран
                    surfaceHolder.unlockCanvasAndPost(canvas);
                }
            }
        }
    }
}

To ensure that the result does not look very fresh, we’ll increase the downloaded image three times, and slightly shift it to the center of the screen. We do this using the transformation matrix. Also, no more than once every 30 milliseconds, we will rotate the picture 2 degrees around its center. Drawing on canvas itself is, of course, better to put in a separate method, but in this case we just clear the screen and draw an image. So you can leave it as it is.

Start and end drawing


Now, after we wrote the thread that controls the rendering, let's go back to editing our SurfaceView class. In the surfaceCreated () method, create a stream and start it. And in the surfaceDestroyed () method, we finish its work. As a result, the MySurfaceView class will take the following form:

public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
    private DrawThread drawThread;
    public MySurfaceView(Context context) {
        super(context);
        getHolder().addCallback(this);
    }
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {	
    }
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        drawThread = new DrawThread(getHolder(), getResources());
        drawThread.setRunning(true);
        drawThread.start();
    }
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        boolean retry = true;
        // завершаем работу потока
        drawThread.setRunning(false);
        while (retry) {
            try {
                drawThread.join();
                retry = false;
            } catch (InterruptedException e) {
                // если не получилось, то будем пытаться еще и еще
            }
        }
    }
}

It should be noted that the creation of the stream must be performed in the surfaceCreated () method. In the LunarLander example from the official documentation, the creation of the rendering stream occurs in the constructor of the class inherited from SurfaceView. But with this approach, an error may occur. If you minimize the application by pressing the Home key on the device, and then open it again, an IllegalThreadStateException will occur.

An application activity might look like this:

public class SurfaceViewActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new MySurfaceView(this));
    }
}


The result of the program looks like this (due to rotation, the image is a bit blurry, but on the device it looks quite acceptable):


Conclusion


The above method of rendering graphics, although somewhat more complicated than drawing on canvas a simple View, but in some cases is more preferable. The difference can be noticed if the application needs to redraw the graphics very often. Auxiliary flow helps to better control this process.
And in conclusion, I would like to provide links to some of the resources that I used when creating the article:
  1. An example of the game LunarLander from the official documentation
  2. Article describing the use of SurfaceView

Also popular now: