Журнал LinuxFormat - перейти на главную

LXF162:Android

Материал из Linuxformat
Перейти к: навигация, поиск


Программирование. Наладим взаимодействие Android и Google Android

Содержание

Фо­то на кар­те

Джуль­ет­та Кемп пи­шет ста­тьи для раз­лич­ных из­да­ний и сей­час ду­ма­ет над тем, как бы вос­поль­зо­вать­ся HTTP-под­клю­че­ни­ем сво­его те­ле­фо­на к сер­ве­ру.

(thumbnail)
Наш эксперт. Джуль­ет­та Кемп пи­шет про­стую про­грам­му, ко­то­рая при­вя­жет фо­то­гра­фии к Google Maps с GPS.

На этом уро­ке мы рас­смот­рим ин­тер­фейс 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 со­дер­жит на­прав­ление дви­жения в гра­ду­сах к восто­ку от гео­гра­фи­че­­ско­­го се­ве­ра. На эму­ля­то­ре этих дан­ных нет, но на ре­аль­ном уст­рой­ст­ве они бы­ва­ют.

LXF162.code android.lo opt.jpeg

» 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);

}


Най­ди­те на 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

(thumbnail)
> Кар­та и от­мет­ка в 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.

Ском­пи­ли­руй­те про­грам­му, и, сняв несколь­ко фо­то­гра­фий и от­пра­вив несколь­ко ме­сто­по­ло­жений эму­ля­то­ру, вы уви­ди­те на кар­те несколь­ко от­ме­ток.

(thumbnail)
> Из кар­ты вы­рос­ли де­ре­вья и ре­ка. Что­бы ста­ло еще луч­ше, до­ра­бо­тай­те код — пускай фо­то­гра­фия по­вер­нется на нуж­ный угол.

Щел­ка­ем по от­мет­кам на кар­те

Те­перь по­за­бо­тим­ся, что­бы по щелч­ку на от­мет­ке по­ка­зы­ва­лось сделанная фо­то­гра­фия. Для на­ча­ла соз­да­дим ба­зо­вый ме­тод об­ра­бот­ки щелч­ка на мет­ке с по­мо­щью диа­ло­га 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 оста­ют­ся на ва­ше ус­мот­ре­ние. |

Персональные инструменты
купить
подписаться
Яндекс.Метрика