192 votes

API Google Maps Android v2 - InfoWindow interactif (comme dans les cartes de google android originales)

Je suis en train de faire la coutume InfoWindow après un clic sur un marqueur avec la nouvelle version de Google Maps API v2. Je veux qu'il ressemble à l'original de l'application maps de Google. Comme ceci:

Example Image

Quand j'ai ImageButton à l'intérieur, sa ne fonctionne pas - l'ensemble de l' InfoWindow est sélectionné, et pas seulement l' ImageButton. J'ai lu que c'est parce qu'il n'y a pas un View lui-même, mais c'est de l'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 infos de windows, une fenêtre d'informations n'est pas une Vue en direct, plutôt que la vue est rendu comme une image sur le carte. De ce fait, tous les auditeurs vous définissez sur le point de vue sont ignorés et vous ne pouvez pas distinguer entre les événements de clic sur les différentes parties de le point de vue. Il vous est conseillé de ne pas placer des éléments interactifs, tels que des boutons, des cases à cocher ou des saisies de texte - à l'intérieur de votre personnalisé info de la fenêtre.

Mais si l'utilisation de Google, il doit y avoir un moyen de le faire. Quelqu'un aurait-il une idée?

339voto

chose007 Points 1391

Je cherchais une solution à ce problème moi-même avec pas de chance, j'ai donc dû rouler pour moi et j'aimerais partager ici avec vous. (S'il vous plaît excuser mon mauvais anglais) (C'est un peu fou pour répondre à un autre tchèque gars en anglais :-) )

