Android GoogleMapのInfoWindow(吹き出し)をカスタマイズする その2
前回は、InfoWindowをカスタマイズし、吹き出しに画像とタイトルと説明を表示できるようにしました。今回は、吹き出しの画像、タイトル、説明を自由に入力できるようにしていきます。
先に概要です。 今回のポイントは、次の三点です。 ②ダイアログの入力内容をMarkerのタグで保持する。 ③吹き出しを描画するタイミング(getInfoContents())で、Markerのタグから入力内容を取得し、吹き出しに反映する。 これで以下のことができるようになりました。 本アプリは、子供(4才)と一緒に宝探しゲームをすることを目的に作っています。
そのため、アイコンはRPGチックなものをチョイスしています。
試しに公園で試してみたところ、以下の改善点が見つかりました。 また、ソース全文を載せると長くなってしまうので、今後はGitに登録するようにしたい。 [Android] Spinner をカスタマイズして画像リストを表示する ダイアログ | Android デベロッパー | Android Developers方針
ソースコード
MyDialogFragment.java
public class MyDialogFragment extends DialogFragment {
private OnDialogFragmentListener listener;
public interface OnDialogFragmentListener {
void onDialogResult(int selectedItemResourceId, String title, String snippet);
}
public void setDialogFragmentListener(OnDialogFragmentListener listener) {
this.listener = listener;
}
@NonNull
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
// カスタムダイアログのビューを生成
View dialogView = requireActivity().getLayoutInflater().inflate(R.layout.custom_dialog, null);
// 吹き出しに表示する画像を選択するスピナーを生成
SpinnerAdapter adapter = new SpinnerAdapter(getActivity());
Spinner spinner = dialogView.findViewById(R.id.sp_icon);
spinner.setAdapter(adapter);
// ダイアログの作成
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setView(dialogView)
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// ダイアログでOKをクリックした場合の操作
if (null != listener) {
// リスナー登録されている場合、リソースIDとタイトルと説明の情報を返却する
listener.onDialogResult(
(int)adapter.getItem(spinner.getSelectedItemPosition()),
((EditText) dialogView.findViewById(R.id.et_title)).getText().toString(),
((EditText) dialogView.findViewById(R.id.et_snipet)).getText().toString()
);
}
}
})
.setNegativeButton("Cancel", null);
return builder.create();
}
}
SpinnerAdapter.java
public class SpinnerAdapter extends BaseAdapter {
private final LayoutInflater inflater;
private final int[] imageIDs;
private final String[] itemNames;
public SpinnerAdapter(Context context) {
inflater = LayoutInflater.from(context);
// スピナーに登録する画像の名前リストをstring.xmlから取得
String[] spinnerImages = context.getResources().getStringArray(R.array.spinner_image_names);
itemNames = context.getResources().getStringArray(R.array.spinner_item_names);
// 画像のリソースIDリストを取得
imageIDs = new int[spinnerImages.length];
for (int i=0; i < spinnerImages.length; i++) {
imageIDs[i] = context.getResources().getIdentifier(
spinnerImages[i],
"drawable",
context.getPackageName());
}
}
@Override
public int getCount() {
return imageIDs.length;
}
@Override
public Object getItem(int position) {
return imageIDs[position];
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (null == convertView) {
convertView = inflater.inflate(R.layout.spinner_layout, null);
}
((ImageView) convertView.findViewById(R.id.iv_item)).setImageResource(imageIDs[position]);
((TextView) convertView.findViewById(R.id.tv_item)).setText(itemNames[position]);
return convertView;
}
}
custom_dialog.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="8dp"
android:paddingTop="8dp">
<Spinner
android:id="@+id/sp_icon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/item_title_hint"
android:inputType="textPersonName"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sp_icon" />
<EditText
android:id="@+id/et_snipet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/item_snipet_hint"
android:inputType="textPersonName"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_title" />
</androidx.constraintlayout.widget.ConstraintLayout>
spinner_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<!-- アイコンを表示するイメージビュー -->
<ImageView
android:id="@+id/iv_item"
android:layout_width="32dp"
android:layout_height="32dp" />
<TextView
android:id="@+id/tv_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingLeft="16dp"
android:textSize="24sp" />
</LinearLayout>
MapsActivity.java
public class MapsActivity extends FragmentActivity implements OnMapReadyCallback {
private GoogleMap mMap;
private ActivityMapsBinding binding;
private Marker marker;
private FusedLocationProviderClient flpClient = null;
private MySensorManager mySensorManager = null;
private LocationCallback locationCallback = null;
private LatLng tmpLatLng;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMapsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
.findFragmentById(R.id.map);
mapFragment.getMapAsync(this);
// 位置情報が変更された際に、通知を受け取るコールバックメソッドを定義
locationCallback = new LocationCallback() {
@Override
public void onLocationResult(@NonNull LocationResult locationResult) {
super.onLocationResult(locationResult);
Location location = locationResult.getLastLocation();
LatLng latlng = new LatLng(location.getLatitude(), location.getLongitude());
mMap.moveCamera(CameraUpdateFactory.newLatLng(latlng));
marker.setPosition(latlng);
marker.setRotation(mySensorManager.getAzimuth());
}
};
mySensorManager = new MySensorManager(this);
}
@Override
protected void onResume() {
super.onResume();
startPositioning();
}
@Override
protected void onPause() {
super.onPause();
stopPositioning();
}
@Override
public void onMapReady(GoogleMap googleMap) {
mMap = googleMap;
LatLng sydney = new LatLng(-34, 151);
marker = mMap.addMarker(new MarkerOptions().position(sydney).title("Marker in Sydney"));
marker.setIcon(BitmapDescriptorFactory.fromResource(R.drawable.marker));
mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney));
mMap.setInfoWindowAdapter(new GoogleMap.InfoWindowAdapter() {
@Override
public View getInfoContents(@NonNull Marker marker) {
// info_window_layout.xml のビューを生成
View view = getLayoutInflater().inflate(R.layout.info_window_layout, null);
// イメージビューを取得
ImageView imgView = view.findViewById(R.id.imageView);
InfoContents contents = (InfoContents) marker.getTag();
if (null == contents) {
return null;
}
imgView.setImageResource(contents.resourceId);
((TextView) view.findViewById(R.id.tv_title)).setText(contents.title);
((TextView) view.findViewById(R.id.tv_snipet)).setText(contents.snipet);
return view;
}
@Nullable
@Override
public View getInfoWindow(@NonNull Marker marker) {
return null;
}
});
// 長押しクリックイベントをセット
mMap.setOnMapLongClickListener(new GoogleMap.OnMapLongClickListener() {
@Override
public void onMapLongClick(@NonNull LatLng latLng) {
tmpLatLng = latLng;
// アイコン選択ダイアログを表示
MyDialogFragment dialogFragment = new MyDialogFragment();
dialogFragment.setDialogFragmentListener(listener);
dialogFragment.show(getSupportFragmentManager(), "custom_dialog");
}
});
}
private final MyDialogFragment.OnDialogFragmentListener listener =
new MyDialogFragment.OnDialogFragmentListener() {
@Override
public void onDialogResult(int selectedItemResourceId, String title, String snippet) {
// 長押しクリックイベントをキャッチしたらマーカーを追加
Marker itemMarker = mMap.addMarker(new MarkerOptions().position(tmpLatLng));
InfoContents contents = new InfoContents();
contents.resourceId = selectedItemResourceId;
contents.title = title;
contents.snipet = snippet;
itemMarker.setTag(contents);
}
};
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (1 != requestCode) {
return;
}
// ユーザが許可してくれた場合は、位置情報の取得を開始する
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startPositioning();
} else {
Toast.makeText(this, "Permission Error.", Toast.LENGTH_SHORT).show();
}
}
/**
* 位置情報の取得を開始する。
*/
private void startPositioning() {
// 位置情報へのアクセス許可チェック
if (!checkPermission()) {
return;
}
// 位置情報のリクエストを生成する
LocationRequest request = LocationRequest.create();
request.setInterval(1000);
request.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
// 位置情報の更新をリクエストする
flpClient = LocationServices.getFusedLocationProviderClient(this);
flpClient.requestLocationUpdates(request, locationCallback, null);
// センサーの取得を開始
mySensorManager.startSensor();
}
/**
* 位置情報の取得を停止する。
*/
private void stopPositioning() {
if (null != flpClient) {
flpClient.removeLocationUpdates(locationCallback);
}
if (null != mySensorManager) {
mySensorManager.stopSensor();
}
}
/**
* 位置情報へのアクセスが許可されているかチェックする。<br>
* 許可されていてない場合、パーミッションリクエストを行う。
* @return ture:許可 / false:未許可
*/
private boolean checkPermission() {
// アクセス許可チェック
if (checkSelfPermission(ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
return true;
}
// パーミッションリクエスト
requestPermissions(new String[] {ACCESS_FINE_LOCATION},1);
return false;
}
/**
* 吹き出しに表示するコンテンツを保持するクラス
*/
private class InfoContents {
private int resourceId;
private String title;
private String snipet;
}
}
今回のポイント
①ダイアログでの入力内容をコールバックで通知する。 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setView(dialogView)
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// ダイアログでOKをクリックした場合の操作
if (null != listener) {
// リスナー登録されている場合、リソースIDとタイトルと説明の情報を返却する
listener.onDialogResult(
(int)adapter.getItem(spinner.getSelectedItemPosition()),
((EditText) dialogView.findViewById(R.id.et_title)).getText().toString(),
((EditText) dialogView.findViewById(R.id.et_snipet)).getText().toString()
);
}
}
})
.setNegativeButton("Cancel", null);
private final MyDialogFragment.OnDialogFragmentListener listener =
new MyDialogFragment.OnDialogFragmentListener() {
@Override
public void onDialogResult(int selectedItemResourceId, String title, String snippet) {
// 長押しクリックイベントをキャッチしたらマーカーを追加
Marker itemMarker = mMap.addMarker(new MarkerOptions().position(tmpLatLng));
InfoContents contents = new InfoContents();
contents.resourceId = selectedItemResourceId;
contents.title = title;
contents.snipet = snippet;
itemMarker.setTag(contents);
}
};
@Override
public View getInfoContents(@NonNull Marker marker) {
// info_window_layout.xml のビューを生成
View view = getLayoutInflater().inflate(R.layout.info_window_layout, null);
// イメージビューを取得
ImageView imgView = view.findViewById(R.id.imageView);
InfoContents contents = (InfoContents) marker.getTag();
if (null == contents) {
return null;
}
imgView.setImageResource(contents.resourceId);
((TextView) view.findViewById(R.id.tv_title)).setText(contents.title);
((TextView) view.findViewById(R.id.tv_snipet)).setText(contents.snipet);
return view;
}
実行結果
今後の課題
次回以降改善していきたいと思います。
→ジオフェンスで音を鳴らす等イベントを増やす
→マップ中心座標の更新ON/OFFをスイッチできるようにする
→通常の地図と航空写真をスイッチできるようにする参考
リファレンス
素材について