[解決済み] Google Maps Android API v2 - インタラクティブ インフォウィンドウ (オリジナルのGoogleマップのような)
質問
私は、カスタムメイドの
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つのパートに分かれる別の解決策を思いつきました。
最初の部分。
最初の部分は、ボタンのクリックをキャッチして、何らかのアクションを起こすことができるようにすることです。私のアイデアは次のとおりです。
- InfoWindowAdapterで作成したカスタムinfoWindowへの参照を保持する。
-
ラップ
MapFragment
(またはMapView
) を、カスタムViewGroup (私の場合はMapWrapperLayout) 内に作成します。 -
をオーバーライドして
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アプリでさえも使用されています :)。ビューをインフォウィンドウに表示できない理由は理解できます。しかし、ビューを使用せずにこの効果を達成する方法については、何らかの方法があるはずです。
関連
-
[解決済み】Android "ビュー階層を作成した元のスレッドだけが、そのビューに触れることができる"
-
プログラム "git.exe "を実行できない場合の正しい解決方法です。CreateProcessエラー=2
-
ジャークとして。起動アクティビティを特定できませんでした。デフォルトのアクティビティが見つかりません アクティビティ起動中のエラー
-
android:EMSのプロパティ
-
エラー:未宣言の識別子(AS)の使用
-
Androidの美しいSeekBarスタイルのカスタマイズ
-
Android基本アプレット
-
Androidのカラーグラデーション実装のまとめ
-
アンドロイドスタジオ学習入門
-
[解決済み] Google Maps JS API v3 - シンプルな複数マーカーの例
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
gitlab 設定エラー。リモートリポジトリから読み込めなかったか、ホストキーの検証に失敗しました。
-
Android Studio を 3.6.3 にアップデートした後、構成 :classpath のアーティファクトをすべて解決できない。
-
android E/RecyclerView﹕ アダプタが接続されていないため、レイアウトをスキップする。
-
Android: インポートモジュールエラー Android リソースのリンクに失敗しました
-
指定された子にはすでに親がいます。まず、その子の親に対して removeView() をコールする必要があります。
-
JVMのエラーに遭遇しました。Java Runtime Environmentによって致命的なエラーが検出されました。
-
ライブラリをモジュールとしてインポートする際にエラーが発生しました。Error:A problem occurred configuring project ':library'.
-
Androidプロセス生存のためのソリューション
-
アンドロイドリストビュー
-
android.content.ActivityNotFoundException を解決します。Intent問題を処理するActivityが見つからない