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

941

J'ai construit un simple lecteur de musique 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 comptant les secondes / minutes de la progression de la chanson. Donc , je mets un TextViewdans la mise en page, obtenir avec findViewById()dans onCreate()et mettre cela en 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 le SeekBar- créer la vue dans onCreate, puis la toucher run()- et cela ne me donne pas cette plainte.

Herp Derp
la source

Réponses:

1897

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 code pour cela:

runOnUiThread(new Runnable() {

    @Override
    public void run() {

        // Stuff that updates the UI

    }
});

Documentation pour Activity.runOnUiThread.

Il suffit d'imbriquer cela 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 commencez à contourner l'objectif du thread d'arrière-plan.

Providence
la source
5
travaillé comme un charme. pour moi, le seul problème ici est que je voulais faire une error.setText(res.toString());méthode à l'intérieur de la méthode run (), mais je ne pouvais pas utiliser le res car ce n'était pas final .. dommage
noloman
64
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 j'avais appelé runOnUiThread à partir de l'objet Activity. Je devais faire quelque chose comme myActivityObject.runOnUiThread(etc)
Kirby
1
@Kirby Merci pour cette référence. Vous pouvez simplement faire 'MainActivity.this' et cela devrait également fonctionner afin que vous n'ayez pas à garder de référence à votre classe d'activité.
JRomero
24
Il m'a fallu un certain temps pour comprendre que runOnUiThread()c'était une méthode d'activité. J'exécutais mon code dans un fragment. J'ai fini par faire getActivity().runOnUiThread(etc)et ça a marché. Fantastique!;
lejonl
Pouvons-nous arrêter l'exécution de la tâche qui est écrite dans le corps de la méthode 'runOnUiThread'?
Karan Sharma
143

J'ai résolu cela en mettant à l' runOnUiThread( new Runnable(){ ..intérieur 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();
Günay Gültekin
la source
2
Celui-ci a basculé. Merci pour les informations qui peuvent également être utilisées dans n'importe quel autre fil.
Nabin
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.
Pierre Maoui
2
Un aspect important est qu'il wait(5000);ne se trouve pas dans Runnable, sinon votre interface utilisateur se figera pendant la période d'attente. Vous devriez envisager d'utiliser à la AsyncTaskplace de Thread pour des opérations comme celles-ci.
Martin
c'est si mauvais pour une fuite de mémoire
Rafael Lima
Pourquoi s'embêter avec le bloc synchronisé? Le code qu'il contient semble raisonnablement sûr pour les threads (bien que je sois tout à fait prêt à manger mes mots).
David
69

Ma solution à cela:

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.

Angelo Angeles
la source
Erreur: (73, 67) erreur: l'ensemble de méthodes non statique (chaîne) ne peut pas être référencé à partir d'un contexte statique
1
J'ai le même problème avec mes classes de test. Cela a fonctionné comme un charme pour moi. Cependant, en remplaçant runOnUiThreadpar runTestOnUiThread. Merci
DaddyMoe
28

Habituellement, toute action impliquant l'interface utilisateur doit être effectuée dans le thread principal ou d'interface utilisateur, c'est-à-dire celui dans lequel la onCreate()gestion des événements est exécutée. Une façon de s'en assurer est d'utiliser runOnUiThread () , une autre utilise des gestionnaires.

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

Voir Filetage sans douleur .

grosses pierres
la source
L'article sur le filetage indolore sur ce lien est maintenant un 404. Voici un lien vers un article de blog (plus ancien?) Sur le filetage indolore - android-developers.blogspot.com/2009/05/pireless-threading.html
Tony Adams
20

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

Dans mon cas, je veux mettre à jour un ProgressDialog avec le modèle d'observateur . Ma vue implémente l'observateur et remplace la méthode de mise à jour.

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

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

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

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 "Exemple ProgressDialog avec un deuxième fil".

Jonathan
la source
10

Vous pouvez utiliser le gestionnaire 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);
                                                        }
                                                    });
Bilal Mustafa
la source
7

Je vois que vous avez accepté la réponse de @ providence. Au cas où, vous pouvez également utiliser le gestionnaire! Tout d'abord, faites 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;
            }
            }
        }
    };

Faites une méthode.

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

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

showHandler(true);
David Dimalanta
la source
7

J'ai eu un problème similaire, et ma solution est moche, 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
}
Błażej
la source
2
@ R.jzadeh c'est agréable d'entendre ça. Depuis le moment où j'ai écrit cette réponse, vous pouvez probablement le faire mieux maintenant :)
Błażej
6

J'utilise Handleravec 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);
Sankar Behera
la source
5

Utilisez ce code, et pas besoin de runOnUiThreadfonctionner:

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();
}
Hamid
la source
5

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

Entrez la description de l'image ici

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

Uddhav Gautam
la source
4

Cela est arrivé à mon quand j'ai appelé pour un changement d'interface utilisateur à partir d'un doInBackgroundau Asynctasklieu d'utiliseronPostExecute .

