Google Maps Android API v2 - 交互式信息窗口(就像在原始的 android 谷歌地图中一样) [英] Google Maps Android API v2 - Interactive InfoWindow (like in original android google maps)

查看:18
本文介绍了Google Maps Android API v2 - 交互式信息窗口(就像在原始的 android 谷歌地图中一样)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试使用新的 Google Maps API v2 在点击标记后制作自定义 InfoWindow.我希望它看起来像 Google 的原始地图应用程序.像这样:

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 时,它不起作用 - 整个 InfoWindow 被选中,而不仅仅是 ImageButton.我读到这是因为没有 View 本身但它是快照,因此无法区分各个项目.

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.

文档中(感谢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?

推荐答案

我自己正在寻找解决这个问题的方法,但运气不佳,所以我不得不自己动手,在这里与您分享.(请原谅我的英语不好)(用英语回答另一个捷克人有点疯狂:-))

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

我尝试的第一件事是使用旧的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.

此时,我正在考虑其他一些解决方案.我意识到我实际上并不需要那么大的自由 - 显示我的自定义视图及其附带的所有可能性(例如动画进度条等).我认为即使是谷歌工程师也不在谷歌地图应用程序中这样做是有充分理由的.我所需要的只是 InfoWindow 上的一两个按钮,该按钮将显示按下状态并在单击时触发一些操作.所以我想出了另一个解决方案,它分为两部分:

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. 保留对在 InfoWindowAdapter 中创建的自定义信息窗口的引用.
  2. MapFragment(或 MapView)包裹在自定义 ViewGroup 中(我的称为 MapWrapperLayout)
  3. 覆盖 MapWrapperLayout 的 dispatchTouchEvent 并且(如果当前显示 InfoWindow)首先将 MotionEvents 路由到之前创建的 InfoWindow.如果它不消耗 MotionEvents(比如因为你没有点击 InfoWindow 内的任何可点击区域等),那么(并且只有这样)让事件进入 MapWrapperLayout 的超类,以便它最终被传递到地图.
  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.

这是 MapWrapperLayout 的源代码:

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.

第二部分:剩下的问题是,很明显,您在屏幕上看不到 InfoWindow 的任何 UI 更改.为此,您必须手动调用 Marker.showInfoWindow.现在,如果您在 InfoWindow 中执行一些永久性更改(例如将按钮的标签更改为其他内容),这就足够了.

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.

但是显示按钮按下状态或类似的东西更复杂.第一个问题是,(至少)我无法让 InfoWindow 显示正常按钮的按下状态.即使我长时间按下按钮,它也只是在屏幕上保持未按下状态.我相信这是由地图框架本身处理的事情,它可能确保不会在信息窗口中显示任何瞬态.但我可能是错的,我没有试图找出这一点.

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 附加到按钮并在按钮被按下或释放时手动将其背景切换到两个自定义可绘制对象 - 一个按钮处于正常状态另一个处于按下状态.这不是很好,但它有效:).现在我能够在屏幕上看到按钮在正常状态和按下状态之间切换.

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.

还有最后一个小故障——如果你点击按钮太快,它不会显示按下状态——它只是保持正常状态(尽管点击本身被触发,所以按钮工作").至少这是它在我的 Galaxy Nexus 上的显示方式.所以我做的最后一件事是我延迟了按下状态的按钮.这也很丑陋,我不确定它在一些旧的、速度较慢的设备上如何工作,但我怀疑即使是地图框架本身也会做这样的事情.您可以自己尝试一下 - 当您单击整个 InfoWindow 时,它会保持按下状态一段时间,然后正常按钮会这样做(再次 - 至少在我的手机上).这实际上也是它在原始 Google 地图应用上的工作方式.

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

这是我使用的自定义 InfoWindow 布局文件:

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>

测试活动布局文件(MapFragmentMapWrapperLayout 中):

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

就是这样.到目前为止,我只在我的 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 框架的理由(顺便说一句.我真的真的会喜欢避免使用带有片段等的新应用程序).我只是不明白为什么 Google 不向开发人员提供某种官方方式来在 InfoWindows 上设置按钮.这是一种很常见的设计模式,而且这种模式甚至在官方的谷歌地图应用程序中也有使用:).我理解他们不能让您的视图在 InfoWindows 中实时显示"的原因 - 这可能会在移动和滚动地图时降低性能.但是应该有一些方法可以在不使用视图的情况下实现这种效果.

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.

这篇关于Google Maps Android API v2 - 交互式信息窗口(就像在原始的 android 谷歌地图中一样)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