воскресенье, 13 мая 2012 г.

Работа с GPS и Google Maps в Android

Продолжаем осваивать аппаратные возможности Android-смартфонов. В предыдущих постах мы  изучили, как использовать в наших приложениях микрофон и камеру. Теперь возьмёмся за более сложную тему: GPS. Сложность, конечно же, относительная. В сети есть масса примеров кода, с помощью которого можно получить координаты клиента, поэтому чтобы сделать наш пример интереснее, соединим получение координат с их использованием. В этой статье вы узнаете как получить доступ к Google Maps API, отобразить карту с различными её "плюшками" и вывести на неё наши координаты, полученные различными способами: по спутникам, данным мобильной сети или wifi.
Итак, приступим:

Получаем ключ к Google Maps API

Чтобы иметь возможность использовать Google Maps в своём Android-приложении, нужно получить ключ. Ключ приложения генерируется на этой странице на основании сертификата, которым будет в дальнейшем подписано ваше приложение. Обычно готовое приложение подписывается отдельным сертификатом, а в процессе разработки используется другой из хранилища debug.keystore, которое находится в каталоге .android вашей домашней директории. В этом случае вам потребуется сгенерировать два ключа и не забыть заменить отладочный ключ на "боевой", перед финальной сборкой приложения. Для генерации ключа вам потребуется получить  "сertificate fingerprint" командой
keytool -list -keystore ~/.android/debug.keystore
Пароль от debug.keystore обычно - пустая строка.

Доступы и библиотеки

Google Maps API в состав Android SDK входит как отдельная библиотека, поэтому её нужно отдельно подключить в AndroidManifest.xml вашего приложения командой

Эта строка должна находиться внутри тега "application".
Также не забудем попросить разрешения на получение координат и доступ в интернет:

android.permission.ACCESS_FINE_LOCATION
android.permission.INTERNET
android.permission.ACCESS_WIFI_STATE



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


Рисуем гуглокарту

Карту нам нарисует MapView, в конструктор которого мы передаём ключ, полученный на первом шаге. Сразу после создания желательно установить zoom level карты (1 - весь мир на экране, больше - детальнее) , а также методом setCenter(new GeoPoint(latitude, longitude)) спозиционировать карту куда-нибудь. Обе операции выполняем при помощи MapController-а, который получаем из экземпляра MapView методом getController(). Чтобы показать кнопки изменения масштаба вызовем метод setBuiltInZoomControls(true). И, главное, не забудьте сделать карту кликабельной методом setClickable(true).
Далее "вставляем" MapView в RelativeLayout вместе с остальными компонентами. В нашем случае это будет панель с двумя элементами управления: переключателем режима спутниковых фотографий и пиктограммой для возврата карты к текущему местоположению.
В целом картинка должна быть примерно такой, как на иллюстрации слева.
Все картинки возьмём из android.R.drawable, чтобы не морочить себе голову подготовкой своей графики.

Получаем текущие координаты

Вот мы и подошли к самому интересному. Весь код нашего приложения помещается в одном файле главного Activity:

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Point;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.Toast;
 
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
 
public class GMapsActivity extends MapActivity {
 
 private MapView map;
 private LocationManager manager;
 private Location loc;
 private LocationListener listener = new LocationListener() {
 
  @Override
  public void onLocationChanged(Location loc) {
   setLocation(loc);
   GeoPoint p = new GeoPoint((int) (loc.getLatitude() * 1e6), (int) (loc.getLongitude() * 1e6));
   map.getOverlays().add(new MarkerOverlay(p));
   map.invalidate();
   map.getController().animateTo(p);
  }
 
  @Override
  public void onProviderDisabled(String provider) {
   Toast.makeText(GMapsActivity.this, provider + " disabled", Toast.LENGTH_SHORT).show();
  }
 
  @Override
  public void onProviderEnabled(String provider) {
   Toast.makeText(GMapsActivity.this, provider + " enabled", Toast.LENGTH_SHORT).show();
  }
 
  @Override
  public void onStatusChanged(String provider, int status, Bundle extras) {
  }
 };
 
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  manager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
  loc = getLastCopords();
  buildUI();
  requestNewCoordinates();
 }
 
 @Override
 protected void onPause() {
  super.onPause();
  if (manager != null)
   manager.removeUpdates(listener);
 }
 
 public void setLocation(Location loc) {
  this.loc = loc;
 }
 
 private Location getLastCopords() {
  String[] providers = new String[] 
    { LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER, LocationManager.PASSIVE_PROVIDER };
  Location loc = null;
  for (String provider : providers) {
   loc = manager.getLastKnownLocation(provider);
   if (loc != null) {
    break;
   }
  }
  return loc;
 }
 
 private void buildUI() {
  RelativeLayout root = new RelativeLayout(this);
 
  // creating map with zoom controls and set center
  map = new MapView(this, "0kUYZ329eS_2MX4EyZ6YbJq4KFLm0hjiK1zjxLw");
  map.setBuiltInZoomControls(true);
  map.setClickable(true);
  MapController controller = map.getController();
  if (loc != null) {
   GeoPoint p = new GeoPoint((int) (loc.getLatitude() * 1e6), (int) (loc.getLongitude() * 1e6));
   controller.setCenter(p);
   map.getOverlays().add(new MarkerOverlay(p));
  } else {
   controller.setCenter(new GeoPoint(0, 0));
  }
  controller.setZoom(4);
  root.addView(map, LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
 
  // creating custom controls panel
  LinearLayout panel = new LinearLayout(this);
  panel.setOrientation(LinearLayout.VERTICAL);
  panel.setBackgroundColor(Color.argb(200, 200, 200, 200));
  RelativeLayout.LayoutParams plp = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
  plp.addRule(RelativeLayout.ALIGN_PARENT_TOP | RelativeLayout.ALIGN_PARENT_RIGHT);
 
  // map mode button
  ImageView mode = new ImageView(this);
  mode.setImageResource(android.R.drawable.ic_menu_mapmode);
  mode.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
    map.setSatellite(!map.isSatellite());
   }
  });
  panel.addView(mode, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
 
  // go to my location button
  ImageView my = new ImageView(this);
  my.setImageResource(android.R.drawable.ic_menu_mylocation);
  my.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
    if (loc != null) {
     map.getController()
      .animateTo(new GeoPoint((int) (loc.getLatitude() * 1e6), (int) (loc.getLongitude() * 1e6)));
    } else {
     Toast.makeText(GMapsActivity.this, "Coordinates is not found", Toast.LENGTH_SHORT).show();
    }
   }
  });
  panel.addView(my, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
  root.addView(panel, plp);
 
  // show all
  setContentView(root, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
 }
 
 private void requestNewCoordinates() {
  manager.removeUpdates(listener);
 
  final WifiManager wfManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
  if (manager.getAllProviders().contains(LocationManager.GPS_PROVIDER) 
    && manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
   manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener);
  } else if (manager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) 
    && wfManager.isWifiEnabled()) {
   manager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, listener);
  } else if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.ECLAIR_MR1 
    && manager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER)) {
   manager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, 0, listener);
  } else {
   Toast.makeText(this, "Location providers not found", Toast.LENGTH_SHORT).show();
  }
 }
 
 @Override
 protected boolean isRouteDisplayed() {
  return false;
 }
 
 private class MarkerOverlay extends Overlay {
  private GeoPoint p;
 
  public MarkerOverlay(GeoPoint p) {
   this.p = p;
  }
 
  @Override
  public boolean draw(Canvas canvas, MapView mapView, boolean shadow, long when) {
   super.draw(canvas, mapView, shadow);
 
   // translate the GeoPoint to screen pixels
   Point screenPts = new Point();
   mapView.getProjection().toPixels(p, screenPts);
 
   // add the marker
   Bitmap bmp = BitmapFactory.decodeResource(getResources(), android.R.drawable.ic_menu_myplaces);
   canvas.drawBitmap(bmp, screenPts.x, screenPts.y - 50, null);
   return true;
  }
 }
}

Координаты текущего местоположения тут мы получаем дважды: перед отрисовкой карты (последние известные с прошлого поиска) и после обновления.
Первое получение координат мы делаем в методе getLastCopords(). Эта операция выполняется достаточно быстро, чтобы не тормозить нам построение интерфейса. Она может завершиться неудачно, и в этом случае мы устанавливаем карту в точку new GeoPoint(0, 0). В случае успеха устанавливаем карту в нужное место и устанавливаем маркер текущего местоположения методом map.getOverlays().add(new MarkerOverlay(p)).
Важно учесть, что в нашем устройстве есть три источника получения координат.
Первый: GPS - даёт самые точные координаты, но работает медленнее всех и может ничего не найти. Второй: сеть wifi - отвечает быстро, относительно точно и всегда, когда wifi вообще есть в наличии. Третий: (с версии Android 2.2): PASSIVE_PROVIDER - сигнал соты оператора. Даёт координаты с точностью до нескольких кварталов, медленно, но практически всегда. Для получения данных с прошлого поиска скорость не имеет значения, поэтому опрашиваем провайдеров по очереди, начиная с самого точного.
Чтобы обновить координаты, подписываемся на результат обновления лучшего в данный момент провайдера в методе  requestNewCoordinates(). При этом проверяем, разрешено ли использование спутников (может быть выключено пользователем ради экономии заряда батареи), есть ли сеть wifi и т.п.
При запуске обновления передаём выбранному провайдеру экземпляр LocationListener-а, в методе onLocationChanged которого мы получим новые координаты, передвинем на них карту и поставим маркер.
При выходе из приложения (в onPause()) нужно не забыть остановить обновление координат у выбранного ранее провайдера методом removeUpdates(listener), чтобы не разряжать пользователю батарею напрасно.

Отлаживаем приложение

Несколько слов о отладке "координатного" приложения. Есть одна проблема, которая может попить кровушки у вас при отладке приложения на реальном устройстве. Различные провайдеры могут не давать события locationChanged очень долго или совсем. Например, PASSIVE_PROVIDER у меня вернул координаты только при отключении и повторном включении передачи данных. Спутники вообще могут быть недоступны в помещении. Поэтому мой совет: выполняйте отладку на эмуляторе, а событие locationChanged тут можно вызвать очень просто:
path/to/android-sdk/platform-tools/adb emu geo fix -121.45356 46.51119
Эта команда в консоли переместит вас в уютное заснеженое ущелье где-то в северной америке :)

5 комментариев:

  1. Спасибо Вам большое за статью! Очень понравилось)
    еще в тему-тоже полезным показалось http://www.enterra.ru/blog/gps-android/

    ОтветитьУдалить
  2. Интересно, доступно, понятно. Спасибо.

    ОтветитьУдалить
  3. Этот комментарий был удален администратором блога.

    ОтветитьУдалить
  4. > PASSIVE_PROVIDER - сигнал соты оператора.

    Это неверно. PASSIVE_PROVIDER предоставляет обновления местоположения тогда, когда местоположение обновляется в принципе каким-либо способом (например, в другом приложении) без самостоятельного запроса.

    ОтветитьУдалить
  5. Note: The Google Maps Android API v2 uses a new system of managing keys. Existing keys from a Google Maps Android v1 application, commonly known as MapView, will not work with the v2 API.

    MapView уже не актуален.... ?

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