Android Multiple SurfaceViews

nhaarman picture nhaarman · Apr 13, 2011 · Viewed 20k times · Source

I'm trying to work with 3 SurfaceViews on one screen, one on top half (BoardView), one on bottom half (StatusView), and the last one as an extra layer above the top half (TileView) (see main.xml).

I created a class MySurfaceView, which is extended by BoardView, StatusView and TileView.

I've got multiple problems with this.

Let me first give the code.

main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@color/main_background">

    <com.niek.test.BoardView
        android:id="@+id/boardview"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />

    <FrameLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_below="@+id/boardview">
        <com.niek.test.StatusView
            android:id="@+id/statusview"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:background="#F0931E"
            android:layout_below="@+id/boardview" />

            <com.niek.test.TileView
                android:id="@+id/tileview"
                android:layout_width="180dip"
                android:layout_height="60dip"
                android:layout_gravity="bottom"/>


    </FrameLayout>
</RelativeLayout>

MainActivity.java:

package com.niek.test;

public class MainActivity extends Activity {

    private Board board;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        board = new Board();
        BoardView boardView = (BoardView) findViewById(R.id.boardview);
        boardView.setBoard(board);
        StatusView statusView = (StatusView) findViewById(R.id.statusview);
        statusView.setBoard(board);
    }
}

MySurfaceView.java

package com.niek.test;

public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {

    protected DrawThread drawThread;

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        getHolder().addCallback(this);
        setFocusable(true);

        drawThread = new DrawThread(getHolder());
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        // TODO Auto-generated method stub

    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // TODO Auto-generated method stub
        drawThread.setRunning(true);
        drawThread.start();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        // we have to tell thread to shut down & wait for it to finish, or else
        // it might touch the Surface after we return and explode
        boolean retry = true;
        drawThread.setRunning(false);
        while (retry) {
            try {
                drawThread.join();
                retry = false;
            } catch (InterruptedException e) {
                // we will try it again and again...
            }
        }
    }

    protected class DrawThread extends Thread {
        private SurfaceHolder surfaceHolder;
        private boolean isRunning;

        public DrawThread(SurfaceHolder surfaceHolder) {
            this.surfaceHolder = surfaceHolder;
            isRunning = false;
        }

        public void setRunning(boolean run) {
            isRunning = run;
        }

        public void run() {
            Canvas c;
            while (isRunning) {
                try {
                    Thread.sleep(100);
                } catch (Exception e) {
                    // TODO: handle exception
                }
                c = null;
                try {
                    c = surfaceHolder.lockCanvas(null);
                    synchronized (surfaceHolder) {
                        onDraw(c);
                        postInvalidate();
                    }
                } finally {
                    // do this in a finally so that if an exception is thrown
                    // during the above, we don't leave the Surface in an
                    // inconsistent state
                    if (c != null) {
                        surfaceHolder.unlockCanvasAndPost(c);
                    }
                }
            }
        }
    }

}

These three classes extend MySurfaceView:

BoardView.java

package com.niek.test;


public class BoardView extends MySurfaceView {

    private int squareSize, marginX, marginY;

    private Board board;

    Paint boardBorder;

    public BoardView(Context context, AttributeSet attrs) {
        super(context, attrs);
        board = null;
    }

    public void setBoard(Board board) {
        this.board = board;
    }

    private void init(SurfaceHolder holder) {
        Canvas canvas = null;
        try {
            canvas = holder.lockCanvas();
            /* Initialize the board */
            squareSize = canvas.getWidth() / Board.GRIDSIZE;

            /* Size the view */
            LayoutParams lp = getLayoutParams();
            lp.height = (squareSize * Board.GRIDSIZE) + 4;
            setLayoutParams(lp);

            /* Place the board neatly in the center */
            marginX = (canvas.getWidth() - (squareSize * Board.GRIDSIZE)) / 2;
            marginY = 1;
        } finally {
            holder.unlockCanvasAndPost(canvas);
        }

        boardBorder = new Paint();
        boardBorder.setColor(Color.RED);
        boardBorder.setStyle(Style.STROKE);
    }

