web-dev-qa-db-fra.com

API Google Maps Android v2 - Fenêtre d'information interactive (comme dans Google Maps d'origine)

J'essaie de personnaliser InfoWindow après un clic sur un marqueur avec la nouvelle API Google Maps v2. Je veux que cela ressemble à l'application de cartes originale de Google. Comme ça:

Example Image

Lorsque j'ai ImageButton à l'intérieur, cela ne fonctionne pas - l'intégralité de la InfoWindow est sélectionnée et pas seulement la ImageButton. J'ai lu que c'est parce qu'il n'y a pas de View en soi, mais un instantané, de sorte que les éléments individuels ne peuvent pas être distingués les uns des autres. 

EDIT: Dans la documentation (grâce à Disco S2 ):

Comme mentionné dans la section précédente sur les fenêtres d’information, une fenêtre d’information n’est pas une vue en direct, la vue est rendue sous forme d’image sur le carte. En conséquence, les écouteurs que vous avez définis dans la vue sont ignorés et vous ne pouvez pas distinguer les événements de clic sur différentes parties de la vue. Il est conseillé de ne pas placer de composants interactifs - tels que en tant que boutons, cases à cocher ou entrées de texte - dans vos informations personnalisées la fenêtre.

Mais si Google l'utilise, il doit y avoir un moyen de le faire. Est-ce que quelqu'un a une idée?

181
user1943012

Je cherchais moi-même une solution à ce problème, sans succès, et je devais donc lancer le mien que je voudrais 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ée était d'utiliser un bon vieux PopupWindow. C'est assez facile - il suffit d'écouter la OnMarkerClickListener, puis d'afficher une PopupWindow personnalisée au-dessus du marqueur. Certains des gars de StackOverflow ont suggéré cette solution et elle a l’air très bonne au premier abord. Mais le problème avec cette solution apparaît lorsque vous commencez à déplacer la carte. Vous devez déplacer vous-même la variable PopupWindow, ce qui est possible (en écoutant certains événements onTouch), mais à mon humble avis, vous ne pouvez pas lui donner une apparence suffisante, en particulier sur certains appareils lents. Si vous le faites simplement, il "saute" d'un endroit à un autre. Vous pouvez également utiliser des animations pour peaufiner ces sauts, mais de cette façon, la PopupWindow sera toujours "un pas en arrière" là où elle devrait se trouver sur la carte, ce que je n’aime tout simplement pas. 

À ce stade, je pensais à une autre solution. J'ai réalisé que je n'avais en fait pas vraiment besoin de cette liberté - pour afficher mes vues personnalisées avec toutes les possibilités qui s'offrent à elle (telles que des barres de progression animées, etc.). Je pense qu'il y a une bonne raison pour laquelle même les ingénieurs de Google ne le font pas de cette manière 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 des actions lorsque l'utilisateur cliquera dessus. Alors je suis venu avec une autre solution qui se scinde en deux parties: 

