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.
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.
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();
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.
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 .
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".
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);
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
}
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);
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);
}
});
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.
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.
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();
}
Lorsque vous utilisez AsyncTask Mettez à jour l'interface utilisateur dans la méthode onPostExecute
@Override
protected void onPostExecute(String s) {
// Update UI here
}
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.
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.
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.");
}
}
Où 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.
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);
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
}
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)
}
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.
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);
}
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
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 .
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.