Программирование под android

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Приветствую!

Стар становлюсь и ленив - не хочется метаться по форумам, регистрироваться, получать сертификаты ****** :) - поэтому спрошу для начала здесь.

Итак: есть тут кто, кто баловался (или сурьёзно) программингом под андроид в Android Studio? Хочется немного пообщаться, позадавать разные вопросы, пополучать ответы.

Из первых вопросов - есть парочку. Но для начала вводные: хочу освоить андроид, уже пару лет как в полный рост стоит вопрос написания приложения для контроля/мониторинга теплицы (конфигуратор под винду - есть, вебморда - есть, поддержка MQTT - есть, удалённое управление через внешний сервис - есть, а вот приложухи под андроид - нема).

Первый этап, как известно - это продумывание архитектуры. Приложение должно уметь соединяться с контроллером как по IP-адресу в локальной сети, так и напрямую к Wi-Fi-точке доступа, которую выставляет наружу контроллер. Со всякими permission в манифесте приложения - более-менее понятно.

Вопрос в архитектуре. Как это вижу я: есть глобальный класс, скажем TransportFactory, который, по запросу - создаёт транспорт нужного типа, возвращает его singleton. Например, попросили мы подключиться по IP, вызвали (псевдокод):

ITransport ipTransport = TransportFactory.createTransport(TransportType.IP);

Затем подписываемся на события транспорта, типа OnConnect и т.п. Коннектимся, куда надо, и вперёд - для простоты архитектуры не будем вводить слой Endpoint, т.к. конечная точка подсоединения - у нас одна, одновременно работаем только с одним контроллером, при необходимости - просто переподключаемся, и всё.

Транспорт нам предоставляет метод отсыла запроса на контроллер, например, Transport.get(command).  И по коллбэку он дёргает вызывающую сторону, когда разбирает ответ от контроллера. Тут тоже более менее всё очевидно, механизм знаком.

Собственно, чего я пока не догоняю: 

1. Как сделать, чтобы в масштабе всего приложения (неважно, какое Activity сейчас на экране) тикал какой-нибудь таймер (когда приложение активно, конечно), по тику которого я буду обрабатывать очередь команд к транспорту? Насколько правильно будет использовать в этом случае Service (что-то мне подсказывает, что сервис - не для того)?

2. Как быть с life cycle у Activity, ведь если Activity плюнет запрос на команду в транспорт (работа асинхронная), указав себя, как получателя ответа, то может произойти ситуация, когда Activity либо уже убито, либо - пересоздано при повороте экрана. Чо делать? Делать своё глобальное хранилище активных Activity (сорри за тавтологию), и лепить посредника, разруливающего эту ситуацию?

3.  Простой способ сделать все Activity реагирующими на события транспорта? Наследовать их от своего интерфейса?

4. Если у кого под рукой ссылка - дайте пж инфу по Design guide для GUI приложух: что принято, какие пожелания и т.п. 

5. Если кто может предложить ссылочку на хорошие текстовые уроки (кроме https://startandroid.ru/ - его читаю) - буду признателен.

Короче - если кому интересно тут пообсуждать и поучить дурачка уму-разуму - я только "за". Заранее благодарен.

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015
bwn
Offline
Зарегистрирован: 25.08.2014

DIYMan пишет:

регистрироваться, получать сертификаты ******** :) 

ОФФ.

Не, если нужен, только скажи.))))

 

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

Я кое-чего делаю в Студии ;))) Задавай свои вопросы. На первый ты ответил.

2. не делай так. делай работу с транспортом от фонового объекта, тогда он не умрет и не перезапустится. Нужны подробности, скорее всего ты велосипед изобретаешь. Ты не поверишь, сколько всего уже под Андроид написано. ;)))

3. например так. а можно, как я написал выше, обрабатывать события в фоне, а в активити кидать что-то умное.

4. мода всё время меняется. Вроде уже и материал-дезайн выходит из моды. Мне нравятся примочки из материала, только их часто проще самому написать. Я вот себе надписи в "пулях"  - сделал за день, вместе с лейаутом, адаптирующимся под надпись размером, реакцией на нажатие ... блекджеком и арфистками... Щас подумал - надо их еще "дрожать" научить, с вибрацией ;)))

Вообще всегда модна лаконичность. Дизайн а-ла хром браузер под Андроид - по мне - так лучший на все времена... Хотя - на вкус и цвет...

======================

самое главное  - вообще всё есть в developer.android.com. Остальное есть в куче уроков, которые просто по яндеху открваются.

-----------------------

Как пишу я:

Я работаю под Линухом. Тачка под Студию особенная, я долго шел к конфигурации и осталось куда апгрейлить. Я скажу честно, моя конфигурация - минимально допустимая, когда была слабее - был онанизм, вместо программирования.

Итак: CoreI5 4 ядра, 16Гиг, SSD240. Еще раз - это минимум!

на Линухе 4 рабочих стола. На одном - студия, на остальных трёх - файерфоксы, у каждого открыто по 20 вкладок, в основном поиск, примеры и developer.android.com в разных местах. В Студии открыто НЕСКОЛЬКО проектов. Для отработки кусочков проекта. Разных активити, или даже только частей.

-NMi-
Offline
Зарегистрирован: 20.08.2018

wdrakula пишет:

Итак: CoreI5 4 ядра, 16Гиг, SSD240. Еще раз - это минимум!

Так точно,это минимум. Ещё пару вопросов тогда: линукс какой и разрешение на мониторе какое?

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

-NMi- пишет:

wdrakula пишет:

Итак: CoreI5 4 ядра, 16Гиг, SSD240. Еще раз - это минимум!

Так точно,это минимум. Ещё пару вопросов тогда: линукс какой и разрешение на мониторе какое?

Ты для дополнения совета интересуешься или самому нужно?

1. Линух нужен 64-битный, иначе очень неспешно эмуляция тилипона будет работать, без поддержки ядром. У меня давняя любовь к дистрибутиву Минт, им и пользуюсь. 

2. Очень желательно 1080. В Линухе это решается виртуальным экраном. Если под Виндой - строго FullHD надо.Но у меня 768, из-за этого есть места в Студии, итить её писателей в сраку, где вне экрана одна из важных настроек... честно говоря пока только одна такая нашлась. ;)) ФулШД - системые требования к Студии. Я на 768 живу вроде без особого напряга.

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

wdrakula пишет:

Я кое-чего делаю в Студии ;))) Задавай свои вопросы. На первый ты ответил.