Première partie:
La première partie consiste à pouvoir attraper les clics sur les boutons pour déclencher une action. Mon idée est la suivante:

  1. Conservez une référence à l'infoWindow personnalisé créé dans InfoWindowAdapter.
  2. Enroulez la MapFragment (ou MapView) dans un ViewGroup personnalisé (le mien s'appelle MapWrapperLayout)
  3. Ignore le dispatchTouchEvent de la MapWrapperLayout et (si l'InfoWindow est actuellement affichée) achemine d'abord MotionEvents à 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 ensuite), laissez les événements descendre dans la super-classe de MapWrapperLayout afin qu'ils soient finalement transmis à 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 {
    /**
     * Reference to a GoogleMap object 
     */
    private GoogleMap map;

    /**
     * Vertical offset in pixels between the bottom Edge of our InfoWindow 
     * and the marker position (by default it's bottom Edge too).
     * It's a good idea to use custom markers and also the InfoWindow frame, 
     * because we probably can't rely on the sizes of the default marker and frame. 
     */
    private int bottomOffsetPixels;

    /**
     * A currently selected marker 
     */
    private Marker marker;

    /**
     * Our custom view which is returned from either the InfoWindowAdapter.getInfoContents 
     * or InfoWindowAdapter.getInfoWindow
     */
    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);
    }

    /**
     * Must be called before we can route the touch events
     */
    public void init(GoogleMap map, int bottomOffsetPixels) {
        this.map = map;
        this.bottomOffsetPixels = bottomOffsetPixels;
    }

    /**
     * Best to be called from either the InfoWindowAdapter.getInfoContents 
     * or InfoWindowAdapter.getInfoWindow. 
     */
    public void setMarkerWithInfoWindow(Marker marker, View infoWindow) {
        this.marker = marker;
        this.infoWindow = infoWindow;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean ret = false;
        // Make sure that the infoWindow is shown and we have all the needed references
        if (marker != null && marker.isInfoWindowShown() && map != null && infoWindow != null) {
            // Get a marker position on the screen
            Point point = map.getProjection().toScreenLocation(marker.getPosition());

            // Make a copy of the MotionEvent and adjust it's location
            // so it is relative to the infoWindow left top corner
            MotionEvent copyEv = MotionEvent.obtain(ev);
            copyEv.offsetLocation(
                -point.x + (infoWindow.getWidth() / 2), 
                -point.y + infoWindow.getHeight() + bottomOffsetPixels);

            // Dispatch the adjusted MotionEvent to the infoWindow
            ret = infoWindow.dispatchTouchEvent(copyEv);
        }
        // If the infoWindow consumed the touch event, then just return true.
        // Otherwise pass this event to the super class and return it's result
        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 qui reste est que, de toute évidence, vous ne pouvez voir aucune modification de l'interface utilisateur de votre InfoWindow à l'écran. Pour ce faire, vous devez appeler manuellement Marker.showInfoWindow. Désormais, si vous apportez des modifications permanentes à votre InfoWindow (par exemple, en changeant le libellé de votre bouton), cela suffit. 

Mais afficher un bouton enfoncé ou quelque chose de ce genre est plus compliqué. Le premier problème est que (du moins) je n'ai pas pu faire en sorte que l'InfoWindow affiche l'état enfoncé du bouton normal. Même si j’appuyais longtemps sur le bouton, il ne restait pas appuyé sur l’écran. Je pense que c'est quelque chose qui est géré par le framework de carte lui-même, ce qui permet de s'assurer de ne montrer aucun état transitoire dans les fenêtres d'information. Mais je peux me tromper, je n'ai pas essayé de le savoir.

Ce que j’ai fait, c’est une autre tentative malicieuse: j’ai attaché une OnTouchListener au bouton et modifié manuellement l’arrière-plan lorsque le bouton a été actionné ou relâché en deux tirables personnalisés - l’un avec un bouton dans un état normal et l’autre dans un état appuyé. Ce n'est pas très gentil, mais ça marche :). Maintenant, je pouvais voir le bouton basculer entre les états normal et pressé sur l'écran. 

Il reste encore un dernier problème - si vous cliquez sur le bouton trop rapidement, il n’affiche pas l’état enfoncé - il reste dans son état normal (bien que le clic lui-même soit déclenché, le bouton «fonctionne»). Au moins, c’est ce qui se passe sur mon Galaxy Nexus. Donc, la dernière chose que j'ai faite est que j'ai retardé un peu le bouton lorsqu'il est pressé. C’est également assez moche et je ne sais pas comment cela fonctionnerait sur des appareils plus anciens et lents, mais je soupçonne que même le cadre cartographique lui-même fait quelque chose comme cela. Vous pouvez l’essayer vous-même. Lorsque vous cliquez sur l’InfoWindow entière, elle reste enfoncée un peu plus longtemps, puis les boutons normaux le sont (encore une fois - du moins sur mon téléphone). Et c’est ainsi que cela fonctionne même sur l’application Google Maps originale.Quoi qu'il en soit, j'ai écrit moi-même 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; // We need to delay releasing of the view a little so it shows the pressed state on the screen case MotionEvent.ACTION_UP: handler.postDelayed(confirmClickRunnable, 150); break; case MotionEvent.ACTION_CANCEL: endPress(); break; default: break; } } else { // If the touch goes outside of the view's area // (like when moving finger out of the pressed button) // just release the press 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); } } }; /** * This is called after a successful click */ protected abstract void onClickConfirmed(View v, Marker marker); }

<?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>

<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>

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 initialization
        // 39 - default marker height
        // 20 - offset between the default InfoWindow bottom Edge and it's content bottom Edge 
        mapWrapperLayout.init(map, getPixelsFromDp(this, 39 + 20)); 

        // We want to reuse the info window for all the markers, 
        // so let's create only one class member instance
        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);

        // Setting custom OnTouchListener which deals with the pressed state
        // so it shows up 
        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) {
                // Here we can perform some action triggered after clicking the button
                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) {
                // Setting up the infoWindow with current's marker info
                infoTitle.setText(marker.getTitle());
                infoSnippet.setText(marker.getSnippet());
                infoButtonListener.setMarker(marker);

                // We must call this to set the current marker and infoWindow references
                // to the MapWrapperLayout
                mapWrapperLayout.setMarkerWithInfoWindow(marker, infoWindow);
                return infoWindow;
            }
        });

        // Let's add a couple of markers
        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);
    }
}

