1190 votes

Android "Seul le fil d'origine qui a créé une hiérarchie de vues peut toucher ses vues."

J'ai construit un simple lecteur de musique dans Android. La vue pour chaque chanson contient une SeekBar, implémentée comme ceci :

public class Song extends Activity implements OnClickListener,Runnable {
    private SeekBar progress;
    private MediaPlayer mp;

    // ...

    private ServiceConnection onService = new ServiceConnection() {
          public void onServiceConnected(ComponentName className,
            IBinder rawBinder) {
              appService = ((MPService.LocalBinder)rawBinder).getService(); // service that handles the MediaPlayer
              progress.setVisibility(SeekBar.VISIBLE);
              progress.setProgress(0);
              mp = appService.getMP();
              appService.playSong(title);
              progress.setMax(mp.getDuration());
              new Thread(Song.this).start();
          }
          public void onServiceDisconnected(ComponentName classname) {
              appService = null;
          }
    };

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.song);

        // ...

        progress = (SeekBar) findViewById(R.id.progress);

        // ...
    }

    public void run() {
    int pos = 0;
    int total = mp.getDuration();
    while (mp != null && pos<total) {
        try {
            Thread.sleep(1000);
            pos = appService.getSongPosition();
        } catch (InterruptedException e) {
            return;
        } catch (Exception e) {
            return;
        }
        progress.setProgress(pos);
    }
}

Cela fonctionne bien. Maintenant, je veux un timer qui compte les secondes/minutes de la progression de la chanson. Je mets donc un TextView dans la mise en page, obtenez-le avec findViewById() en onCreate() et mettez ça dans run() après progress.setProgress(pos) :

String time = String.format("%d:%d",
            TimeUnit.MILLISECONDS.toMinutes(pos),
            TimeUnit.MILLISECONDS.toSeconds(pos),
            TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(
                    pos))
            );
currentTime.setText(time);  // currentTime = (TextView) findViewById(R.id.current_time);

Mais cette dernière ligne me donne l'exception :

Android.view.ViewRoot$CalledFromWrongThreadException : Seul le thread d'origine qui a créé une hiérarchie de vues peut toucher ses vues.

Pourtant, je fais essentiellement la même chose ici que je fais avec les SeekBar - créer la vue dans onCreate puis en le touchant dans run() - et ça ne me donne pas cette plainte.

2280voto

providence Points 6647

Vous devez déplacer la partie de la tâche d'arrière-plan qui met à jour l'interface utilisateur sur le thread principal. Il existe un simple morceau de code pour cela :

runOnUiThread(new Runnable() {

    @Override
    public void run() {

        // Stuff that updates the UI

    }
});

Documentation pour Activity.runOnUiThread .

Il suffit de l'imbriquer dans la méthode qui s'exécute en arrière-plan, puis de copier-coller le code qui implémente les mises à jour au milieu du bloc. N'incluez que la plus petite quantité de code possible, sinon vous allez à l'encontre de l'objectif du thread d'arrière-plan.

5 votes

A fonctionné comme un charme. Pour moi, le seul problème est que je voulais faire une error.setText(res.toString()); à l'intérieur de la méthode run(), mais je n'ai pas pu utiliser la res parce qu'elle n'était pas finale dommage.

78 votes

Un bref commentaire à ce sujet. J'avais un thread séparé qui essayait de modifier l'interface utilisateur, et le code ci-dessus fonctionnait, mais je devais appeler runOnUiThread depuis l'objet Activity. Je devais faire quelque chose comme myActivityObject.runOnUiThread(etc)

1 votes

@Kirby Merci pour cette référence. Vous pouvez simplement faire 'MainActivity.this' et cela devrait fonctionner aussi bien, vous n'avez pas besoin de garder la référence à votre classe d'activité.

158voto

Günay Gültekin Points 425

J'ai résolu ce problème en mettant runOnUiThread( new Runnable(){ .. à l'intérieur de run() :

thread = new Thread(){
        @Override
        public void run() {
            try {
                synchronized (this) {
                    wait(5000);

                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            dbloadingInfo.setVisibility(View.VISIBLE);
                            bar.setVisibility(View.INVISIBLE);
                            loadingText.setVisibility(View.INVISIBLE);
                        }
                    });

                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Intent mainActivity = new Intent(getApplicationContext(),MainActivity.class);
            startActivity(mainActivity);
        };
    };  
    thread.start();

