Android "Только исходный поток, создавший иерархию представлений, может касаться его представлений".
Я построил простой музыкальный проигрыватель в Android. Представление для каждой песни содержит SeekBar, реализованный следующим образом:
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);
}
}
Это отлично работает. Теперь я хочу таймер, считающий секунды / минуты хода песни. Поэтому я положил TextView
в макете, получить его с findViewById()
в onCreate()
и положить это в run()
после 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);
Но эта последняя строка дает мне исключение:
android.view.ViewRoot $ CalledFromWrongThreadException: только исходный поток, создавший иерархию представлений, может касаться его представлений.
Все же я делаю в основном то же самое здесь, что я делаю с SeekBar
- создание представления в onCreate
затем прикоснуться к run()
- и это не дает мне эту жалобу.
34 ответа
Вы должны переместить часть фоновой задачи, которая обновляет пользовательский интерфейс, в основной поток. Для этого есть простой фрагмент кода:
runOnUiThread(new Runnable() {
@Override
public void run() {
// Stuff that updates the UI
}
});
Документация для Activity.runOnUiThread
,
Просто вложите это в метод, работающий в фоновом режиме, а затем скопируйте и вставьте код, который реализует любые обновления в середине блока. Включите только наименьшее возможное количество кода, в противном случае вы начнете побеждать назначение фонового потока.
Я решил это, поставив runOnUiThread( new Runnable(){ ..
внутри 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();
Мое решение этого:
private void setText(final TextView text,final String value){
runOnUiThread(new Runnable() {
@Override
public void run() {
text.setText(value);
}
});
}
Вызовите этот метод в фоновом потоке.
Сопрограммы Kotlin могут сделать ваш код более лаконичным и читаемым следующим образом:
MainScope().launch {
withContext(Dispatchers.Default) {
//TODO("Background processing...")
}
TODO("Update UI here!")
}
Или наоборот:
GlobalScope.launch {
//TODO("Background processing...")
withContext(Dispatchers.Main) {
// TODO("Update UI here!")
}
TODO("Continue background processing...")
}
Обычно любое действие с пользовательским интерфейсом должно выполняться в основном потоке или потоке пользовательского интерфейса, то есть в том, в котором onCreate()
и обработка событий выполняются. Один из способов убедиться в этом - использовать runOnUiThread (), другой - использовать обработчики.
ProgressBar.setProgress()
есть механизм, для которого он всегда будет выполняться в основном потоке, поэтому он работал.
См. Безболезненная нить.
Вы можете использовать Handler для удаления вида, не нарушая основной поток пользовательского интерфейса. Вот пример кода
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
//do stuff like remove view etc
adapter.remove(selecteditem);
}
});
Котлин Ответ
Мы должны использовать UI Thread для правильной работы. Мы можем использовать UI Thread в Kotlin:
runOnUiThread(Runnable {
//TODO: Your job is here..!
})
@canerkaseler
Я был в этой ситуации, но я нашел решение с помощью объекта Handler.
В моем случае я хочу обновить ProgressDialog с помощью шаблона наблюдателя. Мой взгляд реализует обозреватель и переопределяет метод обновления.
Итак, мой основной поток создает представление, а другой поток вызывает метод update, который обновляет ProgressDialop и....:
Только оригинальный поток, создавший иерархию представлений, может касаться его представлений.
Возможно решить проблему с Объектом Обработчика.
Ниже приведены различные части моего кода:
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);
}
}
Это объяснение можно найти на этой странице, и вы должны прочитать "Пример ProgressDialog со вторым потоком".
Я вижу, что вы приняли ответ @ провидения. На всякий случай, вы также можете использовать обработчик тоже! Сначала сделайте поля int.
private static final int SHOW_LOG = 1;
private static final int HIDE_LOG = 0;
Затем создайте экземпляр обработчика как поле.
//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;
}
}
}
};
Сделай метод.
//TODO __________[ Callbacks ]__________
@Override
public void showHandler(boolean show)
{
handler.sendEmptyMessage(show ? SHOW_LOG : HIDE_LOG);
}
Наконец, поместите это в onCreate()
метод.
showHandler(true);
Используйте этот код, и нет необходимости 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();
}
Я столкнулся с подобной проблемой, и ни один из методов, упомянутых выше, не помог мне. В конце концов, это помогло мне:
Device.BeginInvokeOnMainThread(() =>
{
myMethod();
});
Я нашел этот драгоценный камень здесь.
У меня была похожая проблема, и мое решение некрасиво, но оно работает:
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
}
Я использую Handler
с Looper.getMainLooper()
, Это работало нормально для меня.
Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
// Any UI task, example
textView.setText("your text");
}
};
handler.sendEmptyMessage(1);
Это явно выдает ошибку. Он говорит, какой поток создал вид, только тот, который может касаться его. Это потому, что созданный вид находится внутри пространства этого потока. Создание представления (GUI) происходит в потоке пользовательского интерфейса (основного). Таким образом, вы всегда используете поток пользовательского интерфейса для доступа к этим методам.
На рисунке выше переменная прогресса находится внутри пространства потока пользовательского интерфейса. Таким образом, только поток пользовательского интерфейса может получить доступ к этой переменной. Здесь вы получаете доступ к прогрессу через новую Thread(), и именно поэтому вы получили ошибку.
Для однострочной версии
runOnUiThread()
подход, вы можете использовать лямбда-функцию, то есть:
runOnUiThread(() -> doStuff(Object, myValue));
где
doStuff()
can представляет некоторый метод, используемый для изменения значения некоторого объекта пользовательского интерфейса (установка текста, изменение цветов и т. д.).
Я считаю, что это намного удобнее при попытке обновить несколько объектов пользовательского интерфейса без необходимости в 6-строчном определении Runnable для каждого, как указано в , получившем ответенаибольшее количество голосов , что ни в коем случае не является неправильным, оно просто занимает намного больше места, и я быть менее читаемым.
Итак, это:
runOnUiThread(new Runnable() {
@Override
public void run() {
doStuff(myTextView, "myNewText");
}
});
может стать таким:
runOnUiThread(() -> doStuff(myTextView, "myNewText"));
где определение doStuff лежит в другом месте.
Или, если вам не нужно быть настолько универсальным, и вам просто нужно установить текст объекта TextView:
runOnUiThread(() -> myTextView.setText("myNewText"));
Это случилось с моим, когда я призвал к изменению интерфейса от doInBackground
от Asynctask
Вместо того, чтобы использовать onPostExecute
,
Работа с пользовательским интерфейсом в onPostExecute
решил мою проблему.
Для тех, кто использует фрагмент:
(context as Activity).runOnUiThread {
//TODO
}
Я работал с классом, который не содержал ссылку на контекст. Так что я не смог использовать runOnUIThread();
я использовал view.post();
и это было решено.
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);
При использовании AsyncTask обновите интерфейс в методе onPostExecute
@Override
protected void onPostExecute(String s) {
// Update UI here
}
Если вы не хотите использовать runOnUiThread
API, вы можете на самом деле реализовать AsynTask
для операций, которые занимают несколько секунд. Но в этом случае, также после обработки вашей работы в doinBackground()
, вам нужно вернуть готовый вид в onPostExecute()
, Реализация Android позволяет только основным потокам пользовательского интерфейса взаимодействовать с представлениями.
Это трассировка стека упомянутого исключения
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)
Так что, если вы идете копать, то вы узнаете,
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
Где mThread инициализируется в конструкторе, как показано ниже
mThread = Thread.currentThread();
Все, что я хочу сказать, это то, что когда мы создавали конкретное представление, мы создавали его в потоке пользовательского интерфейса, а затем пытались изменить его в рабочем потоке.
Мы можем проверить это через приведенный ниже фрагмент кода
Thread.currentThread().getName()
когда мы раздуваем макет и позже, где вы получаете исключение.
Если вы используете Kotlin Coroutine, попробуйте этот код:
GlobalScope.launch {
//Write a background operation
withContext(Dispatchers.Main) {
// write a UI operation
}
`enter code here`}
Если вы просто хотите сделать недействительной (вызвать функцию перерисовки / перерисовки) из вашей ветки, не связанной с пользовательским интерфейсом, используйте postInvalidate()
myView.postInvalidate();
Это опубликует недействительный запрос в UI-потоке.
Для получения дополнительной информации: что-делает-postinvalidate-do
Что ж, вы можете сделать это вот так.
https://developer.android.com/reference/android/view/View#post(java.lang.Runnable)
Простой подход
currentTime.post(new Runnable(){
@Override
public void run() {
currentTime.setText(time);
}
}
это также обеспечивает задержку
https://developer.android.com/reference/android/view/View#postDelayed(java.lang.Runnable,%20long)
Для людей, борющихся в Котлине, это работает так:
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)
}
В моем случае у меня есть EditText
в адаптере, и это уже в потоке пользовательского интерфейса. Однако при загрузке этого действия происходит сбой с этой ошибкой.
Мое решение мне нужно удалить <requestFocus />
из EditText в XML.
Для меня проблема была в том, что я звонил onProgressUpdate()
явно из моего кода. Это не должно быть сделано. я звонил publishProgress()
вместо этого и это решило ошибку.
Если вы не смогли найти UIThread, вы можете использовать этот способ.
yourcurrentcontext означает, что вам нужно проанализировать текущий контекст
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();
В Kotlin просто поместите свой код в метод активности 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)
}
Если вы находитесь внутри фрагмента, вам также необходимо получить объект действия, поскольку runOnUIThread - это метод действия.
Пример на Kotlin с некоторым окружающим контекстом, чтобы было понятнее - в этом примере выполняется переход от фрагмента камеры к фрагменту галереи:
// Setup image capture listener which is triggered after photo has been taken
imageCapture.takePicture(
outputOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
Log.d(TAG, "Photo capture succeeded: $savedUri")
//Do whatever work you do when image is saved
//Now ask navigator to move to new tab - as this
//updates UI do on the UI thread
activity?.runOnUiThread( {
Navigation.findNavController(
requireActivity(), R.id.fragment_container
).navigate(CameraFragmentDirections
.actionCameraToGallery(outputDirectory.absolutePath))
})