Спасибо ;) Собственно, потихоньку разбираюсь в текущей моде :)). Решил делать на Navigation Drawer и фрагментах, а для связи элементов UI и обновляемых данных - буду юзать ViewModel, как показали примеры, которые набросал - вещь чрезвычайно мощная (ну кто бы мог сомневаться, там унутре паттерн Observer, и подписка на изменения - легчайшая).

С ViewModel можно снять львиную долю головняков с жизненным циклом Activity и Fragment, имхо. Ну а с остальным - разберусь.

Снял видосик, там по таймеру в модель приходят "новые" данные, при этом видно, что один Fragment, выводящий эти данные - юзается и как отдельный фрагмент, так и в составе динамически созданных страниц компонента ViewPager (который, в свою очередь, лежит внутри Fragment - прям Кащеева игла :)) ). Короче, то, что мне нужно было, т.к. интерфейс придётся создавать "на лету", поскольку у контроллера, в зависимости от прошивки - доступны те или иные модули.

Само видео, вдруг кому интересно: https://youtu.be/lJgbM1EDCBI

Убил полдня, как показала практика, сам язык - ну шоб я так жил, он простой, как три рубля. А вот SDK - надо читать, там чего только не наворочено. Ну и - привыкать к архитектуре, как обычно.

Надеюсь, всё получится. 

-NMi-
Offline
Зарегистрирован: 20.08.2018

wdrakula пишет:

Ты для дополнения совета интересуешься или самому нужно?

Самому нужно. Вот думаю на 2К монитор пересесть, вот и спрашиваю.

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Вдруг кому будет интересно - столкнулся с одним неочевидным нюансом при использовании таймеров внутри фрагментов, а именно: хочется, чтобы каждый фрагмент (по сути, он отображает какой-то статус, например, состояние полива) - мог запрашивать с контроллера информацию по таймеру, пока фрагмент активен.

Так вот - если фрагмент просто помещается в корневой фрагмент - то перекрыв onPause, внутри можно легко остановить таймер, и всё - видно, что таймер не тикает, вызывается onDestroy у фрагмента.

Однако, если внутри фрагмента поюзать ViewPager, в который через свой адаптер поместить несколько фрагментов, как вкладки - то если у такого размещённого фрагмента есть унутре тикающий таймер, то при переключении на другой экран - вызывается только onPause у родительского фрагмента, и... всё. То есть несмотря на то, что ViewPager вроде бы как владеет помещёнными тудыть фрагментами - он ни слухом ни духом про то, что родительский фрагмент - уже не алё. Т.е. нет всплытия событий, скажем так.

Решил (не знаю, правильно или нет) - добавлением в адаптер метода pause, в котором делаю просто:

 // посылаем всем фрагментам в списке сигнал к остановке
        public void pause()
        {
            for(int i=0;i<myFragments.size();i++)
            {
                myFragments.get(i).onPause();
            }

        }

Но что-то меня терзают смутные сомнения. Дело в том, что при таком раскладе не вызывается событие onDestroy, т.е. андроид сам ничего с этими вложенными фрагментами не делает. Как бы память не засрать. Хотя, с другой стороны - родительский фрагмент уничтожается, и, по идее - память, выделенная под дочерние, тоже должна почистится.

Короче: буду в андроид студии смотреть память, для начала.

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

А так - песня, а не возможности! Транспорты сделал наследуемыми от абстрактного класса, коллбэк событий транспорта - интерфейсом. Фабрика транспортов, на будущее. Ну и самое классное - модель с LiveData событиями транспорта. Даёт сия дичь чудное поведение: кто захотел - подписался, и будет получать все события транспорта. Ну а там уже - смотри, тебе или не тебе ответ, например.

А отсыл сообщений - вообще класс: в таймере плюнул в транспорт - и забыл. Куски кода - ниже.

Модель с LiveData событий транспорта:

public class TransportEventsModel extends ViewModel
{
    private MutableLiveData<ConnectionStatus> mConnected;

    public TransportEventsModel() {
        mConnected = new MutableLiveData<>();
        mConnected.setValue(new ConnectionStatus(false));

        mAnswer =  new MutableLiveData<>();
    }
    public LiveData<ConnectionStatus> getConnected() {
        return mConnected;
    }
    public void setConnected(boolean c)
    {
        Log.d("TransportEventsModel", "setConnected = " + Boolean.toString(c));
        mConnected.setValue(new ConnectionStatus(c));
    }

    private MutableLiveData<TransportAnswerWrapper> mAnswer;
    public LiveData<TransportAnswerWrapper> getAnswer() {
        return mAnswer;
    }
    public void setAnswer(TransportAnswerWrapper a)
    {
        mAnswer.setValue(a);
    }
}

Туть - подписка на события транспорта:

TransportEventsModel tem = ViewModelProviders.of(requireActivity()).get(TransportEventsModel.class);

final TextView textViewStatus = (TextView) root.findViewById(R.id.text_status);
        tem.getConnected().observe(requireActivity(), new Observer<ConnectionStatus>() {
            @Override
            public void onChanged(@Nullable ConnectionStatus s) {
                Log.d("CONNECTION_STATE", s.toString());
                String str = s.Connected ? "СОЕДИНЕНО!" : "НЕТ СОЕДИНЕНИЯ!";
                textViewStatus.setText(str);

                disconnectBtn.setEnabled(s.Connected);
                connectBtn.setEnabled(!s.Connected);
            }
        });

        // подписываемся на любой ответ от сервера
        tem.getAnswer().observe(requireActivity(), new Observer<TransportAnswerWrapper>() {
            @Override
            public void onChanged(TransportAnswerWrapper transportAnswerWrapper) {
                if(transportAnswerWrapper.Message.Tag == TransportMessageTag.DoorState) {
                    // наше сообщение, с состоянием дверей!
                    Log.d("CATCH DOOR STATE", transportAnswerWrapper.Answer.Raw);
                }
            }
        });

Ну и - отправка сообщений по таймеру в транспорт, через фабрику:

updateTimer = new Timer();
        isTimerRunning = true;

        updateTimer.scheduleAtFixedRate(new TimerTask() {


                                  @Override
                                  public void run() {


                                      Log.d("HOME TIMER","TIMER RUN");

                                      requireActivity().runOnUiThread(new Runnable() {
                                          @Override
                                          public void run() {

                                              Transport t = TransportFactory.getInstance().getCurrentTransport();
                                              if(t != null && t.isConnected())
                                              {
                                                  Log.d("HOME TIMER","REQUEST DOOR STATE...");
                                                  t.send(TransportMessage.get("DOOR|STATE|0",TransportMessageTag.DoorState));
                                              }
                                          }
                                      });
                                  }

                              },
//Set how long before to start calling the TimerTask (in milliseconds)
                0,
//Set the amount of time between each execution (in milliseconds)
                3000);

