Мобильное приложение Android
1. Создаем новый проект на основании сгенерированого шаблона. Для этого в Eclipse необходимо выполнить импорт, как это сделать подробно описано здесь.
2. Займемся картой. В состав FBA включены вспомогательные классы, облегчающие новичку работу с офлайн картами Mapsforge. Один из таких классов – BaseRouteMapActivity, активити предназначенная для отображения точек маршрута и навигации по ним. Его и будем использовать.
2.1 Создаем свой класс, наследник от BaseRouteMapActivity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | public class MapsforgeRouteMap extends BaseRouteMapActivity { /* * На сколько должна изменится текущая позиция (в метрах) прежде чем будет * получено новое значение координат */ public static int DEF_LOCATION_MIN_DISTANCE = 500; /* * Максимальное время, которое должно пройти, прежде чем пользователь * получает обновление местоположения. */ public static long DEF_LOCATION_MIN_TIME = 5000 * 60; //Москва, нулевой километр private static final double DEFAULT_GEOPOINT_LAT = 55.755831; private static final double DEFAULT_GEOPOINT_LNG = 37.617673; //Файл карты private static final String MAP_FILE = "ru_moscow.map"; private MapView mMapView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_route_map); init(); } private void init() { mMapView = (MapView) findViewById(R.id.mapView); //Инициализация карты onCreateMapView(mMapView); } @Override public File getMapFile() { return new File(getAppSettings().getBuckupDir(),MAP_FILE); } @Override public GeoPoint getMapCenterPoint() { return MapHelper.toGeoPoint(DEFAULT_GEOPOINT_LAT, DEFAULT_GEOPOINT_LNG); } @Override public int getLocationMinDistance() { return DEF_LOCATION_MIN_DISTANCE; } @Override public long getLocationMinTime() { return DEF_LOCATION_MIN_TIME; } @Override public boolean showCurrentPosition() { return true; } @Override public boolean requestLocationUpdates() { return true; } @Override protected void onRouteItemSelect(RouteOverlayItem<?> item) { } @Override public void onLocationChanged(Location location) { } } |
public class MapsforgeRouteMap extends BaseRouteMapActivity { /* * На сколько должна изменится текущая позиция (в метрах) прежде чем будет * получено новое значение координат */ public static int DEF_LOCATION_MIN_DISTANCE = 500; /* * Максимальное время, которое должно пройти, прежде чем пользователь * получает обновление местоположения. */ public static long DEF_LOCATION_MIN_TIME = 5000 * 60; //Москва, нулевой километр private static final double DEFAULT_GEOPOINT_LAT = 55.755831; private static final double DEFAULT_GEOPOINT_LNG = 37.617673; //Файл карты private static final String MAP_FILE = "ru_moscow.map"; private MapView mMapView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_route_map); init(); } private void init() { mMapView = (MapView) findViewById(R.id.mapView); //Инициализация карты onCreateMapView(mMapView); } @Override public File getMapFile() { return new File(getAppSettings().getBuckupDir(),MAP_FILE); } @Override public GeoPoint getMapCenterPoint() { return MapHelper.toGeoPoint(DEFAULT_GEOPOINT_LAT, DEFAULT_GEOPOINT_LNG); } @Override public int getLocationMinDistance() { return DEF_LOCATION_MIN_DISTANCE; } @Override public long getLocationMinTime() { return DEF_LOCATION_MIN_TIME; } @Override public boolean showCurrentPosition() { return true; } @Override public boolean requestLocationUpdates() { return true; } @Override protected void onRouteItemSelect(RouteOverlayItem<?> item) { } @Override public void onLocationChanged(Location location) { } }
Отображать будем карту Москвы, которую необходимо поместить на эмулятор в каталог /mnt/sdcard/backup/<имя пакета>. Этот каталог будет создан автоматически при первом запуска программы. Где взять файл карты или как создать самому, смотрите в конце статьи.
При запуске наша карта будет отцентрирована по координатам переданным в методе getMapCenterPoint(). Функция showCurrentPosition() должна возвращать true, если вы ходите отображать маркер с вашей текущей позицией.
Если методом requestLocationUpdates() включено отслеживание координат, то при изменении будет вызываться обработчик onLocationChanged. Настройки частоты обновления координат задаются через getLocationMinDistance() и getLocationMinTime(). По умолчанию используются оба провайдера (GPS и NETWORK) для получения координат, изменить можно в BaseMapActivity т.к исходный код открыт.
В макете activity_route_map.xml размещаем элемент карты:
1 2 3 4 5 | <org.mapsforge.android.maps.MapView android:id="@+id/mapView" android:layout_width="fill_parent" android:layout_height="fill_parent" > </org.mapsforge.android.maps.MapView> |
<org.mapsforge.android.maps.MapView android:id="@+id/mapView" android:layout_width="fill_parent" android:layout_height="fill_parent" > </org.mapsforge.android.maps.MapView>
В принципе, этого достаточно для отображения карты. Также на карте отображается маркер с текущей позицией и есть возможность обработать событие при изменении координат.
2.2 Добавление слоя с точками маршрута выполняется с помощью метода addRouteOverlay, в который передается список элементов List<RouteOverlayItem<T>>. T это тип доп. данных хранимых вместе с элементом, в вашем случае это CatalogSalesPoint. Подготовим список наших торговых точек:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | private void initDummyRoute() throws SQLException { //DAO для отображения картинки и для записи координат mStorageDao = new CatalogAddInfoStorageDao(getConnectionSource()); mSalesPointDao = new CatalogSalesPointDao(getConnectionSource()); // Выбрать все торговые точки CatalogSalesPointDao salePointDao = new CatalogSalesPointDao(getConnectionSource()); List<CatalogSalesPoint> lst = salePointDao.select(); // Список точек маршрута отображаемый на карте routeItems = new ArrayList<RouteOverlayItem<CatalogSalesPoint>>(); // Для точек, у которых не установлены координаты final double lng = DEFAULT_GEOPOINT_LNG; double lat = DEFAULT_GEOPOINT_LAT; int count = lst.size(); for (int i = 0; i < count; i++) { CatalogSalesPoint salesPoint = lst.get(i); boolean movable = (salesPoint.lat == 0 || salesPoint.lng == 0); int resIdDrawable = R.drawable.fba_map_maker_green; GeoPoint geoPoint = new GeoPoint(salesPoint.lat, salesPoint.lng); if (movable) { resIdDrawable = R.drawable.fba_map_marker_red; lat -= 0.003f; geoPoint = new GeoPoint(lat, lng); } //На маркере вывести порядковый номер Drawable marker = MapHelper.makeNumberedMarker(this, resIdDrawable,i + 1); RouteOverlayItem<CatalogSalesPoint> routePoint = new RouteOverlayItem<CatalogSalesPoint>( geoPoint, marker, salesPoint); routePoint.setMovable(movable); routePoint.setOrdinal(i + 1); routeItems.add(routePoint); } Drawable defaultMarker = getResources().getDrawable(R.drawable.fba_map_marker_orange); addRouteOverlay(mMapView, routeItems, defaultMarker); } |
private void initDummyRoute() throws SQLException { //DAO для отображения картинки и для записи координат mStorageDao = new CatalogAddInfoStorageDao(getConnectionSource()); mSalesPointDao = new CatalogSalesPointDao(getConnectionSource()); // Выбрать все торговые точки CatalogSalesPointDao salePointDao = new CatalogSalesPointDao(getConnectionSource()); List<CatalogSalesPoint> lst = salePointDao.select(); // Список точек маршрута отображаемый на карте routeItems = new ArrayList<RouteOverlayItem<CatalogSalesPoint>>(); // Для точек, у которых не установлены координаты final double lng = DEFAULT_GEOPOINT_LNG; double lat = DEFAULT_GEOPOINT_LAT; int count = lst.size(); for (int i = 0; i < count; i++) { CatalogSalesPoint salesPoint = lst.get(i); boolean movable = (salesPoint.lat == 0 || salesPoint.lng == 0); int resIdDrawable = R.drawable.fba_map_maker_green; GeoPoint geoPoint = new GeoPoint(salesPoint.lat, salesPoint.lng); if (movable) { resIdDrawable = R.drawable.fba_map_marker_red; lat -= 0.003f; geoPoint = new GeoPoint(lat, lng); } //На маркере вывести порядковый номер Drawable marker = MapHelper.makeNumberedMarker(this, resIdDrawable,i + 1); RouteOverlayItem<CatalogSalesPoint> routePoint = new RouteOverlayItem<CatalogSalesPoint>( geoPoint, marker, salesPoint); routePoint.setMovable(movable); routePoint.setOrdinal(i + 1); routeItems.add(routePoint); } Drawable defaultMarker = getResources().getDrawable(R.drawable.fba_map_marker_orange); addRouteOverlay(mMapView, routeItems, defaultMarker); }
и добавим вызов этого метода в init().
Здесь из локальной базы выбираются все элементы справочника “Торговые точки” и добавляются в коллекцию отображаемых элементов. Причем для точек, у которых установлены координаты в 1С, маркер будет зеленого цвета. Точки без координат размещаются по центру карты с маркером красного цвета. Пользователь может перемещать их по карте с целью уточнения координат.
2.3 Давайте добавим реакцию на клик по маркеру в обработчик onRouteItemSelect:
1 2 | CatalogSalesPoint salesPoint = (CatalogSalesPoint) item.getData(); inflatePopup(salesPoint); |
CatalogSalesPoint salesPoint = (CatalogSalesPoint) item.getData(); inflatePopup(salesPoint);
Поля класса и метод отображающий дополнительную информацию о торговой точке:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | private ObjectView mSalesPointView, mFotoStorageView; /* * Имена реквизитов отображаемых в всплывающем окне (имена полей класса) */ private static String[] fields = new String[] { CatalogSalesPoint.FIELD_NAME_DESCRIPTION, CatalogSalesPoint.FIELD_NAME_ADRESS, CatalogSalesPoint.FIELD_NAME_PHONE, CatalogSalesPoint.FIELD_NAME_SITE }; /* * Идентификаторы view-элементов для отображения реквизитов */ private static int[] ids = new int[] { R.id.tvDescription, R.id.tvAdress, R.id.tvPhone, R.id.tvSite}; /* * Заполнить данные по торговой точке на всплывающем окне и отобразить его */ private void inflatePopup(CatalogSalesPoint salesPoint) throws SQLException { if (CatalogSalesPoint.isEmpty(salesPoint.foto)) { mFotoStorageView.setVisibility(View.GONE); } else { //Прочитать ссылку на фото mStorageDao.refresh(salesPoint.foto); mFotoStorageView.build(salesPoint.foto, getHelper(), new String[] { CatalogAddInfoStorage.FIELD_NAME_STORAGE }, new int[] { R.id.ivFoto }); mFotoStorageView.setVisibility(View.VISIBLE); } mSalesPointView.build(salesPoint, getHelper(), fields, ids); } |
private ObjectView mSalesPointView, mFotoStorageView; /* * Имена реквизитов отображаемых в всплывающем окне (имена полей класса) */ private static String[] fields = new String[] { CatalogSalesPoint.FIELD_NAME_DESCRIPTION, CatalogSalesPoint.FIELD_NAME_ADRESS, CatalogSalesPoint.FIELD_NAME_PHONE, CatalogSalesPoint.FIELD_NAME_SITE }; /* * Идентификаторы view-элементов для отображения реквизитов */ private static int[] ids = new int[] { R.id.tvDescription, R.id.tvAdress, R.id.tvPhone, R.id.tvSite}; /* * Заполнить данные по торговой точке на всплывающем окне и отобразить его */ private void inflatePopup(CatalogSalesPoint salesPoint) throws SQLException { if (CatalogSalesPoint.isEmpty(salesPoint.foto)) { mFotoStorageView.setVisibility(View.GONE); } else { //Прочитать ссылку на фото mStorageDao.refresh(salesPoint.foto); mFotoStorageView.build(salesPoint.foto, getHelper(), new String[] { CatalogAddInfoStorage.FIELD_NAME_STORAGE }, new int[] { R.id.ivFoto }); mFotoStorageView.setVisibility(View.VISIBLE); } mSalesPointView.build(salesPoint, getHelper(), fields, ids); }
Для отображения всплывающего окна используется макет map_popup.xml (разметка приведена частично):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <ru.profi1c.engine.widget.ObjectView android:id="@+id/ovSalesPoint"> <ru.profi1c.engine.widget.ObjectView android:id="@+id/ovFotoStorage" > <ImageView android:id="@+id/ivFoto"/> </ru.profi1c.engine.widget.ObjectView> <TextView android:id="@+id/tvDescription"/> <TextView android:id="@+id/tvAdress"/> <TextView android:id="@+id/tvSite" android:autoLink="web"/> <TextView android:id="@+id/tvPhone" android:autoLink="phone"/> </ru.profi1c.engine.widget.ObjectView> |
<ru.profi1c.engine.widget.ObjectView android:id="@+id/ovSalesPoint"> <ru.profi1c.engine.widget.ObjectView android:id="@+id/ovFotoStorage" > <ImageView android:id="@+id/ivFoto"/> </ru.profi1c.engine.widget.ObjectView> <TextView android:id="@+id/tvDescription"/> <TextView android:id="@+id/tvAdress"/> <TextView android:id="@+id/tvSite" android:autoLink="web"/> <TextView android:id="@+id/tvPhone" android:autoLink="phone"/> </ru.profi1c.engine.widget.ObjectView>
На макете отображается информация по одной торговой точке, реквизиты “Наименование”, “Адрес”, “Сайт” и “Телефон” автоматически проецируются на TextView-элементы в mSalesPointView.build(…).
Для отображения фото используется подчиненный объект ovFotoStorage с дочерним ImageView. Фото считывается из базы и проецируется на ImаgeView в методе mFotoStorageView.build(…).
А так как в реквизите “Фото” хранится только ссылка на справочник “Хранилище дополнительной информации”, ее необходимо предварительно считать методом refresh.
Инициализацию добавляем в метод в init()
1 2 | mSalesPointView = (ObjectView) findViewById(R.id.ovSalesPoint); mFotoStorageView = (ObjectView) findViewById(R.id.ovFotoStorage); |
mSalesPointView = (ObjectView) findViewById(R.id.ovSalesPoint); mFotoStorageView = (ObjectView) findViewById(R.id.ovFotoStorage);
Макет map_popup.xml можно установить как кастомный для Toast, но в этом примере он встроен в основной макет activity_route_map.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" > <org.mapsforge.android.maps.MapView android:id="@+id/mapView" android:layout_width="fill_parent" android:layout_height="fill_parent" > </org.mapsforge.android.maps.MapView> <include android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_marginTop="10dp" layout="@layout/map_popup" android:visibility="gone" /> </FrameLayout> |
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" > <org.mapsforge.android.maps.MapView android:id="@+id/mapView" android:layout_width="fill_parent" android:layout_height="fill_parent" > </org.mapsforge.android.maps.MapView> <include android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_marginTop="10dp" layout="@layout/map_popup" android:visibility="gone" /> </FrameLayout>
Для управления видимостью используются методы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /* * Скрыть всплывающее окно */ private void closePopup(){ if(mSalesPointView.getVisibility() == View.VISIBLE){ mSalesPointView.setVisibility(View.GONE); } } /* * Показать всплывающее окно */ private void showPopup(){ if(mSalesPointView.getVisibility() != View.VISIBLE){ mSalesPointView.setVisibility(View.VISIBLE); } } |
/* * Скрыть всплывающее окно */ private void closePopup(){ if(mSalesPointView.getVisibility() == View.VISIBLE){ mSalesPointView.setVisibility(View.GONE); } } /* * Показать всплывающее окно */ private void showPopup(){ if(mSalesPointView.getVisibility() != View.VISIBLE){ mSalesPointView.setVisibility(View.VISIBLE); } }
Скрывается всплывающее сообщение по клику на нем, а отображается при нажатии на маркер точки.
Приложение готово, запустите его на эмуляторе, настройте параметры авторизации (как в 1С) и выполните обмен с базой. Пример работы приложения:
Клик по номеру телефона откроет стандартную “звонилку”, а по клику на ссылку будет запущен браузер. Такое поведение достигается простой установкой свойства android:autoLink для TextView.
Полный код примера доступен в SVN хранилище: https://xp-dev.com/svn/fba_toolkit_public/samples/fbaSampleAuditPoint/
Дополнительно в нем показано как сохранить координаты у перемещенных точек и обновить маркер.
Где взять карты
Некоторые карты планируем размещать на нашем сайте, приоритет у городов (областей) непокрытых с достаточной детализацией на Google Maps и карт, подготовленных для клиентов FBA. Краткая инструкция по созданию файла карты приведена там же.
Надеюсь, мне удалось хоть чуть-чуть развеять миф о том, что разработка мобильных бизнес-приложений это «сложно, дорого и долго».