La première chose que j'ai essayé était d'utiliser un bon vieux PopupWindow. Il est assez facile - il n'y a qu'à écouter la OnMarkerClickListener et de montrer ensuite une coutume PopupWindow au-dessus du marqueur. Certains autres gars ici sur StackOverflow a suggéré cette solution, et il semble effectivement assez bon au premier coup d'œil. Mais le problème avec cette solution s'affiche lorsque vous commencez à déplacer la carte autour. Vous devez déplacer les PopupWindow en quelque sorte vous-même, ce qui est possible (par l'écoute de certains onTouch événements) mais à mon humble avis, vous ne pouvez pas le faire paraître assez bon, surtout sur certains périphériques lents. Si vous faites de la manière la plus simple qu'il "saute" d'un endroit à l'autre. Vous pouvez également utiliser quelques animations à polir ces sauts, mais de cette façon, le PopupWindow sera toujours "un pas en arrière" là où il devrait être sur la carte que je n'aime pas.

À ce point, je pensais à une autre solution. J'ai réalisé que je n'ai actuellement pas vraiment besoin de beaucoup plus de liberté pour montrer à mes vues personnalisées avec toutes les possibilités qui vient avec elle (comme animé barres de progression, etc.). Je pense qu'il y a une bonne raison pourquoi même les ingénieurs google de ne pas faire de cette façon dans l'application Google Maps. Tout ce que je besoin est un bouton ou deux sur la bulle informative qui montrera l'état activé et déclencher des actions lorsque l'utilisateur clique dessus. Donc, je suis venu avec une autre solution qui se divise en deux parties:

Première partie:
La première partie est d'être en mesure d'intercepter les clics sur les boutons pour déclencher une action. Mon idée est la suivante:

  1. De conserver une référence à la coutume infoWindow créé dans le InfoWindowAdapter.
  2. Envelopper le MapFragment (ou MapView) à l'intérieur d'une coutume ViewGroup (le mien s'appelle MapWrapperLayout)
  3. Remplacer le MapWrapperLayout de dispatchTouchEvent et (si l'InfoWindow est actuellement affichée à l') première route du MotionEvents précédemment créé InfoWindow. Si il ne mange pas de la MotionEvents (comme parce que vous n'avez pas cliquez sur une zone cliquable à l'intérieur de l'InfoWindow etc.) ensuite (et seulement ensuite) que les événements aller en bas de la MapWrapperLayout est super classe, donc il sera finalement livré à la carte.

Voici la MapWrapperLayout du code source:

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 va rendre la vue à l'intérieur de la InfoView "vivre" encore une fois - la OnClickListeners va déclencher etc.

Deuxième partie: Le problème reste, que de toute évidence, vous ne pouvez pas voir tout changement de l'INTERFACE de votre bulle informative sur l'écran. Pour cela, vous devez manuellement appel Marqueur.showInfoWindow. Maintenant, si vous effectuez certains des changements permanents dans votre InfoWindow (comme changer l'étiquette de votre bouton pour autre chose), ce est assez bon.

Mais en montrant une touche de l'état ou quelque chose de cette nature est plus compliqué. Le premier problème, c'est que (au moins) je n'étais pas capable de faire de l'InfoWindow montrer normal du bouton pressé de l'état. Même si j'ai appuyé sur le bouton pendant une longue période, il est juste resté non pressée et non cuite sur l'écran. Je crois que c'est quelque chose qui est géré par le plan-cadre lui-même qui a probablement fait en sorte de ne pas montrer un état transitoire dans les fenêtres d'informations. Mais je peux me tromper, je n'ai pas essayer de la trouver.

Ce que j'ai fait est un autre méchant hack - j'ai attaché une OnTouchListener sur le bouton et manuellement la mise sous tension de l'arrière-plan lorsque le bouton a été enfoncé ou relâché à deux personnalisé un drawable - une avec un bouton dans un état normal et l'autre dans un état activé. Ce n'est pas très joli, mais il fonctionne :). Maintenant, j'étais en mesure de voir le bouton de commutation entre le mode normal d'pressé les états sur l'écran.

Il y a encore un dernier problème - si vous cliquez sur le bouton trop vite, il ne montre pas l'état pressé qu'il reste dans son état normal (bien que le clic est lui-même tiré de sorte que le bouton "marche"). Au moins c'est qu'il s'affiche sur mon Galaxy Nexus. Ainsi, la dernière chose que j'ai fait c'est que j'ai retardé le bouton dans son état enfoncé un peu. C'est assez moche et je ne suis pas sûr de savoir comment serait-il travailler sur certains anciens, périphériques lents, mais je pense que même dans le cadre du map elle-même fait quelque chose comme ça. Vous pouvez essayer vous-même - lorsque vous cliquez sur l'ensemble de l'InfoWindow, il reste dans un état enfoncé un peu plus longtemps, puis normal, boutons de faire (encore une fois - au moins sur mon téléphone). Et c'est en fait la façon dont il fonctionne même dans l'original de l'application Google Maps.

De toute façon, je l'ai écrit moi-même une classe personnalisée qui gère les boutons de changements d'état 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);
}

Ici, la coutume InfoWindow fichier de mise en page 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>

Test de l'activité mise en page du fichier (MapFragment étant à l'intérieur de la 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'un test de l'activité, qui les colles tout cela ensemble:

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

C'est tout. Jusqu'à présent j'ai uniquement testé sur mon Galaxy Nexus (4.2.1) et la Nexus 7 (également 4.2.1), je vais l'essayer sur certains de pain d'épice téléphone quand j'ai une chance. Une limitation que j'ai trouvé jusqu'à présent est que vous ne pouvez pas faire glisser la carte où est votre bouton sur l'écran et déplacer la carte. Il pourrait probablement être surmonté d'une certaine manière, mais pour l'instant je peux vivre avec ça.

Je sais que c'est un vilain hack mais je n'ai pas trouver quelque chose de mieux et j'ai besoin de ce modèle de conception si mal que cela serait vraiment une raison de revenir à la map v1-cadre (btw. Je voudrais vraiment vraiment à éviter pour une nouvelle application avec les fragments, etc.). Je ne comprends juste pas pourquoi Google n'offre pas les développeurs de certains officiels de façon à avoir un bouton sur InfoWindows. C'est une commune modèle de conception, en outre, ce modèle est utilisé même dans l'officiel de Google Maps app :). Je comprends les raisons pour lesquelles ils ne peuvent pas simplement faire de votre point de vue "live" dans le InfoWindows - ce serait probablement tuer performance lors du déplacement et du défilement de la carte autour. Mais il devrait y avoir une façon comment obtenir cet effet, sans l'aide de points de vue.

9voto

deville Points 2004

Voici mon point de vue sur le problème. Je crée AbsoluteLayout de superposition qui contient des informations de la Fenêtre (un affichage normal avec tous les bits de l'interactivité et les capacités de dessin). Puis j'ai commencer Handler qui synchronise la fenêtre info de la position de la position du point sur la carte toutes les 16 ms. Semble fou, mais en fait, il fonctionne.

Vidéo de démonstration: https://www.youtube.com/watch?v=bT9RpH4p9mU (prendre en compte que le rendement est diminué en raison de l'émulateur et l'enregistrement vidéo en cours d'exécution simultanément).

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

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

-1voto

Wooff Points 145

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

GoogleMap étant un fragment, il devrait être possible d’intercepter l’événement onClick de marqueur et voir la vue personnalisée du fragment . Un fragment de carte sera toujours visible sur l’arrière-plan. Tout le monde essayé ? N’importe quelle raison pourquoi il ne pourrait pas fonctionner ?

L’inconvénient, c’est que ce fragment de la carte serait figée sur le fond, jusqu'à ce qu’un fragment d’informations personnalisé retourne le contrôle à elle.

-8voto

zeeshan0026 Points 1918

Il est vraiment très simple.

googleMap.setInfoWindowAdapter(new InfoWindowAdapter() {

            // Use default InfoWindow frame
            @Override
            public View getInfoWindow(Marker marker) {              
                return null;
            }           

            // Defines the contents of the InfoWindow
            @Override
            public View getInfoContents(Marker marker) {

                // Getting view from the layout file info_window_layout
                View v = getLayoutInflater().inflate(R.layout.info_window_layout, null);

                // Getting reference to the TextView to set title
                TextView note = (TextView) v.findViewById(R.id.note);

                note.setText(marker.getTitle() );

                // Returning the view containing InfoWindow contents
                return v;

            }

        });

Ajoutez simplement le code ci-dessus dans votre classe où vous êtes à l'aide de GoogleMap. R. layout.info_window_layout est notre coutume de mise en page qui montre le point de vue qui viendra à la place de l'infowindow. J'ai juste ajouté le textview ici. Vous pouvez ajouter à rajouter ici pour faire comme l'exemple de composant logiciel enfichable. Mon info_window_layout a été

<?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:orientation="vertical" 
    >

        <TextView 
        android:id="@+id/note"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

J'espère que ça aidera. Nous pouvons trouver un exemple de travail de la coutume à l'infowindow http://wptrafficanalyzer.in/blog/customizing-infowindow-contents-in-google-map-android-api-v2-using-infowindowadapter/#comment-39731

ÉDITÉ : Ce code montre comment nous pouvons ajouter une vue personnalisée sur l'infoWindow. Ce code ne gère pas les clics sur la Coutume Afficher les éléments. Donc, il est proche de la réponse, mais pas exactement la réponse c'est pourquoi Il n'est pas accepté comme réponse.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X