Нраицца. Очень нраицца. Чем-то напоминает C# - только позамороченней архитектура немного. Но - нраицца.

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Набор буковок в предыдущем сообщении - делает чудные вещи: не надо заботиться об актуальности данных, LiveData всё само сделает, и позовёт подписчика, когда надо.

Что касается транспорта, то он вызывает коллбэки типа 

onLineReceived(Transport sender, TransportMessage sourceMessage, TransportAnswer answer)

Работать с этим - тоже крайне просто: в MainActivity создали транспорт, и подписались на его события:

Transport transport = TransportFactory.getInstance().createTransport(TransportType.LocalIP);
        TransportFactory.getInstance().setCurrentTransport(transport);
        transport.setCallback(this);

Потом - получили ссылку на общую модель событий транспорта:

TransportEventsModel tem = ViewModelProviders.of(this).get(TransportEventsModel.class);

А в событии от транспорта - лёгким движением руки сообщаем куче страждущих, что пришёл ответ от сервера:

@Override
    public void onLineReceived(Transport sender, TransportMessage sourceMessage, TransportAnswer answer)
    {
        Log.d("TRANSPORT","RAW: " + answer.Raw + "; MESSAGE IS: " + sourceMessage.toString());

        // печатаем параметры ответа
        Log.d("MESSAGE_TAG","" + sourceMessage.Tag.toString());

       Log.d("PARAMS_COUNT","" + answer.Params.length);
        Log.d("IS_OK_ANSWER","" + answer.IsOkAnswer);
        for(int i=0;i<answer.Params.length;i++)
        {
            Log.d("PARAM #" + i, answer.Params[i]);
        }

        // сообщаем подписчикам, что пришёл ответ от контроллера, интересующиеся - сами разберут
        tem.setAnswer(new TransportAnswerWrapper(sourceMessage,answer));
    }

И всё. Вообще всё. Не надо возиться с самописными костылями - так, определил с десяток интерфейсов и врапперов, чтобы таскать туда/сюда композитные данные - и жить весело. Я даже в этом разрезе не понимаю - нафига нужны какие-то Bundle для таскания данных между Activity или фрагментами, если есть LiveData, в котором тупо плюсом Observer? Оно, конечно, LiveData вроде как на UI заточено, типа, чтобы состояние держать - но кого останавливали такие мелочи? :)))

Буду дальше рыть это чудо.

З.Ы. И да - всякие Transport и прочие TransportMessage - это самопис, заточенный под проект. Обвязка - минимальна, сам в шоке. 

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

Молодец! Кино посмотрел, код почитал.

Единственное замечание, просто дело вкуса: Я не люблю рассылать событие. Можно поднять флаг о том, что есть что-то полезное. А получатели "возбудятся" от поднятия флага и сами прочтут, что положено. Хотя при современных скоростях java-машины это уже похеру.

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

wdrakula пишет:
Единственное замечание, просто дело вкуса: Я не люблю рассылать событие.

Я сегодня полдня убил с фрагментами. У них свой life cycle, и LiveData с этим зоопарком - работает крайне занимательно. Приложение крашилось, наверное, раз 100, из-за этого грёбаного цикла жизни фрагментов.

Вот тут как раз впору вспомнить процитированное :) Но я не сдаюсь - уж больно LiveData интересная штука. Да и вообще: там путей решения задачи - вагон и тележка, чего только уже не наколбасили в SDK. Не получится с LiveData - да не вопрос, сделаю свой механизм.

Фрагменты - меня когда-нибудь добьют :) Но отказываться от них - не собираюсь, ооочень вкусная вещь для UI. Короче - как обычно: надо пару месяцев мордой в грязь попадать, набить шишки - и потом уже пойдёт, как по маслу (ттт).

wdrakula пишет:

А получатели "возбудятся" от поднятия флага 

Как они возбудятся? Там есть что-то типа семафоров? Поделись, пж, поподробнее. Надо сразу по прибытию данных уведомить всех подписчиков, идеальный вариант для этого - паттерн Observer, он как раз уже есть из коробки в LiveData.

Короче - колись, разные методы решения насущных задач - всегда полезно почитать ;)

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

Штоп я советы давал, мне нужно понять, что ты делаешь. Пока я не совсем понимаю.

Если ты делаешь визуализацию для мониторинга теплицы, как написано в первом посте, то я не понимаю, зачем тебе сообщения во фрагменты передавать? Это же не посадка на Марс в реальном времени? Ты в нормальном фоновом процессе получаешь данные, нормализуешь, скидываешь в SQL базу. А Визуализатор - ну просто отдельная игрушка. По таймеру проверяет наличие изменений и перерисовывает  текущее окно, если изменения затронули изображение. А у тебя, мне показалось, подход как к веб-морде на JS. Все данные - сразу обработчику, и пусть отрисовывает. Так многие пишут, но современный тилипон - это довольно мощный компьютер.

Но возможно мы мыслим немного по-разному, что и хорошо.

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

wdrakula пишет:

Штоп я советы давал, мне нужно понять, что ты делаешь. Пока я не совсем понимаю.

Если ты делаешь визуализацию для мониторинга теплицы, как написано в первом посте, то я не понимаю, зачем тебе сообщения во фрагменты передавать? Это же не посадка на Марс в реальном времени? Ты в нормальном фоновом процессе получаешь данные, нормализуешь, скидываешь в SQL базу. А Визуализатор - ну просто отдельная игрушка. По таймеру проверяет наличие изменений и перерисовывает  текущее окно, если изменения затронули изображение. А у тебя, мне показалось, подход как к веб-морде на JS. Все данные - сразу обработчику, и пусть отрисовывает. Так многие пишут, но современный тилипон - это довольно мощный компьютер.

Но возможно мы мыслим немного по-разному, что и хорошо.

Пойми - там есть нюансы, тот же Life cycle у Activity. Когда приложение свернули и потом развернули - Activity с фрагментами меняют состояние. Когда приложение закрыли по кнопке "назад", потом с общего списка достали - там другая песня: Activity пересоздаётся. И есть немного геморроя с поддержанием актуальных данных. Именно под это и заточена LiveData. 

А что касается хранения данных - так это похрен, где, тут ты прав. Хоть в базе, хоть где. Но вот LiveData даёт тебе возможность автоматом получать last known good данные при создании Activity или фрагмента. Автоматически. Т.е. просто пихаешь туда данные - и всё, кому надо - получат. 

Короче - это тупо удобно. Более того - концепция фрагментов очень помогает при формировании layout для различных ориентаций экрана, как минимум. Фрагменты - можно скрывать/показывать, когда надо. Очень мощно.

