- PVSM.RU - https://www.pvsm.ru -
Недавно портировал довольно большой проект с Qt (C++) на Android (Java), в процессе работы часто приходилось применять динамическое связывание объектов. Беда состояла в том что связывание (binding) в отличие от привычных сигналов и слотов в Qt в Java реализовано через лисенеры (listeners), и сколько я не пытался себя убедить что способ этот равноценен и тоже имеет место быть такого же удобства как при использовании сигналов и слотов достичь не удавалось.
Например, нам нужно связать бегунок (QSlider в Qt или SeekBar в Android) с каким либо действием, хотя бы привязать другой бегунок который будет послушно перемещаться следом за первым. В Qt подобная операция выглядит следующим образом:
// Создаём бегунки
QSlider *primary = new QSlider(this);
QSlider *secondary = new QSlider(this);
// Размещаем в слое, инициализируем
// ...
// Связываем перемещение первого со вторым
connect(primary, SIGNAL(valueChanged(int)), secondary, SLOT(setValue(int)));
В результате получаем связь сигнала valueChanged() бегунка primary со слотом setValue() бегунка secondary. То же самое на Android:
// Создаём бегунки
SeekBar firstBar = (SeekBar)findViewById(R.id.firstSeekBar);
SeekBar secondBar = (SeekBar)findViewById(R.id.secondSeekBar);
// Инициализируем
// ...
// Связываем перемещение первого со вторым
firstBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
@Override
// ........
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
secondBar.setProgress(progress);
}
});
И в результате получаем тоже самое, то есть связываем перемещение firstBar и secondBar.
Давайте разберём что здесь происходит. Где-то в недрах firstBar есть переменная типа OnSeekBarChangeListener которая при наступлении перемещения проверится на null и если вдруг окажется ненулевой, а так оно и случится, будет вызван её метод onProgressChanged() с соответствующими параметрами который в свою очередь вызовет secondBar.setProgress(progress) и установит значение второго бегунка.
Всё предельно ясно и понятно, хотя и несколько громоздко. Qt в данном случае более лаконичен, хотя для реализации динамического связывания выходит за рамки C++ догенеривая код в процессе сборки проекта с помощью MOC (Meta Object Compiler). За лаконичность приходится расплачиваться, и это становится очевидным когда в процессе отладки попадаешь в догенереный код. Но к счастью, если следовать простейшим правилам делать это приходится крайне редко.
Но вернёмся к Android. Все классы Android API имеют достаточный набор лисенеров чтобы обеспечить удобство их использования, но что делать если в наличии большой массив кода оперирующий сигналами и слотами? Погуглив, я нашёл несколько реализаций сигналов и слотов на Java, самая достойная из которых, что не удивительно, в составе библиотеки Qt Jambi [1], несправедливо забытой реализации Qt на Java. Отличная реализация однако не устроила меня по нескольким причинам, самая веская из которых, несоответствие синтаксиса оригиналу, странно что одна и та же технология в составе библиотек под C++ и Java реализована столь различно.
В результате появилась идея реализовать сигналы и слоты под Android на Java самостоятельно.
Реализовать на Java механизм сигналов и слотов максимально приближенный к синтаксису Qt C++ используя Android API.
После нескольких попыток был написан Java класс Connector менее чем о 600 строках имеющий несколько статических методов и статическую карту (Map) сигналов и слотов, по сути являющийся синглтоном (singleton). Весь функционал заключён в четырёх статических методах:
В примере с бегунками коннект выглядел бы так:
// К сожалению стандартные элементы Android API требуют реализации лиснера
private static class SeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
private Object mSender = null;
public SeekBarChangeListener(Object sender) {
mSender = sender;
}
@Override
// ........
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
Connector.emit(mSender, "progressChanged", progress);
}
}
// Создаём бегунки
SeekBar firstBar = (SeekBar)findViewById(R.id.firstSeekBar);
SeekBar secondBar = (SeekBar)findViewById(R.id.secondSeekBar);
// Инициализируем
// ...
// Учим firsBar посылать сигналы
firstBar.setOnSeekBarChangeListener(new SeekBarChangeListener(firstBar));
// Связываем перемещение первого со вторым
Connector.connect(firstBar, "SIGNAL(progressChanged(int))"
, secondBar, "SLOT(setProgress(int))");
С первого взгляда может показаться что реализация с помощью Connector'а более громоздка чем с помощью лисенера, но не стоит забывать что SeekBar класс заточенный под использование лисенера, как и все другие стандартные классы Android API, потому приходится использовать врапперы (wrappers). Гораздо большую выгоду можно получить используя коннектор при разработке своих классов, или портируя Qt проекты на Android:
Более сложные примеры, подробно о деталях реализации, читайте в следующей статье.
(Продолжение следует)
Автор: AlexanderOfitserov
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/19121
Ссылки в тексте:
[1] Qt Jambi: http://doc.qt.digia.com/qq/qq20-jambi.html#qtjambiresources
[2] Источник: http://habrahabr.ru/post/157363/
Нажмите здесь для печати.