Je sais que c’est un vilain bidouillage, mais je n’ai simplement rien trouvé de mieux et j’ai tellement besoin de ce motif de conception que c’est vraiment une raison de revenir au framework de la carte v1 (qui, en fait, 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 très courant. De plus, ce modèle est utilisé même dans l’application officielle de Google Maps :). Je comprends les raisons pour lesquelles ils ne peuvent pas simplement faire en sorte que vos points de vue soient "vivants" dans InfoWindows. Cela réduirait probablement les performances lors du déplacement et du défilement de la carte. Mais il devrait y avoir un moyen de parvenir à cet effet sans utiliser de vues.

I know this is an ugly hack but I just didn't find anything better and I need this design pattern so badly that this would really be a reason to go back to the map v1 framework (which btw. I would really really like to avoid for a new app with fragments etc.). I just don't understand why Google doesn't offer developers some official way to have a button on InfoWindows. It's such a common design pattern, moreover this pattern is used even in the official Google Maps app :). I understand the reasons why they can't just make your views "live" in the InfoWindows - this would probably kill performance when moving and scrolling map around. But there should be some way how to achieve this effect without using views.

320
chose007

Je vois que cette question est déjà ancienne mais quand même ...

Nous avons créé une bibliothèque SIPMLE dans notre entreprise pour réaliser ce qui est souhaité - une fenêtre d’information interactive avec vues et tout. Vous pouvez le vérifier sur github .

J'espère que ça aide :)

11
definera

Voici mon point de vue sur le problème. Je crée une superposition AbsoluteLayout qui contient la fenêtre d’information (une vue normale avec toutes les possibilités d’interactivité et de dessin). Ensuite, je lance Handler qui synchronise la position de la fenêtre d’information avec la position du point sur la carte toutes les 16 ms. Cela semble fou, mais fonctionne réellement.

Vidéo de démonstration: https://www.youtube.com/watch?v=bT9RpH4p9mU (tenez compte du fait que les performances sont réduites en raison de l’émulation et de l’enregistrement vidéo exécutés simultanément).

Code de la démo: https://github.com/deville/info-window-demo

Un article fournissant des détails (en russe): http://habrahabr.ru/post/213415/

8
Andrii Chernenko

Pour ceux qui n'ont pas pu obtenir choose007's, répondez et exécutez

Si clickListener ne fonctionne pas correctement à tout moment dans la solution chose007's, essayez d'implémenter View.onTouchListener au lieu de clickListener. Gérez l'événement tactile à l'aide de l'action ACTION_UP ou ACTION_DOWN. Pour une raison quelconque, mappage infoWindow provoque un comportement étrange lors de la distribution vers clickListeners

