199

I am trying to a make custom InfoWindow after a click on a marker with the new Google Maps API v2. I want it to look like in the original maps application by Google. Like this:

Example Image

When I have ImageButton inside, its not working - the entire InfoWindow is slected and not just the ImageButton. I read that it is because there isn't a View itself but it's snapshot, so individual items cannot be distinguished from each other.

EDIT: In the documentation (thanks to Disco S2):

As mentioned in the previous section on info windows, an info window is not a live View, rather the view is rendered as an image onto the map. As a result, any listeners you set on the view are disregarded and you cannot distinguish between click events on various parts of the view. You are advised not to place interactive components — such as buttons, checkboxes, or text inputs — within your custom info window.

But if Google use it, there must be some way to make it. Does anyone have any idea?

albert c braun
  • 2,650
  • 1
  • 22
  • 32
user1943012
  • 2,043
  • 3
  • 13
  • 7
  • "its not working" is not an especially useful description of your symptoms. Here is a sample project that shows having a custom info window with an image: https://github.com/commonsguy/cw-omnibus/tree/master/MapsV2/Popups – CommonsWare Jan 02 '13 at 15:03
  • 8
    @CommonsWare I wrote reason of that. From original documentation Note: The info window that is drawn is not a live view. The view is rendered as an image (using `View.draw(Canvas)`)... This means that any subsequent changes to the view will not be reflected by the info window on the map. To update the info window later Furthermore, the info window will not respect any of the interactivity typical for a normal view such as touch or gesture events. However you can listen to a generic click event on the whole info window as described in the section below. ... in your example is only textview – user1943012 Jan 02 '13 at 15:11
  • Hi, how did you get the my location button to appear below the action bar when in full screen mode? Thanks! – tkblackbelt Feb 22 '13 at 07:17
  • @tkblackbelt try `relativeLayout`'s – Driss Bounouar Jun 11 '13 at 14:28
  • 47
    I don't think that this question should be closed. It's a legit question to a problem I'm also facing. – marienke Oct 01 '14 at 08:25
  • 12
    I can see this so often, many of the most useful questions with answer have been closed. A question might not always fit the exact requirements of a proper question but if we have a well explained PROBLEM and a well explained ANSWER then there is no reason to close the question. People use Stackoverflow to exchange knowledge and closing a question thread which is so actively and so productive as "off-topic" does not seem like a good idea to me.. – John Dec 05 '14 at 03:09

7 Answers7

344

