Как получить доступ и изменить содержимое панели действий из другого потока - PullRequest
0 голосов
/ 07 ноября 2018

Я провожу некоторые эксперименты, чтобы получить опыт работы с потоками и задачами в разработке под Android с использованием последней версии Android Studio на Windows 10 Pro x64. Я написал две версии очень простого приложения с двумя кнопками, чтобы показать, как важно использовать отдельные потоки для очень тяжелых работ, чтобы приложение не зависало: одной кнопкой я могу изменить цвет фона макета, а другой кнопкой. Я запускаю счетчик от 0 до 10000000, показывающий добавочное значение на панели действий. Этот счетчик может задерживать отзывчивость приложения на несколько секунд, поэтому при его запуске и до его окончания невозможно изменить цвет фона. Это тот случай, когда хорошей идеей является создание отдельной задачи для этой тяжелой работы.

Две версии приложения различаются, потому что у первой нет отдельной задачи для счетчика (так что во время подсчета можно испытать отсутствие реакции приложения), а у второй -

Это исходный код первой версии:

package com.example.testasync;

import android.graphics.Color;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;

public class MainActivity extends AppCompatActivity {

    ActionBar actionBar;
    int flagBackgroundColor = 0;
    LinearLayout linearLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        actionBar = getSupportActionBar();
        actionBar.setTitle("Counter: 0");
        linearLayout = new LinearLayout(this);
        linearLayout.setOrientation(LinearLayout.VERTICAL);
        Button btnStartCounter = new Button(this);
        Button btnChangeBackgroundColor = new Button(this);
        btnStartCounter.setText("Start counter");
        btnChangeBackgroundColor.setText("Change background color");

        btnChangeBackgroundColor.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                switch (flagBackgroundColor) {
                    case 0:
                        linearLayout.setBackgroundColor(Color.BLACK);
                        flagBackgroundColor++;
                        break;
                    case 1:
                        linearLayout.setBackgroundColor(Color.RED);
                        flagBackgroundColor++;
                        break;
                    case 2:
                        linearLayout.setBackgroundColor(Color.GREEN);
                        flagBackgroundColor++;
                        break;
                    case 3:
                        linearLayout.setBackgroundColor(Color.GRAY);
                        flagBackgroundColor++;
                        break;
                    case 4:
                        linearLayout.setBackgroundColor(Color.WHITE);
                        flagBackgroundColor = 0;
                        break;
                }
            }
        });

        btnStartCounter.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int i;
                actionBar.setTitle("Counter: 0");
                for(i=0; i<10_000_000; i++) {
                    actionBar.setTitle("Counter: " + i);
                }
            }
        });

        linearLayout.addView(btnStartCounter);
        linearLayout.addView(btnChangeBackgroundColor);
        setContentView(linearLayout);
    }
}

Второй исходный код, где я использую отдельную задачу для инкрементного цикла for, выглядит так:

package com.example.testasync;

import android.graphics.Color;
import android.os.AsyncTask;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;

public class MainActivity extends AppCompatActivity {

    ActionBar actionBar;
    int flagBackgroundColor = 0;
    LinearLayout linearLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        actionBar = getSupportActionBar();
        actionBar.setTitle("Counter: 0");
        linearLayout = new LinearLayout(this);
        linearLayout.setOrientation(LinearLayout.VERTICAL);
        Button btnStartCounter = new Button(this);
        Button btnChangeBackgroundColor = new Button(this);
        btnStartCounter.setText("Start counter");
        btnChangeBackgroundColor.setText("Change background color");

        btnChangeBackgroundColor.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                switch (flagBackgroundColor) {
                    case 0:
                        linearLayout.setBackgroundColor(Color.BLACK);
                        flagBackgroundColor++;
                        break;
                    case 1:
                        linearLayout.setBackgroundColor(Color.RED);
                        flagBackgroundColor++;
                        break;
                    case 2:
                        linearLayout.setBackgroundColor(Color.GREEN);
                        flagBackgroundColor++;
                        break;
                    case 3:
                        linearLayout.setBackgroundColor(Color.GRAY);
                        flagBackgroundColor++;
                        break;
                    case 4:
                        linearLayout.setBackgroundColor(Color.WHITE);
                        flagBackgroundColor = 0;
                        break;
                }
            }
        });

        btnStartCounter.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MyTask myTask = new MyTask();
                myTask.execute();
            }
        });

        linearLayout.addView(btnStartCounter);
        linearLayout.addView(btnChangeBackgroundColor);
        setContentView(linearLayout);
    }

    class MyTask extends AsyncTask<Void, Void, Void> {

        @Override
        protected Void doInBackground(Void... voids) {
            int i;
            actionBar.setTitle("Counter: 0");
            for(i=0; i<10_000_000; i++) {
                actionBar.setTitle("Counter: " + i);
            }
            return null;
        }
    }
}

Когда я нажимаю кнопку запуска счетчика, я получаю следующую ошибку:

 Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

Поиск в Интернете. Я обнаружил, что для решения подобных проблем решение заключается в использовании runOnUiThread (), но при этом я получаю тот же результат, что и в первом исходном коде: приложение застревает в ожидании окончания счетчика. Как я могу показать инкрементное значение счетчика на панели действий, не останавливая интерфейс? Есть ли способ для доступа и изменения содержимого панели действий из другого потока? Есть ли другой подход, который я не знаю?

Ответы [ 3 ]

0 голосов
/ 07 ноября 2018

Вы не должны изменять число из метода "doInBackground". Класс AsyncTask имеет метод с именем onProgressUpdate. Просто переопределите его и обновите свой интерфейс отсюда. из вашего doInBackground метода вы вызываете onProgressUpdate(yourvalue) и затем обновляете пользовательский интерфейс. Также обратите внимание, что я изменил параметр второго типа вашего класса asyncTask с Void на Int.

(Я реализовал решение не в IDE, возможны синтаксические ошибки, но концепция такая)

class MyTask extends AsyncTask<Void, Int, Void> {

        @Override
        protected Void doInBackground(Void... voids) {
            int i;
            actionBar.setTitle("Counter: 0");
            for(i=0; i<10_000_000; i++) {
                publishUpdate(i)
            }
            return null;
        }

        @Overrdie
        protected void onProgressUpdate(Int.. ints){
        actionBar.setTitle("Counter: " + ints[0]);
        }

    }
0 голосов
/ 07 ноября 2018

Либо вы можете сразу получить доступ к пользовательскому интерфейсу

new Handler(Looper.getMainLooper()).post(new Runnable()
{
    @Override
    public void run()
    {
        //Access UI code from here
    }
});

Или вы можете использовать обратный вызов

Начиная с интерфейса

public interface MyCallback {
    void count(int i);
}

Как запустить задачу и получить обратный вызов

MyTask myTask = new MyTask(new MyCallback()
{
    @Override
    public void count(int i)
    {
        actionBar.setTitle("Counter: " + i);
    }
});
myTask.execute();

Расширение MyTask для включения обратного вызова

static class MyTask extends AsyncTask<Void, Void, Void>
{
    private MyCallback _callback;

    MyTask(MyCallback callback) {
        _callback = callback;
    }

    @Override
    protected Void doInBackground(Void... voids) {
        for(int i = 0; i < 100; i++) {
            _callback.count(i);
        }
        return null;
    }
}
0 голосов
/ 07 ноября 2018

Вы можете использовать onPostExecute для взаимодействия с пользовательским интерфейсом. Пожалуйста, прочитайте ref

Примечание: onPreExecute() и onProgressUpdate() также могут связываться с потоком пользовательского интерфейса. Но doInBackground вызывается только из рабочего потока.

...