Под катом описаны основные приемы работы с картой, такие как добавление и управление маркерами, способы перемещения камеры над картой, управление зумом, построение маршрута и геокодинг. А так же ограничения и способы их обхода.
Источник
На написание статьи меня сподвиг собственный опыт, который я извлек при написании приложения для курьеров использующего в своей работе гугл карты. Так что все скриншоты и возможное упоминание бизнес логики будут происходить в контексте построения интерфейса курьера.
К сожалению Google Maps SDK for Android не позволяет изменять положение кнопок управления, т.н. UI controls, к ним относятся: IndoorLevelPicker — показ поэтажного плана строений, Compass — компас, My Location button — перейти на карте к текущему местоположению, Map toolbar — кнопи построения маршрута и открытия карты, а так же ZoomControls — увеличения и уменьшения маштаба карты.
На примере Map toolbar и ZoomControls посмотрим какие сложности могут возникнуть из-за невозможности сменить положение контролов и как это обойти.
Проблемы с отображением UI controls из SDK (выделено оранжевым) и их кастомные аналоги (выделено зеленым)
В данном случае у нас в правом нижнем углу расположена кнопка (floating action button) перехода к списку адресов заказов на доставку, на картинке слева видно, что ZoomControls оказались под ней и практически недоступны для нажатия. На картинке справа, при нажатии на маркер, появляются кнопки из Map toolbar, они так же оказались под кнопкой перехода к списку заказов.
Решение
Первое что нам необходимо сделать — это скрыть отображение оригинальных кнопок. Сделать это можно переопределив метод onMapReady, он вызывается в тот момент, когда карта готова к использованию.
private GoogleMap mMap;
private UiSettings uiSettings;
@Override
public void onMapReady(GoogleMap googleMap) {
mMap = googleMap;
uiSettings = mMap.getUiSettings();
//Не показывать кнопки Zoom
uiSettings.setZoomControlsEnabled(false);
//Не показывать кнопки построить маршрут и открыть карту из SDK
uiSettings.setMapToolbarEnabled(false);
}
Добавляем в верстку фрагмента нужные кнопки, там где они должны быть в соответствии с нашим дизайном:
Расположение кастомных кнопок управления картой
Затем в методе onCreateView указываем действия, которые должны произойти при нажатии на наши кнопки:
private ImageButton imageButtonZoomIn;
private ImageButton imageButtonZoomOut;
private ImageButton imageButtonRoute;
private GoogleMap mMap;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
//Увеличить карту
imageButtonZoomIn = view.findViewById(R.id.imageButtonZoomIn);
imageButtonZoomIn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mMap.animateCamera(CameraUpdateFactory.zoomIn());
}
});
//Уменьшить карту
imageButtonZoomOut = view.findViewById(R.id.imageButtonZoomOut);
imageButtonZoomOut.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mMap.animateCamera(CameraUpdateFactory.zoomOut());
}
});
//Открыть гугл карту и построить маршрут
imageButtonRoute = view.findViewById(R.id.imageButtonRoute);
imageButtonRoute.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String latitude = String.valueOf(activMarker.getPosition().latitude);
String longitude = String.valueOf(activMarker.getPosition().longitude);
Uri gmmIntentUri = Uri.parse("google.navigation:q=" + latitude + "," + longitude);
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
mapIntent.setPackage("com.google.android.apps.maps");
try{
if (mapIntent.resolveActivity(Objects.requireNonNull(getActivity()).getPackageManager()) != null) {
startActivity(mapIntent);
}
}catch (NullPointerException e){
Log.e(TAG, "onClick: NullPointerException: Couldn't open map." + e.getMessage() );
Toast.makeText(getActivity(), "Couldn't open map", Toast.LENGTH_SHORT).show();
}
}
});
}
Особенность метода animateCamera в том, что маштаб изменяется плавно, а не мгновенно и если нужно, например, отключить анимацию конкретной кнопки зума, по достижению максимального или минимального маштаба, то для этого нужно переопределить метод onCameraIdle, который вызывается в момент прекращения изменения маштаба карты.
@Override
public void onCameraIdle() {
if (mMap.getCameraPosition().zoom == mMap.getMinZoomLevel()){
//при минимальном зуме, делаем неактивной кнопку минус
imageButtonZoomOut.setEnabled(false);
imageButtonZoomIn.setEnabled(true);
}else if (mMap.getCameraPosition().zoom == mMap.getMaxZoomLevel()){
//при максимальном зуме, делаем неактивной кнопку плюс
imageButtonZoomOut.setEnabled(true);
imageButtonZoomIn.setEnabled(false);
}else {
//во всех остальных случаях обе кнопки активны
imageButtonZoomOut.setEnabled(true);
imageButtonZoomIn.setEnabled(true);
}
}
Что бы выполнить какое либо действие с маркером(кроме перетаскивания), например, создать новый заказ, удалить случайно поставленный маркер, перейти в уже существующий заказ или позвонить по телефону казанному в заказе добавляем в верстку соответствующие кнопки управления и прописываем их обработчики.
Кнопки управления маркером
private GoogleMap mMap;
private ImageButton imageButtonRoute;
@Override
public void onMapReady(GoogleMap googleMap) {
mMap = googleMap;
mMap.setOnMapClickListener(new GoogleMap.OnMapClickListener() {
@Override
public void onMapClick(LatLng latLng) {
imageButtonRoute.setVisibility(View.GONE);
if (myMarker !=null){
//Удаляем старый маркер
myMarker.remove();
}
//Добавляем маркер на карту
myMarker = mMap.addMarker(new MarkerOptions()
.position(latLng)
//Указываем название маркера
.title(Objects.requireNonNull(getContext()).getString(R.string.title_on_marker_to_new_order))
//Значение true , означает что маркер при длительном таче можно перетаскивать
.draggable(true));
myMarker.setTag(null);
//Показываем кастомные кнопки управления
imageButtonAddMarker.setVisibility(View.VISIBLE);
imageButtonRemoveMarker.setVisibility(View.VISIBLE);
}
});
}
private ImageButton imageButtonAddMarker;
private Marker myMarker;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
imageButtonAddMarker = view.findViewById(R.id.imageButtonAddMarker);
imageButtonAddMarker.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (myMarker !=null && myMarker.getTag()==null) {
//Скрываем кастомные кнопки управления
imageButtonAddMarker.setVisibility(View.GONE);
imageButtonRemoveMarker.setVisibility(View.GONE);
//Делаем что либо, в данном случае открываем фрагмент для ввода содержимого заказа
listener.openOrderContentsFragmentFromMap(null, myMarker);
//Удаляем маркер с карты, если он больше ненужен
myMarker.remove();
}
}
});
}
Еще одна особенность, это то что в SDK нет кнопки для удаления поставленного на карту маркера. Для этого тоже делаем свою кнопку:
private ImageButton imageButtonRemoveMarker;
private Marker myMarker;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
imageButtonRemoveMarker = view.findViewById(R.id.imageButtonRemoveMarker);
imageButtonRemoveMarker.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (myMarker !=null && myMarker.getTag()==null){
//Скрываем кнопки управления
imageButtonAddMarker.setVisibility(View.GONE);
imageButtonRemoveMarker.setVisibility(View.GONE);
//Удаляем маркер с карты
myMarker.remove();
}
}
});
}
При нажатии на маркер открывается его заголовок, нажатие на который так же можно использовать для совершения какого либо действия, я при нажатии на заголовок нового маркера создаю новый заказ на доставку курьеру, а на маркер уже существующего заказа — открываю подробную информацию о доставке, включающюю перечень товаров.
private GoogleMap mMap;
@Override
public void onMapReady(GoogleMap googleMap) {
mMap = googleMap;
mMap.setOnInfoWindowClickListener(new GoogleMap.OnInfoWindowClickListener() {
@Override
public void onInfoWindowClick(Marker marker) {
if (marker.getTag()==null) {
//Совершаем действие с новым маркером:
//например создаем новый заказ на доставку
listener.openOrderContentsFragmentFromMap(null, marker);
if (myMarker != null) {
//Удаляем маркер с карты, если он больше ненужен
myMarker.remove();
}
}
else {
//Совершаем действие с уже существующим маркером:
//например мы ранее вывели адреса курьерских доставок на карту
listener.openOrderContentsFragment((Long) marker.getTag());
}
}
});
}
Процесс вывода нескольких маркеров (считай списка заказов) на карту ничем принципиально не отличается от вывода одного маркера. Маркер состоит из координат (position), заголовка (title), мелкого текста под заголовком (snippet) и тэга (setTag) — его можно использовать для идентификации множества маркеров на карте.
Несколько маркеров на карте
public void drawListMarker(List<InfoMarker> latLngList) {
if (latLngList == null || latLngList.size() == 0) {
return;
}
//очищаем карту от маркеров
mMap.clear();
LatLngBounds.Builder builder = new LatLngBounds.Builder();
boolean fiarstGreean = true;
int count = 1;
for (InfoMarker latLng : latLngList) {
BitmapDescriptor icon;
if (fiarstGreean){
//первый отрисованный маркер будет зеленого цвета
//т.к. заказы отсортированны по времени и первый на доставку выделен цветом
icon = BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_GREEN);
fiarstGreean = false;
}
else {
icon = latLng.getIcon();
}
//добавляем маркер на карту в цикле
mMap.addMarker(new MarkerOptions().position(latLng.getLatLng()).title(String.valueOf(count)).snippet(latLng.getTitle()).icon(icon)).setTag(latLng.getIdOrder());
builder.include(latLng.getLatLng());
count++;
}
//отображаем на карте участок с заданным маштабом
CameraUpdate cameraUpdate;
if (loaded) {
cameraUpdate = CameraUpdateFactory
.newLatLngBounds(builder.build(), 100);
} else {
cameraUpdate = CameraUpdateFactory.newLatLng(builder.build().getCenter());
}
mMap.moveCamera(cameraUpdate);
mMap.animateCamera(CameraUpdateFactory.zoomIn());
mMap.animateCamera(CameraUpdateFactory.zoomTo(10), 1000, null);
}
Пара слов о геокодере
Геокодер используется для получения адреса, на основании координат. Поставив маркер на карте и нажав кнопку добавить заказ, мы получаем географические координаты нужной точки, т.е. широту и долготу. Но для удобства пользователя неплохо будет показать адрес в человекочитаемом виде, т.е., например, страна, город, улица, дом.
Google Maps SDK содержит класс Geocoder, вызвав его метод getFromLocation можно получить массив адресов по указанным координатам.
Для того, что бы не блокировать UI thread долгими, особенно если медленный или недоступный интернет, вызовами — будем использовать RxJava:
Полученный адрес точки на карте на основании географических координат
LatLng position = myMarker.getPosition();
Location location = new Location("new");
location.setLatitude(position.latitude);
location.setLongitude(position.longitude);
LocationRepostiory locationRepostiory = new LocationRepostiory(context, location);
locationRepostiory.getLastLocation().
observeOn(SchedulerProvider.getInstance().ui()).
subscribeOn(SchedulerProvider.getInstance().computation()).
subscribe(locationString -> {
if(editTextAddress.length()==0){
//Тут нам возвращается представление адреса в виде строки, полученное на основе географических координат
editTextAddress.setText(locationString);
}
}, throwable -> {
});
public class LocationRepostiory {
private Context context;
private Location location;
public LocationRepostiory(Context context, Location location) {
this.context = context;
this.location = location;
}
public Single<String> getLastLocation() {
return Single.create(this::subscribeOnLocation);
}
private void subscribeOnLocation(SingleEmitter<String> e) {
Geocoder geocoder = new Geocoder(context, Locale.getDefault());
String errorMessage = "";
List<Address> addresses = null;
try {
//получить адрес на основании координат
addresses = geocoder.getFromLocation(
location.getLatitude(),
location.getLongitude(),
// In this sample, get just a single address.
1);
} catch (IOException ioException) {
//Исключение, когда проблемы сети или проблем I/O.
errorMessage = context.getString(R.string.service_not_available);
Log.e(TAG, errorMessage, ioException);
} catch (IllegalArgumentException illegalArgumentException) {
// Исключение, когда неверные значения широты или долготы
errorMessage = context.getString(R.string.invalid_lat_long_used);
Log.e(TAG, errorMessage + ". " +
"Latitude = " + location.getLatitude() +
", Longitude = " +
location.getLongitude(), illegalArgumentException);
}
// Обработка события, когда адрес не найден
if (addresses == null || addresses.size() == 0) {
if (errorMessage.isEmpty()) {
errorMessage = context.getString(R.string.no_address_found);
Log.e(TAG, errorMessage);
}
} else {
Address address = addresses.get(0);
ArrayList<String> addressFragments = new ArrayList<String>();
// Получаем и отправляем обратно в поток.
for (int i = 0; i <= address.getMaxAddressLineIndex(); i++) {
e.onSuccess(address.getAddressLine(i));
}
}
}
}
puyol_dev2
Немного оффтоп. Но меня всегда немного раздражает, когда джависты и не только используют подобные конструкции
А если activMarker.getPosition() возвращает null, то ты схватываешь NPE
1C_LAN Автор
Как правило код под спойлером предназначен для объяснения конкретного события. Перегружать его простейшими обработчиками — значит уменьшать читаемость… ихмо.
puyol_dev2
Имхо, вызовы функции от функции снижают читаемость по сравнению с присвоением переменной с нужным именем каждому такому вызову