Gérer l'interface utilisateur a onPostExecuterésolu mon problème.

Jonathan dos Santos
la source
1
Merci Jonathan. C'était aussi mon problème mais j'ai dû faire un peu plus de lecture pour comprendre ce que vous vouliez dire ici. Pour quelqu'un d'autre, onPostExecutec'est aussi une méthode de AsyncTaskmais elle s'exécute sur le thread d'interface utilisateur. Voir ici: blog.teamtreehouse.com/all-about-android-asynctasks
ciaranodc
4

Les coroutines Kotlin peuvent rendre votre code plus concis et lisible comme ceci:

MainScope().launch {
    withContext(Dispatchers.Default) {
        //TODO("Background processing...")
    }
    TODO("Update UI here!")
}

Ou vice versa:

GlobalScope.launch {
    //TODO("Background processing...")
    withContext(Dispatchers.Main) {
        // TODO("Update UI here!")
    }
    TODO("Continue background processing...")
}
KenIchi
la source
3

Je travaillais avec une classe qui ne contenait aucune référence au contexte. Il ne m'a donc pas été possible d'utiliser ce que runOnUIThread();j'ai utilisé view.post();et cela 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);
Ifta
la source
Quelle est l'analogie avec audioMessageet tvPlayDurationau code des questions?
gotwo
audioMessageest un objet support de la vue texte. tvPlayDurationest la vue texte que nous voulons mettre à jour à partir d'un thread non UI. Dans la question ci-dessus, currentTimeest la vue texte mais elle n'a pas d'objet support.
Ifta
3

Lors de l'utilisation de AsyncTask Mettre à jour l'interface utilisateur dans la méthode onPostExecute

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

     }
Deepak Kataria
la source
cela m'est arrivé. je mettais à jour l'interface utilisateur dans doinbackground de la tâche asynk.
mehmoodnisar125
3

J'étais confronté à un problème similaire et aucune des méthodes mentionnées ci-dessus ne fonctionnait pour moi. En fin de compte, cela a fait l'affaire pour moi:

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

J'ai trouvé ce bijou ici .

Hagbard
la source
2

Ceci 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, vous découvrez

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 et essayons plus tard 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.

Amit Yadav
la source
2

Si vous ne souhaitez pas utiliser l' runOnUiThreadAPI, vous pouvez en fait l'implémenter AsynTaskpour les opérations qui prennent quelques secondes à terminer. Mais dans ce cas, également après avoir traité votre travail doinBackground(), vous devez renvoyer la vue terminée dans onPostExecute(). L'implémentation Android permet uniquement au thread d'interface utilisateur principal d'interagir avec les vues.

Sam
la source
2

Si vous souhaitez simplement invalider (appeler la fonction repeindre / redessiner) à 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: what-does-postinvalidate-do

Nalin
la source
1

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

lecteur d'esprit
la source
1

Dans mon cas, j'ai EditText dans l'adaptateur, et c'est déjà dans le thread d'interface utilisateur. Cependant, lorsque cette activité se charge, elle se bloque avec cette erreur.

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

Sruit A.Suk
la source
1

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

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)
    }
Tarun Kumar
la source
0

Résolu: Mettez simplement cette méthode dans la classe doInBackround ... et passez 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);

    }
Kaushal Sachan
la source
0

Dans mon cas, l'appelant appelle trop de fois en peu de temps obtiendra cette erreur, j'ai simplement mis la vérification du temps écoulé 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
    }
Fruit
la source
La meilleure solution serait de désactiver le bouton au clic et de le réactiver à la fin de l'action.
lsrom
@lsrom Dans mon cas, ce n'est pas si simple car l'appelant est une bibliothèque tierce interne et hors de mon contrôle.
Fruit
0

Si vous ne parvenez pas à trouver un UIThread, vous pouvez utiliser cette méthode.

votre contexte actuel signifie que vous devez analyser le contexte actuel

 new Thread(new Runnable() {
        public void run() {
            while (true) {
                (Activity) yourcurrentcontext).runOnUiThread(new Runnable() {
                    public void run() { 
                        Log.d("Thread Log","I am from UI Thread");
                    }
                });
                try {
                    Thread.sleep(1000);
                } catch (Exception ex) {

                }
            }
        }
    }).start();
Udara Kasun
la source
0

Kotlin Answer

Nous devons utiliser UI Thread pour le travail avec vrai chemin. Nous pouvons utiliser UI Thread dans Kotlin:

runOnUiThread(Runnable {
   //TODO: Your job is here..!
})

@canerkaseler

canerkaseler
la source
0

Dans Kotlin, mettez simplement votre code dans la méthode d'activité runOnUiThread

runOnUiThread{
    // write your code here, for example
    val task = Runnable {
            Handler().postDelayed({
                var smzHtcList = mDb?.smzHtcReferralDao()?.getAll()
                tv_showSmzHtcList.text = smzHtcList.toString()
            }, 10)

        }
    mDbWorkerThread.postTask(task)
}
Raheel Khan
la source