Как стать автором
Обновить

Работа с интерфейсом в Google Maps SDK для Android

Время на прочтение9 мин
Количество просмотров10K
Данная статья будет полезна тем, кто ранее не использовал в своей работе Google Maps SDK.

Под катом описаны основные приемы работы с картой, такие как добавление и управление маркерами, способы перемещения камеры над картой, управление зумом, построение маршрута и геокодинг. А так же ограничения и способы их обхода.

image
Источник

На написание статьи меня сподвиг собственный опыт, который я извлек при написании приложения для курьеров использующего в своей работе гугл карты. Так что все скриншоты и возможное упоминание бизнес логики будут происходить в контексте построения интерфейса курьера.

К сожалению Google Maps SDK for Android не позволяет изменять положение кнопок управления, т.н. UI controls, к ним относятся: IndoorLevelPicker — показ поэтажного плана строений, Compass — компас, My Location button — перейти на карте к текущему местоположению, Map toolbar — кнопи построения маршрута и открытия карты, а так же ZoomControls — увеличения и уменьшения маштаба карты.

На примере Map toolbar и ZoomControls посмотрим какие сложности могут возникнуть из-за невозможности сменить положение контролов и как это обойти.

image
Проблемы с отображением UI controls из SDK (выделено оранжевым) и их кастомные аналоги (выделено зеленым)

В данном случае у нас в правом нижнем углу расположена кнопка (floating action button) перехода к списку адресов заказов на доставку, на картинке слева видно, что ZoomControls оказались под ней и практически недоступны для нажатия. На картинке справа, при нажатии на маркер, появляются кнопки из Map toolbar, они так же оказались под кнопкой перехода к списку заказов.

Решение

Первое что нам необходимо сделать — это скрыть отображение оригинальных кнопок. Сделать это можно переопределив метод onMapReady, он вызывается в тот момент, когда карта готова к использованию.

Не показывать кнопки Zoom контрола и не показывать кнопки построить маршрут из SDK
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);
}


Добавляем в верстку фрагмента нужные кнопки, там где они должны быть в соответствии с нашим дизайном:

image
Расположение кастомных кнопок управления картой

Затем в методе 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);
        }
    }


Что бы выполнить какое либо действие с маркером(кроме перетаскивания), например, создать новый заказ, удалить случайно поставленный маркер, перейти в уже существующий заказ или позвонить по телефону казанному в заказе добавляем в верстку соответствующие кнопки управления и прописываем их обработчики.

image
Кнопки управления маркером

Обработка нажатия на карту для добавления маркера и отображения кастомных кнопок управления
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) — его можно использовать для идентификации множества маркеров на карте.

image
Несколько маркеров на карте

Отрисовка нескольких маркеров с заданными координатами
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:

image
Полученный адрес точки на карте на основании географических координат

Использование Java RX для обращения к геокодеру Google

                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 -> {
                        });


Текст класса LocationRepostiory в котором происходит обратное геокодирование
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));
            }

        }
    }
}

Теги:
Хабы:
+3
Комментарии6

Публикации

Истории

Работа

Java разработчик
358 вакансий

Ближайшие события