Android MapView a onTouch


Nemálo mě, a nejenom mě, štve absence něčeho, jako je onTouch() v mapách na Androidu, protože potřebuju vytvořit dynamické načítání overlayů. MapView sice má onTouchEvent(), ale že by bylo možné jej použít nějakým mnou zamýšleným způsobem, to zrovna ne. Zkoušel jsem pár způsobů, které tu nastíním a taky jsem zkusil jedno ne zrovna dvakrát moc fér řešení. Ale zoufalí lidé se uchylují k zoufalým věcem, však to znáte… Jen to vezmu hodně hopem, protože mám moře jiných věcí na dělání, ale pár lidem jsem to slíbil a sliby se mají plnit a to nejen o Vánocích ;-).

Zatímco natahování je poměrně jasné (AsyncTask, který bude natahovat data ve čtverci, jehož směrem jsem se posunul – na základě kolize souřadnic tohoto čtverce a viewportu – aby to pořád netahalo jak blbé), taková trivialita jako je onTouch už tak jasná není.

Nejprve jsem se snažil přimět k rozumu metodu onTouchEvent() jak na MapView, tak i na Overlayi, bohužel bezúspěšně. Zajímalo by mě, jestli je tento event pozůstatek něčeho zaniklého, nebo se na něj jenom nějak zapomnělo. Tento způsob by měl fungovat nějak takto, což vypadá poměrně prakticky a použitelně – ve switchi checkovat eventy:

[sourcecode language=“java“]public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
. . .
}
return super.onTouchEvent(ev);
}[/sourcecode]

Bohužel – z nějakého důvodu tato metoda vůbec nebyla volána. Po nějakém googlení jsem narazil na blog Juriho Strumpflohnera, který řešil stejný problém a vyšpekuloval ho následovně: na mapView navěsit vlastní OnTouchListener(), čili přidat jednoduchou anonymní třídu:

[sourcecode language=“java“]mapView.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent ev) {
return false;
}
});[/sourcecode]

Pokud teda dojde k eventu Touch, jsou procházeny všechny views a případné implementace listenerů v nich. V listeneru implementovaná metoda onTouch je volána nejméně jednou v závislosti na tom, zda vrátí true či false. Pokud vrací false, provede se pouze poprvé a systém je tak chytrý, že už jde příště rovnou na jistotu (aka přímo po metodě, která vrátila true), což je zároveň i kámen úrazu: je nutné zajistit, aby byla volána i metoda „níže“, protože se při true mapa nepohybuje (ale náš onTouch je volaný vždycky). Je teda nutné předat „nižšímu view“ tento event. Nejprve jsem volal dispatchTouchEvent() na view, který přišel do onTouch, ale to je blbost, protože tento view je vlastně onen MapEvent, nad kterým byl navěšen tento listener, takže došlo k zacyklení a StackOverflowError. Takže jsem třídu odanonymizoval a předávám si do ní Context, což je aktivita, uvnitř které je mapView:

[sourcecode language=“java“]public class MyTouchListener implements View.OnTouchListener {
private Activity activity;

public MyTouchListener(Activity activity) {
super();
this.activity = activity;
}

public boolean onTouch(View v, MotionEvent ev) {
activity.dispatchTouchEvent(ev);
return true;
}
}[/sourcecode]

Taky to ovšem nejede, ale oproti dispatchování do view to vydrží o chvíli déle (a pak to stejně přeteče zásobník a zdechne).

Jak už to bývá, funkční řešení bývají ta nejjednodušší, takže dneska tenhle blogpost konečně dorazím!

Ono totiž stačí rozšířit map view a přepsat v něm onTouchEvent, nebo onInterceptTouchEvent (jak je popsáno tady). V čem je zakopán pudl? V ničem, opravdu to funguje. Předchozí Class Cast Exception se dá zbavit velice jednoduše – definovat správný typ v layoutu! Pojďme tedy na to, nejprve vlastní mapView:

[sourcecode language=“java“]package cz.shmoula.pat;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;

import com.google.android.maps.MapView;

public class AreaMapView extends MapView {
private Context mContext;

public AreaMapView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
mContext = context;
}

public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_MOVE) {
Log.i("!!!!!", "Pohyb!");
}
return super.onTouchEvent(ev);
}
}[/sourcecode]

Teď následuje onen výše zmíněný kámen úrazu, který dříve hulákal Class Cast výjimkou a je to docela logické, ale nevěděl jsem, že je možné vkládat do layoutů vlastní komponenty. Teď už to vím, tak je možné psát něco zhruba takového:

[sourcecode language=“xml“]<?xml version="1.0" encoding="utf-8"?>

<cz.shmoula.pat.AreaMapView
xmlns:android="http://schemas.android.com/apk/res/android"

android:id="@+id/areaMapView"

android:layout_width="fill_parent"
android:layout_height="fill_parent"

android:clickable="true"
android:apiKey="api_klíč"
/>
[/sourcecode]

No a v aktivitě se tato komponenta dohledá každému známým způsobem:

[sourcecode language=“java“]AreaMapView mapView = (AreaMapView) findViewById(R.id.areaMapView);[/sourcecode]

Toť vše! Opravdu, v jednoduchosti je síla. Takže teď přidat nějaké spočítání delta a pokud bylo pohnuto viewportem více, než je výše uvedený čtverec v onu stranu, tak načíst další. Na závěr se ještě musím přiznat, že toto řešení jsem nevyplodil sám, ale sprostě se nechal inspirovat OpenCachingem od Garmina, který mě nakopl na správnou cestu směrem k výsledku. Při pídění se po řešení jsem taky narazil na projekt mapview-overlay-manager, ve kterém se nachází Lazy loading a který si nechávám k dalšímu prostudování a hádám, že nebudu sám.


Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *