1. ホーム
  2. android

[解決済み] Google Maps Android API v2 - インタラクティブ インフォウィンドウ (オリジナルのGoogleマップのような)

2022-04-28 04:01:16

質問

私は、カスタムメイドの InfoWindow 新しいGoogle Maps API v2でマーカーをクリックした後に、Googleのオリジナルの地図アプリケーションのように表示させたいのです。このように。

がある場合 ImageButton の内部では、動作しません。 InfoWindow が選択されるだけでなく ImageButton . がないからだと読みました。 View が、スナップショットなので、個々のアイテムが区別できない。

EDITです。 での ドキュメント (おかげさまで ディスコS2 ):

情報ウィンドウの項で述べたように、情報ウィンドウは はライブビューではなく、画像としてビューにレンダリングされます。 マップを作成します。その結果、ビューに設定したリスナーはすべて無視されます。 の様々な部分のクリックイベントを区別することはできません。 を表示します。のようなインタラクティブなコンポーネントは配置しないことをお勧めします。 ボタン、チェックボックス、テキスト入力などのカスタム情報 ウィンドウに表示されます。

でも、Googleが使うなら、何か作り方があるはず。どなたかご存じないですか?

解決方法は?

私はこの問題の解決策を探していましたが、うまくいかなかったので、自分なりに解決策を考え、ここで皆さんにお伝えしたいと思います。(私の悪い英語を許してください) (それは英語で他のチェコの男に答えるために少しクレイジーです :-) )

最初に試したのは、古き良き時代の PopupWindow . これはとても簡単なことです。 OnMarkerClickListener を表示し、カスタム PopupWindow マーカーの上に StackOverflowで他の人がこの解決策を提案し、実際、一見するとかなり良さそうです。しかし、この解決策の問題は、マップを動かし始めたときに現れます。この場合 PopupWindow それは可能ですが(onTouchイベントをリッスンすることで)、IMHOは、特に遅いデバイスでは、それを十分に見栄えよくすることはできません。単純な方法で行うと、ある場所から別の場所へと飛び回るようになります。アニメーションを使用してジャンプに磨きをかけることもできますが、この方法では PopupWindow このままでは、地図上のあるべき位置から常に一歩遅れていることになり、私はそれが嫌なのです。

この時点で、何か別の解決策を考えていました。カスタムビューに付随するあらゆる可能性(アニメーションのプログレスバーなど)を表示するためには、実はそれほど多くの自由度は必要ないことに気づいたのです。Googleのエンジニアでさえ、Google Mapsアプリでこの方法をとらないのには、それなりの理由があるのだと思います。必要なのは、押された状態を表示し、クリックされたときに何らかのアクションを引き起こす、InfoWindow上の1つか2つのボタンだけです。そこで、2つのパートに分かれる別の解決策を思いつきました。

最初の部分。

最初の部分は、ボタンのクリックをキャッチして、何らかのアクションを起こすことができるようにすることです。私のアイデアは次のとおりです。

  1. InfoWindowAdapterで作成したカスタムinfoWindowへの参照を保持する。
  2. ラップ MapFragment (または MapView ) を、カスタムViewGroup (私の場合はMapWrapperLayout) 内に作成します。
  3. をオーバーライドして MapWrapperLayout の dispatchTouchEvent を使用して、(InfoWindow が現在表示されている場合)まず MotionEvent を以前に作成した InfoWindow にルーティングします。もし、InfoWindowがMotionEventを消費しない場合(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);
    }
}

これにより、InfoView内のビューが再び生き返り、OnClickListenersがトリガーされるようになります。

第2部 残りの問題は、明らかに、InfoWindowのUIの変更を画面上で見ることができないことです。そのため、手動でMarker.showInfoWindowを呼び出す必要があります。ただし、InfoWindowに恒久的な変更を加える場合(ボタンのラベルを別のものに変更するなど)には、これで十分です。

しかし、ボタンが押された状態などを表示するのはもっと複雑です。最初の問題は、(少なくとも)私は通常のボタンの押された状態をInfoWindowに表示させることができなかったということです。ボタンを長時間押しても、画面上では押されていない状態のままでした。これは、おそらくマップフレームワーク自体が、情報ウィンドウに過渡的な状態を表示しないように処理するものだと思います。しかし、私はこれを見つけようとしなかったので、私は間違っているかもしれません。

私が行ったのは、もう一つの厄介なハックです。 OnTouchListener をボタンに追加し、ボタンが押されたり離されたりしたときの背景を、2つのカスタムdrawable(1つは通常状態のボタン、もう1つは押された状態のボタン)に手動で切り替えています。これはあまりいいものではありませんが、動作します :)。これで、ボタンが通常状態と押された状態の間で切り替わるのを画面上で確認することができるようになりました。

ボタンを速くクリックすると、押された状態が表示されず、通常の状態のままになります(ただし、クリック自体は行われているので、ボタンは動作します)。少なくとも、私のGalaxy Nexusではこのように表示されます。そこで最後に、ボタンが押された状態を少し遅らせました。これもかなり不細工で、古いデバイスや遅いデバイスではどうなるかわかりませんが、マップフレームワーク自体もこのようなことをしていると思われます。InfoWindow全体をクリックすると、通常のボタンよりも少し長く押された状態が続きます(繰り返しますが、少なくとも私の携帯電話では)。そしてこれは、オリジナルのGoogle Mapsアプリでも実際に動作している方法です。

とにかく、ボタンの状態の変化と、私が述べた他のすべてのことを処理するカスタムクラスを自分で書いたので、ここにそのコードを示します。

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のレイアウトファイルです。

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

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

そして最後に、これらすべてを統合するテストアクティビティーのソースコードです。

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) でしか試していませんが、機会があれば、Gingerbread携帯でも試してみようと思っています。今のところ、画面上のボタンの位置から地図をドラッグして移動させることができないのが難点です。これは何とか克服できるかもしれませんが、今のところ我慢しています。

これは醜いハックだと分かっていますが、他に良いものが見つからなかったのと、このデザインパターンがとても必要なので、map v1フレームワークに戻る理由になってしまいます(ちなみに、map v1フレームワークには、"map "と "map "があります)。フラグメントなどを使った新しいアプリでは避けたいところです)。なぜGoogleは開発者にInfoWindowsにボタンを表示する公式な方法を提供しないのか、理解できません。これは一般的なデザインパターンであり、さらにこのパターンは公式のGoogle Mapsアプリでさえも使用されています :)。ビューをインフォウィンドウに表示できない理由は理解できます。しかし、ビューを使用せずにこの効果を達成する方法については、何らかの方法があるはずです。