
Часто під час розробки проєкту виникає ситуація, коли поставлені завдання важко вирішити за допомогою наявних стандартних інструментів розробки. Тоді на допомогу приходять сторонні ресурси у вигляді бібліотек та інших корисностей. Так сталося і з нашим новоствореним застосунком MegaSOS для платформи Android. Але давайте по порядку розглянемо кожну деталь.
Android — це найшвидше зростаюча мобільна операційна система, заснована на ядрі Linux, яка має власну реалізацію Java від Google. Це дітище пошукового гіганта, який не лише випустив платформу з відкритим вихідним кодом, але й створив Open Handset Alliance, завдяки якій у нас є SDK у нашому розпорядженні.
Google адаптував багато своїх сервісів під мобільну платформу, і Google Maps є одним з них. Саме про це ми сьогодні і говоритимемо.
API Google Maps для Android v2 є досить актуальним на даний момент. Думаю, немає сенсу говорити про те, як підключити сервіс, отримати ключі та почати працювати — таку інформацію легко знайти в Інтернеті. Тож перейдемо до суті.
При проєктуванні завдання полягало в тому, щоб відобразити маркери на карті, які діляться на кілька типів (за допомогою кластеризації маркерів Google Maps). Але, наприклад, сусідні маркери при зумі накладалися один на одного, і все виглядало як одне безперервне розмиття. Їх потрібно було згрупувати - кластеризувати - з огляду на тип кожного маркера. Стандартний інструментарій виглядав незадовільно для вирішення таких проблем. Звісно, можна було створити свій клас, який успадковує маркери, і одночасно розширити його. Але навіщо винаходити велосипед, якщо подібну проблему можна було вирішити раніше? Після ретельного пошуку ми знайшли дві бібліотеки, які розроблені, якщо не повністю, то в значній мірі, щоб спростити наше життя без читання величезних посібників. Це невибагливі Android Maps Extensions і іноді неперевершений Clusterkraft.
Android Maps Extensions
Ця бібліотека розширює можливості сервісу GoogleMap та супутніх функцій для Android API v2. Її головна перевага — це настільки необхідна для нас кластеризація перекриваючих маркерів. Це можна побачити на наступних скріншотах:
Розробники вказали цілий список класів і методів, які можна використовувати для згаданих завдань:
- Кластеризація маркерів за допомогою
GoogleMap.setClustering(ClusteringSettings)
ClusteringSettings.clusterOptionsProvider(ClusterOptionsProvider)
є обов'язковим для коректної роботиClusteringSettings.addMarkersDynamically
при наявності занадто великої кількості маркерівClusteringSettings.clusterSize
для контролю... розміру кластеруObject getData()
таsetData(Object)
наMarker, Circle, GroundOverlay, Polygon, Polyline
абоTileOverlay
List
таGoogleMap.getMarkers() List
GoogleMap.getCircles() List
тощо.GoogleMap.getGroundOverlays() boolean Circle.contains(LatLng)
Marker GoogleMap.getMarkerShowingInfoWindow()
List
GoogleMap.getDisplayedMarkers() float GoogleMap.getMinZoomLevelNotClustered(Marker)
Marker.animatePosition(LatLng target, AnimationSettings settings)
Marker.setClusterGroup(int)
На момент розробки нашого проєкту ця бібліотека не могла посилатися на різні групи маркерів. Це призвело до кластеризації маркерів у Google Maps абсолютно скрізь, тому врешті-решт ми вирішили не використовувати її. Ми повідомили про проблему розробникам бібліотеки, які, в свою чергу, не залишили нас без уваги, і тепер у нас є чудовий і зручний метод Marker.setClusterGroup(int)
.
Бібліотеку легко використовувати, для її інтеграції потрібно просто імпортувати її як зовнішній модуль. Потім, використовуючи основну активність SupportMapFragment
або MapView
в xml-файлі, викликайте getExtendedMap
замість getMap
, який використовується в стандартній бібліотеці.
Clusterkraft
Цю бібліотеку було використано в цьому проєкті замість Maps Extensions, і вона одразу ж довела свою позитивну сторону. По-перше, її робота виявилася набагато більш стабільною, динамічне додавання маркерів працювало набагато швидше, а кластеризація виконувалася з приємною анімацією. По-друге, вона показала клас, який надає можливість замінити іконку кластеру маркерів залежно від його вмісту.
Для інтеграції бібліотеку імпортували як модуль.
Макет активності, що відображає карту, виглядає так:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".Map"> <FrameLayout android:id="@+id/frame_map" android:layout_width="match_parent" android:layout_height="match_parent" class="com.stfalcon.megasos.views.MySupportMapFragment"> </FrameLayout> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/transparent" /> </RelativeLayout>
У цьому випадку для карти був використаний FrameLayout - frame_map
. Порожній FrameLayout
з прозорим фоном виконував роль «магічного заклинання», яке вирішувало проблему з відображенням карти на пристроях з версіями до Ice Cream Sandwich (коли відкривається/закривається спливаюче меню або чорна область у формі вікна залишалася на карті).
Метод onCreate
виглядає наступним чином:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_map); SupportMapFragment mapFragment = SupportMapFragment.newInstance(); getSupportFragmentManager().beginTransaction().add(R.id.frame_map, mapFragment, "MAP").commit(); }
У методі onPostCreate
ініціалізується карта, і ми можемо додати маркер:
@Override public void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); GoogleMap myMap = ((SupportMapFragment) getSupportFragmentManager().findFragmentByTag("MAP")).getMap(); myMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); initMap(); MarkerOptions options = new MarkerOptions() .anchor(0.5f, 0.5f) .position(new LatLng(latitude, longitude)) .draggable(false) .icon(BitmapDescriptorFactory .fromResource(marker_resource)); myMap.addMarker(options); updateMap(); initMap(); }
Ініціалізація карти:
private void initMap() { if (map == null) { SupportMapFragment mapFragment = ((SupportMapFragment) getSupportFragmentManager().findFragmentByTag("MAP")); if (mapFragment != null) { map = mapFragment.getMap(); if (map != null) { UiSettings uiSettings = map.getUiSettings(); uiSettings.setAllGesturesEnabled(false); uiSettings.setScrollGesturesEnabled(true); uiSettings.setZoomGesturesEnabled(true); map.setOnCameraChangeListener(new GoogleMap.OnCameraChangeListener() { @Override public void onCameraChange(CameraPosition arg0) { moveMapCameraToBoundsAndInitClusterkraf(); MapPointsProvider.getInstance().generate(mCallback, check_sos, check_company); initMap(); } }); } } } else { moveMapCameraToBoundsAndInitClusterkraf(); } }
Встановлення камери на попередню позицію при зміні орієнтації:
private void moveMapCameraToBoundsAndInitClusterkraf() { if (map != null && options != null && inputPoints != null) { try { if (restoreCameraPosition != null) { /** * якщо доступна restoreCameraPosition, перемістіть камеру * туди */ map.moveCamera(CameraUpdateFactory.newCameraPosition(restoreCameraPosition)); restoreCameraPosition = null; } initClusterkraf(); } catch (IllegalStateException ise) { } } }
Ініціалізація Clusterkraft (з додаванням кожного маркера / групи маркерів вам потрібно повторно провести кластеризацію):
private void initClusterkraf() { if (map != null && inputPoints != null && inputPoints.size() > 0) { com.twotoasters.clusterkraf.Options options = new com.twotoasters.clusterkraf.Options(); applyDemoOptionsToClusterkrafOptions(options); if (clusterkraf == null) { clusterkraf = new Clusterkraf(map, options, inputPoints); } else { clusterkraf.clear(); //map.clear(); clusterkraf.addAll(inputPoints); } if (!check_sos && !check_company) { clusterkraf.clear(); } } if (map != null && inputPoints.size() == 0 && clusterkraf != null) { if (!check_sos && !check_company) { clusterkraf.clear(); } } }
Клас, який описує правила кластеризації маркерів, використовує відповідні піктограми для них. Якщо це кластер, то клас додає кількість елементів у піктіограмі з відповідним кольором:
```htmlpublic class MapMarkerOptionsChooser extends MarkerOptionsChooser { private final WeakReference``` ```htmlcontextRef; private final Paint clusterPaintLarge; private final Paint clusterPaintMedium; private final Paint clusterPaintSmall; public MapMarkerOptionsChooser(Context context) { this.contextRef = new WeakReference (context); Resources res = context.getResources(); clusterPaintMedium = new Paint(); clusterPaintMedium.setColor(Color.BLACK); clusterPaintMedium.setAlpha(255); clusterPaintMedium.setTextAlign(Paint.Align.CENTER); clusterPaintMedium.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)); clusterPaintMedium.setTextSize(res.getDimension(R.dimen.cluster_text_size_medium)); clusterPaintSmall = new Paint(clusterPaintMedium); clusterPaintSmall.setTextSize(res.getDimension(R.dimen.cluster_text_size_small)); clusterPaintLarge = new Paint(clusterPaintMedium); clusterPaintLarge.setTextSize(res.getDimension(R.dimen.cluster_text_size_large)); } @Override public void choose(MarkerOptions markerOptions, ClusterPoint clusterPoint) { Context context = contextRef.get(); if (context != null) { Resources res = context.getResources(); boolean isCluster = clusterPoint.size() > 1; BitmapDescriptor icon; String title; if (isCluster) { int company = 0; int sos = 0; int clusterSize = clusterPoint.size(); for (int i = 0; i < clusterSize; i++) { if (((MarkerData) clusterPoint.getPointAtOffset(i).getTag()).getType() == MarkerData.SOS) { sos++; } if (((MarkerData) clusterPoint.getPointAtOffset(i).getTag()).getType() == MarkerData.COMPANY) { company++; } } if (sos > 0 && company == 0) { icon = BitmapDescriptorFactory.fromBitmap(getClusterBitmap(res, R.drawable.ic_map_group, clusterSize, Color.WHITE)); } else if (sos == 0 && company > 0) { icon = BitmapDescriptorFactory.fromBitmap(getClusterBitmap(res, R.drawable.ic_map_group_companies, clusterSize, Color.BLACK)); } else { icon = BitmapDescriptorFactory.fromBitmap(getClusterBitmap(res, R.drawable.ic_map_group_of_groups, clusterSize, Color.BLACK)); } } else { MarkerData data = (MarkerData) clusterPoint.getPointAtOffset(0).getTag(); if (data.getType() == MarkerData.SOS) { icon = BitmapDescriptorFactory.fromResource(R.drawable.ic_map_sos_pin); } else { icon = BitmapDescriptorFactory.fromResource(R.drawable.ic_map_company_pin); } } markerOptions.icon(icon); markerOptions.anchor(0.5f, 1.0f); } } @SuppressLint("NewApi") private Bitmap getClusterBitmap(Resources res, int resourceId, int clusterSize, int color) { BitmapFactory.Options options = new BitmapFactory.Options(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { options.inMutable = true; } Bitmap bitmap = BitmapFactory.decodeResource(res, resourceId, options); if (!bitmap.isMutable()) { bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true); } Canvas canvas = new Canvas(bitmap); Paint paint = null; float originY; if (clusterSize < 100) { paint = clusterPaintLarge; originY = bitmap.getHeight() * 0.64f; } else if (clusterSize < 1000) { paint = clusterPaintMedium; originY = bitmap.getHeight() * 0.6f; } else { paint = clusterPaintSmall; originY = bitmap.getHeight() * 0.56f; } paint.setColor(color); canvas.drawText(String.valueOf(clusterSize), bitmap.getWidth() * 0.5f, originY, paint); return bitmap; } }
Проте нам не вдалося уникнути деяких проблем. При використанні бібліотеки подія onCameraChangePosition
перестала працювати. Як рішення, ми використали View.OnTouchListener
з певними умовами.
І нарешті, ось порада щодо стандартних інформаційних вікон («вспливаючих» вікон над маркерами). Якщо ви хочете відобразити будь-який процес у них або призначити певні події їх окремим елементам, ви можете використовувати вспливаючі вікна для цього, оскільки стандартне інформаційне вікно відображає «неживий» View
(тобто, як скріншот, який не має анімації в межах вікна) і обробляє лише кліки на його поверхні.