2 votes

Celui-là a été génial. Merci pour cette information, cela peut aussi être utilisé dans n'importe quel autre fil.

0 votes

Merci, c'est vraiment triste de créer un fil de discussion pour revenir au fil de l'interface utilisateur mais seule cette solution a sauvé mon cas.

2 votes

Un aspect important est que wait(5000); n'est pas à l'intérieur du Runnable, sinon votre interface se figera pendant la période d'attente. Vous devriez envisager d'utiliser AsyncTask au lieu de Thread pour des opérations comme celles-ci.

87voto

Angelo Angeles Points 38

Ma solution à ce problème :

private void setText(final TextView text,final String value){
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            text.setText(value);
        }
    });
}

Appelez cette méthode sur un thread d'arrière-plan.

0 votes

Erreur :(73, 67) error : non-static method set(String) cannot be referenced from a static context

1 votes

J'ai le même problème avec mes classes de test. Cela a marché comme un charme pour moi. Cependant, en remplaçant runOnUiThread con runTestOnUiThread . Merci

34voto

bigstones Points 9636

En général, toute action impliquant l'interface utilisateur doit être effectuée dans le fil d'exécution principal ou UI, c'est-à-dire celui dans lequel l'interface utilisateur est utilisée. onCreate() et le traitement des événements sont exécutés. Une façon d'en être sûr est d'utiliser runOnUiThread() une autre consiste à utiliser des gestionnaires.

ProgressBar.setProgress() a un mécanisme pour lequel il s'exécutera toujours sur le thread principal, c'est pourquoi il a fonctionné.

Voir L'épilation sans douleur .

0 votes

L'article sur l'enfilage indolore figurant sur ce lien est maintenant un 404. Voici un lien vers un article de blog (plus ancien ?) sur l'enfilage sans-douleur. Android-developers.blogspot.com/2009/05/painless-threading.html

22voto

Jonathan ''FR'' Points 152

J'ai été dans cette situation, mais j'ai trouvé une solution avec l'objet Handler.

Dans mon cas, je veux mettre à jour un ProgressDialog avec l'information suivante Schéma d'observation . Ma vue implémente un observateur et surcharge la méthode de mise à jour.

Ainsi, mon thread principal crée la vue et un autre thread appelle la méthode de mise à jour qui met à jour la ProgressDialop et.... :

Seul le thread original qui a créé une hiérarchie de vues peut toucher sa vues.

Il est possible de résoudre le problème avec l'objet Handler.

Ci-dessous, différentes parties de mon code :

public class ViewExecution extends Activity implements Observer{

    static final int PROGRESS_DIALOG = 0;
    ProgressDialog progressDialog;
    int currentNumber;

    public void onCreate(Bundle savedInstanceState) {

        currentNumber = 0;
        final Button launchPolicyButton =  ((Button) this.findViewById(R.id.launchButton));
        launchPolicyButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                showDialog(PROGRESS_DIALOG);
            }
        });
    }

    @Override
    protected Dialog onCreateDialog(int id) {
        switch(id) {
        case PROGRESS_DIALOG:
            progressDialog = new ProgressDialog(this);
            progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
            progressDialog.setMessage("Loading");
            progressDialog.setCancelable(true);
            return progressDialog;
        default:
            return null;
        }
    }

    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
        switch(id) {
        case PROGRESS_DIALOG:
            progressDialog.setProgress(0);
        }

    }

    // Define the Handler that receives messages from the thread and update the progress
    final Handler handler = new Handler() {
        public void handleMessage(Message msg) {
            int current = msg.arg1;
            progressDialog.setProgress(current);
            if (current >= 100){
                removeDialog (PROGRESS_DIALOG);
            }
        }
    };

    // The method called by the observer (the second thread)
    @Override
    public void update(Observable obs, Object arg1) {

        Message msg = handler.obtainMessage();
        msg.arg1 = ++currentPluginNumber;
        handler.sendMessage(msg);
    }
}

Cette explication peut être trouvée sur cette page et vous devez lire l'"Exemple de ProgressDialog avec un deuxième thread".

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X