Что касается "по таймеру" - вот этого как раз и не хочется, по таймеру интерфейс обновлять. Смотри: допустим, проверяется факт наличия данных по таймеру, раз в 5 секунд. Свернули приложение, таймер остановился, до следующей проверки - 4 секунды. Развернули приложение, и оп - в полях пусто, патамушта андроид пересоздал Activity. И шо делать? Ждать 4 секунды, пока таймер не тикнет и не обновит нам данные? Не, так не очень, совсем не очень. Потому как если нет изменений - он, судя по твоему сообщению - ничего нам не обновит, изменений-то нету. А состояние фрагмента или Activity при пересоздании - пустое, нет там в полях нихрена, грубо говоря. Это ж не взрослый комп, где пока приложуха работает - всё сохраняется для отображения, хоть сворачивай, хоть разворачивай. В андроиде - другие лыжи, совсем.

А вот LiveData - сразу придёт при пересоздании Activity, и мы там сразу выцепим последние известные данные. Как-то так. Правда, стоит сказать, что я реализовал и свой механизм, типа last known good в классе обработки данных: он при приходе ответа от железяки в теплице просто сохраняет его у себя в HashMap, и всё. Ну а кому надо - тот в onResume может позвать этот синглтон и выцепить последние данные. Так что можно и без LiveData, совсем. Просто это удобно, и ничего больше.

Вот, смотри, новый видосик снял, там продемонстрирована эмуляция парочки ответов от контроллера, плюс видно, как работают фрагменты (показываются только тогда, когда есть в контроллере теплицы модуль, дающий информацию, нужную фрагменту). Плюс - продемонстрировал, что состояние сохраняется и по кнопке "назад", и при сворачивании приложения.

Собственно, вот: https://youtu.be/Pdi70kCVXzE

А в базе хранить - мне не надо, не того полёта птица.

З.Ы, ЗА интересный диалог - гран мерси ;)

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

Я про фрагменты всё знаю, спасибо! ;))) Лайвдата - не пользовал, не было пока нужды. Просмтотрел описание - да, удобно. Кто ж говорит.

Теперь про активити: там есть много способов отслеживание пересоздания и запуска. Есть небольшой стринг, который передается при перезапуске, если его хватает. Можно сделать объект. Можно в фоновом потоке отслеживать запуски, выходы и перезапуски. Можно, кстати это правда удобно, запретить смену ориентации экрана. При запуске нужно не по таймеру отрисовывать, а просто - по факту запуска. Но это все мелочи.

А так - всё у тебя отлично! Я никоим образом не критикую! Здорово, что на одного java любителя стало больше. Это ты еще анимации не писал! Там вообще волшебство.

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

wdrakula пишет:

 Можно, кстати это правда удобно, запретить смену ориентации экрана. 

Да, в первой версии так и сделаю - только портретная, и всё. А то я с ума сойду, пока всё освою :)

wdrakula пишет:

Это ты еще анимации не писал! Там вообще волшебство.

Ты про Action? Вскользь пролистал, учёл, что можно кучу всего интересного делать, но пока - пропущу. Украшательства оставим на потом. Хотя.... :))))))

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

wdrakula пишет:

 Это ты еще анимации не писал! Там вообще волшебство.

Попробовал для фрагмента поюзать 

setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right);

Добавление одной строчки кода - и как забавно они теперь выезжают при показе! Действительно - волшебство :)

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

Я еще подумал: нужно повторять типовые элементы управления:

потянуть вниз для перезагрузки данных. При запуске - ластноунгуд из базы. Так и Почта России и браузер и Гмайл ...да всё, что я юзаю.

Поворот экрана - перехватывать и обрабатывать самостоятельно, как браузер делает.

 

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Ну со всякими тонкостями мы постепенно разберёмся ;) Зырь, чего уже получается - сделал анимацию мал-мала, и ViewPager с динамически обновляемыми данными. Пока на всех трёх страницах - один и то же адаптер, но архитектурно - всё предусмотрел, надо только адаптеры вывода написать, и усё будет ;)

Видос захвата экрана эмулятора: https://youtu.be/drQi7DJtf9w 

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

wdrakula пишет:

потянуть вниз для перезагрузки данных. 

Yes, sir! Сделано, т-щ генерал! Вот: https://youtu.be/bjK5eB2Tryk

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Напомните пж старый мем программерский, хоть убей, забыл и не могу найти. Суть примерная: чувак прочитал самоучитель для чайников, и побёг обновлять резюме, в котором написал что-то типа "Advanced C++ skills".

Вот я щас так же себя чувствую: прямо так и тянет резюме обновить, добавив строчки про андроид :)))))))))

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

DIYMan пишет:

Напомните пж старый мем программерский, хоть убей, забыл и не могу найти. Суть примерная: чувак прочитал самоучитель для чайников, и побёг обновлять резюме, в котором написал что-то типа "Advanced C++ skills".

Вот я щас так же себя чувствую: прямо так и тянет резюме обновить, добавив строчки про андроид :)))))))))

;))))

Точно так! Андроид и java так затягивают и на первых же шагах дают такой высокий уровень отдачи (если раньше ООП не было для тебя матерным выражением!), что кажется, будто всё вообще элементарно и щаз я стану гуру андроида!!! Потом трезвеешь.

Ты еще потом свой апк начни рефакторить до нормальных 50к хотя бы, вместо штатных Студийных мегабайт ;))).  Заранее не стану пугать... всё решаемо.

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

wdrakula пишет:

Ты еще потом свой апк начни рефакторить до нормальных 50к хотя бы, вместо штатных Студийных мегабайт ;))).  Заранее не стану пугать... всё решаемо.

Ты этта - незнакомыми словами не ругайся :)) Рано ещё до рефакторинга, я щас борюсь с ViewPager и переименованием датчика. Эта зараза при перелистывании страниц пересоздаёт фрагменты, как бог на душу положит. И контекст из-за этого постоянно меняется. Пока поборол введением статических членов. 

Если интересно - по long click на list item, который суть фрагмент с CardView внутри - надо отобразить всплывающее меню, а по клику на пункт меню - тупо перерисовать этот item с другим именем. Так вот: если не сохранять контекст adapterView в обработчике OnItemLongClickListener, то потом, по приходу onContextItemSelected - контекст может быть не тот. В общем - неочевидное поведение, если не знать жизненный цикл фрагментов (а я его не знаю, только самые основные понятия).

