Введение
Общее описание
Структура приложения
Уроки
Урок 1
Создание приложения, экраны, меню, список, локализация
Урок 2
Просмотр видео (youtube), панель ввода
Урок 3
Карты, PlusMinus, пользовательская обработка экранов
Урок 4
Завершение разработки, закрепление материала
Урок 5
Интро, авторизация, фото
Урок 6
Боковое меню, горизонтальный список, пагинация, раскрывающийся список, баркод-сканер, распознавание речи, поиск, листание страниц
Урок 7
Работа с базами данных SQLite, свайп, componentDateDiapason
Урок 8
Спиннер, календарь, пиккер и события
Урок 9
Работа с пуш сообщениями, BadgeTextView
Урок 10
Анимация, кастомные компоненты
Описание библиотеки
Приложения

Урок 3

На этом уроке мы закрепим пройденный материал, рассмотрим другие способы его применения и научимся работать с такими компонентами как карта и plusMinus. А также рассмотрим как подключать пользовательскую (кастомную) обработку для экранов

Постановка задачи (задание урока)

Нужно описать все экраны, на которые можно перейти с экрана REPAIRS_MAIN.

Их дизайн приведен на следующих рисунках:

Рис. 1 экран SERVICE
Рис. 1 экран SERVICE
Рис. 2 экран SERVICE
Рис. 2 экран SERVICE
Рис. 3 экран ITEM_FORM_REPAIR
Рис. 3 экран ITEM_FORM_REPAIR
Рис. 4 экран REPAIRS_CALC
Рис. 4 экран REPAIRS_CALC
Рис. 5 экран ITEM_FORM_SERVICE
Рис. 5 экран ITEM_FORM_SERVICE
Рис. 6 экран ITEM_FORM_SERVICE
Рис. 6 экран ITEM_FORM_SERVICE
Рис. 7 экран REPAIRS_CALC
Рис. 7 экран REPAIRS_CALC
Рис. 8 экран ITEM_FORM_SERVICE
Рис. 8 экран ITEM_FORM_SERVICE
Рис. 9 экран ITEM_FORM_SERVICE
Рис. 9 экран ITEM_FORM_SERVICE

На рисунках 1 и 2 изображены разные части одного и того же экрана (SERVICE) в двух состояниях: просто карта и после клика на маркер. На рисунках 4 и 7 также изображен один экран (REPAIRS_CALC) в двух состояниях - выбрано 2 сервиса и выбрано 3 сервиса. Аналогично и экраны на рисунках 5, 6, 8 и 9 изображен один (ITEM_FORM_SERVICE) с заполненными и незаполненными данными и с количеством выбранных сервисов 2 и 3.

Описание API

Экран SERVICE URL depro/services/markers, ответ:
[
    {
        "address":"Полтава, ул. Светлая 3б",
        "title":"ЦЕНТРАЛЬНЫЙ РЕГИОН",
        "phones":"+38 (068) 300-02-70 ",
        "latitude":49.548965,
        "longitude":34.537758,
        "stationId":632,
        "mark_id":1
    },
    {
        "address":"Сумы, ул. Кировоградская, 2",
        "title":"ЦЕНТРАЛЬНЫЙ РЕГИОН",
        "phones":"+38 (068) 300-02-70 ",
        "latitude":50.89666,
        "longitude":34.844627,
        "stationId":632,
        "mark_id":2
    },
.......
]

Экран SERVICE URL depro/services/service, ответ:
[
    {
        "serviceId":1422,
        "price":6100,
        "title":"Промывание гидравлики бензином (порция 200-300 грамм)"
    },
    {
        "serviceId":1421,
        "price":5000,
        "title":"Регулировочные работы"
    },
...
]

Экраны ITEM_FORM_SERVICE и ITEM_FORM_REPAIR URL depro/send/requestServices, метод POST отправка данных .
    {"name":"Петр","phone":"+38055555","comment":"Ремонт всего","stationId":"650",
        "serviceIds":[{"id":1421,"total":2},{"id":1419,"total":3},{"id":1418,"total":2}]}
    Для экрана ITEM_FORM_REPAIR поле serviceIds не передается.
    Ответ {"result":"oK"}

Карта показывается с начальными координатами центра latitude = 48.3794327, longitude = 31.1655807. Начальный зум = 5. Маркеры используются собственного дизайна - R.drawable.pin. При клике на любой маркер появляется нижняя панель с описанием соответствующего сервиса и кнопками “Оставить заявку” и “стоимость ремонтных услуг”. При клике на первую осуществляется переход на экран ITEM_FORM_REPAIR. При клике на вторую - на экран ITEM_FORM_SERVICE.

Работа экрана ITEM_FORM_REPAIR аналогична работе экрана ITEM_FORM со второго урока.

