Using a custom SurfaceView and thread for Android

2020-03-06 04:10发布

问题:

I am trying to use SurfaceView, but the lockCanvas(null) is confusing, and the app freezes when I exit the activity. Also, nothing is displayed, even though the tutorial I was using was working perfectly fine, and I don't understand what I am doing wrong. Please help.

回答1:

The solution is probably that setWillNotDraw(false); was not called.

Therefore, a referential implementation that works properly would be the following:

public class GameSurfaceView extends SurfaceView implements SurfaceHolder.Callback
{
    private SurfaceHolder holder;

    private MyThread myThread;

    private GameController gameController;

    private Paint paint;

    private int width;
    private int height;

    public GameSurfaceView(Context context, GameController gameController, int width, int height)
    {
        super(context);
        holder = getHolder(); 

        holder.addCallback(this);

        this.gameController = gameController;
        this.width = width;
        this.height = height;
        paint = new Paint();
        //initialize paint object parameters

        setWillNotDraw(false); //this line is very important!
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder)
    {
    }

    @Override
    // This is always called at least once, after surfaceCreated
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
    {
        if (myThread == null)
        {
            myThread = new MyThread(holder, gameController);
            myThread.setRunning(true);
            myThread.start();
        }
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder)
    {
        boolean retry = true;
        myThread.setRunning(false);
        while (retry)
        {
            try
            {
                myThread.join();
                retry = false;
            }
            catch (InterruptedException e)
            {
                Log.d(getClass().getSimpleName(), "Interrupted Exception", e);
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        System.out.println(event.getX() + " " + event.getY());
        gameController.onTouchEvent(event); //handle user interaction
        return super.onTouchEvent(event);
    }

    @Override
    protected void onDraw(Canvas canvas)
    {
        super.onDraw(canvas);
        canvas.drawText("Hello world!", width/20, 20, paint);
        gameController.draw(canvas);
    }

    public Thread getThread()
    {
        return thread;
    }

    public class MyThread extends Thread
    {
        private SurfaceHolder holder;
        private boolean running = false;

        private GameController gameController;

        public MyThread(SurfaceHolder holder, GameController gameController)
        {
            this.holder = holder;
            this.gameController = gameController;
        }

        @Override
        public void run()
        {
            Canvas canvas = null;
            while (running)
            {
                gameController.update(); //update the time between last update() call and now
                try
                {
                    canvas = holder.lockCanvas(null);
                    synchronized (holder)
                    {
                        postInvalidate();
                    }
                }
                finally
                {
                    if (canvas != null)
                    {
                        holder.unlockCanvasAndPost(canvas);
                    }
                }
            }

        }

        public void setRunning(boolean b)
        {
            running = b;
        }
    }
}

The surface itself doesn't really want to answer what its width and height is, therefore it's easier if we get its measurements from the outside, and provide it for our custom View from the outside:

//Fragment onCreateView()
    Display display = getActivity().getWindowManager().getDefaultDisplay();
    RelativeLayout rl = (RelativeLayout)view.findViewById(R.id.gameplay_container);
    rl.addView(new GameSurfaceView(this.getActivity().getApplicationContext(), gameController, display.getWidth(), display.getHeight()));
    return view;
}

Now we are able to draw onto the SurfaceView from our own Thread, and we are also able to seperate all game-related logic in a GameController. The GameController is responsible for input handling and game event updates, as per the following:

    public void update()
    {
        long delta = getTimeManager().getDeltaTime(System.currentTimeMillis());
        long timeChunk;
        if (isGameOver == false)
        {
            for (long i = 0; i < delta; i += 20)
            {
                long iNext = i + 20;
                if (iNext > delta)
                {
                    timeChunk = delta - i;
                }
                else
                {
                    timeChunk = iNext - i;
                }
                // ...update game entities based on the miliseconds provided in timeChunk
    }

And to get the delta amount of time (the time that passed by since the previous update) is the following:

    long temp = currentTimeMillis - oldTime;
    this.oldTime = currentTimeMillis;
    return temp;

I hope that will be helpful to you later :)