А ты говоришь - рефакторинг. Я тут ещё на живом смартфоне ещё не тестировал, всё в эмуляторе сижу. И чуется мне, что весь секас ещё впереди, есть подозрение, что многие вещи я делаю через жопу. Вот например, про контексты:

    private ListView listView; // список, привязанный к нашему контексту
	
	// а это - для грамотного выбора контекста внутри ViewPager
	private static int selectedListItemIndex;
    private static ListView contextView; 
	
	
 public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        // Inflate the layout for this fragment
        View root = inflater.inflate(R.layout.fragment_sensors_page, container, false);
		 listView = root.findViewById(R.id.list_sensors);
		 
		 // создаём наш адаптер для списка
        switch(sensorType)
        {
            case Temperature:
                items  = new ArrayList<TemperatureValue>();
                listAdapter = new SensorsListAdapter(requireActivity(), R.layout.temperature_item, R.drawable.thermometer_50, SensorType.Temperature,items);
                listView.setAdapter(listAdapter);
                break;
		 
		 //..... ТУТА ЕЩЁ МНОГО ЧЕГО .....
		 
		 return root;
}



listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
            @Override
            public boolean onItemLongClick(AdapterView<?> adapterView, View view, int i, long l) {

			 // ЭТО static ЧЛЕН КЛАССА
                selectedListItemIndex = i;
				 // ЭТО static ЧЛЕН КЛАССА ТОЖЕ
                contextView = (ListView) adapterView;				
                return false;
            }
        });
		
		
@Override
    public boolean onContextItemSelected(@NonNull MenuItem item) {

        int itemId = item.getItemId();
        switch (itemId)
        {
            case IDM_RENAME:
            {
                //TODO: переименовать выбранный датчик!!!
                String newName = "НОВОЕ ИМЯ";
                
				// ВИДИШЬ, КАК ПРИХОДИТСЯ ДОБИРАТЬСЯ ДО МОЕГО АДАПТЕРА - ЧЕРЕЗ СТАТИЧЕСКИЙ ЧЛЕН КЛАССА,
				// ЧТОБЫ НЕ ПОТЕРЯТЬ КОНТЕКСТ.
                ((RenameSensorCallback)contextView.getAdapter()).onRenameSensor(contextView,selectedListItemIndex,newName);
            }
            break;
        }
        return true;
    }		

Вот даже не знаю, насколько это правильно - так вольно делать статические члены, чтобы сохранить туда View? Ничего не потечёт?

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

DIYMan пишет:

 Ничего не потечёт?

Подробно потом посмотрю, прости. А так - тыж утечку сразу в отладчике видишь? Странный вопрос ты задаешь. Что в Студии хорошо - так это то, что все ресурсы она показывает очень подробно.

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