На экране REPAIRS_CALC показывается список возможных ремонтов с указанием стоимости работ. Во всем приложении стоимости указаны в копейках. Имеется возможность установить количество работ кнопками + и -. Общая сумма пересчитывается при каждом изменении количества ремонтов.

Работа экрана ITEM_FORM_SERVICE частично совпадает с работой экрана ITEM_FORM_REPAIR. Дополнительно в нем имеется список ремонтов для которых количество больше нуля и которые устанавливаются на экране REPAIRS_CALC. На экране ITEM_FORM_SERVICE количество позиций в списке не более двух. При клике на “весь список” будут показаны все ремонты с ненулевым количеством. Имеется возможность удалить из списка некоторые виды ремонта, либо удалить все. Изменения в списке ремонтов должны отображаться на экране REPAIRS_CALC в случае возврата на него.

Описание экранов

Добавим в класс MyDeclareScreens.java описание всех указанных в постановке экранов (приведены ниже), а затем поясним новые компоненты.

    fragment(SERVICE, R.layout.fragment_service).animate(AS.RL)
        .componentMap(R.id.map, 
            model(API.MARKER_MAP), 
            new ParamMap(true).levelZoom(5f)
                .coordinateValue(48.3794327, 31.1655807)
                .markerImg(0, R.drawable.pin)
                .markerClick(R.id.infoWindow, true),
            navigator(start(R.id.requisition, ITEM_FORM_REPAIR), back(R.id.requisition),
                handler(R.id.phones, VH.DIAL_UP),
                handler(R.id.cost, VH.GET_DATA, model(API.REPAIRS)
                        .addField("total,amount", Field.TYPE_INTEGER, 0),
                    after(handler(0, VH.SET_GLOBAL, "services"),
                        start(R.id.cost, REPAIRS_CALC))),
                back(R.id.cost)));

    fragment(REPAIRS_CALC, R.layout.fragment_repairs_calc).animate(AS.RL)
        .navigator(back(R.id.back),
            start(R.id.request, ITEM_FORM_SERVICE))
        .plusMinus(R.id.total, R.id.plus, R.id.minus, null,
            new Multiply(0, "price", "amount"))
        .list(model(GLOBAL, "services"),
            view(R.id.recycler, R.layout.item_repairs_calc))
        .componentTotal(R.id.sum, R.id.recycler, R.id.total, null, "amount");

    fragment(ITEM_FORM_REPAIR, R.layout.fragment_item_form_repair).animate(AS.RL)
        .navigator(back(R.id.back))
        .component(TC.PANEL_ENTER, null,
            view(R.id.panel),
            navigator(handler(R.id.country, COUNTRY_CODE_PH, after(assignValue(R.id.codePlus))),
                handler(R.id.add_comment, COMMENT, PS.RECORD, "comment",
                    after(assignValue(R.id.comment), show(R.id.panel_comment))),
                handler(R.id.edit, COMMENT, PS.RECORD, "comment", after(assignValue(R.id.comment))),
                handler(R.id.apply, VH.CLICK_SEND,
                    model(POST, API.SEND_SERVICES, "name,phone,comment,stationId"),
                    after(start(THANKS)))))
        .enabled(R.id.apply, R.id.name,  R.id.phone);

    fragment(ITEM_FORM_SERVICE, R.layout.fragment_item_form_service, ItemServiceMore.class).animate(AS.RL)
        .navigator(handler(R.id.clear, VH.CLICK_VIEW),
            handler(R.id.all_list, VH.CLICK_VIEW), back(R.id.back))
        .list(model(GLOBAL, "services").filters(2, filter("total", FO.more, 0)),
            view(R.id.recycler, R.layout.item_form_service),
            navigator(handler(R.id.delete, VH.CLICK_VIEW)))
        .component(TC.PANEL_ENTER, null,
            view(R.id.panel),
            navigator(handler(R.id.country, COUNTRY_CODE_PH, after(assignValue(R.id.codePlus))),
                handler(R.id.add_comment, COMMENT, PS.RECORD, "comment",
                    after(assignValue(R.id.comment), show(R.id.panel_comment))),
                handler(R.id.edit, COMMENT, PS.RECORD, "comment", after(assignValue(R.id.comment))),
                handler(R.id.apply, VH.CLICK_SEND,
                    model(POST, API.SEND_SERVICES, "name,phone,comment,stationId"),
                    after(start(THANKS)), false)))
        .enabled(R.id.apply, R.id.name,  R.id.phone);

Экран ITEM_FORM_SERVICE использует класс ItemServiceMore с пользовательской обработкой. На этом этапе нужно создать этот класс (файл), его содержание приведено ниже. В конце урока мы опишем работу этого класса.

Так как у нас используется карта, то необходимо получить ключ для нашего приложения. Детальная инструкция находится здесь. Этот ключ заносим в строковый ресурс с именем (в нашем случае) google_maps_key. А в манифесте устанавливаем в элементах meta-data информацию об этом ключе:

    <meta-data
        android:name="com.google.android.geo.API_KEY"
        android:value="@string/google_maps_key"/>