    @Override
    public void onDraw(Canvas canvas) {
        drawBoard(board, canvas);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        init(holder);
        super.surfaceCreated(holder);
    }

    private void drawBoard(Board board, Canvas canvas) {
        synchronized (board) {
            if (board != null) {
                for (Square[] ys : board.getSquares()) {
                    for (Square xs : ys) {
                        xs.onDraw(canvas, squareSize, squareSize, marginX, marginY);
                    }
                }
            }   
            canvas.drawRect(marginX - 1, marginY - 1, marginX + squareSize * Board.GRIDSIZE + 1, marginY + squareSize * Board.GRIDSIZE + 1, boardBorder);
        }
    }
}

StatusView.java

package com.niek.test;

public class StatusView extends MySurfaceView {

    private Board board;
    private Paint textPaint;

    public StatusView(Context context, AttributeSet attrs) {
        super(context, attrs);
        board = null;

        textPaint = new Paint();
        textPaint.setColor(Color.BLACK);
        textPaint.setTextSize(20);
        textPaint.setTypeface(Typeface.DEFAULT_BOLD);
    }

    public void setBoard(Board board) {
        this.board = board;
    }

    int tmp=0;
    @Override
    public void onDraw(Canvas c) {
        if (board != null) {
            c.drawText(tmp+"", 10, 20, textPaint);          
            tmp++;
            System.out.println(tmp);
        }
    }
}

TileView.java

package com.niek.test;

public class TileView extends MySurfaceView {

    public TileView(Context context, AttributeSet attrs) {
        super(context, attrs);
        System.out.println(0);
    }


    int tmp =0;
    @Override
    public void onDraw(Canvas c) {
        System.out.println(2);
        Paint p= new Paint();
        p.setColor(Color.RED);
        c.drawColor(Color.RED);
        c.drawText(tmp+"",10,10,p);
        tmp++;

    }

}

Now what are my problems?

First off, as you can see in MySurfaceView I've got this:

try {
    c = surfaceHolder.lockCanvas(null);
    synchronized (surfaceHolder) {
        onDraw(c);
        postInvalidate();
    }
}

When I only use onDraw(c), only the BoardView gets drawn, the StatusView doesn't get drawn, but the tmp increments in the onDraw of StatusView are being executed. When I only use postInvalidate(), same story, but only StatusView gets drawn, BoardView doesn't. So that's why I use both methods, and both Views get drawn.

Then there's TileView, the System.out(2) is being shown in logcat, but the view doesn't get drawn. It is a black square instead of the red square I ask it to be in the onDraw method.

When I turn the screen off and then on again, the TileView does get drawn, and the tmp increments are shown.

Who can help me?

For clarity, I've created this based on this tutorial.

Answer

fadden picture fadden · Mar 30, 2014

You can have multiple SurfaceViewsin one layout. The "Multi-surface test" activity in Grafika has three.

The first post cited in @nonsleepr's answer was followed up 9 months later with this post by the same author, which mentioned the existence of SurfaceView#setZOrderMediaOverlay().

The key thing to understand is that SurfaceView is not a regular view. When your app comes to the foreground it gets a surface to draw on. Everything in your app's UI is rendered onto the app's surface by the app, and then that surface is composited with other surfaces (like the status bar and navigation bar) by the system compositor. When you create a SurfaceView, it's actually creating an entirely new surface that is composited by the system, not by your app.

You can control the Z-ordering (i.e. "depth") of the SurfaceView surface very loosely. There are four positions, from top to bottom:

  • SurfaceView + ZOrderOnTop
  • (app UI goes here)
  • SurfaceView + ZOrderMediaOverlay
  • SurfaceView (default)

If you have two SurfaceViews at the same depth, and they overlap, the results are undefined -- one will "win", but you can't control which.

The system compositor on modern devices is very efficient when you have N surfaces. At N+1 surfaces you hit a performance cliff. So while you can have three SurfaceViews, you're generally better off keeping the number down. The value of N varies from device to device.

Update: if you really want to understand how SurfaceView works, see the Android System-Level Graphics doc.