I was looking for a solution to this problem myself with no luck, so I had to roll my own which I would like to share here with you. (Please excuse my bad English) (It's a little crazy to answer another Czech guy in English :-) )

The first thing I tried was to use a good old PopupWindow. It's quite easy - one only has to listen to the OnMarkerClickListener and then show a custom PopupWindow above the marker. Some other guys here on StackOverflow suggested this solution and it actually looks quite good at first glance. But the problem with this solution shows up when you start to move the map around. You have to move the PopupWindow somehow yourself which is possible (by listening to some onTouch events) but IMHO you can't make it look good enough, especially on some slow devices. If you do it the simple way it "jumps" around from one spot to another. You could also use some animations to polish those jumps but this way the PopupWindow will always be "a step behind" where it should be on the map which I just don't like.

At this point, I was thinking about some other solution. I realized that I actually don't really need that much freedom - to show my custom views with all the possibilities that come with it (like animated progress bars etc.). I think there is a good reason why even the google engineers don't do it this way in the Google Maps app. All I need is a button or two on the InfoWindow that will show a pressed state and trigger some actions when clicked. So I came up with another solution which splits up into two parts:

First part:
The first part is to be able to catch the clicks on the buttons to trigger some action. My idea is as follows:

  1. Keep a reference to the custom infoWindow created in the InfoWindowAdapter.
  2. Wrap the MapFragment (or MapView) inside a custom ViewGroup (mine is called MapWrapperLayout)
  3. Override the MapWrapperLayout's dispatchTouchEvent and (if the InfoWindow is currently shown) first route the MotionEvents to the previously created InfoWindow. If it doesn't consume the MotionEvents (like because you didn't click on any clickable area inside InfoWindow etc.) then (and only then) let the events go down to the MapWrapperLayout's superclass so it will eventually be delivered to the map.

Here is the MapWrapperLayout's source code:

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

All this will make the views inside the InfoView "live" again - the OnClickListeners will start triggering etc.

Second part: The remaining problem is, that obviously, you can't see any UI changes of your InfoWindow on screen. To do that you have to manually call Marker.showInfoWindow. Now, if you perform some permanent change in your InfoWindow (like changing the label of your button to something else), this is good enough.

But showing a button pressed state or something of that nature is more complicated. The first problem is, that (at least) I wasn't able to make the InfoWindow show normal button's pressed state. Even if I pressed the button for a long time, it just remained unpressed on the screen. I believe this is something that is handled by the map framework itself which probably makes sure not to show any transient state in the info windows. But I could be wrong, I didn't try to find this out.

What I did is another nasty hack - I attached an OnTouchListener to the button and manually switched it's background when the button was pressed or released to two custom drawables - one with a button in a normal state and the other one in a pressed state. This is not very nice, but it works :). Now I was able to see the button switching between normal to pressed states on the screen.

There is still one last glitch - if you click the button too fast, it doesn't show the pressed state - it just remains in its normal state (although the click itself is fired so the button "works"). At least this is how it shows up on my Galaxy Nexus. So the last thing I did is that I delayed the button in it's pressed state a little. This is also quite ugly and I'm not sure how would it work on some older, slow devices but I suspect that even the map framework itself does something like this. You can try it yourself - when you click the whole InfoWindow, it remains in a pressed state a little longer, then normal buttons do (again - at least on my phone). And this is actually how it works even on the original Google Maps app.

Anyway, I wrote myself a custom class which handles the buttons state changes and all the other things I mentioned, so here is the 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);
}

Here is a custom InfoWindow layout file that I used:

<?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 activity layout file (MapFragment being inside the 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>

And finally source code of a test activity, which glues all this together:

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

That's it. So far I only tested this on my Galaxy Nexus (4.2.1) and Nexus 7 (also 4.2.1), I will try it on some Gingerbread phone when I have a chance. A limitation I found so far is that you can't drag the map from where is your button on the screen and move the map around. It could probably be overcome somehow but for now, I can live with that.

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.

Maddy
  • 4,525
  • 4
  • 33
  • 53
chose007
  • 3,541
  • 1
  • 13
  • 6
  • 6
    interesting solution. I want to try this. How was the performance? – Patrick Jackson Mar 26 '13 at 16:46
  • 1
    i have a issue, toast show after first click button & rortate device. – user2251725 May 28 '13 at 05:24
  • 1
    change return type of onTouch(View v,Motion event) of OnInfoWindowElemTouchListener abstract class to true.Then Toast show after click, means touch identify on touch. – user2251725 Jun 01 '13 at 13:15
  • 10
    need to replace `view.setBackground(bgDrawablePressed);` with `if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { view.setBackgroundDrawable(bgDrawablePressed); }else{ view.setBackground(bgDrawablePressed); }` to work in Android Gingerbread , Note: there is one more occurrence of `view.setBackground` in code. – Balaji Jun 24 '13 at 09:32
  • 1
    A new optimization for the OnInfoWindowElemTouchListener class: Use view.setPressed() to avoid handling two backgrounds drawables. In this way you can customize the backgrounds using drawable state selectors or any other standard way of assigning backgrounds, just like in a normal layout. – rgrocha Jul 01 '13 at 09:58
  • @Balaji, I needed to add your code snippet as you suggested that make my code working in newer devices. Thank You, too. – Chintan Raghwani Jul 24 '13 at 13:18
  • 1
    @ChintanRaghwani the original code in the answer will work in newer devices (say jelly bean) , the code snippet in my comment is to make it work for all android build versions. – Balaji Jul 24 '13 at 13:40
  • Hi @chose007! Thank you very much for your answer but i have a small issue. I was able to show a custom view on marker click but still the default white layout is visible. How to hide that .. – KK_07k11A0585 Jul 25 '13 at 13:52
  • 4
    KK_07k11A0585: It seems that your InfoWindowAdapter implementation is probably wrong. There are two methods to override: getInfoWindow() and getInfoContents(). The framework first calls the getInfoWindow() method and (only) if it returns null, it calls the getInfoContents(). If you return some View from getInfoWindow, the framework will lay it inside the default info window frame - which in your case you don't want to. So just return null from getInfoWindow and return you View from getInfoContent and it should be fine then. – chose007 Jul 30 '13 at 14:09
  • 18
    If anybody is interested, I'm using this solution in my own app - CG Transit (you can find it on Google Play). After two months it has been used by several tens of thousands of users and nobody complained about it yet. – chose007 Jul 30 '13 at 14:15
  • 1
    I tried your code and yet it worked fine, until I tried opening a dialog from one of the buttons I added. Didn't work. It's saying "BadTokenException: Unable to add window - token null is not for an application". You got any clue on this? EDIT: Fixed it. I used "getApplicationContext()" instead of "this" when building the AlertDialog. Using "this" fixes it. – Sebastian Oct 27 '13 at 10:43
  • @KK_07k11A0585 hey i m also facing same prob like yours. did you solved it.. and how ? – Dory Dec 19 '13 at 06:23
  • 1
    @chose007 thnks,grt answer !!! Can we have as many buttons on info window and how it handles the click event of one, can all button click event be handled same way. – Dory Dec 19 '13 at 09:35
  • The `MapView` doesn't clear the transient state of the info window. The reason click listeners don't work properly is that the press release event (and also the press event inside scrolling containers) is posted on the `ViewRoot` run queue instead of a `Handler` if the `View` is not attached to a `Window`, and thus will only execute when the `ViewRoot` is refreshed. My [implementation](https://gist.github.com/corsair992/8313269) bypasses this issue by creating an invisible wrapper for the info window and adding it to the `MapView` wrapper. See the [next comment](http://goo.gl/ftMqI3) for more. – corsair992 Jan 08 '14 at 08:15
  • ... The wrapper intercepts invalidate and layout request calls from the info window and dispatches them to the `MapView` by calling `showInfoWindow()` on the `Marker` (with a refresh buffer to ensure that the state is shown). This should make the info window fully live, although the refresh rate will be much lower than usual. Also, it's easy to enable scrolling on the map while pressing a button by dispatching the touch event to the `MapView` also, but then the whole window will be displayed as selected by the `MapView`. – corsair992 Jan 08 '14 at 08:16
  • Thanks for this awesome solution! Is it possible here to make asynchronous update of such InfoWindow, already displayed to the user? The case is about loading images from network *EDIT*: solution here http://stackoverflow.com/questions/15503266/dynamic-contents-in-maps-v2-infowindow – Sergii Jan 17 '14 at 23:38
  • 1
    is it possible to use getInfoWindow() instead of getInfoContents()? – maza23 Feb 14 '14 at 14:09
  • i try this code but i has nullpointerException arise.. at com.ngshah.goglemapv2withlazyloading.MainActivity.onCreate(MainActivity.java:69) arise error exception in this code map.setInfoWindowAdapter(new InfoWindowAdapter() { } – Gaurav Pansheriya Feb 17 '14 at 12:14
  • I loose my gestures after click on marker. Everything is ok, untill I click on marker. After that I can't do zoom by fingers. It seems to me that some view gets my gestures. Any idea? – edi233 Mar 03 '14 at 12:20
  • did anyone else got the pressed state to work? I implemented the code as above but used a state drawable, and view.pressed(). And do I need to set the marker and showInfoView() on start/end pressed for this to work? – TouchBoarder Mar 06 '14 at 23:19
  • I tried but there is no feedback after clicking on it. Debug and realise that startPress() is being called but endPress() is not... Anyone face the same problem? – Lee Yi Hong Mar 09 '14 at 09:08
  • I sorry for spamming... But I found out the reason why mine didn't manage trigger endPress(). I made a mistake with the click element. Instead of Button, I use ImageView. After changing the ImageView to Button. It works perfectly :) Thanks @chose007 – Lee Yi Hong Mar 09 '14 at 09:26
  • I tryed this code, but like @GauravPansheriya i have a null pointer exceptiom in the main activity. mapWrapperLayout is ever null final MapWrapperLayout mapWrapperLayout = (MapWrapperLayout)findViewById(R.id.map_relative_layout); – Frank Mar 13 '14 at 16:25
  • @chose007 i m trying to create the custom adapter of infowindow and get that but when i try to get the button click on it thats time i am not get the button click.. i think this listener not work in custom adapter of infowindow.. any help..?? – Gorgeous_DroidVirus May 12 '14 at 05:17
  • @chose007 I have use your solution for making button click work for infowindow but when change getInfoContents to use getInfoWindow for create my Map overlay it is now work perfect for deliver click event of Button ,while view return from getInfoContents work well.Do you known how to make it work perfect like it work for getInfoContents.i have to use getInfoWindow because i want to change Frame of Overlay as well. – Herry Jun 02 '14 at 06:54
  • @chose007 i think in your app CG Transityou are using getInfoContents to create view for your Overlay infowindow,Right?but if you change it to getInfoWindowthen it willl effect Click event of Button let me known same happen with you . – Herry Jun 02 '14 at 07:42
  • The solution looks huge but is as simple as Ctrl+C..Ctrl+V – Pranav Mahajan Jun 03 '14 at 10:51
  • Thanks for the answer! Anyone posted it on github? =) – Deinlandel Aug 28 '14 at 08:55
  • Hi ! I am using the same approach to get a live infowindow with buttons. Though I cannot animate the progress bar inside but it's fine. The problem comes when I navigate back from the map fragment and come back to it. I found the infowindow started behaving like the default one. Now it is no more live. Do you have any solution for this ? – Code Word Sep 18 '14 at 20:19
  • 2
    how do you determine the bottom offset if you're not using a standard infowindow? – Rarw Dec 20 '14 at 21:47
  • I add my custom layout. It shows the white rectangle in the background of my custom layout. How to remove the white rectangle. – Manikandan Jan 07 '15 at 07:03
  • I have a close button in the infowindow, when I click the button, I need to close the infowindow. I just set the visibility of the infowindow to gone. But, when I again click any of the markers, the infowindow doesn't appear. How to fix it? – Manikandan Jan 07 '15 at 10:10
  • I like to do something on snippet text click. But it is not triggered. How to achieve this? – Manikandan Jan 13 '15 at 12:26
  • Hi @manikandan.. instead of touch listner implement onclick listener..Me also working on same.. it is working but sometimes it is not working..I am wondering why it is not working.. – shyam.y Jan 13 '15 at 16:51
  • hi @chose007 please help me,, i am facing problem with clicking on textview instead of button.. i have 3 textviews with 3 different listners. I changed touch listener with clicklistener.. working fine with one marker.. but for other markers the listners are not calling..please help me.. – shyam.y Jan 13 '15 at 17:02
  • @chose007 Will this solution work for Map work for clustering. – Rethinavel Jan 31 '15 at 12:39
  • I tried using this hack, it worked at first, but for my use case, I need multiple buttons to be clicked in the InfoWindow. The first button fires the onClickConfirmed callback, however the second button doesnt. Any modifications needed to make multiple buttons work? – patrickjason91 Mar 04 '15 at 08:04
  • Hi, i tried like, write on click listener for more than one button means, listener is not working.. only, single button listener is working fine.. can you reply is it possible to write on click for more than one buttton – harikrishnan Apr 02 '15 at 11:23
  • How do you deal with different resolutions needing a different bottomOffsetPixels? You cannot hard code bottomOffsetPixels – Barodapride Apr 06 '15 at 13:22
  • If we load a map fragment inside the fragment than whether this should be supported or not. – Jaffar Raza Apr 10 '15 at 07:51
  • 2
    Change OnInfoWindowElemTouchListener onTouch to return true; and this work for me. – Volodymyr Kulyk Apr 15 '15 at 14:50
  • For a weird reason, `View.OnClickListener` doesn't work on Android 7. Using a `View.OnTouchListener` filtered on `MotionEvent.ACTION_UP` works fine as a work-around. – Marc Plano-Lesay Sep 13 '16 at 12:30
  • This is amazing, not only is it the perfect solution for my map, but it also taught me a lot about how to make custom views. – Carson Sep 14 '16 at 03:15
  • @chose007 great solution, but I am facing a small issue, how can I make infowindow transparent? I can make it transparent but returning getInfoWindow a non null value, but then click event doesn't work – Mithun Sarker Shuvro Nov 27 '16 at 06:54
  • Do you know how to change this implementation for a gridview? I outlined my question here: http://stackoverflow.com/questions/43655642/v2-map-android-gridview-inside-infowindow-implementing-onitemclicklistener – Fullhdpixel Apr 27 '17 at 10:51
  • 1
    Can any buddy help.. This is not working for me. I followed all the steps mentioned in the answer but no luck. Checked in OS 7 in device MOTO G4+. – Yog Guru Jun 16 '17 at 06:08
  • Can you suggest how to make Edit text work in this? I have added Edit text but I am unable to click on it. https://prnt.sc/NcF6ZwDo6STJ – Jasmin Sojitra Apr 20 '22 at 11:23
  • I tried to integrate this code to my app (I don't need step 2, but simple need two separate buttons clickable on the info window) and for some reason when the info window pops up the buttons are disabled. Not sure why. The click handlers are attached. – Csaba Toth Nov 15 '22 at 05:43
14

I see that this question is already old but still...

We made a sipmle library at our company for achieving what is desired - An interactive info window with views and everything. You can check it out on github.

I hope it helps :)

dephinera
  • 3,703
  • 11
  • 41
  • 75
9

Here's my take on the problem. I create AbsoluteLayout overlay which contains Info Window (a regular view with every bit of interactivity and drawing capabilities). Then I start Handler which synchronizes the info window's position with position of point on the map every 16 ms. Sounds crazy, but actually works.

Demo video: https://www.youtube.com/watch?v=bT9RpH4p9mU (take into account that performance is decreased because of emulator and video recording running simultaneously).

Code of the demo: https://github.com/deville/info-window-demo

An article providing details (in Russian): http://habrahabr.ru/post/213415/

Andrii Chernenko
  • 9,873
  • 7
  • 71
  • 89
2

For those who couldn't get choose007's answer up and running

If clickListener is not working properly at all times in chose007's solution, try to implement View.onTouchListener instead of clickListener. Handle touch event using any of the action ACTION_UP or ACTION_DOWN. For some reason, maps infoWindow causes some weird behaviour when dispatching to 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 : This is how I did it step by step

First inflate your own infowindow (global variable) somewhere in your activity/fragment. Mine is within fragment. Also insure that root view in your infowindow layout is linearlayout (for some reason relativelayout was taking full width of screen in 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;

Then in onMapReady callback of google maps android api (follow this if you donot know what onMapReady is Maps > Documentation - Getting Started )

   @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 initialization http://stackoverflow.com/questions/14123243/google-maps-android-api-v2- interactive-infowindow-like-in-original-android-go/15040761#15040761 39 - default marker height 20 - offset between the default InfoWindow bottom edge and it's content bottom edge */

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

SetInfoWindow method

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

Handle marker click separately

    @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;
    }
Prags
  • 2,457
  • 2
  • 21
  • 38
Manish Jangid
  • 185
  • 1
  • 2
  • 9
  • can you show me your code ? I was able to log touch events successfully but not click events so I went for this implementation. Also, you need to implement choose007's solution properly. – Manish Jangid May 04 '16 at 11:49
  • I shared my complete implementation. Let me know if you have any queries – Manish Jangid May 04 '16 at 12:21
-1

Just a speculation, I have not enough experience to try it... )-:

Since GoogleMap is a fragment, it should be possible to catch marker onClick event and show custom fragment view. A map fragment will be still visible on the background. Does anybody tried it? Any reason why it could not work?

The disadvantage is that map fragment would be freezed on backgroud, until a custom info fragment return control to it.

Wooff
  • 1,091
  • 12
  • 23
-3

I have build a sample android studio project for this question.

output screen shots :-

enter image description here

enter image description here

enter image description here

Download full project source code Click here

Please note: you have to add your API key in Androidmanifest.xml

Premkumar Manipillai
  • 2,121
  • 23
  • 24
-8

It is really 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;

            }

        });

Just add above code in your class where you are using GoogleMap. R.layout.info_window_layout is our custom layout that is showing the view that will come in place of infowindow. I just added the textview here. You can add additonal view here to make it like the sample snap. My info_window_layout was

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

I hope it will help. We can find a working example of custom infowindow at http://wptrafficanalyzer.in/blog/customizing-infowindow-contents-in-google-map-android-api-v2-using-infowindowadapter/#comment-39731

EDITED : This code is shows how we can add custom view on infoWindow. This code did not handle the clicks on Custom View items. So it is close to answer but not exactly the answer that's why It is not accepted as answer.

Zeeshan Mirza
  • 4,549
  • 3
  • 22
  • 32
  • Do you override getInfoContents and getInfoWindow where you create the markers? For example, if I am adding different kinds of markers and using different methods, would I just override within the body of each of those methods where I create the different markers? – Rarw Feb 28 '13 at 17:33
  • Yes I override getInfoContents and getInfoWindow. – Zeeshan Mirza Feb 28 '13 at 17:48
  • Just tried this out and it works jus like inflating any other xml layout - good job – Rarw Feb 28 '13 at 18:19
  • 31
    It looks like you didnt read my question. I know how to create custom view inside infowindow. Which i dont know is how to make custom button inside and listen to its click events. – user1943012 Mar 11 '13 at 13:46
  • 5
    This solution does not answer the original question – biddulph.r Apr 24 '13 at 10:32
  • actually i place a button on the popup dialog.i write onClickListener for that button.but it is not working. how to write listener for the button in the popup dialog???? @zeeshan0026 – Venkat Apr 29 '13 at 04:09
  • Hello I have checked your code and it works fine for me but i have one more que that Can we change the position of infowindow from Upper side to right side of marker ??? – Harsh Trivedi Apr 30 '13 at 11:12
  • zeeshan i just want to know is it possible or not ? – Harsh Trivedi Apr 30 '13 at 11:13
  • This solution does not answer the question – TlmaK0 May 27 '13 at 20:02
  • you only receive callbacks for clicks on the info window. – LostPuppy May 28 '13 at 23:13
  • @TlmaK0 We know this is not answer and I also mention it at the end of the answer. – Zeeshan Mirza May 29 '13 at 05:14
  • This isn't really the solution, it's just restating the question. This adapter takes a snapshot of the view by rendering it to a bitmap, it's not interactive. – HaMMeReD Jun 26 '13 at 22:40
  • It does not seems to really answer the original question. Maybe you could expand the answer to be a full answer? Otherwise it seems no idea to upvote this answer. – Johan Karlsson Mar 06 '15 at 09:40