а также задать uses-permission ACCESS_FINE_LOCATION или ACCESS_COARSE_LOCATION

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
и / или
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

Если все введено правильно, то при старте приложения будут отображаться (и работать) все указанные экраны.

Теперь объясним те новые конструкции, которые были использованы при описании экранов урока.

Объяснение работы новых компонентов

Экран SERVICE

Здесь один новый компонент - карта (componentMap). Первый параметр - R.id.map - указывает на элемент разметки, который должен быть MapView или его наследник. Если Вы внимательно смотрели дизайн экрана SERVICE, то обратили внимание, что элементы управления зумом (+-) и location используются не стандартные, а фантазия наших дизайнеров. Поэтому вместо MapView мы используем элемент ComponMapView, для которого задаем атрибуты zoomPlus, zoomMinus и location.

Модель задается стандартно и указывает откуда будем получать список маркеров.

Параметры карты в библиотеке декларативного программирования DePro задаются конструкцией new ParamMap(true) с дополнительным функционалом. Здесь параметр true указывает на необходимость использования локализации смартфона; levelZoom задает начальный зум карты; coordinateValue - начальные координаты центра карты; markerImg - иконки маркеров; markerClick - указывает панель (SheetBottom) которая будет показана при клике на маркер. В ней будут отображаться данные маркера. Навигатор применяется для этой панели.

