Je cherchais moi-même une solution à ce problème sans succès, j'ai donc dû lancer la mienne que j'aimerais partager ici avec vous. (Veuillez excuser mon mauvais anglais) (C'est un peu fou de répondre à un autre tchèque en anglais :-))
La première chose que j'ai essayé a été d'utiliser un bon vieux PopupWindow
. C'est assez simple - il suffit d'écouter OnMarkerClickListener
et d'afficher une personnalisation PopupWindow
au-dessus du marqueur. Certains autres gars ici sur StackOverflow ont suggéré cette solution et cela semble plutôt bon à première vue. Mais le problème avec cette solution apparaît lorsque vous commencez à déplacer la carte. Vous devez déplacer PopupWindow
vous-même d'une manière ou d'une autre, ce qui est possible (en écoutant certains événements onTouch), mais à mon humble avis, vous ne pouvez pas le faire paraître assez bien, en particulier sur certains appareils lents. Si vous le faites de la manière simple, il "saute" d'un endroit à un autre. Vous pouvez également utiliser des animations pour peaufiner ces sauts, mais de cette façon, il y PopupWindow
aura toujours "un pas en arrière" là où il devrait être sur la carte, ce que je n'aime tout simplement pas.
À ce stade, je réfléchissais à une autre solution. J'ai réalisé que je n'avais pas vraiment besoin de beaucoup de liberté - pour montrer mes vues personnalisées avec toutes les possibilités qui vont avec (comme des barres de progression animées, etc.). Je pense qu'il y a une bonne raison pour laquelle même les ingénieurs Google ne le font pas de cette façon dans l'application Google Maps. Tout ce dont j'ai besoin est un bouton ou deux sur l'InfoWindow qui affichera un état enfoncé et déclenchera certaines actions lorsque vous cliquez dessus. J'ai donc proposé une autre solution qui se divise en deux parties:
Première partie:
La première partie est de pouvoir attraper les clics sur les boutons pour déclencher une action. Mon idée est la suivante:
- Conservez une référence à l'infoWindow personnalisée créée dans InfoWindowAdapter.
- Enveloppez le
MapFragment
(ou MapView
) dans un ViewGroup personnalisé (le mien s'appelle MapWrapperLayout)
- Remplacez le
MapWrapperLayout
dispatchTouchEvent de et (si l'InfoWindow est actuellement affiché) acheminez d'abord les MotionEvents vers l'InfoWindow créée précédemment. S'il ne consomme pas les MotionEvents (par exemple, parce que vous n'avez cliqué sur aucune zone cliquable dans InfoWindow, etc.) alors (et seulement alors) laissez les événements descendre à la superclasse de MapWrapperLayout afin qu'il soit finalement livré à la carte.
Voici le code source de MapWrapperLayout:
package com.circlegate.tt.cg.an.lib.map;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.Marker;
import android.content.Context;
import android.graphics.Point;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.RelativeLayout;
public class MapWrapperLayout extends RelativeLayout {
private GoogleMap map;
private int bottomOffsetPixels;
private Marker marker;
private View infoWindow;
public MapWrapperLayout(Context context) {
super(context);
}
public MapWrapperLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MapWrapperLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void init(GoogleMap map, int bottomOffsetPixels) {
this.map = map;
this.bottomOffsetPixels = bottomOffsetPixels;
}
public void setMarkerWithInfoWindow(Marker marker, View infoWindow) {
this.marker = marker;
this.infoWindow = infoWindow;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean ret = false;
if (marker != null && marker.isInfoWindowShown() && map != null && infoWindow != null) {
Point point = map.getProjection().toScreenLocation(marker.getPosition());
MotionEvent copyEv = MotionEvent.obtain(ev);
copyEv.offsetLocation(
-point.x + (infoWindow.getWidth() / 2),
-point.y + infoWindow.getHeight() + bottomOffsetPixels);
ret = infoWindow.dispatchTouchEvent(copyEv);
}
return ret || super.dispatchTouchEvent(ev);
}
}
Tout cela rendra les vues à l'intérieur de l'InfoView "live" à nouveau - les OnClickListeners commenceront à se déclencher, etc.
Deuxième partie:
le problème restant est que, de toute évidence, vous ne pouvez pas voir les modifications de l'interface utilisateur de votre InfoWindow à l'écran. Pour ce faire, vous devez appeler manuellement Marker.showInfoWindow. Maintenant, si vous effectuez un changement permanent dans votre InfoWindow (comme changer le libellé de votre bouton pour quelque chose d'autre), cela suffit.
Mais montrer l'état d'un bouton enfoncé ou quelque chose de cette nature est plus compliqué. Le premier problème est que (au moins) je n'ai pas pu faire en sorte que InfoWindow affiche l'état normal du bouton enfoncé. Même si j'ai appuyé sur le bouton pendant un long moment, il est juste resté non appuyé sur l'écran. Je pense que c'est quelque chose qui est géré par le cadre de la carte lui-même, ce qui garantit probablement de ne pas afficher d'état transitoire dans les fenêtres d'informations. Mais je peux me tromper, je n'ai pas essayé de le découvrir.
Ce que j'ai fait est un autre hack désagréable - j'ai attaché un OnTouchListener
au bouton et changé manuellement son arrière-plan lorsque le bouton a été enfoncé ou relâché sur deux dessinables personnalisés - l'un avec un bouton dans un état normal et l'autre dans un état enfoncé. Ce n'est pas très agréable, mais ça marche :). Maintenant, j'ai pu voir le bouton basculer entre les états normal et pressé à l'écran.
Il y a encore un dernier problème - si vous cliquez sur le bouton trop rapidement, il ne montre pas l'état enfoncé - il reste juste dans son état normal (bien que le clic lui-même soit déclenché donc le bouton "fonctionne"). Au moins, c'est ainsi que cela apparaît sur mon Galaxy Nexus. Donc, la dernière chose que j'ai faite, c'est que j'ai retardé un peu le bouton dans son état enfoncé. C'est aussi assez moche et je ne sais pas comment cela fonctionnerait sur certains appareils plus anciens et lents, mais je soupçonne que même le cadre de la carte lui-même fait quelque chose comme ça. Vous pouvez l'essayer vous-même - lorsque vous cliquez sur toute l'InfoWindow, il reste dans un état enfoncé un peu plus longtemps, puis les boutons normaux le font (encore une fois - au moins sur mon téléphone). Et c'est en fait ainsi que cela fonctionne même sur l'application originale Google Maps.
Quoi qu'il en soit, je me suis écrit une classe personnalisée qui gère les changements d'état des boutons et toutes les autres choses que j'ai mentionnées, voici donc le code:
package com.circlegate.tt.cg.an.lib.map;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import com.google.android.gms.maps.model.Marker;
public abstract class OnInfoWindowElemTouchListener implements OnTouchListener {
private final View view;
private final Drawable bgDrawableNormal;
private final Drawable bgDrawablePressed;
private final Handler handler = new Handler();
private Marker marker;
private boolean pressed = false;
public OnInfoWindowElemTouchListener(View view, Drawable bgDrawableNormal, Drawable bgDrawablePressed) {
this.view = view;
this.bgDrawableNormal = bgDrawableNormal;
this.bgDrawablePressed = bgDrawablePressed;
}
public void setMarker(Marker marker) {
this.marker = marker;
}
@Override
public boolean onTouch(View vv, MotionEvent event) {
if (0 <= event.getX() && event.getX() <= view.getWidth() &&
0 <= event.getY() && event.getY() <= view.getHeight())
{
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN: startPress(); break;
case MotionEvent.ACTION_UP: handler.postDelayed(confirmClickRunnable, 150); break;
case MotionEvent.ACTION_CANCEL: endPress(); break;
default: break;
}
}
else {
endPress();
}
return false;
}
private void startPress() {
if (!pressed) {
pressed = true;
handler.removeCallbacks(confirmClickRunnable);
view.setBackground(bgDrawablePressed);
if (marker != null)
marker.showInfoWindow();
}
}
private boolean endPress() {
if (pressed) {
this.pressed = false;
handler.removeCallbacks(confirmClickRunnable);
view.setBackground(bgDrawableNormal);
if (marker != null)
marker.showInfoWindow();
return true;
}
else
return false;
}
private final Runnable confirmClickRunnable = new Runnable() {
public void run() {
if (endPress()) {
onClickConfirmed(view, marker);
}
}
};
protected abstract void onClickConfirmed(View v, Marker marker);
}
Voici un fichier de mise en page InfoWindow personnalisé que j'ai utilisé:
<?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"
android:gravity="center_vertical" >
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginRight="10dp" >
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="Title" />
<TextView
android:id="@+id/snippet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="snippet" />
</LinearLayout>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button" />
</LinearLayout>
Fichier de mise en page de l'activité de test (se MapFragment
trouvant à l'intérieur du MapWrapperLayout
):
<com.circlegate.tt.cg.an.lib.map.MapWrapperLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/map_relative_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<fragment
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="com.google.android.gms.maps.MapFragment" />
</com.circlegate.tt.cg.an.lib.map.MapWrapperLayout>
Et enfin le code source d'une activité de test, qui rassemble tout cela:
package com.circlegate.testapp;
import com.circlegate.tt.cg.an.lib.map.MapWrapperLayout;
import com.circlegate.tt.cg.an.lib.map.OnInfoWindowElemTouchListener;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.InfoWindowAdapter;
import com.google.android.gms.maps.MapFragment;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends Activity {
private ViewGroup infoWindow;
private TextView infoTitle;
private TextView infoSnippet;
private Button infoButton;
private OnInfoWindowElemTouchListener infoButtonListener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final MapFragment mapFragment = (MapFragment)getFragmentManager().findFragmentById(R.id.map);
final MapWrapperLayout mapWrapperLayout = (MapWrapperLayout)findViewById(R.id.map_relative_layout);
final GoogleMap map = mapFragment.getMap();
mapWrapperLayout.init(map, getPixelsFromDp(this, 39 + 20));
this.infoWindow = (ViewGroup)getLayoutInflater().inflate(R.layout.info_window, null);
this.infoTitle = (TextView)infoWindow.findViewById(R.id.title);
this.infoSnippet = (TextView)infoWindow.findViewById(R.id.snippet);
this.infoButton = (Button)infoWindow.findViewById(R.id.button);
this.infoButtonListener = new OnInfoWindowElemTouchListener(infoButton,
getResources().getDrawable(R.drawable.btn_default_normal_holo_light),
getResources().getDrawable(R.drawable.btn_default_pressed_holo_light))
{
@Override
protected void onClickConfirmed(View v, Marker marker) {
Toast.makeText(MainActivity.this, marker.getTitle() + "'s button clicked!", Toast.LENGTH_SHORT).show();
}
};
this.infoButton.setOnTouchListener(infoButtonListener);
map.setInfoWindowAdapter(new InfoWindowAdapter() {
@Override
public View getInfoWindow(Marker marker) {
return null;
}
@Override
public View getInfoContents(Marker marker) {
infoTitle.setText(marker.getTitle());
infoSnippet.setText(marker.getSnippet());
infoButtonListener.setMarker(marker);
mapWrapperLayout.setMarkerWithInfoWindow(marker, infoWindow);
return infoWindow;
}
});
map.addMarker(new MarkerOptions()
.title("Prague")
.snippet("Czech Republic")
.position(new LatLng(50.08, 14.43)));
map.addMarker(new MarkerOptions()
.title("Paris")
.snippet("France")
.position(new LatLng(48.86,2.33)));
map.addMarker(new MarkerOptions()
.title("London")
.snippet("United Kingdom")
.position(new LatLng(51.51,-0.1)));
}
public static int getPixelsFromDp(Context context, float dp) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int)(dp * scale + 0.5f);
}
}
C'est ça. Jusqu'à présent, je n'ai testé cela que sur mon Galaxy Nexus (4.2.1) et Nexus 7 (également 4.2.1), je vais l'essayer sur un téléphone Gingerbread lorsque j'en aurai l'occasion. Une limitation que j'ai trouvée jusqu'à présent est que vous ne pouvez pas faire glisser la carte d'où se trouve votre bouton à l'écran et déplacer la carte. Cela pourrait probablement être surmonté d'une manière ou d'une autre, mais pour l'instant, je peux vivre avec ça.
Je sais que c'est un truc moche mais je n'ai rien trouvé de mieux et j'ai tellement besoin de ce modèle de conception que ce serait vraiment une raison de revenir au framework map v1 (ce que j'aimerais vraiment éviter pour une nouvelle application avec des fragments, etc.). Je ne comprends tout simplement pas pourquoi Google n'offre pas aux développeurs un moyen officiel d'avoir un bouton sur InfoWindows. C'est un modèle de conception si courant, de plus, ce modèle est utilisé même dans l'application officielle Google Maps :). Je comprends les raisons pour lesquelles ils ne peuvent pas simplement faire "vivre" vos vues dans InfoWindows - cela tuerait probablement les performances lors du déplacement et du défilement de la carte. Mais il devrait y avoir un moyen d'obtenir cet effet sans utiliser de vues.