Сижу в машине у магазина, жду супругу ;))) пересмотрел твой код. Да ты действительно не до конца понимаешь жизненный цикл. Я на такое тоже назывался. Не развернут новый фрагмент полностью, ты рано проверял контекст. В андроид девелопер уточни - там есть какой-то колбэк, который точно вызывается когда все устаканилось.
Я на похожее назывался, когда спрашивал размеры рут окна... Рано спрашивал. :((

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

wdrakula пишет:

А так - тыж утечку сразу в отладчике видишь? Странный вопрос ты задаешь. Что в Студии хорошо - так это то, что все ресурсы она показывает очень подробно.

Да вроде в профайлере всё спокойно, все графики горизонтальны. Но лучше перебздеть и спросить, для надёжи :)

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

wdrakula пишет:
Сижу в машине у магазина, жду супругу ;))) пересмотрел твой код. Да ты действительно не до конца понимаешь жизненный цикл. Я на такое тоже назывался. Не развернут новый фрагмент полностью, ты рано проверял контекст. В андроид девелопер уточни - там есть какой-то колбэк, который точно вызывается когда все устаканилось. Я на похожее назывался, когда спрашивал размеры рут окна... Рано спрашивал. :((

Не, там дело не в этом, имхо. Смотри: список - динамически обновляемый. Т.е. по приходу новых данных - вызывается notifyDataChanged у моего адаптера списка, и он - может раздуть новое View для записи в списке, т.к. это поведение - оговорено в доке. 

Activity, с которым я работаю - оно вот оно, главное, на экране, в состоянии Resumed. ViewPager, который держит фрагменты, описывающие страницы - при этом сам занимается менеджментом этих фрагментов, приаттачивая/убирая их для своего контейнера.

Получаем следующее: список обновился - и нет гарантии, что у нужного там list item остался тот же view, а не был раздут новый в методе onCreateView адаптера. Соответственно, надо этот вопрос как-то решить, решение - выше, оно работает.

Далее, что мне непонятно. По сути, во ViewPager расположены три фрагмента одного класса SensorPageFragment. У его layout для ListView прописан id - list_sensors. Т.е. при добавлении страниц во ViewPager - создаются три экземпляра одного фрагмента, которые в своём методе onCreateView получают ссылку на ListView своего layout, и хранят его в _нестатическом_ поле класса.

И вот что получается, если по долгому клику просто в лог выводить переданный индекс выбранного элемента списка, сохранить этот индекс в _нестатический_ член класса, а по выбору пункта меню - выводить этот же индекс из _нестатического_ члена класса:

1. Удержали курсор на втором элементе списка, на первой вкладке. В логе - 1, т.е. его индекс. Кликаем выпадающее меню - в логе 1, всё верно.

2. Переходим на вторую страницу, удерживаем элемент номер 8, ожидаем в логе увидеть 7 - видим 7, т.е. индекс элемента. Кликаем на выпадающем меню, ожидая увидеть в логе 7 - а видим 1! Т.е. какой-то эффективный менеджер позвал НЕ ТОТ экземпляр класса нашего фрагмента! При этом совершенно точно, что фрагмент первой страницы находится в состоянии Paused - но почему вызывается его экземпляр?

Подчеркну - по событию ОТ СИСТЕМЫ, я сам ничего не делаю на этих этапах, и описал только минимальный пример. Что я понимаю не так, и почему эта хрень тыкается не в экземпляр класса активного на данный момент на экране фрагмента?

Как может быть не развернут полностью фрагмент, если события по клику на ListView - отрабатывают? Я же вижу это на экране. 

З.Ы. Ок, проверю на isAdded(), и если где-то косяк с неполной подготовкой фрагмента - будет видно, выброшу исключение.

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Вот наглядная демонстрация. Вводные: ViewPager, в адаптере FragmentStatePagerAdapter создаются три экземпляра одного класса SensorsPageFragment, помещаются в локальный список адаптера, и далее этот список скармливается ViewPager по его требованиям.

Внутри SensorsPageFragment есть ListView, в котором, в методе onCreateView:

1. Цепляется setOnItemLongClickListener на ListView;

2. Переопределяются onCreateContextMenu и onContextItemSelected;

3. Есть член класса int selectedIndex;

4. Есть член класса ListView context;

5. В OnItemLongClickListener - просто сохраняем переданный нам индекс нажатого пункта меню в переменную selectedIndex класса, а также сохраняем переданный обработчиком OnItemLongClickListener в параметре adapterView контекст - в нашу переменную context;

6. В методе onContextItemSelected - просто выводим значение этой переменной.

Вот что в логе, по комментариям - понятно, надеюсь:


// ПЕРЕШЁЛ С ПЕРВОЙ СТРАНИЦЫ НА ВТОРУЮ, ВИДНО, ЧТО ПЕРВАЯ СТРАНИЦА СТАНОВИТСЯ НА ПАУЗУ
D/SENSOR_PAGE_FRAGMENT: onPause: ВКЛАДКА ТЕМПЕРАТУР
D/SENSOR_PAGE_FRAGMENT: onDestroy: ВКЛАДКА ТЕМПЕРАТУР
    onDetach: ВКЛАДКА ТЕМПЕРАТУР
	
// WTF ??? ПОЧЕМУ ОНА ОПЯТЬ СОЗДАЁТСЯ? 
D/SENSOR_PAGE_FRAGMENT: onAttach: ВКЛАДКА ТЕМПЕРАТУР
D/SENSOR_PAGE_FRAGMENT: onCreate: ВКЛАДКА ТЕМПЕРАТУР
    onCreateView: ВКЛАДКА ТЕМПЕРАТУР
D/SENSOR_PAGE_FRAGMENT: onResume: ВКЛАДКА ТЕМПЕРАТУР

	
// НАЖАЛ НА ПЕРВЫЙ ПУНКТ В СПИСКЕ, НА СТРАНИЦЕ ВКЛАДКА ВЛАЖНОСТИ	
D/SENSOR_PAGE_FRAGMENT: OnItemLongClickListener: ВКЛАДКА ВЛАЖНОСТИ, index = 0
    LIST VIEW LONG CLICK, INDEX: 0
D/SENSOR_PAGE_FRAGMENT: LIST VIEW CONTEXT MENU: ВКЛАДКА ВЛАЖНОСТИ
D/SENSOR_PAGE_FRAGMENT: LIST VIEW MENU ITEM SELECTED: ВКЛАДКА ВЛАЖНОСТИ

// НАЖАЛ НА ЧЕТВЁРТЫЙ ПУНКТ СПИСКА НА ВКЛАДКЕ ОСВЕЩЁННОСТИ
D/SENSOR_PAGE_FRAGMENT: OnItemLongClickListener: ВКЛАДКА ОСВЕЩЁННОСТИ, index = 3
    LIST VIEW LONG CLICK, INDEX: 3
	
// КАКОГО ХЕРА ВЫЗЫВАЕТСЯ ОБРАБОТЧИК В ДРУГОЙ ВКЛАДКЕ ???????	
D/SENSOR_PAGE_FRAGMENT: LIST VIEW MENU ITEM SELECTED: ВКЛАДКА ВЛАЖНОСТИ
D/TemperatureListAdapter: RENAME SENSOR: 0 // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

Если непонятно, объясню: кликаем на первом элементе списка второй вкладки, видим, что индекс - 0, всё верно. Выпадающее меню не щёлкаем. Перемещаемся на третью вкладку, кликаем на четвёртом элементе списка, в OnItemLongClickListener - видим, как и ожидалось - 3, кликаем выпадающее меню, попадаем в обработчик onContextItemSelected ДРУГОГО ЭКЗЕМПЛЯРА КЛАССА!!!

WTF, спрашивается? Почему контекст ВНЕЗАПНО поменялся? Страница как была на экране, так и есть, своё состояние она не меняла (нет вызовов onPause и т.п.). ПА-ЧИ-МУ?

Дальше - всё ещё интересней: начало крэшится в эмуляторе, достаточно только выбрать один раз в одном списке пункт - и вот такое:

D/SENSOR_PAGE_FRAGMENT: onAttach: ВКЛАДКА ТЕМПЕРАТУР
    onCreate: ВКЛАДКА ТЕМПЕРАТУР
    onAttach: ВКЛАДКА ВЛАЖНОСТИ
    onCreate: ВКЛАДКА ВЛАЖНОСТИ
D/SENSOR_PAGE_FRAGMENT: onCreateView: ВКЛАДКА ТЕМПЕРАТУР
D/SENSOR_PAGE_FRAGMENT: onResume: ВКЛАДКА ТЕМПЕРАТУР
D/SENSOR_PAGE_FRAGMENT: onCreateView: ВКЛАДКА ВЛАЖНОСТИ
D/SENSOR_PAGE_FRAGMENT: onResume: ВКЛАДКА ВЛАЖНОСТИ
D/SENSOR_PAGE_FRAGMENT: onAttach: ВКЛАДКА ОСВЕЩЁННОСТИ
    onCreate: ВКЛАДКА ОСВЕЩЁННОСТИ
    onCreateView: ВКЛАДКА ОСВЕЩЁННОСТИ
    onResume: ВКЛАДКА ОСВЕЩЁННОСТИ
	
// переход на третью вкладку
D/SENSOR_PAGE_FRAGMENT: onPause: ВКЛАДКА ТЕМПЕРАТУР
D/SENSOR_PAGE_FRAGMENT: onDestroy: ВКЛАДКА ТЕМПЕРАТУР
    onDetach: ВКЛАДКА ТЕМПЕРАТУР

// щёлкнул по элементу списка
D/SENSOR_PAGE_FRAGMENT: OnItemLongClickListener: ВКЛАДКА ОСВЕЩЁННОСТИ, index = 3
    LIST VIEW LONG CLICK, INDEX: 3
	
	// показалось контекстное меню, контекст - правильный
    LIST VIEW CONTEXT MENU: ВКЛАДКА ОСВЕЩЁННОСТИ
	
// выбрал выпадающее меню, и - ВНЕЗАПНО, не тот контекст!
	D/SENSOR_PAGE_FRAGMENT: LIST VIEW MENU ITEM SELECTED: ВКЛАДКА ВЛАЖНОСТИ

И тут ннна по мордасам:

E/MessageQueue-JNI: java.lang.NullPointerException: Attempt to invoke virtual method 'android.widget.ListAdapter android.widget.ListView.getAdapter()' on a null object reference

Патамушта для вкладки освещённости ни разу не вызывался лонгклик и там контекст, сохраняемый в нестатический метод класса - пустой.

Вопрос: объясни, почему так вольно контекст меняется? Если я кликнул по одному списку, два обработчика пришли для одного списка, а третий - для другого? Хрень какая-то.

З.Ы. Если context и selectedIndex сделать статическими членами - всё работает как часы. Но эта херня меня убивает, т.к. не покидает ощущение, что это НЕ-ПРА-ВИЛЬ-НО. Хочу понять, что не так.

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

сели с супругой бухать... завтра утром погляжу, сорян.

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Да, крашится, ессно, вот здесь:

@Override
    public boolean onContextItemSelected(@NonNull MenuItem item) {
        //boolean result = super.onContextItemSelected(item);

        Log.d(TAG,"LIST VIEW MENU ITEM SELECTED: " + name);
        int itemId = item.getItemId();
        switch (itemId)
        {
            case IDM_RENAME:
            {
                //TODO: переименовать выбранный датчик!!!
                String newName = "НОВОЕ ИМЯ";
                Log.d(TAG,"SHOW RENAME DIALOG!!!");
                ((RenameSensorCallback)contextView.getAdapter()).onRenameSensor(contextView,selectedListItemIndex,newName);
            }
            break;
        }
        return true;
    }

Где contextView - тот самый контекст, сохранённый в методе OnItemLongClickListener. Где-то какой-то transition effect произошёл, и почему-то съезжает контекст.

Цитата:

- Бен, ай нид хэлп! (с) "Брат-2"

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

wdrakula пишет:

сели с супругой бухать... завтра утром погляжу, сорян.

Ну как обычно :) Приятного!

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Хха, млять! Ннате: https://stackoverflow.com/questions/9753213/wrong-fragment-in-viewpager-receives-oncontextitemselected-call

Цитата оттуда:

Цитата:

So this is some sort of idiotic design decision by Google

Чота ржу :)))

З.Ы. Собственно, там и решение проблемы, не надо статические члены больше. 

З.З.Ы. Хотя getUserVisibleHint - объявлен deprecated, надо ещё рыть. Проблема-то есть, и она - точно не моя ;)

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Короче, пока оставил getUserVisibleHint(), будем посмотреть, надо дорыть эту проблему до конца.

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Во, сделал переименование датчиков, и тренды больших изменений значений - иконки подмаргивают при этом:

https://youtu.be/ZbMdrt3cvV8

Эдак скоро я дойду до момента, когда не надо будет эмулировать ответы контроллера, а реально надо будет уже к нему коннектиться. Страшшшна! :))

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

Хоть я и не очень трезв, но круто.

Про стековерфлоу - все там берем советы! ;)) только их оценивать и фильтровать надо. Смотри, что подходит лучше.

RG22EM
Offline
Зарегистрирован: 27.08.2016

кстати, ты спрашивал про Линух, посмотри XUBUNTU, может понравится

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

RG22EM пишет:

кстати, ты спрашивал про Линух, посмотри XUBUNTU, может понравится


Будь добр отвечать тому, кто спрашивал. Иначе реплика повисает в воздухе.
Так кто спрашивал? Ни я ни Даймон, а кто?

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

RG22EM пишет:

кстати, ты спрашивал про Линух, посмотри XUBUNTU, может понравится

Я? Я не спрашивал, совсем.

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Открываю для себя новые особенности андроида. Взять, например, Toast. Пока эта подсказка висит на экране - другие не показываются. В условиях сложного приложения, когда Toast могут попросить показать из разных мест - надо быть уверенным, что он покажется, несмотря на то, что там висит предыдущий.

Пример для моего проекта: наполняю транспорт работой с Socket. В эмуляторе нет поддержки сети (придётся таки уже переползать на железный смартфон). При старте соединения один фрагмент показывает Toast с текстом "Соединяемся...". Далее - уже MainActivity подписывается на события транспорта, и там показывает Toast, например, "Ошибка соединения".

Поскольку поддержки сети в эмуляторе не видно (может, я не нашёл), то не успевает исчезнуть сообщение "Соединяемся", как уже происходит событие "ошибка соединения". В этом событии - надо показать Toast, перекрыв им тот, что сейчас на экране - иначе всё будет рассинхронизировано по времени показа.

Посему - написал простенький синглтон, который хранит в себе ссылку на предыдущий показанный Toast, и перед вызовом makeText - отменяет показ предыдущего. Таким образом, если взаимодействовать с Toast через этот синглтон, то в рамках всего приложения неважно, кто показал Toast. Короче - маленький личный удобняк.

Собственно, вот:

import android.content.Context;
import android.widget.Toast;

public class ToastManager {

    private static ToastManager INSTANCE = null;
    private static Toast internalToast = null;

    private ToastManager() {}

        public static ToastManager getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new ToastManager();
        }
        return(INSTANCE);
    }

    public Toast makeText(Context context, CharSequence text, int duration)
    {
        if(internalToast != null)
        {
            internalToast.cancel();
        }

        internalToast = Toast.makeText(context,text,duration);
        return internalToast;
    }
}

 

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

мило.

А поддержка сети в эмуляторе конечно есть. Я там браузер открывал ;)))) Так что - ищите и обрящите, толците и да отверзится вам! ;))) Вот!

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

wdrakula пишет:

А поддержка сети в эмуляторе конечно есть. Я там браузер открывал ;)))) Так что - ищите и обрящите, толците и да отверзится вам! ;))) Вот!

Спс, буду разбираться ;)

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

wdrakula пишет:

А поддержка сети в эмуляторе конечно есть. Я там браузер открывал ;)))) 

Ок, браузер я там и правда открыл, и даже на arduino.ru зашёл. Значит ли это, что эмулятор сможет видеть все устройства в локалке? Для ясности - рабочий комп и контроллер теплицы - соединяются к точке доступа, которую предоставляет смартфон. Т.е. они, естественно, в локалке, что подтверждается работоспособностью вебморды (развёрнутой на рабочем компе), которая запрашивает с контроллера данные.

Насколько в этом функционален эмулятор? Есть опыт?

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

DIYMan пишет:
Есть опыт?

Нет, не писал кода требующего IP.

Эмулятор телефона - железный, основан на QEMU. То есть если браузер работает - весь IP стек должен работать.

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

wdrakula пишет:

То есть если браузер работает - весь IP стек должен работать.

Спс, будем надеяться, что так и есть :)

 

RG22EM
Offline
Зарегистрирован: 27.08.2016

DIYMan пишет:

RG22EM пишет:

кстати, ты спрашивал про Линух, посмотри XUBUNTU, может понравится

Я? Я не спрашивал, совсем.

ТЫШ  и

Здесь

 

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

RG22EM пишет:

DIYMan пишет:

RG22EM пишет:

кстати, ты спрашивал про Линух, посмотри XUBUNTU, может понравится

Я? Я не спрашивал, совсем.

ТЫШ  и

Здесь

 

