web-dev-qa-db-fra.com

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

J'ai construit un lecteur de musique simple sous Android. La vue de chaque chanson contient un SeekBar, implémenté 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 une minuterie qui compte les secondes/minutes de la progression de la chanson. Donc, je mets une TextView dans la mise en page, le récupère avec findViewById() dans onCreate() et le place 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 fondamentalement la même chose ici que je fais avec SeekBar - créer la vue dans onCreate, puis la toucher dans run() - et cela ne me donne pas cette plainte.

782
herpderp

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

runOnUiThread(new Runnable() {

    @Override
    public void run() {

        // Stuff that updates the UI

    }
});

Documentation pour Activity.runOnUiThread .

Emboîtez-le simplement dans la méthode qui s'exécute en arrière-plan, puis copiez-collez le code qui implémente les mises à jour au milieu du bloc. Incluez seulement la plus petite quantité de code possible, sinon vous commencerez à vaincre le but du thread d'arrière-plan.

1646
providence

J'ai résolu ceci en plaçant runOnUiThread( new Runnable(){ .. dans 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();
121
Günay Gültekin

Ma solution à ceci:

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.

49
Angelo Angeles

En règle générale, toute action impliquant l'interface utilisateur doit être effectuée dans le thread principal ou le thread de l'interface utilisateur, c'est-à-dire celui dans lequel onCreate() et la gestion des événements sont exécutés. Une façon de s'en assurer consiste à utiliser runOnUiThread () , une autre consiste à utiliser des gestionnaires.

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

Voir Filetage indolore .

23
bigstones

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

Dans mon cas, je souhaite mettre à jour un ProgressDialog avec le modèle observer . Mon affichage implémente observ et remplace la méthode update.

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

Seul le thread d'origine 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 se trouve sur cette page , et vous devez lire le "Exemple ProgressDialog avec un deuxième thread".

19
Jonathan

Je vois que vous avez accepté la réponse de @ providence. Juste au cas où, vous pouvez également utiliser le gestionnaire aussi! Commençons par les champs int.

    private static final int SHOW_LOG = 1;
    private static final int HIDE_LOG = 0;

Ensuite, créez une instance de gestionnaire en tant que champ.

    //TODO __________[ Handler ]__________
    @SuppressLint("HandlerLeak")
    protected Handler handler = new Handler()
    {
        @Override
        public void handleMessage(Message msg)
        {
            // Put code here...

            // Set a switch statement to toggle it on or off.
            switch(msg.what)
            {
            case SHOW_LOG:
            {
                ads.setVisibility(View.VISIBLE);
                break;
            }
            case HIDE_LOG:
            {
                ads.setVisibility(View.GONE);
                break;
            }
            }
        }
    };

Faire une méthode.

//TODO __________[ Callbacks ]__________
@Override
public void showHandler(boolean show)
{
    handler.sendEmptyMessage(show ? SHOW_LOG : HIDE_LOG);
}

Enfin, mettez ceci à la méthode onCreate().

showHandler(true);
6
David Dimalanta

J'ai eu un problème similaire, et ma solution est laide, mais cela fonctionne:

void showCode() {
    hideRegisterMessage(); // Hides view 
    final Handler handler = new Handler();
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            showRegisterMessage(); // Shows view
        }
    }, 3000); // After 3 seconds
}
6
Błażej

J'utilise Handler avec Looper.getMainLooper(). Cela a bien fonctionné pour moi.

    Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
              // Any UI task, example
              textView.setText("your text");
        }
    };
    handler.sendEmptyMessage(1);
5
Sankar Behera

Vous pouvez utiliser Handler pour supprimer la vue sans perturber le thread d'interface utilisateur principal . Voici un exemple de code

new Handler(Looper.getMainLooper()).post(new Runnable() {
                                                        @Override
                                                        public void run() {
                                                           //do stuff like remove view etc
                                                            adapter.remove(selecteditem);
                                                        }
                                                    });
5
Bilal Mustafa

Cela jette explicitement une erreur. Il est indiqué quel que soit le fil créé une vue, seul celui qui peut toucher ses vues. C'est parce que la vue créée se trouve dans l'espace de ce fil. La création de vue (GUI) se produit dans le fil de l'interface utilisateur (principal). Ainsi, vous utilisez toujours le thread d'interface utilisateur pour accéder à ces méthodes. 

 Enter image description here

Dans l'image ci-dessus, la variable de progression se trouve dans l'espace du thread d'interface utilisateur. Ainsi, seul le thread d'interface utilisateur peut accéder à cette variable. Ici, vous accédez au progrès via new Thread (), et c’est pourquoi vous avez une erreur. 

4
Uddhav Gautam

Utilisez ce code, et nul besoin de la fonction runOnUiThread:

private Handler handler;
private Runnable handlerTask;

void StartTimer(){
    handler = new Handler();   
    handlerTask = new Runnable()
    {
        @Override 
        public void run() { 
            // do something  
            textView.setText("some text");
            handler.postDelayed(handlerTask, 1000);    
        }
    };
    handlerTask.run();
}
3
Hamid

Lorsque vous utilisez AsyncTask Mettez à jour l'interface utilisateur dans la méthode onPostExecute

    @Override
    protected void onPostExecute(String s) {
   // Update UI here

     }
2
Deepak Kataria