Новые обработчики навигатора. back(R.id.requisition) указывает, что при клике на элемент разметки с id = requisition нужно закрыть панель. Обработчик handler(R.id.cost, VH.GET_DATA, model(API.REPAIRS).addField("total,amount", Field.TYPE_INTEGER, 0), after(handler(0, VH.SET_GLOBAL, "services") вводит список ремонтных работ.

В постановке задачи указано, что каждый из экранов REPAIRS_CALC и ITEM_FORM_SERVICE могут изменять информацию о выбранных ремонтах с отображением на этих изменений на другом. Это возможно если они будут иметь доступ к одним и тем же данным. Для этих целей в библиотеке предусмотрен механизм глобальных переменных. Обработчик handler(0, VH.SET_GLOBAL, "services") записывает полученные данные о ремонтах в глобальную переменную с именем "services". На экране REPAIRS_CALC мы будем устанавливать значения количества ремонтов. Так как в структуре исходных данных нет соответствующего поля, то для модели используется дополнительный функционал .addField("total,amount", Field.TYPE_INTEGER, 0) который к каждой записи исходных данных добавляет поля total и amount.

Экран REPAIRS_CALC

Компонент типа list стандартный, единственно в его модели указано использовать глобальную переменную "services".

Компонент .plusMinus(R.id.total, R.id.plus, R.id.minus, null, new Multiply(0, "price", "amount")) обеспечивает по клику на View R.id.plus или R.id.minus увеличение или уменьшение значения элемента R.id.total. Элемент R.id.total - это библиотечный класс PlusMinus. Конструкция Multiply обеспечивает умножение значения поля "price" на значение элемента total и занесение результата в переменную "amount". Конструкций Multiply может быть много.

В принципе, компонент plusMinus работает независимо. Однако в связке с компонентом componentTotal его поведение существенно изменяется.

Компонент componentTotal(R.id.sum, R.id.recycler, R.id.total, null, "amount") служит для подсчета итогов в списке (компоненте типа list) и занесения суммы в элемент R.id.sum. В нашем случае используется расширенная форма этого компонента. А именно, указан третий параметр (R.id.total) который указывает при изменении какого компонента нужно перезапускать componentTotal. Кроме того, если он (R.id.total) соответствует как в нашем случае компоненту plusMinus, то plusMinus связывается со всеми элементами списка компонента типа list. Поэтому такие элементы компонента plusMinus как R.id.total, R.id.plus, R.id.minus находятся в R.layout.item_repairs_calc, а переменные "price" и "amount" относятся к каждой записи списка.

Таким образом при клике на R.id.plus, R.id.minus каждого элемента списка будет изменено количество и сумма ("amount") этого элемента, а также будет выполнен пересчет итогов в componentTotal.

Экран ITEM_FORM_SERVICE

Практически все в описании этого экрана нам знакомо. Требует пояснения дополнительный для модели функционал .filters(2, filter("total", FO.more, 0)). В наше конкретном случае он значит, что данные модели фильтруются. В результирующий список будет включено не более 2 элементов, у которых переменная "total" больше нуля.

В описании экрана третьим параметром указан ItemServiceMore.class. Он служит для задания пользовательских (кастомных) действий) при работе экрана. Потребность в такой обработке иногда возникает если средствами библиотеки нельзя описать все нужные действия. Этот класс для нашего экрана имеет вид:

public class ItemServiceMore extends MoreWork {

    // розрахунок суми
    @Override
    public void afterChangeData(BaseComponent baseComponent) {
        if (baseComponent.paramMV.paramView.viewId == R.id.recycler) {
            int count = 0;
            if (baseComponent.listRecords != null) {
                int sum = 0;
                for (Record rec : baseComponent.listRecords) {
                    int amount = rec.getInt("amount");
                    if (amount > 0) {
                        count++;
                        sum += amount;
                    }
                }
                ComponTextView tv = (ComponTextView) parentLayout.findViewById(R.id.sum);
                if (tv != null) {
                    tv.setData(sum);
                }
                View v = parentLayout.findViewById(R.id.all_list);
                if (count > 2) {
                    v.setVisibility(View.VISIBLE);
                } else {
                    v.setVisibility(View.GONE);
                }
                View v1 = parentLayout.findViewById(R.id.clear);
                if (count > 0) {
                    v1.setVisibility(View.VISIBLE);
                } else {
                    v1.setVisibility(View.GONE);
                }
            }
        }
    }

    @Override
    public void clickView(View viewClick, View parentView, BaseComponent baseComponent, Record rec, int position) {
        BaseComponent bc = screen.getComponent(R.id.recycler);
        if (bc == null) return;
        switch (viewClick.getId()) {
            case R.id.clear:
                if (bc.listRecords != null) {
                    for (Record rec1 : bc.listRecords) {
                        Field ff = rec1.getField("amount");
                        ff.value = 0;
                        ff = rec1.getField("total");
                        ff.value = 0;
                    }
                    bc.setFilterData();
                }
                break;
            case R.id.delete:
                if (rec != null) {
                    Field ff = rec.getField("amount");
                    ff.value = 0;
                    ff = rec.getField("total");
                    ff.value = 0;
                    bc.setFilterData();
                }
                break;
            case R.id.all_list:
                TextView all_list = parentView.findViewById(R.id.all_list);
                Filters filters = bc.paramMV.paramModel.filters;
                if (filters.maxSize < Integer.MAX_VALUE) {
                    filters.maxSize = Integer.MAX_VALUE;
                    all_list.setText(activity.getString(R.string.hide));
                } else {
                    filters.maxSize = 2;
                    all_list.setText(activity.getString(R.string.all_list));
                }
                bc.setFilterData();
                break;
        }
    }

    @Override
    public void setPostParam(int viewId, Record rec) {
        if (viewId == R.id.apply) {
            ListRecords serv = new ListRecords();
            RecyclerComponent rc = (RecyclerComponent)screen.getComponent(R.id.recycler);
            for (Record rr : rc.listRecords) {
                int total = rr.getInt("total");
                if (total > 0) {
                    Record recServ = new Record();
                    recServ.add(new Field("id", Field.TYPE_INTEGER, rr.getInt("serviceId")));
                    recServ.add(new Field("total", Field.TYPE_INTEGER, total));
                    serv.add(recServ);
                }
            }
            rec.add(new Field("serviceIds", Field.TYPE_LIST_RECORD, serv));
        }
    }
}

Этот файл нужно создать.

Описание работы пользовательских классов

В начале опишем кратко структуру данных в библиотеке. Минимальной порцией данных является поле (Field) оно характеризуется именем, типом, значением. Типы могут быть: TYPE_STRING, TYPE_INTEGER, ... Совокупность полей является запись (Record). Массив (список) записей является ListRecords. Поля могут быть также типа TYPE_RECORD и TYPE_LIST_RECORD. С некоторой натяжкой можно провести аналогию между библиотечным типом Record и JSONObject в JSON.org.

Класс ItemServiceMore наследуется от библиотечного класса MoreWork, который содержит много методов обратного вызова библиотеки. В данном случае мы используем только три метода:

- afterChangeData вызывается после изменения данных в компоненте. В нашем случае он используется для подсчета суммы по выбранным ремонтам и управляет видимостью кнопок. Так как у нас может быть несколько компонентов, то мы предусматриваем обработку только для интересующего нас компонента (if (baseComponent.paramMV.paramView.viewId == R.id.recycler) { );

- clickView вызывается при кликах на элементы разметки. Метод будет вызываться только для тех элементов которым в навигаторе обработчик задает тип обработки CLICK_VIEW. В нашем случае предусмотрена обработка кликов на R.id.clear - очистка всего списка; R.id.delete (изображение крестика на элементе списка) - удаление из списка одного элемента

- setPostParam вызывается при формировании данных для отсылки на сервер (метод POST). Те дополнительные данные, которые разработчик хочет передать на сервер, он формирует в виде полей (Field) и добавляет в запись (rec).

Лучше пакета DePro может быть только искусственный интеллект
Задать вопрос
Отправить вопрос