谷歌地图Android的API V2 - 互动信息窗口(如在原生Android谷歌地图)地图、互动、如在、窗口

2023-09-11 10:34:17 作者:Transparent(透明)

我后与新的谷歌地图API V2标志点击尝试将进行自定义信息窗口。我希望它看起来像由谷歌原来的地图应用程序。像这样的:

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:

当我有的ImageButton 里面,它不工作 - 整个信息窗口的slected而不仅仅是的ImageButton 。我看,这是因为没有一个查看本身,而是它的快照,所以个别项目不能相互区别开来。

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.

编辑: 在文档(感谢的迪斯科S2 ):

正如在信息窗口的previous部分,一个信息窗口   是不是活的视图,而该视图呈现为一个图像到   地图。这样一来,您对视图设置的任何监听器被忽略   你不能在各部分点击事件区分   风景。建议您不要将交互式组件 - 例如   如按钮,复选框或文本输入 - 在您的自定义信息   窗口。

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?

推荐答案

我一直在寻找这个问题的解决方案,我没有运气,所以我不得不推出自己的,我想在这里跟大家分享一下。 (请原谅我的英语不好)(这是一个有点疯狂英语回答:-)另一个捷克人)

I was looking for a solution of 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 :-) )

我想的第一件事是用好老PopupWindow。这是很容易 - 人们只要听OnMarkerClickListener,然后显示自定义PopupWindow在标记上。在这里StackOverflow的其他一些人提出这个解决方案,它实际上乍看上去相当不错。但是,这种解决方案的问题显示出来,当你开始移动地图。你必须将PopupWindow不知何故自己这是可能的(通过听一些onTouch事件),但恕我直言,你不能使它看起来不够好,尤其是在一些速度较慢的设备。如果你这样做了简单的方法是跳围绕从一个点到另一个。你也可以使用一些动画擦亮那些跳跃,但这样的PopupWindow总是会在那里它应该是在地图上,我只是不喜欢落后一步。

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.

在这一点上,我想一些其他的解决办法。我意识到,我其实并不需要那么多的自由 - 以显示我的自定义视图与所有附带它(如动画的进度条等)的可能性。我觉得这是一个很好的理由,甚至谷歌的工程师们不这样做,这种方式在谷歌地图应用程序。所有我需要的是两个上会显示出pressed状态,点击时触发一些动作信息窗口的按钮。于是我想出了另一种解决方案,它分裂成两个部分:

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

在保持一个参考的InfoWindowAdapter创建的自定义信息窗口。 裹的MapFragment(或图形页面)的自定义里面的ViewGroup(我叫MapWrapperLayout) 重写MapWrapperLayout的dispatchTouchEvent和(如果信息窗口是当前显示)第一条路线的MotionEvents到pviously创建信息窗口的$ P $。如果它不消耗MotionEvents(比如因为你没有点击里面的信息窗口等任何可点击区域),那么(只有这样)让活动深入到MapWrapperLayout的超类,所以它最终会被传递到地图

下面是MapWrapperLayout源$ C ​​$ C:

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

这一切都会使InfoView的内部的意见活了 - 在OnClickListeners将开始触发等

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

第二部分: 剩下的问题是,那显然你不能看到你的信息窗口在屏幕上的任何用户界面的变化。要做到这一点,你必须手动调用Marker.showInfoWindow。现在,如果你执行你的信息窗口的一些永久性的改变(如改变你的按钮到别的标签),这是不够好。

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.

但显示一个按钮pressed状态或性质的东西比较复杂。第一个问题是,(至少)我是不是能够使信息窗口正常显示按钮的pressed状态。即使我pressed按钮很长一段时间,它只是保持pssed在屏幕上未$ P $。我相信这是由地图框架本身这可能确保不显示的信息窗口的任何过渡状态处理的事务。但我可能是错的,我并没有试图找出了这一点。

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.

我所做的是另一个讨厌的黑客 - 我重视的OnTouchListener到按钮,手动切换它的背景,当按钮是pressed或释放两种自定义的可绘制 - 之一,在正常状态下的按钮,另一个在pressed状态。这是不是很漂亮,但它的工作原理:)。现在,我可以在屏幕上看到正常的按钮,切换到pressed状态。

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.

还有最后一个小故障 - 如果你按一下按钮太快,它不会显示pressed状态 - 它只是保持在它的正常状态(虽然点击本身发射使按键作品 )。至少,这是它是如何显示在我的Galaxy Nexus的。因此,最后一件事,我做的是我耽误了按钮,在它的pressed状态一点点。这也很丑陋,我不知道会是怎样的一些旧的,速度较慢的设备工作,但我怀疑,即使在地图框架本身做这样的事情。你可以自己尝试一下 - 当你点击整个信息窗口,它仍然在pressed状态一会儿,然后正常的按钮做的(再次 - 至少在我的手机)。这实际上是如何工作的,即使在原有的谷歌地图应用程序。

There is still one last glitch - if you click the button too fast, it doesn't show the pressed state - it just remains in it's 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 in the original Google Maps app.

不管怎样,我写我自己负责处理的按键状态变化,所有我提到的其他事情的自定义类,所以这里是code:

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>

测试活动的布局文件(MapFragment作为MapWrapperLayout内):

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>

和最终来源$ C ​​$一个测试活动,这本胶合在一起的C:

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

就是这样。到目前为止,我只测试了这对我的Galaxy Nexus的(4.2.1)和Nexus 7(也4.2.1),我会尝试一些姜饼手机,当我有机会。一个限制我发现到目前为止,你不能拖从哪里是在屏幕上的按钮,地图和移动地图。它很可能以某种方式解决,但现在我可以住在一起。

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.

我知道这是一个丑陋的黑客攻击,但我只是没有找到更好的东西,我需要这种设计模式如此糟糕,这将是一个真正理由去回到地图V1框架(其中顺便说一句,我真的真的想避免与片段一个新的应用程序等)。我只是不明白,为什么谷歌不提供开发者对信息窗口按钮一些官方的方式。它是这样一个共同的设计模式,而且这种模式被用于甚至在谷歌官方的地图应用:)。我明白他们为什么不能只是让你的意见活在信息窗口的原因 - 这将移动和左右滚动地图时可能会杀了性能。但应该有一些办法如何实现不使用视图这种效果。

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.