简体   繁体   中英

How to Correctly Update Android UI from SurfaceView

So I have an activity with a TextView to hold the score like so:

public class PlayGameActivity extends Activity {
    private GameThread mGameThread;
    private GameView mGameView;

    private Handler mHandler = new Handler();

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

        final TextView scoreView = (TextView) findViewById(R.id.textView2);

        mGameView = (GameView) findViewById(R.id.gameView1);
        mGameThread = mGameView.getThread();

        mHandler.post(new Runnable() {
            @Override
            public void run() {
                if (mGameView != null) {
                    scoreView.setText(mGameThread.getScore());
                }
            }
        });
    }
}

Then in my GameView I have the function declared like so:

class GameView extends SurfaceView implements SurfaceHolder.Callback {
    class GameThread extends Thread {
        int score = 0;

        public String getScore(){
            return Integer.toString(score);
        }

        @Override
        public void run() {
            synchronized (mSurfaceHolder) {
                if (something){
                    score++
                }
            }
        }
    }
}

The problem is that mGameThread.getScore() always returns 0 as if score is not updated when it certainly is.

If anyone has any incite it is greatly appreciated. Also I have striped lots of unnecessary code out of the snippets above. If you want to see the full text you may do so here:

Your Handler runs only once getScore() so you will always just get the initial value. You should try to use postDelayed() .

mHandler.postDelayed(new Runnable() {
    @Override
    public void run() {
        if (mGameView != null) {
            scoreView.setText(mGameThread.getScore());
        }
        mHandler.postDelayed(this, 1000); // run again in 1sec
    }
}, 1000);

As Thomas mentions that Handler should not be used in such a way here is a quote from the doc :

There are two main uses for a Handler: (1) to schedule messages and runnables to be executed as some point in the future; and (2) to enqueue an action to be performed on a different thread than your own.

It fits perfectly in (1)!

There's a better way to update UI data, use event / listener mechanism.

Sitting in a looped thread is not an OOP style programming. If the change of score happens in your app than you definitely have a handle to detect the change. Once detected, update the UI with one simple setText call to your textview.

Do not overcrowd your UI event queue stack as you (maybe unknowingly) do by calling post(....), this will make your UI less responsive to actual user interactions especially if you're doing it in a timed loop. Remember that the UI event queue stack is ONE for the entire system, it is meant to handle user's interactions with the device.

If you need to change UI component from non-UI thread use

     activity.runOnUiThread(Runnable); 

method instead of post(Runnable). This way you may have a bit longer executing code (although not recommended) defined in Runnable. Otherwise make sure that the code in post(Runnable) is very quick to execute and finish.

From where else do you call mHandler.post(Runnable) ? The code you posted adds a single message (from your onCreate) to the message queue of the main thread, which does not make much sense. This message is processed, will print 0, and then that's it. So if you don't periodically call this from somewhere, it will not print anything after the initial 0.

If you're calling Handler.post(Runnable) from somewhere else as well, then make sure you call it from the correct place. It must be called from GameThread each time you want to update your UI. Therefore, you will need a reference to mHandler in GameThread. Eg the constructor of GameThread is a good place to do this (I wouldn't implement GameThread as an inner class, but to each his own). Once GameThread has a reference to mHandler , you can use mHandler.post(Runnable) to update your UI. For example:

    @Override
    public void run() {
        synchronized (mSurfaceHolder) {
            if (something){
                // IMPORTANT: see "important note" at the end of this Answer                          

                mHandler.post(mRunnable); // mRunnable is preinstantiated somewhere
                score++
            }
        }
    }

IMPORTANT NOTE : your GameThread probably uses a way to measure real time (ie it doesn't call everything in an infinite loop without any Thread.sleep(...). Therefore, "update score" messages to the Handler will not be sent excessively. If your game is turn based (and not realtime), then this problem even does not appear at all.

NOTE 2: : I checked your complete code, and it seems your game tick does not have any FPS regulation, ie you don't use eg Thread.sleep(33) or anything similar. Therefore, the solution I wrote above will send very many messages to your UI if your pirate collides (and your score will reach a very high value in a very short time -- I doubt that is what you want ). In fact, your score value will probably overflow very soon, unless the collision ends very very fast. So I suggest that you add FPS balancing to your code, as all games do.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM