LXF162:Android
Olkol (обсуждение | вклад) (Новая страница: «Категория:Постоянные рубрики Категория:Android » Программирование Наладим взаимоде…») |
Olkol (обсуждение | вклад) |
||
Строка 1: | Строка 1: | ||
− | [[Категория:Постоянные рубрики]] [[Категория:Android]] | + | [[Категория:Постоянные рубрики]] |
+ | [[Категория:Android]] | ||
− | + | '''Программирование. Наладим взаимодействие Android и Google | |
− | Android | + | Android''' |
− | Фото на карте | + | ==Фото на карте== |
− | + | ''Джульетта Кемп пишет статьи для различных изданий и сейчас думает над тем, как бы воспользоваться HTTP-подключением своего телефона к серверу.'' | |
− | + | [[Файл:LXF160.code android.expert.png |left |100px |thumb|'''Наш эксперт'''. Джульетта Кемп пишет простую программу, которая привяжет фотографии к Google Maps с GPS.]] | |
− | + | На этом уроке мы рассмотрим интерфейс GPS, а также вернемся к некоторым другим компонентам системы, использованным в наших предыдущих статьях – API карты [Map], базы данных [Database] и камеры [Camera]. Подробно этот код мы разбирать не будем, но весь он присутствует на DVD, поэтому при необходимости его можно скопировать, вставить и заставить работать (обратите внимание, что код, помещенный на DVD, не скомпилируется в том виде, как он есть – нужно создать новый проект, вручную или в Eclipse, и импортировать его). | |
− | Джульетта Кемп пишет статьи для различных изданий и сейчас думает над тем, как бы воспользоваться HTTP-подключением своего телефона к серверу. | + | ===Информация о местоположении=== |
− | + | ||
− | Информация о местоположении | + | |
Информация о местоположении включает не только широту и долготу, но и кучу других параметров. Для доступа к ним есть несколько методов в классе Location, а для их представления в человеко-читаемом виде пригодится метод toString() – результат получится примерно такой: | Информация о местоположении включает не только широту и долготу, но и кучу других параметров. Для доступа к ним есть несколько методов в классе Location, а для их представления в человеко-читаемом виде пригодится метод toString() – результат получится примерно такой: | ||
Строка 34: | Строка 33: | ||
» hasSpeed, Speed – если привязка содержит информацию о скорости, значение Speed будет ненулевым. По умолчанию информации о скорости не содержится. | » hasSpeed, Speed – если привязка содержит информацию о скорости, значение Speed будет ненулевым. По умолчанию информации о скорости не содержится. | ||
» hasBearing, Bearing – если привязка содержит информацию об азимуте, Bearing содержит направление движения в градусах к востоку от географического севера. На эмуляторе этих данных нет, но на реальном устройстве они бывают. | » hasBearing, Bearing – если привязка содержит информацию об азимуте, Bearing содержит направление движения в градусах к востоку от географического севера. На эмуляторе этих данных нет, но на реальном устройстве они бывают. | ||
+ | [[Файл:LXF162.code_android.lo_opt.jpeg |right |400px | ]] | ||
» hasAccuracy, Accuracy – если у провайдера есть данные о точности, Accuracy содержит значение допуска в метрах. | » hasAccuracy, Accuracy – если у провайдера есть данные о точности, Accuracy содержит значение допуска в метрах. | ||
» Extras содержит всю дополнительную информацию о привязке в виде пар «имя/значение». Одним из таких параметров может быть число спутников, использованное для получения привязки. | » Extras содержит всю дополнительную информацию о привязке в виде пар «имя/значение». Одним из таких параметров может быть число спутников, использованное для получения привязки. | ||
Строка 40: | Строка 40: | ||
Если имеется точная информация о высоте, то ее можно записывать через определенные интервалы времени, чтобы отслеживать свой подъем – это особенно удобно при езде на велосипеде. | Если имеется точная информация о высоте, то ее можно записывать через определенные интервалы времени, чтобы отслеживать свой подъем – это особенно удобно при езде на велосипеде. | ||
− | + | ===Шаг 1: Базовая настройка GPS=== | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | Шаг 1: Базовая настройка GPS | + | |
Интерфейс нашего приложения прост: тестовое поле (в котором будет показано местоположение после его определения) и три кнопки: Get Location [Определить местоположение], Take Photo [Сделать снимок] и Show Map of Photos [Показать карту с фотографиями]. На DVD найдите XML-файл res/layout/main.xml. Первые несколько методов в нашем GPSPhoto выглядят так: | Интерфейс нашего приложения прост: тестовое поле (в котором будет показано местоположение после его определения) и три кнопки: Get Location [Определить местоположение], Take Photo [Сделать снимок] и Show Map of Photos [Показать карту с фотографиями]. На DVD найдите XML-файл res/layout/main.xml. Первые несколько методов в нашем GPSPhoto выглядят так: | ||
Строка 143: | Строка 138: | ||
Отправьте одно значение перед перезагрузкой эмулятора, затем нажмите на кнопку Get Location и отправьте еще одно, и вы увидите, как значение изменится. Если вы сделаете это еще раз, значение меняться не будет, так как листенер выключен. Конечно, можно оставить его включенным, но это довольно быстро разрядит батарею, поэтому лучше выключать и снова включать его при необходимости. В нашем случае – при каждом вызове getCurrent Location(), т. е. при нажатии кнопки или при съемке фотографии. | Отправьте одно значение перед перезагрузкой эмулятора, затем нажмите на кнопку Get Location и отправьте еще одно, и вы увидите, как значение изменится. Если вы сделаете это еще раз, значение меняться не будет, так как листенер выключен. Конечно, можно оставить его включенным, но это довольно быстро разрядит батарею, поэтому лучше выключать и снова включать его при необходимости. В нашем случае – при каждом вызове getCurrent Location(), т. е. при нажатии кнопки или при съемке фотографии. | ||
+ | |||
+ | ===Шаг 2: Съемка фотографий=== | ||
+ | |||
+ | Для съемки фотографии можно вызвать стандартную программу работы с камерой с помощью намерения – Intent: | ||
+ | |||
+ | private Uri fileUri; | ||
+ | |||
+ | public void takePhoto() { | ||
+ | |||
+ | Intent i = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); | ||
+ | |||
+ | fileUri = getOutputMediaFileUri(MEDIA_TYPE_IMAGE); | ||
+ | |||
+ | i.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); | ||
+ | |||
+ | startActivityForResult(i, CAPTURE_IMAGE_ACTIVITY_REQ); | ||
+ | |||
+ | } | ||
+ | |||
+ | {{Врезка|left|Заголовок=Скорая помощь |Ширина=15%|Содержание=Чтобы воспользоваться эмулятором камеры, запустите менеджер AVD и добавьте SD-карту в эмулируемое устройство.}} | ||
+ | Найдите на DVD код метода getOutputMediaFileUri(), создающего уникальное имя файла, под которым будет сохранена фотография. Для исправления замеченной ошибки fileUri сделана переменной класса – подробности во врезке «Ошибка Занятия камеры при сохранении фотографии». | ||
+ | |||
+ | Для хранения фотографий на внешней SD-карте нужно добавить следующее право доступа в AndroidManifest.xml: | ||
+ | |||
+ | <uses-permission android:name=”android.permission.WRITE_EXTERNAL_STORAGE” /> | ||
+ | |||
+ | Получив Intent с местоположением фотографии, нужно кое-что с ним сделать: | ||
+ | |||
+ | protected void onActivityResult(int requestCode, int resultCode, Intent data) { | ||
+ | |||
+ | if (requestCode == CAPTURE_IMAGE_ACTIVITY_REQ) { | ||
+ | |||
+ | if (resultCode == RESULT_OK) { | ||
+ | |||
+ | if (data == null) { | ||
+ | |||
+ | // Здесь замечена ошибка! Фото надо сохранить в fileUri | ||
+ | |||
+ | storePhoto(fileUri); | ||
+ | |||
+ | } else { | ||
+ | |||
+ | storePhoto(data.getData()); | ||
+ | |||
+ | } | ||
+ | |||
+ | } else if (resultCode == RESULT_CANCELED) { | ||
+ | |||
+ | // Пользователь отменил операцию; ничего не делаем | ||
+ | |||
+ | } else { | ||
+ | |||
+ | Log.e(TAG, “Вызов фото неудачен!”); | ||
+ | |||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | Информацию об ошибке и борьбе с ней см. ниже. | ||
+ | |||
+ | ====Ошибка Занятия камеры при сохранении фотографии==== | ||
+ | |||
+ | ''В некоторых устройствах при возвращении результата Занятия камеры возникает ошибка, при которой URI сохраненной фотографии не возвращается (несмотря на то, что на самом деле фотография успешно сохранена). | ||
+ | '' | ||
+ | |||
+ | Для обработки этой ошибки в методе onActivityResult() проверим, является ли возвращенное Intent пустым при коде результата OK. Если да, воспользуемся URI файла, который мы передали Занятию камеры. | ||
+ | |||
+ | Однако это значение нельзя просто сохранить в переменной класса, иначе оно будет потеряно при приостановке Занятия. Вместо этого воспользуемся методом onSaveInstanceState(): | ||
+ | |||
+ | private static final String SAVED_FILE_URI = | ||
+ | “fileUri”; | ||
+ | |||
+ | public void onSaveInstanceState(Bundle savedInstanceState) { | ||
+ | |||
+ | if (fileUri != null) { | ||
+ | |||
+ | savedInstanceState.putString(SAVED_FILE_URI, fileUri.toString()); | ||
+ | |||
+ | } | ||
+ | |||
+ | super.onSaveInstanceState(savedInstanceState); | ||
+ | |||
+ | } | ||
+ | |||
+ | public void onCreate(Bundle savedInstanceState) { | ||
+ | |||
+ | super.onCreate(savedInstanceState); | ||
+ | |||
+ | if (savedInstanceState != null) { | ||
+ | |||
+ | String fileString = savedInstanceState. | ||
+ | |||
+ | getString(SAVED_FILE_URI); | ||
+ | |||
+ | if (fileString != null) { fileUri = Uri.parse(fileString); } | ||
+ | |||
+ | } | ||
+ | |||
+ | // .... | ||
+ | |||
+ | } | ||
+ | |||
+ | Он сохраняет значение fileUri при приостановке Занятия и сохраняет его при возобновлении (если оно существует; мы проверяем, что состояние сохранено и не является пустым, чтобы избежать исключения NullPointerException). | ||
+ | |||
+ | Теперь все должно работать хорошо, несмотря на ошибку. | ||
+ | |||
+ | ===База данных=== | ||
+ | |||
+ | Прежде чем писать метод storePhoto(), нужно создать базу данных фотографий: одну таблицу с тремя столбцами (URI фотографии, широта и долгота в формате с плавающей точкой). Для начала напишем вспомогательный класс базы данных, который использует API для работы с SQLite в Android: | ||
+ | |||
+ | public class GPSPhotoDatabaseHelper extends SQLiteOpenHelper { | ||
+ | |||
+ | private static final int DATABASE_VERSION = 1; | ||
+ | |||
+ | private static final String DATABASE_NAME =“GPSPhotoDatabase”; | ||
+ | |||
+ | // Сюда пойдут константы для таблицы и шапки колонок | ||
+ | |||
+ | // А также константа для строки SQL; см. код на DVD | ||
+ | |||
+ | GPSPhotoDatabaseHelper(Context context) { | ||
+ | |||
+ | super(context, DATABASE_NAME, null, DATABASE_VERSION); | ||
+ | |||
+ | } | ||
+ | |||
+ | public void onCreate(SQLiteDatabase db) { | ||
+ | |||
+ | db.execSQL(PHOTO_TABLE_CREATE); | ||
+ | |||
+ | } | ||
+ | |||
+ | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { | ||
+ | |||
+ | db.execSQL(PHOTO_TABLE_DROP); | ||
+ | |||
+ | onCreate(db); | ||
+ | |||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | Затем напишем метод storePhoto() в GPSPhoto, который будет вызываться из onActivityResult(): | ||
+ | |||
+ | private void storePhoto(Uri uri) { | ||
+ | |||
+ | getCurrentLocation(); | ||
+ | |||
+ | GPSPhotoDatabaseHelper dbHelper = | ||
+ | |||
+ | new GPSPhotoDatabaseHelper(getBaseContext()); | ||
+ | |||
+ | SQLiteDatabase db = dbHelper.getWritableDatabase(); | ||
+ | |||
+ | ContentValues values = new ContentValues(); | ||
+ | |||
+ | values.put(GPSPhotoDatabaseHelper.KEY_FILE, uri.toString()); | ||
+ | |||
+ | values.put(GPSPhotoDatabaseHelper.KEY_GPS_LAT, currentLocation.getLatitude()); | ||
+ | |||
+ | values.put(GPSPhotoDatabaseHelper.KEY_GPS_LNG, currentLocation.getLongitude()); | ||
+ | |||
+ | db.insert(GPSPhotoDatabaseHelper.PHOTO_TABLE_NAME, null, values); | ||
+ | |||
+ | db.close(); | ||
+ | |||
+ | } | ||
+ | |||
+ | Этот код почти не требует пояснений: мы получаем текущее местоположение, переменную для работы с базой данных, затем с помощью переменной ContentValues записываем URI и местоположение снимка в базу данных. Теперь фотографии будут храниться в базе данных, и к ним будет легко обратиться при создании карты. | ||
+ | |||
+ | ===Шаг 3: Карты=== | ||
+ | |||
+ | Для настройки карт нужно собрать приложение с целью Google API (а не с обычной целью), и добавить следующую строку в файл AndroidManifest.xml: | ||
+ | |||
+ | <uses-permission android:name=”android.permission.INTERNET” /> | ||
+ | |||
+ | Также в компоненте карты [MapView] нужно воспользоваться ключом API. Для отладки можно получить ключ, командой | ||
+ | |||
+ | keytool -list -alias androiddebugkey -keystore ~/.android/debug.keystore \\ | ||
+ | |||
+ | -keypass android -storepass android | ||
+ | [[Файл:LXF162.code_android.ma_opt.jpeg |right |400px | thumb| > Карта и отметка в GPSPhotoMap.]] | ||
+ | и отправить получившуюся контрольную сумму по ссылке https://developers.google.com/android/maps-api-signup. Информация о том, как получить и инициализировать полноценный ключ, приведена на сайте Google. Затем укажите ключ в XML-файле: | ||
+ | |||
+ | <?xml version=”1.0” encoding=”utf-8”?> | ||
+ | |||
+ | <RelativeLayout .... > | ||
+ | |||
+ | <com.google.android.maps.MapView | ||
+ | |||
+ | android:id=”@+id/mapView” | ||
+ | |||
+ | android:layout_width=”fill_parent” | ||
+ | |||
+ | android:layout_height=”fill_parent” | ||
+ | |||
+ | android:enabled=”true” | ||
+ | |||
+ | android:clickable=”true” | ||
+ | |||
+ | android:apiKey=”xxxxxxxxx” | ||
+ | |||
+ | /> | ||
+ | |||
+ | </RelativeLayout> | ||
+ | |||
+ | Чтобы воспользоваться этой XML-схемой MapView, создайте класс GPSPhotoMap: | ||
+ | |||
+ | public class GPSPhotoMap extends MapActivity { | ||
+ | |||
+ | // Override the methods | ||
+ | |||
+ | @Override | ||
+ | |||
+ | public void onCreate(Bundle savedInstanceState) { | ||
+ | |||
+ | super.onCreate(savedInstanceState); | ||
+ | |||
+ | setContentView(R.layout.map); | ||
+ | |||
+ | } | ||
+ | |||
+ | @Override | ||
+ | |||
+ | protected boolean isRouteDisplayed() { | ||
+ | |||
+ | return false; | ||
+ | |||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | Вызовите его из главного Занятия в методе showMap(), вызываемом при нажатии кнопки Show Map of Photos [Показать карту с фото]: | ||
+ | |||
+ | public void showMap() { | ||
+ | |||
+ | Intent i = new Intent(getBaseContext(), GPSPhotoMap.class); | ||
+ | |||
+ | startActivity(i); | ||
+ | |||
+ | } | ||
+ | |||
+ | Пока карта пуста; добавим же отметки, показывающие места, где сделаны наши снимки. Для этого воспользуемся оверлеем – и унаследуемся от ItemizedOverlay, так как нужно показывать несколько меток. Полный код (и предыдущие руководства) можно найти на DVD, а нам важны следующие методы: | ||
+ | |||
+ | public class PhotoLocationOverlay extends | ||
+ | |||
+ | ItemizedOverlay<OverlayItem> { | ||
+ | |||
+ | private ArrayList<OverlayItem> photos = new ArrayList<OverlayItem>(); | ||
+ | |||
+ | public PhotoLocationOverlay(Drawable marker) { | ||
+ | |||
+ | super(boundCenterBottom(marker)); | ||
+ | |||
+ | populate(); | ||
+ | |||
+ | } | ||
+ | |||
+ | // Добавим новый элемент на нашу карту | ||
+ | |||
+ | public void addItem(GeoPoint p, String photoTitle, String photoLocation) { | ||
+ | |||
+ | OverlayItem photoItem = new OverlayItem(p, photoTitle, photoLocation); | ||
+ | |||
+ | photos.add(photoItem); | ||
+ | |||
+ | populate(); | ||
+ | |||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | Элемент оверлея OverlayItem должен иметь три значения: точку геолокации GeoPoint, заголовок и краткое описание snippet-а. | ||
+ | |||
+ | В методе onCreate() GPSPhotoMap добавим метод для прорисовки оверлея: | ||
+ | |||
+ | List<PhotoWithLocation> photoList; | ||
+ | |||
+ | public void onCreate(Bundle savedInstanceState) { | ||
+ | |||
+ | // ... | ||
+ | |||
+ | showPhotosOnMap(); | ||
+ | |||
+ | } | ||
+ | |||
+ | private void showPhotosOnMap() { | ||
+ | |||
+ | Drawable pin = this.getResources().getDrawable(R.drawable.pin); | ||
+ | |||
+ | PhotoLocationOverlay photoOverlay = new | ||
+ | |||
+ | PhotoLocationOverlay(pin); | ||
+ | |||
+ | MapView mapView = (MapView) findViewById(R.id.mapView); | ||
+ | |||
+ | mapView.getOverlays().add(photoOverlay); | ||
+ | |||
+ | } | ||
+ | |||
+ | Объект Drawable – графическая метка, используемая для каждого элемента; затем мы создаем и добавляем новый оверлей на карту, затем добавляем каждый элемент списка фотографий на карту. | ||
+ | |||
+ | Объединим все вместе | ||
+ | |||
+ | Наконец, нам нужен метод, который получает список фотографий из нашей базы данных и передает его showPhotosOnMap(): | ||
+ | |||
+ | private void getPhotoList() { | ||
+ | |||
+ | GPSPhotoDatabaseHelper db = new GPSPhotoDatabaseHelper | ||
+ | (getBaseContext()); | ||
+ | |||
+ | SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); | ||
+ | |||
+ | builder.setTables(GPSPhotoDatabaseHelper.PHOTO_TABLE_NAME); | ||
+ | |||
+ | String[] PROJECTION = new String[] { | ||
+ | |||
+ | GPSPhotoDatabaseHelper.KEY_FILE, | ||
+ | |||
+ | GPSPhotoDatabaseHelper.KEY_GPS_LAT, | ||
+ | |||
+ | GPSPhotoDatabaseHelper.KEY_GPS_LNG | ||
+ | |||
+ | }; | ||
+ | |||
+ | photoList = builder.query(db.getReadableDatabase(), PROJECTION, null, null, null, null, null); | ||
+ | |||
+ | } | ||
+ | |||
+ | private void showPhotosOnMap() { | ||
+ | |||
+ | // .... | ||
+ | |||
+ | mapView.getOverlays().add(photoOverlay); | ||
+ | |||
+ | getPhotoList(); | ||
+ | |||
+ | photoList.moveToFirst(); | ||
+ | |||
+ | while (photoList.isAfterLast() == false) { | ||
+ | |||
+ | String uri = photoList.getString(photoList.getColumnIndex | ||
+ | (GPSPhotoDatabaseHelper.KEY_FILE)); | ||
+ | |||
+ | float lat = photoList.getFloat(photoList.getColumnIndex | ||
+ | (GPSPhotoDatabaseHelper.KEY_GPS_LAT)); | ||
+ | |||
+ | float lng = photoList.getFloat(photoList.getColumnIndex(GPSPhotoDatabaseHelper.KEY_GPS_LNG)); | ||
+ | |||
+ | GeoPoint gp = new GeoPoint((int)(lat * 1E6), (int)(lng *1E6)); | ||
+ | |||
+ | // Поле ‘title’ не используется, только GeoPoint and snippet | ||
+ | |||
+ | photoOverlay.addItem(gp, null, uri); | ||
+ | |||
+ | photoList.moveToNext(); | ||
+ | |||
+ | } | ||
+ | |||
+ | photoList.close(); | ||
+ | |||
+ | } | ||
+ | |||
+ | Для быстрого и легкого создания базы данных прекрасно подойдет SQLiteQueryBuilder. Достаточно указать только базу данных и проекцию (т. е. необходимые нам столбцы). Остальные параметры query() (здесь все они пусты) – выборка и аргументы выборки (т. е. какие столбцы нужно вернуть), выражения SQL GROUP BY и SQL HAVING, а также порядок сортировки. В проекции PROJECTION нам нужны три столбца, и мы забираем все фото из базы данных, порядок сортировки и все остальное не имеет значения. Поэтому мы выполняем запрос на ReadableDatabase с пустыми параметрами за исключением PROJECTION. | ||
+ | |||
+ | Функция возвращает Cursor, по которому можно проходиться в функции showPhotosOnMap(). Помните, что курсор нужно вручную перевести на первый элемент и затем продолжать итерации, пока он не дойдет до последнего. Каждый элемент добавляется в Overlay, а следовательно, добавится в Map View. | ||
+ | |||
+ | Скомпилируйте программу, и, сняв несколько фотографий и отправив несколько местоположений эмулятору, вы увидите на карте несколько отметок. | ||
+ | |||
+ | [[Файл:LXF162.code_android.ph_opt.jpeg |left |400px |thumb|> Из карты выросли деревья и река. Чтобы стало еще лучше, доработайте код — пускай фотография повернется на нужный угол.]] | ||
+ | |||
+ | ===Щелкаем по отметкам на карте=== | ||
+ | |||
+ | Теперь позаботимся, чтобы по щелчку на отметке показывалось сделанная фотография. Для начала создадим базовый метод обработки щелчка на метке с помощью диалога AlertDialog. | ||
+ | |||
+ | Добавьте следующие методы в PhotoLocationOverlay: | ||
+ | |||
+ | public PhotoLocationOverlay(Drawable marker, | ||
+ | Context context) { | ||
+ | |||
+ | super(boundCenterBottom(marker)); | ||
+ | |||
+ | c = context; | ||
+ | |||
+ | populate(); | ||
+ | |||
+ | } | ||
+ | |||
+ | protected boolean onTap(int index) { | ||
+ | |||
+ | OverlayItem item = photos.get(index); | ||
+ | |||
+ | AlertDialog.Builder dialog = new AlertDialog.Builder(c); | ||
+ | |||
+ | dialog.setTitle(item.getTitle()); | ||
+ | |||
+ | dialog.setMessage(item.getSnippet()); | ||
+ | |||
+ | dialog.show(); | ||
+ | |||
+ | return true; | ||
+ | |||
+ | } | ||
+ | |||
+ | Затем измените данную строку метода showPhotosOnMap() в GPSPhotoMap, передав Context при создании оверлея: | ||
+ | |||
+ | PhotoLocationOverlay photoOverlay = new PhotoLocationOverlay(pin, this) | ||
+ | |||
+ | Запустите программу, и вы увидите, что при щелчке по отметке теперь открывается окошко с путем к соответствующему файлу. Чтобы показать вместо него фотографию, нужен код несколько посложнее. | ||
+ | |||
+ | Употребим обычный Dialog вместо AlertDialog. Перепишем метод onTap(): | ||
+ | |||
+ | protected boolean onTap(int index) { | ||
+ | |||
+ | OverlayItem item = photos.get(index); | ||
+ | |||
+ | Dialog dialog = new Dialog(c); | ||
+ | |||
+ | LayoutInflater inflater =(LayoutInflater) c.getSystemService(Context.LAYOUT_INFLATER_SERVICE); | ||
+ | |||
+ | LinearLayout photoAlert = (LinearLayout) inflater.inflate | ||
+ | (R.layout.photoalert, null); | ||
+ | |||
+ | ImageView photoImage = (ImageView) photoAlert.findViewById(R.id.photo); | ||
+ | |||
+ | try { | ||
+ | |||
+ | InputStream in = new java.net.URL(item.getSnippet()).openStream(); | ||
+ | |||
+ | Bitmap bm = BitmapFactory.decodeStream | ||
+ | (new FlushedInputStream(in)); | ||
+ | |||
+ | photoImage.setImageBitmap(bm); | ||
+ | |||
+ | // Обработаем все возможные исключения | ||
+ | |||
+ | } catch (MalformedURLException e) { | ||
+ | |||
+ | e.printStackTrace(); | ||
+ | |||
+ | } catch (IOException e) { | ||
+ | |||
+ | e.printStackTrace(); | ||
+ | |||
+ | } | ||
+ | |||
+ | dialog.setContentView(photoAlert); | ||
+ | |||
+ | dialog.show(); | ||
+ | |||
+ | return true; | ||
+ | |||
+ | } | ||
+ | |||
+ | Чтобы показать в Dialog изображение вместо текста, нужно создать и использовать пользовательскую раскладку. LayoutInflator ее получит, а мы сможем получить ссылку на ImageView. Ваш XML-файл layout/photoalert.xml должен выглядеть так: | ||
+ | |||
+ | <?xml version=”1.0” encoding=”utf-8”?> | ||
+ | |||
+ | <LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android” | ||
+ | |||
+ | android:layout_width=”wrap_content” | ||
+ | |||
+ | android:layout_height=”wrap_content” > | ||
+ | |||
+ | <ImageView | ||
+ | |||
+ | android:id=”@+id/photo” | ||
+ | |||
+ | android:layout_width=”fill_parent” | ||
+ | |||
+ | android:layout_height=”wrap_content” /> | ||
+ | |||
+ | </LinearLayout> | ||
+ | |||
+ | Наконец, нам требуется преобразовать фотографию в формат bitmap и поместить ее в ImageView. В идеальном варианте нам следовало бы вызвать BitmapFactory.decodeFile(file). | ||
+ | |||
+ | К сожалению, замечена неприятная ошибка, способная привести к тому, что файлы JPG (сохраненные камерой аппарата) могут не декодироваться (см. http://code.google.com/p/android/issues/detail?id=6066). | ||
+ | |||
+ | Вам нужно переписать класс FlushedInputStream; код этого статического класса помещен на DVD. Создав bitmap, покажите окно, и все готово. | ||
+ | |||
+ | Чтобы окно закрывалось без нажатия кнопки Back [Назад], добавьте в код всего одну строку: | ||
+ | |||
+ | dialog.setCanceledOnTouchOutside(true); | ||
+ | |||
+ | Теперь при щелчке на любом свободном месте окно закроется, и мы вернемся к карте. | ||
+ | |||
+ | ===Шаг 4: Улучшаем GPS=== | ||
+ | |||
+ | Чтобы определить местоположение более надежно, перепишем метод isBetterLocation(): | ||
+ | |||
+ | protected boolean isBetterLocation(Location newLocation, Location oldLocation) { | ||
+ | |||
+ | if (oldLocation == null) { | ||
+ | |||
+ | return true; | ||
+ | |||
+ | } | ||
+ | |||
+ | long timeDiff = newLocation.getTime() - oldLocation.getTime(); | ||
+ | |||
+ | if (timeDiff > TWO_MINUTES) { | ||
+ | |||
+ | return true; | ||
+ | |||
+ | } else if (timeDiff < -TWO_MINUTES) { | ||
+ | |||
+ | return false; | ||
+ | |||
+ | } | ||
+ | |||
+ | int accuracyDiff = (int) (newLocation.getAccuracy() - oldLocation.getAccuracy()); | ||
+ | |||
+ | if (accuracyDiff < 0 ) { return true; } | ||
+ | |||
+ | else if (accuracyDiff > 0 ) { return false; } | ||
+ | |||
+ | return false; | ||
+ | |||
+ | } | ||
+ | |||
+ | Он проверяет три вещи: | ||
+ | |||
+ | 1 Если текущего местоположения нет, то новое всяко будет лучше, поэтому возвращаем true. | ||
+ | |||
+ | 2 Если разница во времени между текущим и новым местоположением больше двух минут, то пользователь, вероятно, переместился в пространстве, поэтому воспользуемся новым местоположением (возвращаем true). | ||
+ | |||
+ | 3 Точность выражается в метрах (см. врезку «Информация о местоположении»). Если точность текущего местоположения меньше, чем нового, то (new_accuracy — old_accuracy > 0), и текущее местоположение лучше (возвращаем false). Если наоборот, то лучше новое местоположение, и мы возвращаем true. Если разницы нет, сохраняем текущее местоположение. | ||
+ | |||
+ | Теперь можно вызывать эту функцию в обоих случаях получения нового местоположения – при его обновлении и при получении последнего известного местоположения. На практике мы выделим его в отдельный метод и вызовем его: | ||
+ | |||
+ | public void getCurrentLocation() { | ||
+ | |||
+ | LocationListener locationListener = new LocationListener() { | ||
+ | |||
+ | public void onLocationChanged(Location location) { | ||
+ | |||
+ | checkAndSetLocation(location); | ||
+ | |||
+ | lm.removeUpdates(this); | ||
+ | |||
+ | } | ||
+ | |||
+ | // ... | ||
+ | |||
+ | Location lastKnownLocation = lm.getLastKnownLocation | ||
+ | (PROVIDER); | ||
+ | |||
+ | if (lastKnownLocation != null) { | ||
+ | |||
+ | checkAndSetLocation(lastKnownLocation); | ||
+ | |||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | private void checkAndSetLocation(Location location) { | ||
+ | |||
+ | if (isBetterLocation(location, currentLocation)) { | ||
+ | |||
+ | currentLocation = location; | ||
+ | |||
+ | } | ||
+ | |||
+ | DecimalFormat latLngFormat = new DecimalFormat(“#.###”); | ||
+ | |||
+ | String lat = Double.toString( | ||
+ | |||
+ | Double.valueOf(latLngFormat.format(currentLocation.getLatitude()))); | ||
+ | |||
+ | String lng = Double.toString( | ||
+ | |||
+ | Double.valueOf(latLngFormat.format(currentLocation.getLongitude()))); | ||
+ | |||
+ | locationText.setText(“Lat: “ + lat + “, long: “ + lng); | ||
+ | |||
+ | } | ||
+ | |||
+ | Мы также проверим, доступен ли провайдер GPS, и если нет, воспользуемся провайдером сетевой мобильной связи, добавив следующие строки в getCurrentLocation(): | ||
+ | |||
+ | if (lm.isProviderEnabled(LocationManager.GPS_PROVIDER)) { | ||
+ | |||
+ | PROVIDER = LocationManager.GPS_PROVIDER; | ||
+ | |||
+ | } else { | ||
+ | |||
+ | Toast.makeText(this, “GPS недоступно; берем сетевую ячейку”, | ||
+ | |||
+ | Toast.LENGTH_SHORT).show(); | ||
+ | |||
+ | PROVIDER = LocationManager.NETWORK_PROVIDER; | ||
+ | |||
+ | } | ||
+ | |||
+ | lm.requestLocationUpdates(PROVIDER, 0, 0,locationListener); | ||
+ | |||
+ | Можно и еще улучшить код, написав методы onStatusChanged(), onProviderEnabled() и onProviderDisabled() в LocationListener. Например, можно выдавать окошко с оповещением при отключении провайдера GPS (сначала проверяется, что текущим провайдером является GPS, затем выдается окошко), чтобы пользователь включил его снова. | ||
+ | |||
+ | Если вы хотите пользоваться только провайдером GPS (не сетевым), в этот момент можно даже закрыть приложение, но учтите, что это ограничивает функциональность программы и может очень досаждать пользователем, которые предпочитают не оставлять GPS включенным и довольны примерным местоположением. | ||
+ | |||
+ | В методе onStatusChanged() можно также выдать окошко, если новый статус равен LocationProvider.OUT_OF_SERVICE, TEMPORARILY_UNAVAILABLE или AVAILABLE. Поработайте с этими методами и выберите лучшие варианты для своего приложения. | ||
+ | |||
+ | Желая еще улучшить программу, можно добавить кнопку для отправки фотографии и местоположения в Twitter или в блог. | ||
+ | |||
+ | Также можно улучшить обработку нескольких фотографий, снятых в одном и том же или близких местах (например, показав список при щелчке по отметке). Как всегда, добавляемые возможности и варианты использования фреймворка и API остаются на ваше усмотрение. | |
Текущая версия на 02:37, 10 октября 2018
|
|
|
Программирование. Наладим взаимодействие Android и Google
Android
Содержание |
[править] Фото на карте
Джульетта Кемп пишет статьи для различных изданий и сейчас думает над тем, как бы воспользоваться HTTP-подключением своего телефона к серверу.
На этом уроке мы рассмотрим интерфейс GPS, а также вернемся к некоторым другим компонентам системы, использованным в наших предыдущих статьях – API карты [Map], базы данных [Database] и камеры [Camera]. Подробно этот код мы разбирать не будем, но весь он присутствует на DVD, поэтому при необходимости его можно скопировать, вставить и заставить работать (обратите внимание, что код, помещенный на DVD, не скомпилируется в том виде, как он есть – нужно создать новый проект, вручную или в Eclipse, и импортировать его).
[править] Информация о местоположении
Информация о местоположении включает не только широту и долготу, но и кучу других параметров. Для доступа к ним есть несколько методов в классе Location, а для их представления в человеко-читаемом виде пригодится метод toString() – результат получится примерно такой:
Location[mProvider=gps,mTime=1339624800000,
mLatitude=55.0, mLongitude=0.0,mHasAltitude=true,
mAltitude=0.0,mHasSpeed=false,mSpeed=0.0,
mHasBearing=false,mBearing=0.0,mHasAccuracy=
false,mAccuracy=0.0,mExtras= Bundle[mParcelledData.dataSize=4]]
Вот полная информация об этих параметрах:
» Provider – провайдер местоположения, обычные значения – GPS или Network [Сеть]. » Time – время съемки по Гринвичу (в миллисекундах с 1 января 1970 года). » Latitude и Longitude – как следует из названия, широта и долгота в градусах. » hasAltitude, Altitude – если привязка содержит информацию о высоте над уровнем моря (т. е. если hasAltitude равно true), Altitude содержит высоту в метрах. » hasSpeed, Speed – если привязка содержит информацию о скорости, значение Speed будет ненулевым. По умолчанию информации о скорости не содержится. » hasBearing, Bearing – если привязка содержит информацию об азимуте, Bearing содержит направление движения в градусах к востоку от географического севера. На эмуляторе этих данных нет, но на реальном устройстве они бывают.
» hasAccuracy, Accuracy – если у провайдера есть данные о точности, Accuracy содержит значение допуска в метрах. » Extras содержит всю дополнительную информацию о привязке в виде пар «имя/значение». Одним из таких параметров может быть число спутников, использованное для получения привязки. Полная строка с параметрами расположения не слишком полезна программе, зато очень полезна программисту. Например, с помощью параметра Bearing можно сориентировать стрелку на карте (также есть метод bearingTo(), который возвращает направление для заданной конечной точки).
Если имеется точная информация о высоте, то ее можно записывать через определенные интервалы времени, чтобы отслеживать свой подъем – это особенно удобно при езде на велосипеде.
[править] Шаг 1: Базовая настройка GPS
Интерфейс нашего приложения прост: тестовое поле (в котором будет показано местоположение после его определения) и три кнопки: Get Location [Определить местоположение], Take Photo [Сделать снимок] и Show Map of Photos [Показать карту с фотографиями]. На DVD найдите XML-файл res/layout/main.xml. Первые несколько методов в нашем GPSPhoto выглядят так:
private LocationManager lm; private TextView locationText;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
setUpButtons();
locationText = (TextView) findViewById(R.id.location);
lm = (LocationManager) getSystemService(LOCATION_SERVICE);
}
public void setUpButtons() {
Button getLocationButton = (Button) findViewById (R.id.button_getlocation);
getLocationButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
getCurrentLocation();
}
});
}
Задание объекта LocationManager в методе onCreate() позволит нам работать с различными провайдерами и параметрами местоположения. С его помощью мы получаем провайдер местоположения и текущее местоположение в методе getCurrentLocation():
public void getCurrentLocation() {
LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
locationText.setText(location.toString());
lm.removeUpdates(this);
}
public void onStatusChanged(String provider, int status, Bundle extras) {}
public void onProviderEnabled(String provider) {} public void onProviderDisabled(String provider){}
};
lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListener);
Location lastKnownLocation = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (lastKnownLocation != null) {
locationText.setText(lastKnownLocation.toString());
} else {
locationText.setText(“Последнее местоположение не получить”);
}
}
LocationListener, как можно догадаться по названию, отслеживает изменения местоположения и, заметив таковое, запускает метод onLocationChanged(). Когда местоположение меняется, мы устанавливаем значение текстового поля [TextView] locationText в строковое значение GPS. Остальные методы LocationListener несущественны – пока оставим их пустыми.
По создании LocationListener его нужно зарегистрировать в LocationManager и выбрать провайдера. Здесь мы пользуемся провайдером GPS (если он недоступен, обработку ошибки см. далее). Чтобы воспользоваться провайдером GPS, также нужно установить следующие права доступа в AndroidManifest.xml:
<uses-permission android:name=”android.permission.ACCESS_FINE_LOCATION” />
Иногда на получение информации о местоположении требуется время – в процессе ожидания можно, если удастся, получить последнее известное местоположение вручную и использовать его, пока LocationListener не оповестит нас о следующем.
Если выполнить этот код, у нас получится результат следующего вида:
Location[mProvider=gps,mTime=1338588000000, mLatitude=37.422005,mLongitude=-122.084095,
mHasAltitude=true,mAltitude=0.0,mHasSpeed=false,mSpeed=0.0,mHasBearing=false,mBearing=0.0,mHasAccuracy=false, mAccuracy=0.0,mExtras=Bundle[mParcelledData.dataSize=4]]
Здесь немало параметров, которые могут нам пригодиться – подробности см. во врезке «Информация о местоположениии». Вытащим отсюда широту и долготу и выведем их с тремя знаками после запятой с помощью класса DecimalFormat. Для этого заменим вызов setText() таким кодом:
DecimalFormat latLngFormat = new DecimalFormat(“#.###”);
String lat = Double.toString(Double.valueOf(latLngFormat.format(currentLocation.getLatitude())));
String lng = Double.toString(Double.valueOf(latLngFormat.format(currentLocation.getLongitude()))); locationText.setText(“Шир: “ + lat + “, долг: “ + lng);
Отправьте одно значение перед перезагрузкой эмулятора, затем нажмите на кнопку Get Location и отправьте еще одно, и вы увидите, как значение изменится. Если вы сделаете это еще раз, значение меняться не будет, так как листенер выключен. Конечно, можно оставить его включенным, но это довольно быстро разрядит батарею, поэтому лучше выключать и снова включать его при необходимости. В нашем случае – при каждом вызове getCurrent Location(), т. е. при нажатии кнопки или при съемке фотографии.
[править] Шаг 2: Съемка фотографий
Для съемки фотографии можно вызвать стандартную программу работы с камерой с помощью намерения – Intent:
private Uri fileUri;
public void takePhoto() {
Intent i = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
fileUri = getOutputMediaFileUri(MEDIA_TYPE_IMAGE);
i.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
startActivityForResult(i, CAPTURE_IMAGE_ACTIVITY_REQ);
}
- Метамодернизм в позднем творчестве В.Г. Сорокина
- ЛитРПГ - последняя отрыжка постмодерна
- "Ричард III и семиотика"
- 3D-визуализация обложки Ridero создаем обложку книги при работе над самиздатом.
- Архитектура метамодерна - говоря о современном искусстве, невозможно не поговорить об архитектуре. В данной статье будет отмечено несколько интересных принципов, характерных для построек "новой волны", столь притягательных и скандальных.
- Литература
- Метамодерн
- Рокер-Прометей против изначального зла в «Песне про советскую милицию» Вени Дркина, Автор: Нина Ищенко, к.ф.н, член Союза Писателей ЛНР - перепубликация из журнала "Топос".
- Как избавиться от комаров? Лучшие типы ловушек.
- Что делать если роблокс вылетает на windows
- Что делать, если ребенок смотрит порно?
- Почему собака прыгает на людей при встрече?
- Какое масло лить в Задний дифференциал (мост) Visco diff 38434AA050
- О чем может рассказать хвост вашей кошки?
- Верветки
- Отчетность бюджетных учреждений при закупках по Закону № 223-ФЗ
- Срок исковой давности как правильно рассчитать
- Дмитрий Патрушев минсельхоз будет ли преемником Путина
- Кто такой Владислав Поздняков? Что такое "Мужское Государство" и почему его признали экстремистским в России?
- Как правильно выбрать машинное масло в Димитровграде?
- Как стать богатым и знаменитым в России?
- Почему фильм "Пипец" (Kick-Ass) стал популярен по всему миру?
- Как стать мудрецом?
- Как правильно установить FreeBSD
- Как стать таким как Путин?
- Где лучше жить - в Димитровграде или в Ульяновске?
- Почему город Димитровград так называется?
- Что такое метамодерн?
- ВАЖНО! Временное ограничение движения автотранспортных средств в Димитровграде
- Тарифы на электроэнергию для майнеров предложено повысить
Найдите на DVD код метода getOutputMediaFileUri(), создающего уникальное имя файла, под которым будет сохранена фотография. Для исправления замеченной ошибки fileUri сделана переменной класса – подробности во врезке «Ошибка Занятия камеры при сохранении фотографии».
Для хранения фотографий на внешней SD-карте нужно добавить следующее право доступа в AndroidManifest.xml:
<uses-permission android:name=”android.permission.WRITE_EXTERNAL_STORAGE” />
Получив Intent с местоположением фотографии, нужно кое-что с ним сделать:
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CAPTURE_IMAGE_ACTIVITY_REQ) {
if (resultCode == RESULT_OK) {
if (data == null) {
// Здесь замечена ошибка! Фото надо сохранить в fileUri
storePhoto(fileUri);
} else {
storePhoto(data.getData());
}
} else if (resultCode == RESULT_CANCELED) {
// Пользователь отменил операцию; ничего не делаем
} else {
Log.e(TAG, “Вызов фото неудачен!”);
}
}
}
Информацию об ошибке и борьбе с ней см. ниже.
[править] Ошибка Занятия камеры при сохранении фотографии
В некоторых устройствах при возвращении результата Занятия камеры возникает ошибка, при которой URI сохраненной фотографии не возвращается (несмотря на то, что на самом деле фотография успешно сохранена).
Для обработки этой ошибки в методе onActivityResult() проверим, является ли возвращенное Intent пустым при коде результата OK. Если да, воспользуемся URI файла, который мы передали Занятию камеры.
Однако это значение нельзя просто сохранить в переменной класса, иначе оно будет потеряно при приостановке Занятия. Вместо этого воспользуемся методом onSaveInstanceState():
private static final String SAVED_FILE_URI = “fileUri”;
public void onSaveInstanceState(Bundle savedInstanceState) {
if (fileUri != null) {
savedInstanceState.putString(SAVED_FILE_URI, fileUri.toString());
}
super.onSaveInstanceState(savedInstanceState);
}
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
String fileString = savedInstanceState.
getString(SAVED_FILE_URI);
if (fileString != null) { fileUri = Uri.parse(fileString); }
}
// ....
}
Он сохраняет значение fileUri при приостановке Занятия и сохраняет его при возобновлении (если оно существует; мы проверяем, что состояние сохранено и не является пустым, чтобы избежать исключения NullPointerException).
Теперь все должно работать хорошо, несмотря на ошибку.
[править] База данных
Прежде чем писать метод storePhoto(), нужно создать базу данных фотографий: одну таблицу с тремя столбцами (URI фотографии, широта и долгота в формате с плавающей точкой). Для начала напишем вспомогательный класс базы данных, который использует API для работы с SQLite в Android:
public class GPSPhotoDatabaseHelper extends SQLiteOpenHelper {
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME =“GPSPhotoDatabase”;
// Сюда пойдут константы для таблицы и шапки колонок
// А также константа для строки SQL; см. код на DVD
GPSPhotoDatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
public void onCreate(SQLiteDatabase db) {
db.execSQL(PHOTO_TABLE_CREATE);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(PHOTO_TABLE_DROP);
onCreate(db);
}
}
Затем напишем метод storePhoto() в GPSPhoto, который будет вызываться из onActivityResult():
private void storePhoto(Uri uri) {
getCurrentLocation();
GPSPhotoDatabaseHelper dbHelper =
new GPSPhotoDatabaseHelper(getBaseContext());
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(GPSPhotoDatabaseHelper.KEY_FILE, uri.toString());
values.put(GPSPhotoDatabaseHelper.KEY_GPS_LAT, currentLocation.getLatitude());
values.put(GPSPhotoDatabaseHelper.KEY_GPS_LNG, currentLocation.getLongitude());
db.insert(GPSPhotoDatabaseHelper.PHOTO_TABLE_NAME, null, values);
db.close();
}
Этот код почти не требует пояснений: мы получаем текущее местоположение, переменную для работы с базой данных, затем с помощью переменной ContentValues записываем URI и местоположение снимка в базу данных. Теперь фотографии будут храниться в базе данных, и к ним будет легко обратиться при создании карты.
[править] Шаг 3: Карты
Для настройки карт нужно собрать приложение с целью Google API (а не с обычной целью), и добавить следующую строку в файл AndroidManifest.xml:
<uses-permission android:name=”android.permission.INTERNET” />
Также в компоненте карты [MapView] нужно воспользоваться ключом API. Для отладки можно получить ключ, командой
keytool -list -alias androiddebugkey -keystore ~/.android/debug.keystore \\
-keypass android -storepass android
и отправить получившуюся контрольную сумму по ссылке https://developers.google.com/android/maps-api-signup. Информация о том, как получить и инициализировать полноценный ключ, приведена на сайте Google. Затем укажите ключ в XML-файле:
<?xml version=”1.0” encoding=”utf-8”?>
<RelativeLayout .... >
<com.google.android.maps.MapView
android:id=”@+id/mapView”
android:layout_width=”fill_parent”
android:layout_height=”fill_parent”
android:enabled=”true”
android:clickable=”true”
android:apiKey=”xxxxxxxxx”
/>
</RelativeLayout>
Чтобы воспользоваться этой XML-схемой MapView, создайте класс GPSPhotoMap:
public class GPSPhotoMap extends MapActivity {
// Override the methods
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.map);
}
@Override
protected boolean isRouteDisplayed() {
return false;
}
}
Вызовите его из главного Занятия в методе showMap(), вызываемом при нажатии кнопки Show Map of Photos [Показать карту с фото]:
public void showMap() {
Intent i = new Intent(getBaseContext(), GPSPhotoMap.class);
startActivity(i);
}
Пока карта пуста; добавим же отметки, показывающие места, где сделаны наши снимки. Для этого воспользуемся оверлеем – и унаследуемся от ItemizedOverlay, так как нужно показывать несколько меток. Полный код (и предыдущие руководства) можно найти на DVD, а нам важны следующие методы:
public class PhotoLocationOverlay extends
ItemizedOverlay<OverlayItem> {
private ArrayList<OverlayItem> photos = new ArrayList<OverlayItem>();
public PhotoLocationOverlay(Drawable marker) {
super(boundCenterBottom(marker));
populate();
}
// Добавим новый элемент на нашу карту
public void addItem(GeoPoint p, String photoTitle, String photoLocation) {
OverlayItem photoItem = new OverlayItem(p, photoTitle, photoLocation);
photos.add(photoItem);
populate();
}
}
Элемент оверлея OverlayItem должен иметь три значения: точку геолокации GeoPoint, заголовок и краткое описание snippet-а.
В методе onCreate() GPSPhotoMap добавим метод для прорисовки оверлея:
List<PhotoWithLocation> photoList;
public void onCreate(Bundle savedInstanceState) {
// ...
showPhotosOnMap();
}
private void showPhotosOnMap() {
Drawable pin = this.getResources().getDrawable(R.drawable.pin);
PhotoLocationOverlay photoOverlay = new
PhotoLocationOverlay(pin);
MapView mapView = (MapView) findViewById(R.id.mapView);
mapView.getOverlays().add(photoOverlay);
}
Объект Drawable – графическая метка, используемая для каждого элемента; затем мы создаем и добавляем новый оверлей на карту, затем добавляем каждый элемент списка фотографий на карту.
Объединим все вместе
Наконец, нам нужен метод, который получает список фотографий из нашей базы данных и передает его showPhotosOnMap():
private void getPhotoList() {
GPSPhotoDatabaseHelper db = new GPSPhotoDatabaseHelper (getBaseContext());
SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables(GPSPhotoDatabaseHelper.PHOTO_TABLE_NAME);
String[] PROJECTION = new String[] {
GPSPhotoDatabaseHelper.KEY_FILE,
GPSPhotoDatabaseHelper.KEY_GPS_LAT,
GPSPhotoDatabaseHelper.KEY_GPS_LNG
};
photoList = builder.query(db.getReadableDatabase(), PROJECTION, null, null, null, null, null);
}
private void showPhotosOnMap() {
// ....
mapView.getOverlays().add(photoOverlay);
getPhotoList();
photoList.moveToFirst();
while (photoList.isAfterLast() == false) {
String uri = photoList.getString(photoList.getColumnIndex (GPSPhotoDatabaseHelper.KEY_FILE));
float lat = photoList.getFloat(photoList.getColumnIndex (GPSPhotoDatabaseHelper.KEY_GPS_LAT));
float lng = photoList.getFloat(photoList.getColumnIndex(GPSPhotoDatabaseHelper.KEY_GPS_LNG));
GeoPoint gp = new GeoPoint((int)(lat * 1E6), (int)(lng *1E6));
// Поле ‘title’ не используется, только GeoPoint and snippet
photoOverlay.addItem(gp, null, uri);
photoList.moveToNext();
}
photoList.close();
}
Для быстрого и легкого создания базы данных прекрасно подойдет SQLiteQueryBuilder. Достаточно указать только базу данных и проекцию (т. е. необходимые нам столбцы). Остальные параметры query() (здесь все они пусты) – выборка и аргументы выборки (т. е. какие столбцы нужно вернуть), выражения SQL GROUP BY и SQL HAVING, а также порядок сортировки. В проекции PROJECTION нам нужны три столбца, и мы забираем все фото из базы данных, порядок сортировки и все остальное не имеет значения. Поэтому мы выполняем запрос на ReadableDatabase с пустыми параметрами за исключением PROJECTION.
Функция возвращает Cursor, по которому можно проходиться в функции showPhotosOnMap(). Помните, что курсор нужно вручную перевести на первый элемент и затем продолжать итерации, пока он не дойдет до последнего. Каждый элемент добавляется в Overlay, а следовательно, добавится в Map View.
Скомпилируйте программу, и, сняв несколько фотографий и отправив несколько местоположений эмулятору, вы увидите на карте несколько отметок.
[править] Щелкаем по отметкам на карте
Теперь позаботимся, чтобы по щелчку на отметке показывалось сделанная фотография. Для начала создадим базовый метод обработки щелчка на метке с помощью диалога AlertDialog.
Добавьте следующие методы в PhotoLocationOverlay:
public PhotoLocationOverlay(Drawable marker, Context context) {
super(boundCenterBottom(marker));
c = context;
populate();
}
protected boolean onTap(int index) {
OverlayItem item = photos.get(index);
AlertDialog.Builder dialog = new AlertDialog.Builder(c);
dialog.setTitle(item.getTitle());
dialog.setMessage(item.getSnippet());
dialog.show();
return true;
}
Затем измените данную строку метода showPhotosOnMap() в GPSPhotoMap, передав Context при создании оверлея:
PhotoLocationOverlay photoOverlay = new PhotoLocationOverlay(pin, this)
Запустите программу, и вы увидите, что при щелчке по отметке теперь открывается окошко с путем к соответствующему файлу. Чтобы показать вместо него фотографию, нужен код несколько посложнее.
Употребим обычный Dialog вместо AlertDialog. Перепишем метод onTap():
protected boolean onTap(int index) {
OverlayItem item = photos.get(index);
Dialog dialog = new Dialog(c);
LayoutInflater inflater =(LayoutInflater) c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LinearLayout photoAlert = (LinearLayout) inflater.inflate (R.layout.photoalert, null);
ImageView photoImage = (ImageView) photoAlert.findViewById(R.id.photo);
try {
InputStream in = new java.net.URL(item.getSnippet()).openStream();
Bitmap bm = BitmapFactory.decodeStream (new FlushedInputStream(in));
photoImage.setImageBitmap(bm);
// Обработаем все возможные исключения
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
dialog.setContentView(photoAlert);
dialog.show();
return true;
}
Чтобы показать в Dialog изображение вместо текста, нужно создать и использовать пользовательскую раскладку. LayoutInflator ее получит, а мы сможем получить ссылку на ImageView. Ваш XML-файл layout/photoalert.xml должен выглядеть так:
<?xml version=”1.0” encoding=”utf-8”?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” >
<ImageView
android:id=”@+id/photo”
android:layout_width=”fill_parent”
android:layout_height=”wrap_content” />
</LinearLayout>
Наконец, нам требуется преобразовать фотографию в формат bitmap и поместить ее в ImageView. В идеальном варианте нам следовало бы вызвать BitmapFactory.decodeFile(file).
К сожалению, замечена неприятная ошибка, способная привести к тому, что файлы JPG (сохраненные камерой аппарата) могут не декодироваться (см. http://code.google.com/p/android/issues/detail?id=6066).
Вам нужно переписать класс FlushedInputStream; код этого статического класса помещен на DVD. Создав bitmap, покажите окно, и все готово.
Чтобы окно закрывалось без нажатия кнопки Back [Назад], добавьте в код всего одну строку:
dialog.setCanceledOnTouchOutside(true);
Теперь при щелчке на любом свободном месте окно закроется, и мы вернемся к карте.
[править] Шаг 4: Улучшаем GPS
Чтобы определить местоположение более надежно, перепишем метод isBetterLocation():
protected boolean isBetterLocation(Location newLocation, Location oldLocation) {
if (oldLocation == null) {
return true;
}
long timeDiff = newLocation.getTime() - oldLocation.getTime();
if (timeDiff > TWO_MINUTES) {
return true;
} else if (timeDiff < -TWO_MINUTES) {
return false;
}
int accuracyDiff = (int) (newLocation.getAccuracy() - oldLocation.getAccuracy());
if (accuracyDiff < 0 ) { return true; }
else if (accuracyDiff > 0 ) { return false; }
return false;
}
Он проверяет три вещи:
1 Если текущего местоположения нет, то новое всяко будет лучше, поэтому возвращаем true.
2 Если разница во времени между текущим и новым местоположением больше двух минут, то пользователь, вероятно, переместился в пространстве, поэтому воспользуемся новым местоположением (возвращаем true).
3 Точность выражается в метрах (см. врезку «Информация о местоположении»). Если точность текущего местоположения меньше, чем нового, то (new_accuracy — old_accuracy > 0), и текущее местоположение лучше (возвращаем false). Если наоборот, то лучше новое местоположение, и мы возвращаем true. Если разницы нет, сохраняем текущее местоположение.
Теперь можно вызывать эту функцию в обоих случаях получения нового местоположения – при его обновлении и при получении последнего известного местоположения. На практике мы выделим его в отдельный метод и вызовем его:
public void getCurrentLocation() {
LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
checkAndSetLocation(location);
lm.removeUpdates(this);
}
// ...
Location lastKnownLocation = lm.getLastKnownLocation (PROVIDER);
if (lastKnownLocation != null) {
checkAndSetLocation(lastKnownLocation);
}
}
private void checkAndSetLocation(Location location) {
if (isBetterLocation(location, currentLocation)) {
currentLocation = location;
}
DecimalFormat latLngFormat = new DecimalFormat(“#.###”);
String lat = Double.toString(
Double.valueOf(latLngFormat.format(currentLocation.getLatitude())));
String lng = Double.toString(
Double.valueOf(latLngFormat.format(currentLocation.getLongitude())));
locationText.setText(“Lat: “ + lat + “, long: “ + lng);
}
Мы также проверим, доступен ли провайдер GPS, и если нет, воспользуемся провайдером сетевой мобильной связи, добавив следующие строки в getCurrentLocation():
if (lm.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
PROVIDER = LocationManager.GPS_PROVIDER;
} else {
Toast.makeText(this, “GPS недоступно; берем сетевую ячейку”,
Toast.LENGTH_SHORT).show();
PROVIDER = LocationManager.NETWORK_PROVIDER;
}
lm.requestLocationUpdates(PROVIDER, 0, 0,locationListener);
Можно и еще улучшить код, написав методы onStatusChanged(), onProviderEnabled() и onProviderDisabled() в LocationListener. Например, можно выдавать окошко с оповещением при отключении провайдера GPS (сначала проверяется, что текущим провайдером является GPS, затем выдается окошко), чтобы пользователь включил его снова.
Если вы хотите пользоваться только провайдером GPS (не сетевым), в этот момент можно даже закрыть приложение, но учтите, что это ограничивает функциональность программы и может очень досаждать пользователем, которые предпочитают не оставлять GPS включенным и довольны примерным местоположением.
В методе onStatusChanged() можно также выдать окошко, если новый статус равен LocationProvider.OUT_OF_SERVICE, TEMPORARILY_UNAVAILABLE или AVAILABLE. Поработайте с этими методами и выберите лучшие варианты для своего приложения.
Желая еще улучшить программу, можно добавить кнопку для отправки фотографии и местоположения в Twitter или в блог.
Также можно улучшить обработку нескольких фотографий, снятых в одном и том же или близких местах (например, показав список при щелчке по отметке). Как всегда, добавляемые возможности и варианты использования фреймворка и API остаются на ваше усмотрение. |