Протри глаза и разгляди КТО там спрашивал? (для радистов спойлер - пользователь с ником -NMi-)

(Мне всегда казалось, что коротковолновики имеют альтернативную одаренность.)

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Граф, а ответь мне на простой вопрос, если сможешь, а? Короче есть NavigationView, настраивается вот так:

 NavigationView navigationView = findViewById(R.id.nav_view);
        navController = Navigation.findNavController(this, R.id.nav_host_fragment);

        // Passing each menu ID as a set of Ids because each
        // menu should be considered as top level destinations.
        mAppBarConfiguration = new AppBarConfiguration.Builder(
                R.id.nav_status, R.id.nav_sensors, R.id.nav_devices,
                R.id.nav_settings, R.id.nav_share, R.id.nav_send)
                .setDrawerLayout(drawer)
                .build();

        NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
        NavigationUI.setupWithNavController(navigationView, navController);

Далее - в графе Navigation прописаны global action для фрагментов, чтобы была анимация переходов:

    <fragment
        android:id="@+id/nav_devices"
        android:name="ru.gardenboss.controller.ui.devices.DevicesFragment"
        android:label="@string/menu_devices"
        tools:layout="@layout/fragment_devices" />

    <fragment
        android:id="@+id/nav_settings"
        android:name="ru.gardenboss.controller.ui.settings.SettingsFragment"
        android:label="@string/menu_settings"
        tools:layout="@layout/fragment_settings" />

    <fragment
        android:id="@+id/nav_share"
        android:name="ru.gardenboss.controller.ui.share.ShareFragment"
        android:label="@string/menu_share"
        tools:layout="@layout/fragment_share" />

    <fragment
        android:id="@+id/nav_send"
        android:name="ru.gardenboss.controller.ui.send.SendFragment"
        android:label="@string/menu_send"
        tools:layout="@layout/fragment_send" />
    <action
        android:id="@+id/action_global_nav_sensors"
        app:destination="@id/nav_sensors"
        app:enterAnim="@android:anim/slide_in_left"
        app:popEnterAnim="@android:anim/slide_in_left"/>
    <action
        android:id="@+id/action_global_nav_devices"
        app:destination="@id/nav_devices"
        app:enterAnim="@android:anim/slide_in_left"
        app:popEnterAnim="@android:anim/slide_in_left"/>
    <action
        android:id="@+id/action_global_nav_settings"
        app:destination="@id/nav_settings"
        app:enterAnim="@android:anim/slide_in_left"
        app:popEnterAnim="@android:anim/slide_in_left" />
    <action
        android:id="@+id/action_global_nav_status"
        app:destination="@id/nav_status"
        app:enterAnim="@android:anim/slide_in_left"
        app:popEnterAnim="@android:anim/slide_in_left" />
</navigation>

Далее: есть условный второй фрагмент, на котором кнопка. По клику на кнопке вызываем action:

btnDevices = root.findViewById(R.id.btn_devices);
        btnDevices.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Navigation.findNavController(view).navigate(R.id.action_global_nav_devices);
            }
        });

Всё чудесно, анимация slide_in_left отрабатывает, и можно бы жить весело и припеваючи, но! По кликам на пунктах меню в NavigationView (хрень, которая слева выезжает) - никакой анимации переходов нет. Почему - понятно: в AppBarConfiguration указаны ID пунктов меню, а не ID глобальных action. Если указать там ID глобальных action - пердец, крэш и содомия, потому как такое указание неверно, ессно.

Вопрос: как дать этой сучке понять, что по клику на пунктах меню надо выполнять global action? Ручками переопределять методы - не хочется. Неужели гугл не предусмотрел штатного подхода?

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

Я бы - переопределял. Я вообще не люблю использовать XML. Часто бросил несколько вью в релатив лейаут, а всю внутрянку - просто руками на java. И даже новые контролы я часто создаю из кода, а не в XML. Я так и не смог его полюбить :(((.

Если я пишу для себя, или для одного клиента, я даже стринг ресурсы редко использую, а все строки  - прям в коде ;))))). Не поверишь, но я часто дневную и ночную темы вручную делаю. Мне удобнее. Жуть, да?

-----------------

Это объяснение того, что я даже не искал "штатного Гуглевского" решения через XML. Наверно я неправ. Библиотек анимации листвью, переопределяя методы, добавляя адаптер анимации - полный Гугль, чесслово. На Хабре тока щас читал как прицепить анимацию "листания книжки".

-----------------

ПО ДЕЛУ:

А зачем ты пытаешься акции, описанные внутри навигейшина, прицепить к пунктам меню? Опиши для лейаута конкретного фрагмента свои акции, пусть с теми же действиями. Андроидный XML тем меня и раздражает, что не прозрачный. Акция, описанная внутри навигейшина - это не общее имя акции slide_in_left, а привязанное к конкретике.

Это не проверенное мнение, я бы так попробовал.

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

Я какую-то ерунду выше написал!

Я так понимаю, что НЕ работает при нажатии  в "выдвижном ящике"?

=============

Ты не можешь в AppBarConfiguration.Builder анимации поключить? Только переопределяя методы навигации. Что-то типа onNavDestinationSelected... как-то похоже оно зовется.

Ну и желания у тебя!!! Вот писал бы всё без XML - одинаковым образом бы анимировал все свои менюшки! ;)))

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Короче, решил переопределением двух методов: onNavigationItemSelected и onOptionsItemSelected, и биндингом ID меню к глобальному action.

Раз:

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {

        Log.d(TAG,"onOptionsItemSelected, ID=" + item.getItemId());


        return callMenuItemAction(item) || NavigationUI.onNavDestinationSelected(item, navController)  ||
        super.onOptionsItemSelected(item);


    }

Два:

navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {

                Log.d(TAG,"onNavigationItemSelected LISTENER!!!!");

                callMenuItemAction(item);

                drawer.closeDrawer(GravityCompat.START);

                return true;
            }
        });

Биндинг:

private boolean callMenuItemAction(@NonNull MenuItem item)
    {
        int itemId = item.getItemId();
        switch(itemId)
        {
            case R.id.nav_status:
                navController.navigate(R.id.action_global_nav_status);
                return  true;

            case R.id.nav_devices:
                navController.navigate(R.id.action_global_nav_devices);
                return  true;

            case R.id.nav_sensors:
                navController.navigate(R.id.action_global_nav_sensors);
                return  true;

            case R.id.nav_settings:
                navController.navigate(R.id.action_global_nav_settings);
                return  true;


        }

        return false;
    }

Всё робит. Щас только перепишу биндинг на какой-нибудь map, чтобы не было такой простыни - и всё ;)