C’est ce qui m’est arrivé lorsque j’ai appelé pour que l’interface utilisateur change de doInBackground à partir de Asynctask au lieu de onPostExecute.

Le traitement de l'interface utilisateur dans onPostExecute a résolu mon problème.

2
Jonathan

Si vous ne souhaitez pas utiliser l'API runOnUiThread, vous pouvez en réalité implémenter AsynTask pour les opérations qui prennent quelques secondes. Mais dans ce cas, même après le traitement de votre travail dans doinBackground(), vous devez renvoyer la vue finale dans onPostExecute(). L'implémentation Android permet uniquement au fil principal de l'interface utilisateur d'interagir avec les vues.

1
Sam

C'est la trace de pile de l'exception mentionnée

        at Android.view.ViewRootImpl.checkThread(ViewRootImpl.Java:6149)
        at Android.view.ViewRootImpl.requestLayout(ViewRootImpl.Java:843)
        at Android.view.View.requestLayout(View.Java:16474)
        at Android.view.View.requestLayout(View.Java:16474)
        at Android.view.View.requestLayout(View.Java:16474)
        at Android.view.View.requestLayout(View.Java:16474)
        at Android.widget.RelativeLayout.requestLayout(RelativeLayout.Java:352)
        at Android.view.View.requestLayout(View.Java:16474)
        at Android.widget.RelativeLayout.requestLayout(RelativeLayout.Java:352)
        at Android.view.View.setFlags(View.Java:8938)
        at Android.view.View.setVisibility(View.Java:6066)

Donc, si vous allez creuser, alors vous venez de savoir

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

mThread est initialisé dans le constructeur comme ci-dessous

mThread = Thread.currentThread();

Tout ce que je veux dire, c'est que lorsque nous avons créé une vue particulière, nous l'avons créée sur le thread d'interface utilisateur, puis que nous essayons de la modifier dans un thread de travail.

Nous pouvons le vérifier via l'extrait de code ci-dessous

Thread.currentThread().getName()

lorsque nous gonflons la mise en page et plus tard où vous obtenez une exception.

1
Amit Yadav

Je travaillais avec une classe qui ne contenait pas de référence au contexte. Donc, il n’était pas possible pour moi d’utiliser runOnUIThread(); J'ai utilisé view.post(); et le problème a été résolu.

timer.scheduleAtFixedRate(new TimerTask() {

    @Override
    public void run() {
        final int currentPosition = mediaPlayer.getCurrentPosition();
        audioMessage.seekBar.setProgress(currentPosition / 1000);
        audioMessage.tvPlayDuration.post(new Runnable() {
            @Override
            public void run() {
                audioMessage.tvPlayDuration.setText(ChatDateTimeFormatter.getDuration(currentPosition));
            }
        });
    }
}, 0, 1000);
1
Ifta

Dans mon cas, l’appelant qui appelle trop souvent dans un court laps de temps obtiendra cette erreur. Je mets simplement la vérification du temps supplémentaire pour ne rien faire si trop court, par exemple. ignorer si la fonction est appelée moins de 0,5 seconde:

    private long mLastClickTime = 0;

    public boolean foo() {
        if ( (SystemClock.elapsedRealtime() - mLastClickTime) < 500) {
            return false;
        }
        mLastClickTime = SystemClock.elapsedRealtime();

        //... do ui update
    }
0
林果皞

Pour les personnes en difficulté à Kotlin, cela fonctionne comme suit:

lateinit var runnable: Runnable //global variable

 runOnUiThread { //Lambda
            runnable = Runnable {

                //do something here

                runDelayedHandler(5000)
            }
        }

        runnable.run()

 //you need to keep the handler outside the runnable body to work in kotlin
 fun runDelayedHandler(timeToWait: Long) {

        //Keep it running
        val handler = Handler()
        handler.postDelayed(runnable, timeToWait)
    }
0
Tarun Kumar

Pour moi, le problème était que j'appelais explicitement onProgressUpdate() depuis mon code. Cela ne devrait pas être fait. J'ai appelé publishProgress() à la place et cela a résolu l'erreur.

0
mindreader

Résolu: il suffit de mettre cette méthode dans la classe doInBackround ... et de transmettre le message

public void setProgressText(final String progressText){
        Handler handler = new Handler(Looper.getMainLooper()) {
            @Override
            public void handleMessage(Message msg) {
                // Any UI task, example
                progressDialog.setMessage(progressText);
            }
        };
        handler.sendEmptyMessage(1);

    }
0
Kaushal Sachan

Si vous souhaitez simplement invalider (appelez repaint/redraw function) à partir de votre thread non UI, utilisez postInvalidate ()

myView.postInvalidate();

Cela affichera une demande d'invalidation sur le thread d'interface utilisateur.

Pour plus d’informations: que-soit-après-invalidité-de-faire

0
Nalin

Je faisais face à un problème similaire et aucune des méthodes mentionnées ci-dessus ne fonctionnait pour moi. En fin de compte, cela a fait le tour pour moi:

Device.BeginInvokeOnMainThread(() =>
    {
        myMethod();
    });

J'ai trouvé ce petit bijou ici .

0
Hagbard

Dans mon cas, J'ai EditText dans Adapter et il est déjà dans le fil de l'interface utilisateur. Cependant, lorsque cette activité est chargée, elle se bloque avec cette erreur.

Ma solution est que je dois supprimer <requestFocus /> de EditText en XML.

0
Sruit A.Suk