diff --git a/play-services-api/src/main/aidl/com/google/android/gms/maps/internal/IGoogleMapDelegate.aidl b/play-services-api/src/main/aidl/com/google/android/gms/maps/internal/IGoogleMapDelegate.aidl index af44f1c503a1a8af314a51d942f0fe1da2bb5e61..fe592337f435adcb46a223dc347b13732cb4f085 100644 --- a/play-services-api/src/main/aidl/com/google/android/gms/maps/internal/IGoogleMapDelegate.aidl +++ b/play-services-api/src/main/aidl/com/google/android/gms/maps/internal/IGoogleMapDelegate.aidl @@ -18,6 +18,8 @@ import com.google.android.gms.maps.internal.IOnMapLongClickListener; import com.google.android.gms.maps.internal.IOnMarkerClickListener; import com.google.android.gms.maps.internal.IOnMarkerDragListener; import com.google.android.gms.maps.internal.IOnInfoWindowClickListener; +import com.google.android.gms.maps.internal.IOnInfoWindowCloseListener; +import com.google.android.gms.maps.internal.IOnInfoWindowLongClickListener; import com.google.android.gms.maps.internal.IInfoWindowAdapter; import com.google.android.gms.maps.internal.IOnMapLoadedCallback; import com.google.android.gms.maps.internal.IOnMyLocationChangeListener; @@ -120,9 +122,9 @@ interface IGoogleMapDelegate { void onExitAmbient() = 81; //void setOnGroundOverlayClickListener(IOnGroundOverlayClickListener listener) = 82; - //void setInfoWindowLongClickListener(IOnInfoWindowLongClickListener listener) = 83; + void setInfoWindowLongClickListener(IOnInfoWindowLongClickListener listener) = 83; //void setPolygonClickListener(IOnPolygonClickListener listener) = 84; - //void setInfoWindowCloseListener(IOnInfoWindowCloseListener listener) = 85; + void setInfoWindowCloseListener(IOnInfoWindowCloseListener listener) = 85; //void setPolylineClickListener(IOnPolylineClickListener listener) = 86; //void setCircleClickListener(IOnCircleClickListener listener) = 88; diff --git a/play-services-api/src/main/aidl/com/google/android/gms/maps/internal/IOnInfoWindowCloseListener.aidl b/play-services-api/src/main/aidl/com/google/android/gms/maps/internal/IOnInfoWindowCloseListener.aidl new file mode 100644 index 0000000000000000000000000000000000000000..1ddae39c1baced905977198cf43896ee3b230210 --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/maps/internal/IOnInfoWindowCloseListener.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.maps.internal; + +import com.google.android.gms.maps.model.internal.IMarkerDelegate; + +interface IOnInfoWindowCloseListener { + void onInfoWindowClose(IMarkerDelegate marker); +} diff --git a/play-services-api/src/main/aidl/com/google/android/gms/maps/internal/IOnInfoWindowLongClickListener.aidl b/play-services-api/src/main/aidl/com/google/android/gms/maps/internal/IOnInfoWindowLongClickListener.aidl new file mode 100644 index 0000000000000000000000000000000000000000..536e55e994e067462fd364dff2ca0f4585d3e2be --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/maps/internal/IOnInfoWindowLongClickListener.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.maps.internal; + +import com.google.android.gms.maps.model.internal.IMarkerDelegate; + +interface IOnInfoWindowLongClickListener { + void onInfoWindowLongClick(IMarkerDelegate marker); +} diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt index fa29ac501ade221995c01881f4091879f5efaba9..87d5bdfa07101711a992f9a482cbc97fa5ef8e77 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt @@ -54,6 +54,9 @@ import com.mapbox.mapboxsdk.plugins.annotation.Annotation import com.mapbox.mapboxsdk.style.layers.Property.LINE_CAP_ROUND import com.google.android.gms.dynamic.unwrap import com.mapbox.mapboxsdk.WellKnownTileServer +import org.microg.gms.maps.mapbox.model.DefaultInfoWindowAdapter +import org.microg.gms.maps.mapbox.model.InfoWindow +import org.microg.gms.maps.mapbox.model.getInfoWindowViewFor import com.mapbox.mapboxsdk.camera.CameraUpdateFactory import com.mapbox.mapboxsdk.maps.OnMapReadyCallback import org.microg.gms.maps.MapsConstants.* @@ -100,6 +103,12 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) private var mapLongClickListener: IOnMapLongClickListener? = null private var markerClickListener: IOnMarkerClickListener? = null private var markerDragListener: IOnMarkerDragListener? = null + private var infoWindowAdapter: IInfoWindowAdapter = DefaultInfoWindowAdapter(MapContext(context)) + internal var onInfoWindowClickListener: IOnInfoWindowClickListener? = null + internal var onInfoWindowLongClickListener: IOnInfoWindowLongClickListener? = null + internal var onInfoWindowCloseListener: IOnInfoWindowCloseListener? = null + + var currentInfoWindow: InfoWindow? = null var lineManager: LineManager? = null val pendingLines = mutableSetOf() @@ -483,13 +492,19 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } override fun setOnInfoWindowClickListener(listener: IOnInfoWindowClickListener?) { - Log.d(TAG, "unimplemented Method: setOnInfoWindowClickListener") + onInfoWindowClickListener = listener + } + override fun setInfoWindowLongClickListener(listener: IOnInfoWindowLongClickListener) { + onInfoWindowLongClickListener = listener } - override fun setInfoWindowAdapter(adapter: IInfoWindowAdapter?) { - Log.d(TAG, "unimplemented Method: setInfoWindowAdapter") + override fun setInfoWindowCloseListener(listener: IOnInfoWindowCloseListener) { + onInfoWindowCloseListener = listener + } + override fun setInfoWindowAdapter(adapter: IInfoWindowAdapter?) { + infoWindowAdapter = adapter ?: DefaultInfoWindowAdapter(MapContext(context)) } override fun getTestingHelper(): IObjectWrapper? { @@ -611,6 +626,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) map.addOnCameraMoveListener { try { cameraMoveListener?.onCameraMove() + currentInfoWindow?.update() } catch (e: Exception) { Log.w(TAG, e) } @@ -631,7 +647,11 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } map.addOnMapClickListener { latlng -> try { - mapClickListener?.let { if (!hasSymbolAt(latlng)) it.onMapClick(latlng.toGms()); } + if (!hasSymbolAt(latlng)) { + mapClickListener?.onMapClick(latlng.toGms()) + currentInfoWindow?.close() + currentInfoWindow = null + } } catch (e: Exception) { Log.w(TAG, e) } @@ -688,12 +708,17 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } symbolManager.iconAllowOverlap = true symbolManager.addClickListener { + val marker = markers[it.id] try { - markers[it.id]?.let { markerClickListener?.onMarkerClick(it) } == true + if (markers[it.id]?.let { markerClickListener?.onMarkerClick(it) } == true) { + return@addClickListener true + } } catch (e: Exception) { Log.w(TAG, e) - false + return@addClickListener false } + + marker?.let { showInfoWindow(it) } == true } symbolManager.addDragListener(object : OnSymbolDragListener { override fun onAnnotationDragStarted(annotation: Symbol?) { @@ -706,7 +731,12 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) override fun onAnnotationDrag(annotation: Symbol?) { try { - markers[annotation?.id]?.let { markerDragListener?.onMarkerDrag(it) } + annotation?.let { symbol -> + markers[symbol.id]?.let { marker -> + marker.setPositionWhileDragging(symbol.latLng.toGms()) + markerDragListener?.onMarkerDrag(marker) + } + } } catch (e: Exception) { Log.w(TAG, e) } @@ -756,6 +786,17 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } } + internal fun showInfoWindow(marker: MarkerImpl): Boolean { + infoWindowAdapter.getInfoWindowViewFor(marker, MapContext(context))?.let { infoView -> + currentInfoWindow?.close() + currentInfoWindow = InfoWindow(infoView, this, marker).also { infoWindow -> + mapView?.let { infoWindow.open(it) } + } + return true + } + return false + } + override fun useViewLifecycleWhenInFragment(): Boolean { Log.d(TAG, "unimplemented Method: useViewLifecycleWhenInFragment") return false @@ -777,6 +818,8 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) symbolManager?.onDestroy() symbolManager = null + currentInfoWindow?.close() + pendingMarkers.clear() markers.clear() diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/BitmapDescriptor.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/BitmapDescriptor.kt index e228387da70a2e38d2086471ddb710af52c20cad..f0dc0f882a429daa883419703cbc2378a8317368 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/BitmapDescriptor.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/BitmapDescriptor.kt @@ -24,7 +24,7 @@ import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions import com.mapbox.mapboxsdk.style.layers.Property.ICON_ANCHOR_TOP_LEFT import com.mapbox.mapboxsdk.utils.ColorUtils -open class BitmapDescriptorImpl(private val id: String, private val size: FloatArray) { +open class BitmapDescriptorImpl(private val id: String, internal val size: FloatArray) { open fun applyTo(options: SymbolOptions, anchor: FloatArray, dpiFactor: Float): SymbolOptions { return options.withIconImage(id).withIconAnchor(ICON_ANCHOR_TOP_LEFT).withIconOffset(arrayOf(-anchor[0] * size[0] / dpiFactor, -anchor[1] * size[1] / dpiFactor)) } diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/InfoWindow.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/InfoWindow.kt new file mode 100644 index 0000000000000000000000000000000000000000..c5f00abbda088698e336c6b9e1bffcf1deca56bf --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/InfoWindow.kt @@ -0,0 +1,156 @@ +package org.microg.gms.maps.mapbox.model + +import android.graphics.PointF +import android.view.LayoutInflater +import android.view.View +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.view.ViewManager +import android.widget.FrameLayout +import android.widget.TextView +import com.google.android.gms.dynamic.ObjectWrapper +import com.google.android.gms.dynamic.unwrap +import com.google.android.gms.maps.internal.IInfoWindowAdapter +import com.google.android.gms.maps.model.internal.IMarkerDelegate +import com.mapbox.android.gestures.Utils +import com.mapbox.mapboxsdk.maps.MapView +import org.microg.gms.maps.mapbox.GoogleMapImpl +import org.microg.gms.maps.mapbox.R +import org.microg.gms.maps.mapbox.utils.MapContext +import org.microg.gms.maps.mapbox.utils.toMapbox +import kotlin.math.* + +/** + * `InfoWindow` is a tooltip shown when a [MarkerImpl] is tapped. Only + * one info window is displayed at a time. When the user clicks on a marker, the currently open info + * window will be closed and the new info window will be displayed. If the user clicks the same + * marker while its info window is currently open, the info window will be reopened. + * + * The info window is drawn oriented against the device's screen, centered above its associated + * marker, unless a different info window anchor is set. The default info window contains the title + * in bold and snippet text below the title. + * If neither is set, no default info window is shown. + * + * Based on Mapbox's / MapLibre's [com.mapbox.mapboxsdk.annotations.InfoWindow]. + * + */ + +fun IInfoWindowAdapter.getInfoWindowViewFor(marker: IMarkerDelegate, mapContext: MapContext): View? { + getInfoWindow(marker).unwrap()?.let { return it } + + getInfoContents(marker).unwrap()?.let { view -> + // Detach from previous BubbleLayout parent, if exists + view.parent?.let { (it as ViewManager).removeView(view) } + + return FrameLayout(view.context).apply { + background = mapContext.getDrawable(R.drawable.maps_default_bubble) + val fourDp = Utils.dpToPx(4f) + elevation = fourDp + setPadding(fourDp.toInt(), fourDp.toInt(), fourDp.toInt(), fourDp.toInt() * 3) + addView(view) + } + } + + // When a custom adapter is used, but both methods return null, the default adapter must be used + if (this !is DefaultInfoWindowAdapter) { + return DefaultInfoWindowAdapter(mapContext).getInfoWindowViewFor(marker, mapContext) + } + + return null +} + +class InfoWindow internal constructor( + private val view: View, private val map: GoogleMapImpl, internal val marker: MarkerImpl +) { + private var coordinates: PointF = PointF(0f, 0f) + var isVisible = false + + init { + view.setOnClickListener { + map.onInfoWindowClickListener?.onInfoWindowClick(marker) + } + view.setOnLongClickListener { + map.onInfoWindowLongClickListener?.onInfoWindowLongClick(marker) + true + } + } + + fun open(mapView: MapView) { + val layoutParams: FrameLayout.LayoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT + ) + view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + + close(true) // if it was already opened + mapView.addView(view, layoutParams) + isVisible = true + + // Set correct position + update() + } + + /** + * Close this [InfoWindow] if it is visible, otherwise calling this will do nothing. + * + * @param silent `OnInfoWindowCloseListener` is only called if `silent` is not `false` + */ + fun close(silent: Boolean = false) { + if (isVisible) { + isVisible = false + (view.parent as ViewGroup?)?.removeView(view) + if (!silent) { + map.onInfoWindowCloseListener?.onInfoWindowClose(marker) + } + } + } + + /** + * Updates the position of the displayed view. + */ + fun update() { + map.map?.projection?.toScreenLocation(marker.position.toMapbox())?.let { + coordinates = it + } + + val iconDimensions = marker.getIconDimensions() + val width = iconDimensions?.get(0) ?: 0f + val height = iconDimensions?.get(1) ?: 0f + + view.x = + coordinates.x - view.measuredWidth / 2f + sin(Math.toRadians(marker.rotation.toDouble())).toFloat() * width * marker.getInfoWindowAnchor()[0] + view.y = coordinates.y - view.measuredHeight - max( + height * cos(Math.toRadians(marker.rotation.toDouble())).toFloat() * marker.getInfoWindowAnchor()[1], 0f + ) + } +} + +class DefaultInfoWindowAdapter(val context: MapContext) : IInfoWindowAdapter { + override fun asBinder() = null + + override fun getInfoWindow(marker: IMarkerDelegate?): ObjectWrapper { + + if (marker == null) return ObjectWrapper.wrap(null) + + val showDefaultMarker = (marker.title != null) || (marker.snippet != null) + + return if (!showDefaultMarker) ObjectWrapper.wrap(null) + else ObjectWrapper.wrap( + LayoutInflater.from(context).inflate(R.layout.maps_default_bubble_layout, null, false).apply { + + marker.title?.let { + val titleTextView = findViewById(R.id.title) + titleTextView.text = it + titleTextView.visibility = VISIBLE + } + + marker.snippet?.let { + val snippetTextView = findViewById(R.id.snippet) + snippetTextView.text = it + snippetTextView.visibility = VISIBLE + } + } + ) + } + + override fun getInfoContents(marker: IMarkerDelegate?) = null +} \ No newline at end of file diff --git a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt index 06da3657806e7f903c2ce9150c058a2a5d2c5c90..0d11c4c122c41f42a3d7e462aabfe53efe0f5b8e 100644 --- a/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt +++ b/play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt @@ -35,6 +35,7 @@ class MarkerImpl(private val map: GoogleMapImpl, private val id: String, options private var visible: Boolean = options.isVisible private var rotation: Float = options.rotation private var anchor: FloatArray = floatArrayOf(options.anchorU, options.anchorV) + private var infoWindowAnchor: FloatArray? = null private var icon: BitmapDescriptorImpl? = options.icon?.remoteObject.unwrap() private var alpha: Float = options.alpha private var title: String? = options.title @@ -85,18 +86,34 @@ class MarkerImpl(private val map: GoogleMapImpl, private val id: String, options this.position = position ?: return annotation?.latLng = position.toMapbox() map.symbolManager?.let { update(it) } + map.currentInfoWindow?.update() + } + + /** + * New position is already reflected on map while if drag is in progress. Calling + * `symbolManager.update` would interrupt the drag. + */ + internal fun setPositionWhileDragging(position: LatLng) { + this.position = position + map.currentInfoWindow?.update() } override fun getPosition(): LatLng = position override fun setTitle(title: String?) { this.title = title + map.currentInfoWindow?.let { + if (it.marker == this) it.close() + } } override fun getTitle(): String? = title override fun setSnippet(snippet: String?) { this.snippet = snippet + map.currentInfoWindow?.let { + if (it.marker == this) it.close() + } } override fun getSnippet(): String? = snippet @@ -110,18 +127,22 @@ class MarkerImpl(private val map: GoogleMapImpl, private val id: String, options override fun isDraggable(): Boolean = draggable override fun showInfoWindow() { - Log.d(TAG, "unimplemented Method: showInfoWindow") - infoWindowShown = true + if (isInfoWindowShown) { + // Per docs, don't call `onWindowClose` if info window is re-opened programmatically + map.currentInfoWindow?.close(silent = true) + } + map.showInfoWindow(this) } override fun hideInfoWindow() { - Log.d(TAG, "unimplemented Method: hideInfoWindow") - infoWindowShown = false + if (isInfoWindowShown) { + map.currentInfoWindow?.close() + map.currentInfoWindow = null + } } override fun isInfoWindowShown(): Boolean { - Log.d(TAG, "unimplemented Method: isInfoWindowShow") - return infoWindowShown + return map.currentInfoWindow?.marker == this } override fun setVisible(visible: Boolean) { @@ -162,6 +183,9 @@ class MarkerImpl(private val map: GoogleMapImpl, private val id: String, options anchor = floatArrayOf(x, y) annotation?.let { icon?.applyTo(it, anchor, map.dpiFactor) } map.symbolManager?.let { update(it) } + if (infoWindowAnchor == null) { + map.currentInfoWindow?.update() + } } override fun setFlat(flat: Boolean) { @@ -177,14 +201,18 @@ class MarkerImpl(private val map: GoogleMapImpl, private val id: String, options this.rotation = rotation annotation?.iconRotate = rotation map.symbolManager?.let { update(it) } + map.currentInfoWindow?.update() } override fun getRotation(): Float = rotation override fun setInfoWindowAnchor(x: Float, y: Float) { - Log.d(TAG, "unimplemented Method: setInfoWindowAnchor") + infoWindowAnchor = floatArrayOf(x, y) + map.currentInfoWindow?.update() } + internal fun getInfoWindowAnchor() = infoWindowAnchor ?: floatArrayOf(0.5f, 1f) + override fun setAlpha(alpha: Float) { this.alpha = alpha annotation?.iconOpacity = if (visible) alpha else 0f @@ -214,6 +242,10 @@ class MarkerImpl(private val map: GoogleMapImpl, private val id: String, options Log.d(TAG, "onTransact [unknown]: $code, $data, $flags"); false } + fun getIconDimensions(): FloatArray? { + return icon?.size + } + companion object { private val TAG = "GmsMapMarker" } diff --git a/play-services-maps-core-mapbox/src/main/res/drawable/maps_default_bubble.xml b/play-services-maps-core-mapbox/src/main/res/drawable/maps_default_bubble.xml new file mode 100644 index 0000000000000000000000000000000000000000..c172be81e9d1e8703f5012de272a7e56ed9bdad4 --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/res/drawable/maps_default_bubble.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/play-services-maps-core-mapbox/src/main/res/layout/maps_default_bubble_layout.xml b/play-services-maps-core-mapbox/src/main/res/layout/maps_default_bubble_layout.xml new file mode 100644 index 0000000000000000000000000000000000000000..6f175407c2356c40587b4e6fef53e1ea5cb80339 --- /dev/null +++ b/play-services-maps-core-mapbox/src/main/res/layout/maps_default_bubble_layout.xml @@ -0,0 +1,34 @@ + + + + + + + + \ No newline at end of file diff --git a/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/GoogleMapImpl.java b/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/GoogleMapImpl.java index 87a512091f20bcf01e2b92205fb965a2f666b198..301c7ed2ecfc43db890def0471f1f4f4c180b85b 100644 --- a/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/GoogleMapImpl.java +++ b/play-services-maps-core-vtm/src/main/java/org/microg/gms/maps/vtm/GoogleMapImpl.java @@ -45,6 +45,8 @@ import com.google.android.gms.maps.internal.IOnCameraMoveCanceledListener; import com.google.android.gms.maps.internal.IOnCameraMoveListener; import com.google.android.gms.maps.internal.IOnCameraMoveStartedListener; import com.google.android.gms.maps.internal.IOnInfoWindowClickListener; +import com.google.android.gms.maps.internal.IOnInfoWindowCloseListener; +import com.google.android.gms.maps.internal.IOnInfoWindowLongClickListener; import com.google.android.gms.maps.internal.IOnMapClickListener; import com.google.android.gms.maps.internal.IOnMapLoadedCallback; import com.google.android.gms.maps.internal.IOnMapLongClickListener; @@ -661,6 +663,16 @@ public class GoogleMapImpl extends IGoogleMapDelegate.Stub } + @Override + public void setInfoWindowLongClickListener(IOnInfoWindowLongClickListener listener) throws RemoteException { + Log.d(TAG, "unimplemented Method: setInfoWindowLongClickListener"); + } + + @Override + public void setInfoWindowCloseListener(IOnInfoWindowCloseListener listener) throws RemoteException { + Log.d(TAG, "unimplemented Method: setInfoWindowCloseListener"); + } + @Override public void onStart() throws RemoteException { Log.d(TAG, "unimplemented Method: onStart");