infoWindow.findViewById(R.id.my_view).setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
          int action = MotionEventCompat.getActionMasked(event);
          switch (action){
                case MotionEvent.ACTION_UP:
                    Log.d(TAG,"a view in info window clicked" );
                    break;
                }
                return true;
          }

Edit: Voici comment je l'ai fait étape par étape

Commencez par gonfler votre propre infowindow (variable globale) quelque part dans votre activité/fragment. Le mien est dans le fragment. Assurez-vous également que la vue racine de votre mise en page dans l'infowindow est linéaire (pour une raison quelconque, le retrait relatif prend toute la largeur de l'écran dans infowindow).

infoWindow = (ViewGroup) getActivity().getLayoutInflater().inflate(R.layout.info_window, null);
/* Other global variables used in below code*/
private HashMap<Marker,YourData> mMarkerYourDataHashMap = new HashMap<>();
private GoogleMap mMap;
private MapWrapperLayout mapWrapperLayout;

Puis, dans le rappel onMapReady de Google Maps Api Android (suivez ceci si vous ne savez pas ce que onMapReady est Maps> Documentation - Mise en route )

   @Override
    public void onMapReady(GoogleMap googleMap) {
       /*mMap is global GoogleMap variable in activity/fragment*/
        mMap = googleMap;
       /*Some function to set map UI settings*/ 
        setYourMapSettings();

MapWrapperLayout initialisation http://stackoverflow.com/questions/14123243/google-maps-Android-api-v2- interactive-infowindow-like-in-original-Android-go/15040761 # 15040761 39 - hauteur du marqueur par défaut 20 - décalage entre le bord inférieur par défaut d'InfoWindow et son bord inférieur de contenu * /

        mapWrapperLayout.init(mMap, Utils.getPixelsFromDp(mContext, 39 + 20));

        /*handle marker clicks separately - not necessary*/
       mMap.setOnMarkerClickListener(this);

       mMap.setInfoWindowAdapter(new GoogleMap.InfoWindowAdapter() {
                @Override
                public View getInfoWindow(Marker marker) {
                    return null;
                }

            @Override
            public View getInfoContents(Marker marker) {
                YourData data = mMarkerYourDataHashMap.get(marker);
                setInfoWindow(marker,data);
                mapWrapperLayout.setMarkerWithInfoWindow(marker, infoWindow);
                return infoWindow;
            }
        });
    }

Méthode SetInfoWindow

private void setInfoWindow (final Marker marker, YourData data)
            throws NullPointerException{
        if (data.getVehicleNumber()!=null) {
            ((TextView) infoWindow.findViewById(R.id.VehicelNo))
                    .setText(data.getDeviceId().toString());
        }
        if (data.getSpeed()!=null) {
            ((TextView) infoWindow.findViewById(R.id.txtSpeed))
                    .setText(data.getSpeed());
        }

        //handle dispatched touch event for view click
        infoWindow.findViewById(R.id.any_view).setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = MotionEventCompat.getActionMasked(event);
                switch (action) {
                    case MotionEvent.ACTION_UP:
                        Log.d(TAG,"any_view clicked" );
                        break;
                }
                return true;
            }
        });

Poignée marqueur clic séparément

    @Override
    public boolean onMarkerClick(Marker marker) {
        Log.d(TAG,"on Marker Click called");
        marker.showInfoWindow();
        CameraPosition cameraPosition = new CameraPosition.Builder()
                .target(marker.getPosition())      // Sets the center of the map to Mountain View
                .zoom(10)
                .build();
        mMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition),1000,null);
        return true;
    }
2
Manish Jangid

Juste une spéculation, je n'ai pas assez d'expérience pour l'essayer ...) -:

Comme GoogleMap est un fragment, il devrait être possible d’attraper un événement onClick de marqueur et d’afficher custom fragment view. Un fragment de carte sera toujours visible sur l'arrière-plan. Est-ce que quelqu'un a essayé? Une raison pour laquelle cela ne pourrait pas fonctionner?

L'inconvénient est que le fragment de carte serait gelé sur le fond, jusqu'à ce qu'un fragment d'info personnalisé y retourne le contrôle.

0
Wooff