Google Maps Android API v2 - 交互式InfoWindow(与原版Android谷歌地图一样)

use*_*012 189 android google-maps infowindow google-maps-android-api-2

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

示例图像

当我在ImageButton里面时,它不起作用 - 整个InfoWindow选择而不仅仅是ImageButton.我读到这是因为它View本身没有,但它是快照,所以不能区分各个项目.

编辑:文档中(感谢Disco S2):

正如上一节关于信息窗口中所提到的,信息窗口不是实时视图,而是视图在地图上呈现为图像.因此,您在视图上设置的任何侦听器都将被忽略,并且您无法区分视图各个部分上的单击事件.建议您不要在自定义信息窗口中放置交互式组件(如按钮,复选框或文本输入).

但如果谷歌使用它,必须有一些方法来实现它.有谁有想法吗?

cho*_*007 331

我一直在寻找解决这个问题的方法,没有运气,所以我不得不自己滚动,我想和你分享.(请原谅我的英语不好)(用英语回答另一个捷克人有点疯狂:-))

我尝试的第一件事是使用一个好老PopupWindow.这很简单 - 只需要听一下OnMarkerClickListener,然后PopupWindow在标记上方显示自定义.StackOverflow上的其他一些人提出了这个解决方案,乍一看它实际上看起来很不错.但是,当您开始移动地图时,此解决方案的问题就出现了.你必须自己移动PopupWindow可能的(通过听一些onTouch事件),但恕我直言,你不能使它看起来不够好,特别是在一些慢设备上.如果你这样做,它就会从一个地方"跳跃"到另一个地方.你也可以使用一些动画来修饰那些跳跃,但这样PopupWindow一来,它总是"落后",它应该在地图上,我不喜欢.

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

第一部分:
第一部分是能够捕捉按钮上的点击以触发某些动作.我的想法如下:

  1. 保留对InfoWindowAdapter中创建的自定义infoWindow的引用.
  2. MapFragment(或MapView)包装在自定义ViewGroup中(我的称为MapWrapperLayout)
  3. 覆盖MapWrapperLayoutdispatchTouchEvent和(如果当前显示InfoWindow)首先将MotionEvents路由到先前创建的InfoWindow.如果它没有消耗MotionEvents(就像你没有点击InfoWindow里面的任何可点击区域等那样)那么(并且只有那时)让事件下到MapWrapperLayout的超类,这样它最终会被传递到地图.

这是MapWrapperLayout的源代码:

package com.circlegate.tt.cg.an.lib.map;

import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.Marker;

import android.content.Context;
import android.graphics.Point;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.RelativeLayout;

public class MapWrapperLayout extends RelativeLayout {
    /**
     * Reference to a GoogleMap object 
     */
    private GoogleMap map;

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

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

    /**
     * Our custom view which is returned from either the InfoWindowAdapter.getInfoContents 
     * or InfoWindowAdapter.getInfoWindow
     */
    private View infoWindow;    

    public MapWrapperLayout(Context context) {
        super(context);
    }

    public MapWrapperLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MapWrapperLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

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

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

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

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

            // Dispatch the adjusted MotionEvent to the infoWindow
            ret = infoWindow.dispatchTouchEvent(copyEv);
        }
        // If the infoWindow consumed the touch event, then just return true.
        // Otherwise pass this event to the super class and return it's result
        return ret || super.dispatchTouchEvent(ev);
    }
}
Run Code Online (Sandbox Code Playgroud)

所有这些将使InfoView中的视图再次"实时" - OnClickListeners将开始触发等.

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

但是显示按钮按下状态或某种性质的东西更复杂.第一个问题是,(至少)我无法使InfoWindow显示正常按钮的按下状态.即使我按下按钮很长一段时间,它仍然在屏幕上保持未按下状态.我相信这是由地图框架本身处理的东西,它可能确保不在信息窗口中显示任何瞬态.但我可能是错的,我没有试图找到它.

我所做的是另一个讨厌的黑客 - 我OnTouchListener按下按钮并在按下或释放到两个自定义绘图时手动切换它的背景 - 一个按钮处于正常状态而另一个处于按下状态.这不是很好,但它的工作原理:).现在我能够在屏幕上看到按钮在正常状态和按下状态之间切换.

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

无论如何,我写了一个自定义类来处理按钮状态更改和我提到的所有其他事情,所以这里是代码:

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);
}
Run Code Online (Sandbox Code Playgroud)

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

<?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>
Run Code Online (Sandbox Code Playgroud)

测试活动布局文件(MapFragment在里面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>
Run Code Online (Sandbox Code Playgroud)

最后是测试活动的源代码,它将所有这些粘合在一起:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

而已.到目前为止,我只在Galaxy Nexus(4.2.1)和Nexus 7(也是4.2.1)上测试了这个,我有机会在姜饼手机上尝试一下.到目前为止我发现的限制是你无法从屏幕上的按钮位置拖动地图并移动地图.它可能会以某种方式克服,但就目前而言,我可以忍受.

我知道这是一个丑陋的黑客,但我没有找到更好的东西,我需要这个设计模式如此糟糕,这真的是回到地图v1框架的原因(顺便说一下.我真的很想避免对于带有碎片等的新应用程序.我只是不明白为什么谷歌不向开发人员提供一些在InfoWindows上有按钮的官方方式.这是一种常见的设计模式,而且这种模式甚至可以在官方谷歌地图应用程序中使用:).我理解他们为什么不能在InfoWindows中"实时"显示你的观点的原因 - 这可能会在移动和滚动地图时扼杀性能.但是应该有一些方法如何在不使用视图的情况下实现此效果.

  • 如果有人感兴趣,我在我自己的应用程序中使用此解决方案 - CG Transit(您可以在Google Play上找到它).两个月后,它被成千上万的用户使用,没有人抱怨它. (18认同)
  • 需要替换`view.setBackground(bgDrawablePressed);`with`if(Build.VERSION.SDK_INT <Build.VERSION_CODES.JELLY_BEAN){view.setBackgroundDrawable(bgDrawablePressed); } else {view.setBackground(bgDrawablePressed); 在Android Gingerbread中工作,注意:在代码中还有一次`view.setBackground`. (10认同)
  • 有趣的方案.我想尝试这个.表现如何? (5认同)
  • KK_07k11A0585:您的InfoWindowAdapter实现似乎可能是错误的.有两种方法可以覆盖:getInfoWindow()和getInfoContents().框架首先调用getInfoWindow()方法,并且(仅)如果返回null,则调用getInfoContents().如果从getInfoWindow返回一些View,框架将把它放在默认的信息窗口框架内 - 在你不喜欢的情况下.所以只需从getInfoWindow返回null并从getInfoContent返回View,它应该没问题. (4认同)
  • 如果您没有使用标准信息窗,如何确定底部偏移量? (2认同)
  • 更改OnInfoWindowElemTouchListener onTouch返回true; 这对我有用. (2认同)

def*_*era 12

我看到这个问题已经老了但仍然......

我们在公司建立了一个sipmle库,以实现所需的目标 - 一个包含视图和所有内容的交互式信息窗口.你可以在github上查看.

我希望它有帮助:)


And*_*nko 8

这是我对这个问题的看法.我创建了AbsoluteLayout包含信息窗口的叠加层(具有每一点交互性和绘图功能的常规视图).然后我开始Handler每隔16毫秒将信息窗口的位置与地图上的点位置同步.听起来很疯狂,但确实有效.

演示视频:https://www.youtube.com/watch?v = bT9RpH4p9mU(考虑到由于模拟器和视频录制同时运行而导致性能下降).

演示代码:https://github.com/deville/info-window-demo

提供详细信息的文章(俄文):http://habrahabr.ru/post/213415/