четверг, 15 ноября 2012 г.

Пример использования яндекс карты SDK для ос Андроид.

Задача: написать активити, в котором можно будет перемещая метку на карте, менять координаты и наоборот, меняя координаты в ручную, перемещать метку по карте.

Подготовительный этап состоит из трех пунктов.

1. Для работы с yandex maps SDK необходимо в файле манифеста приложения прописать разрешения на интернет, приблизительную геолокацию и чтение состояния телефона:
<uses-permission android:name="android.permission.INTERNET">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION">
<uses-permission android:name="android.permission.READ_PHONE_STATE">

2. В папку проекта libs помещаем две папки из SDK яндекса:
armeabi
armeabi-v7a
и файл библиотеки:
yandexmapkit-android.jar

Кроме этого, необходимо: создать папку xml в проекте и туда поместить map_layers.xml в папку layout поместить screen_buttons_layout.xml в папку drawable файлы zoom_minus_drawable.xml, zoom_plus_drawable.xml, find_me_drawable.xml а в drawable-hdpi файлы из этого списка
balloon_black.9.png
balloon_tail_black.png
empty_image.png
ic_action_search.png
ic_launcher.png
no_map_image.png
scale.png
sgrayvga.png
sgreenvga.png
sredvga.png
stricolorvga.png
syellowvga.png
user_location_gps.png
user_location_lbs.png
where_am_i_pressed.png
where_am_i.png
zoom_minus_pressed.png
zoom_minus.png
zoom_plus_pressed.png
zoom_plus.png

3. Для работы с апи яндекса необходимо получить ключ.

Все, теперь можно использовать яндекс-карты для решения нашей задачи

Использование яндекс-карт в своем приложении. 

Создаем разметку в layout coordeditor_activity.xml вида, как на рисунке Два поля и mapview от яндекса, он описан так:
<ru.yandex.yandexmapkit.mapview>
    android:id="@+id/map"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:layout_marginBottom="1dip"
    android:layout_marginLeft="2dip"
    android:layout_marginRight="2dip"
    android:layout_marginTop="1dip"
    android:layout_weight="1"
    android:apiKey="ВАШ КЛЮЧ"
</ru.yandex.yandexmapkit.MapView>
Главное тут это указать свой ключ в аттрибуте android:apiKey. Поля для ввода имеют следующие идентификаторы: editX и editY. С разметкой все. Для работы с координатами удобно написать вспомогательный класс Coordinate: класс реализует интерфейс Parcelable, позволяет получать координату в виде строки или в виде числа с плавающей точкой.
//
// класс для работы с координатой
// в ° градусах в виде десятичной дроби (современный вариант)
//
public class Coordinate implements Parcelable {
    // неправильное значение координаты
    private static final double INVALID_COORDINATE = -1000.0;
    // текущее значение координаты
    private double mValue;
    // конструкторы
    public Coordinate(){
        mValue = INVALID_COORDINATE;
    }

    public Coordinate(double c){
        mValue = c;
    }

    public Coordinate(String s){
        try {
            // жестко задан разделитель целой и дробной части
            String decimalSeparator = ".";
            mValue = Double.parseDouble(s.replace(".", decimalSeparator));
            if(isValid()) {
            } else
                mValue = INVALID_COORDINATE;
        } catch (NumberFormatException e) {
            mValue = INVALID_COORDINATE;
        }
    }

    public Coordinate(Parcel in) {
        mValue = in.readDouble();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    /*
     * проверка на правильность значения координаты
     */
    public boolean isValid(){
        return (mValue > INVALID_COORDINATE);
    }

    @Override
    public String toString() {
        // разделитель в координатах - точка
        // см. http://ru.wikipedia.org/wiki/%D0%93%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B5_%D0%BA%D0%BE%D0%BE%D1%80%D0%B4%D0%B8%D0%BD%D0%B0%D1%82%D1%8B
        DecimalFormat nf = new DecimalFormat();
        nf.applyPattern("###.##########");
        nf.setMinimumFractionDigits(4);
        nf.setMaximumFractionDigits(10);
        DecimalFormatSymbols dfs = new DecimalFormatSymbols();
        String decimalSeparator = String.valueOf(dfs.getDecimalSeparator());
        return nf.format(mValue).replace(decimalSeparator, ".");
    };
    
    public double GetValue(){
        return mValue;
    }
    
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeDouble(mValue);
    }
    
    public static final Creator<Coordinate> CREATOR = new Creator<Coordinate>() {
        
        public Coordinate createFromParcel(Parcel in) {
            return new Coordinate(in);
        }

        public Coordinate[] newArray(int size) {
            return new Coordinate[size];
        }
    };
}
Вторым вспомогательным классом будет customOverlay расширяющий класс из SDK DragAndDropOverlay у него будет конструктор:
public customOverlay(MapController arg0) {
    super(arg0);
}
И один перезагруженный метод
@Override
public boolean onSingleTapUp(float arg0, float arg1) {
    if (mTapItem != null) {
        // удаление старой метки(тайла)
        mOverlay.removeOverlayItem(mTapItem);
    }
    // получение координат экрана в рамках вьювера карты
    ScreenPoint s = new ScreenPoint(arg0, arg1);
    // преобразование экранных координат в географические
    GeoPoint g = mMapController.getGeoPoint(s);
    // создание и добавление новой метки(тайла)
    mTapItem = new DragAndDropItem(g, mTapItemBitmap);
    mOverlay.addOverlayItem(mTapItem);
    mMapController.setPositionNoAnimationTo(g);
    // переозначивание координат
    mX = new Coordinate(mTapItem.getGeoPoint().getLon());
    mY = new Coordinate(mTapItem.getGeoPoint().getLat());
    // взведение флага блокировки для предотвращения
    // рекурсивного вызова ручного обновления координат
    mFlgBlockedTextChange = true;
    if (mX.isValid())
        mEditX.setText(mX.toString().replace(".", mDecimalSeparator));
    if (mY.isValid())
        mEditY.setText(mY.toString().replace(".", mDecimalSeparator));
    mFlgBlockedTextChange = false;
    return super.onSingleTapUp(arg0, arg1);
};
Два пояснения, первое - при тапе по карте, карта сдвигается так, чтобы метка(тайл) переместился на место тапа, второй момент, это блокирование слушателя изменения текста на полях(флаг mFlgBlockedTextChange, его добавим позже), куда пишутся координаты, чтобы не было рекурсивного зацикливания: вы меняете положение тайла, тем самым вызываете изменение текста, а это изменение текста вызывает перемещение тайла. Ещё один вспомогательный класс CoordsInputFilter, необходим, чтобы вводимые в текстовые поля данные были координатами, записанными в современном варианте:
/*
 * вспомогательный класс для фильтрации вводимых чисел по маске координат
 */
private class CoordsInputFilter implements InputFilter {
    @Override
    public CharSequence filter(CharSequence source, int start, int end,
    Spanned dest, int dstart, int dend) {
        String checkedText = dest.subSequence(0, dstart).toString()
     + source.subSequence(start, end)
     + dest.subSequence(dend, dest.length()).toString();
        String pattern = getPattern();
        if (!Pattern.matches(pattern, checkedText)) {
            return "";
        }
    return null;
    }
    // маска для ввода координат от -999.9999999999 до 999.9999999999
    private String getPattern() {
        String pattern = "\\+?\\-?[0-9]{0,3}+([" + mDecimalSeparator
     + "]{1}||[" + mDecimalSeparator + "]{1}[0-9]{0,10})?";
            return pattern;
    }
}
Класс использует регулярное выражения для контроля вводимых данных. Его можно самостоятельно оптимизировать
Перейдем к написанию класса главной активити. Объявим необходимые приватные поля. Два поля для ввода координат (EditText) и сама карта:
private EditText mEditX;
private EditText mEditY;
private MapView mMapView;
Метка(тайл) на карте, который можно будет таскать пальцем по карте, тем самым изменять координаты:
private OverlayItem mTapItem;
Слой на карте, его опишем чуть позже
private customOverlay mOverlay;
Два вспомогательных инструмента, первый – для изменения самой карты, второй - для изменения слоей на карте:
private MapController mMapController;
private OverlayManager mOverlayManager;
Сами координаты:
private Coordinate mX;
private Coordinate mY;
Битмап метки(тайла), который ставиться на карту:
private Bitmap mTapItemBitmap;
Разделитель дробной и целой части.
private String mDecimalSeparator;

Вообще этот путь неправильный, но.. Тут две заминки, первая – на некоторых моделях самсунга на цифровой клавиатуре нельзя поставить запятую на русской локали, только точку и вторая – координаты представляются в градусах в виде десятичной дроби, с разделителем в виде точки, это написано в вики. Ещё необходим флаг, для блокировки слушателя изменения текста:
private boolean mFlgBlockedTextChange = false;
Теперь опишем самого слушателя, для координаты икс:
private TextWatcher mXWatcher = new TextWatcher() {
    @Override
    public void onTextChanged(CharSequence s, int start, int before,
    int count) {
        if (!mFlgBlockedTextChange) {
            mX = new Coordinate(s.toString());
        setManualItem();
        }
    }
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count,
    int after) {
  }
    @Override
    public void afterTextChanged(Editable s) {
    }
};
Для координаты игрек аналогичный слушатель, в принципе можно создать свой класс, расширяющий TextWatcher и использовать один его экземпляр для обоих координат. С полями все. Теперь напишем перезагрузку методов, участвующих в жизненном цикле активити, главный метод, когда создается активити:
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.coordeditor_activity);
    mEditX = (EditText) findViewById(R.id.editX);
    mEditY = (EditText) findViewById(R.id.editY);
    mMapView = (MapView) findViewById(R.id.map);
    // установка цифровой клавиатуры для полей ввода
    mEditX.setInputType(InputType.TYPE_NUMBER_FLAG_DECIMAL);
    mEditX.setRawInputType(Configuration.KEYBOARD_12KEY);
    mEditY.setInputType(InputType.TYPE_NUMBER_FLAG_DECIMAL);
    mEditY.setRawInputType(Configuration.KEYBOARD_12KEY);
    // точка, как разделитель целой и дробной части градусов
    // и для строкового представления double
    // проблема на телефонах самсунга, там цифровая клавиатура не реализует
    // правильное поведение разделителя дробной части DecimalSeparator
    // в зависимости от текущей локали
    // http://developer.samsung.com/forum/board/thread/view.do?boardName=GeneralB&messageId=164339&startPage=14&curPage=16
    mDecimalSeparator = ".";
    // установка фильтра для полей ввода, вводить можно только координаты
    mEditX.setFilters(new InputFilter[] { new CoordsInputFilter() });
    mEditY.setFilters(new InputFilter[] { new CoordsInputFilter() });
    if (savedInstanceState == null) {
        SharedPreferences pref = PreferenceManager
     .getDefaultSharedPreferences(this);
    // по умолчанию ставятся координаты пригорода Парижа
    mX = new Coordinate(pref.getString("x", "2.24"));
    mY = new Coordinate(pref.getString("y", "48.44"));
    } else {
        // восстановление координат
        mX = savedInstanceState.getParcelable("geox");
        mY = savedInstanceState.getParcelable("geoy");
    }
    // установка в поля ввода текущих координат
    if (mX.isValid())
        mEditX.setText(mX.toString().replace(".", mDecimalSeparator));
    if (mY.isValid())
        mEditY.setText(mY.toString().replace(".", mDecimalSeparator));
    // текст меняется - меняется и место на карте
    mEditX.addTextChangedListener(mXWatcher);
    mEditY.addTextChangedListener(mYWatcher);
    // Получаем MapController, настраиваем
    mMapController = mMapView.getMapController();
    mOverlayManager = mMapController.getOverlayManager();
    mMapController.setZoomCurrent(10);
    // создание своего слоя
    mOverlay = new customOverlay(mMapController);
    mOverlayManager.addOverlay(mOverlay);
    // установка тайла(метки)
    setManualItem();
}
Тут фишка, когда состояние несохранено, загружаются координаты из файла настроек, причем, если его нет, устанавливаются координаты пригорода города Парижа. Однако, если активити восстанавливается после уничтожения(изменение конфигурации, например, ориентации экрана), то координаты так же восстановятся, а не будут загружены из файла снова. В принципе, в этом примере это действие ускорит старт активити на ничтожную величину. При уничтожении активити, мы будем сохранять текущие координаты в файл настройки:
/*
 * (non-Javadoc)
 * 
 * @see android.app.Activity#onDestroy() при уничтожении активити сохранение
 * координат в преференс приложения
 */
@Override
protected void onDestroy() {
    SharedPreferences pref = PreferenceManager
    .getDefaultSharedPreferences(this);
    Editor editor = pref.edit();
    editor.putString("x", mX.toString());
    editor.putString("y", mY.toString());
    editor.commit();
    super.onDestroy();
}

Единственное, что осталось сделать это добавить меню с ссылкой на лиц. соглашения яндекс.Карт. Для этого в проекте создадим директорию menu с файлом, описывающим наше меню:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/main_menu"
        android:title="@string/main_menu" />
</menu>
Чтобы меню работало, в активити перезагрузим два метода. Один для создания самого меню:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.main_menu, menu);
    return true;
}
А другой для отслеживания нажатия пункта меню:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case (R.id.main_menu): 
            Builder b = new Builder(this);
            b.setMessage(getString(R.string.ya));
            b.create().show();
        break;
    }
    return true;
}
Всё. После выгрузки проекта на устройство или эмулятор картинка будет похожей

На всякий случай, строка ресурса с идентификатором R.string.ya выглядит так: "Приложение использует Яндекс-карты, условия использования сервиса Яндекс.Карты: http://legal.yandex.ru/maps_termsofuse" ссылка должна быть обязательной! Если что-то не получилось, можно добыть исходники на github

1 комментарий:

  1. Есть нюанс при расположении меток на мобильных яндекс картах. Дело в том, что координаты с яндекс карт на браузере не совпадают с координатами на мобильных картах, есть смещение. Поэтому чтобы точно установить метку на мобильных картах пришлось брать координаты непосредственно со смартфона. Для этого и воспользовался вашим решением. От себя только добавил вывод координат в лог, а оттуда уже копировал координаты и вставлял в свое приложение.
    Один вопрос по программе, зачем у вас долгота при показе идет впереди широты, ведь везде наоборот, и в GeoCode вставляется именно так.
    На репозитории у вас главный файл разметки идет со значком ~ в конце поэтому он не импортируется в IDE, и вылетает ошибка.

    ОтветитьУдалить