【发布时间】:2020-01-23 07:20:44
【问题描述】:
Google Map GroundOverlay 不显示叠加图像。
没有错误或任何错误。 (至少不在日志中。)
代码是从 Google IO 2019 应用复制的。
地图片段文件
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mapView.getMapAsync {
it.mapType = GoogleMap.MAP_TYPE_NORMAL
it.addGroundOverlay(
GroundOverlayOptions()
.image(BitmapDescriptorFactory.fromResource(R.drawable.kirirom_map_overlay))
.positionFromBounds(viewModel.resortLocationBounds)
)
}
}
MapViewModel 文件
val resortLocationBounds: LatLngBounds = LatLngBounds(BuildConfig.MAP_VIEWPORT_BOUND_SW, BuildConfig.MAP_VIEWPORT_BOUND_NE)
参考资料:
完整的 MapFragment 文件:
package kh.edu.kit.chain.app.vkclub.features.maps
import android.Manifest
import android.app.Dialog
import android.content.pm.PackageManager
import android.os.Bundle
import android.text.Html
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePaddingRelative
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.MapView
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import com.google.android.gms.maps.model.GroundOverlayOptions
import com.google.android.gms.maps.model.LatLng
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.maps.android.data.geojson.GeoJsonLayer
import kh.edu.kit.chain.app.vkclub.R
import kh.edu.kit.chain.app.vkclub.databinding.MapsFragmentBinding
import kh.edu.kit.chain.app.vkclub.functions.doOnApplyWindowInsets
import kh.edu.kit.chain.app.vkclub.shared.BottomSheetBehavior
import kh.edu.kit.chain.app.vkclub.utils.getDrawableResourceForIcon
import org.koin.android.viewmodel.ext.android.viewModel
class MapsFragment : Fragment() {
companion object {
const val MIN_TIME: Long = 400
const val MIN_DISTANCE: Float = 1000F
private const val MAPVIEW_BUNDLE_KEY = "MapViewBundleKey"
private const val REQUEST_LOCATION_PERMISSION = 1
private const val FRAGMENT_MY_LOCATION_RATIONALE = "my_location_rationale"
// Threshold for when the marker description reaches maximum alpha. Should be a value
// between 0 and 1, inclusive, coinciding with a point between the bottom sheet's
// collapsed (0) and expanded (1) states.
private const val ALPHA_TRANSITION_END = 0.5f
// Threshold for when the marker description reaches minimum alpha. Should be a value
// between 0 and 1, inclusive, coinciding with a point between the bottom sheet's
// collapsed (0) and expanded (1) states.
private const val ALPHA_TRANSITION_START = 0.1f
}
private val viewModel: MapsViewModel by viewModel()
private var mapViewBundle: Bundle? = null
private lateinit var mapView: MapView
private lateinit var binding: MapsFragmentBinding
private lateinit var bottomSheetBehavior: BottomSheetBehavior<*>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
mapViewBundle = savedInstanceState.getBundle(MAPVIEW_BUNDLE_KEY)
}
requireActivity().onBackPressedDispatcher.addCallback(this) {
onBackPressed()
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = MapsFragmentBinding.inflate(inflater, container, false).apply {
lifecycleOwner = viewLifecycleOwner
viewModel = this@MapsFragment.viewModel
}
mapView = binding.map.apply {
onCreate(mapViewBundle)
}
if (savedInstanceState == null) {
viewModel.setMapVariant(MapVariant.DAY)
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
bottomSheetBehavior = BottomSheetBehavior.from(binding.bottomSheet)
val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback {
override fun onStateChanged(bottomSheet: View, newState: Int) {
val rotation = when (newState) {
BottomSheetBehavior.STATE_EXPANDED -> 0f
BottomSheetBehavior.STATE_COLLAPSED -> 180f
BottomSheetBehavior.STATE_HIDDEN -> 180f
else -> return
}
binding.expandIcon.animate().rotationX(rotation).start()
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
}
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback)
binding.bottomSheet.post {
val state = bottomSheetBehavior.state
val slideOffset = when (state) {
BottomSheetBehavior.STATE_EXPANDED -> 1f
BottomSheetBehavior.STATE_COLLAPSED -> 0f
else -> -1f // BottomSheetBehavior.STATE_HIDDEN
}
bottomSheetCallback.onStateChanged(binding.bottomSheet, state)
bottomSheetCallback.onSlide(binding.bottomSheet, slideOffset)
}
val originalPeekHeight = bottomSheetBehavior.peekHeight
binding.root.doOnApplyWindowInsets { _, insets, _ ->
binding.map.getMapAsync {
it.setPadding(0, 0, 0, insets.systemWindowInsetBottom)
}
binding.descriptionScrollview.updatePaddingRelative(bottom = insets.systemWindowInsetBottom)
val gestureInsets = insets.systemGestureInsets
bottomSheetBehavior.peekHeight = gestureInsets.bottom + originalPeekHeight
}
binding.clickable.setOnClickListener {
if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
} else {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
binding.descriptionScrollview.setOnScrollChangeListener { v: NestedScrollView, scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int ->
binding.sheetHeaderShadow.isActivated = v.canScrollVertically(-1)
}
mapView.getMapAsync { googleMap ->
googleMap.apply {
setOnMapClickListener { viewModel.dismissFeatureDetails() }
setOnCameraMoveListener {
viewModel.onZoomChanged(googleMap.cameraPosition.zoom)
}
enableMyLocation(false)
}
}
viewModel.mapVariant.observe(viewLifecycleOwner, Observer {
mapView.getMapAsync { googleMap ->
googleMap.clear()
viewModel.loadMapFeatures(googleMap)
}
})
viewModel.geoJsonLayer.observe(viewLifecycleOwner, Observer {
updateMarkers(it ?: return@Observer)
})
viewModel.selectedMarkerInfo.observe(viewLifecycleOwner, Observer {
updateInfoSheet(it ?: return@Observer)
})
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mapView.getMapAsync {
it.mapType = GoogleMap.MAP_TYPE_NORMAL
it.addGroundOverlay(
GroundOverlayOptions()
.image(BitmapDescriptorFactory.fromResource(R.drawable.kirirom_map_overlay))
.positionFromBounds(viewModel.resortLocationBounds)
)
}
}
private fun updateInfoSheet(markerInfo: MarkerInfo) {
val iconRes = getDrawableResourceForIcon(binding.markerIcon.context, markerInfo.iconName)
binding.markerIcon.apply {
setImageResource(iconRes)
visibility = if (iconRes == 0) View.GONE else View.VISIBLE
}
binding.markerTitle.text = markerInfo.title
binding.markerSubtitle.apply {
text = markerInfo.subtitle
isVisible = !markerInfo.subtitle.isNullOrEmpty()
}
val description = Html.fromHtml(markerInfo.description ?: "")
val hasDescription = description.isNotEmpty()
binding.markerDescription.apply {
text = description
isVisible = hasDescription
}
binding.expandIcon.isVisible = hasDescription
binding.clickable.isVisible = hasDescription
}
private fun onBackPressed(): Boolean {
if (::bottomSheetBehavior.isInitialized &&
bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED
) {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
return true
}
return false
}
private fun updateMarkers(geoJsonLayer: GeoJsonLayer) {
geoJsonLayer.addLayerToMap()
geoJsonLayer.setOnFeatureClickListener { feature ->
viewModel.requestHighlightFeature(feature.id.split(",")[0])
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val mapViewBundle = outState.getBundle(MAPVIEW_BUNDLE_KEY)
?: Bundle().apply { putBundle(MAPVIEW_BUNDLE_KEY, this) }
mapView.onSaveInstanceState(mapViewBundle)
}
override fun onDestroy() {
super.onDestroy()
mapView.onDestroy()
viewModel.onMapDestroyed()
}
override fun onStart() {
super.onStart()
mapView.onStart()
}
override fun onLowMemory() {
super.onLowMemory()
mapView.onLowMemory()
}
override fun onResume() {
super.onResume()
mapView.onResume()
}
override fun onPause() {
super.onPause()
mapView.onPause()
}
override fun onStop() {
super.onStop()
mapView.onStop()
}
private fun requestLocationPermission() {
val context = context ?: return
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
) {
return
}
if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
MyLocationRationaleFragment()
.show(childFragmentManager, FRAGMENT_MY_LOCATION_RATIONALE)
return
}
requestPermissions(
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_LOCATION_PERMISSION
)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_LOCATION_PERMISSION) {
if (grantResults.size == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
enableMyLocation()
} else {
MyLocationRationaleFragment()
.show(childFragmentManager, FRAGMENT_MY_LOCATION_RATIONALE)
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
private fun enableMyLocation(requestPermission: Boolean = false) {
val context = context ?: return
when {
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED -> {
mapView.getMapAsync {
it.isMyLocationEnabled = true
}
viewModel.optIntoMyLocation()
}
requestPermission -> requestLocationPermission()
else -> viewModel.optIntoMyLocation(false)
}
}
class MyLocationRationaleFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(context)
.setMessage(R.string.my_location_rationale)
.setPositiveButton(android.R.string.ok) { _, _ ->
parentFragment!!.requestPermissions(
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_LOCATION_PERMISSION
)
}
.setNegativeButton(android.R.string.cancel, null) // Give up
.create()
}
}
}
完整的 MapViewModel 文件:
package kh.edu.kit.chain.app.vkclub.features.maps
import androidx.lifecycle.*
import com.google.android.gms.maps.CameraUpdate
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.LatLngBounds
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.maps.android.data.geojson.GeoJsonFeature
import com.google.maps.android.data.geojson.GeoJsonLayer
import com.google.maps.android.data.geojson.GeoJsonPoint
import kh.edu.kit.chain.app.vkclub.BuildConfig
import kh.edu.kit.chain.app.vkclub.R
import kh.edu.kit.chain.app.vkclub.domain.map.GeoJsonData
import kh.edu.kit.chain.app.vkclub.domain.map.LoadGeoJsonFeaturesUseCase
import kh.edu.kit.chain.app.vkclub.domain.map.LoadGeoJsonParams
import kh.edu.kit.chain.app.vkclub.functions.Event
import kh.edu.kit.chain.app.vkclub.functions.Result
class MapsViewModel(
private val loadGeoJsonFeaturesUseCase: LoadGeoJsonFeaturesUseCase
) : ViewModel() {
val resortLocationBounds: LatLngBounds = LatLngBounds(BuildConfig.MAP_VIEWPORT_BOUND_SW, BuildConfig.MAP_VIEWPORT_BOUND_NE)
val groundOverlayData = Pair(resortLocationBounds, R.drawable.kirirom_map_overlay)
private val _mapVariant = MutableLiveData<MapVariant>()
val mapVariant = Transformations.distinctUntilChanged(_mapVariant)
private val _mapCenterEvent = MutableLiveData<Event<CameraUpdate>>()
val mapCenterEvent: LiveData<Event<CameraUpdate>>
get() = _mapCenterEvent
private val loadGeoJsonResult = MutableLiveData<Result<GeoJsonData>>()
private val _geoJsonLayer = MediatorLiveData<GeoJsonLayer>()
val geoJsonLayer: LiveData<GeoJsonLayer>
get() = _geoJsonLayer
private val featureLookup: MutableMap<String, GeoJsonFeature> = mutableMapOf()
private var hasLoadedFeature = false
private var requestedFeatureId: String? = null
private val focusZoomLevel = BuildConfig.MAP_CAMERA_FOCUS_ZOOM
private var currentZoomLevel = 16
private val _bottomSheetStateEvent = MediatorLiveData<Event<Int>>()
val bottomSheetStateEvent: LiveData<Event<Int>>
get() = _bottomSheetStateEvent
private val _selectedMarkerInfo = MutableLiveData<MarkerInfo>()
val selectedMarkerInfo: LiveData<MarkerInfo>
get() = _selectedMarkerInfo
init {
_geoJsonLayer.addSource(loadGeoJsonResult) { result ->
if (result is Result.Success) {
hasLoadedFeature = true
setMapFeatures(result.data.featureMap)
_geoJsonLayer.value = result.data.geoJsonLayer
}
}
_bottomSheetStateEvent.addSource(mapVariant) {
dismissFeatureDetails()
}
}
fun setMapVariant(variant: MapVariant) {
_mapVariant.value = variant
}
fun onMapDestroyed() {
hasLoadedFeature = false
featureLookup.clear()
_geoJsonLayer.value = null
}
fun loadMapFeatures(googleMap: GoogleMap) {
val variant = _mapVariant.value ?: return
loadGeoJsonFeaturesUseCase(
LoadGeoJsonParams(googleMap, variant.markersResId),
loadGeoJsonResult
)
}
private fun setMapFeatures(features: Map<String, GeoJsonFeature>) {
featureLookup.clear()
featureLookup.putAll(features)
updateFeaturesVisibility(currentZoomLevel.toFloat())
val featureId = requestedFeatureId ?: return
requestedFeatureId = null
highlightFeature(featureId)
}
fun onZoomChanged(zoom: Float) {
val zoomInt = zoom.toInt()
if (currentZoomLevel != zoomInt) {
currentZoomLevel = zoomInt
updateFeaturesVisibility(zoom)
}
}
private fun updateFeaturesVisibility(zoom: Float) {
val selectedId = selectedMarkerInfo.value?.id
featureLookup.values.forEach { feature ->
if (feature.id != selectedId) {
val minZoom = feature.getProperty("minZoom")?.toFloatOrNull() ?: 0f
feature.pointStyle.isVisible = zoom >= minZoom
}
}
}
fun requestHighlightFeature(featureId: String) {
if (hasLoadedFeature) {
highlightFeature(featureId)
} else {
requestedFeatureId = featureId
}
}
private fun highlightFeature(featureId: String) {
val feature = featureLookup[featureId] ?: return
val geometry = feature.geometry as? GeoJsonPoint ?: return
val update = CameraUpdateFactory.newLatLngZoom(geometry.coordinates, focusZoomLevel)
_mapCenterEvent.value = Event(update)
val title = feature.getProperty("title")
_selectedMarkerInfo.value = MarkerInfo(
featureId,
title,
feature.getProperty("subtitle"),
feature.getProperty("description"),
feature.getProperty("icon")
)
_bottomSheetStateEvent.value = Event(BottomSheetBehavior.STATE_COLLAPSED)
}
fun dismissFeatureDetails() {
_bottomSheetStateEvent.value = Event(BottomSheetBehavior.STATE_HIDDEN)
_selectedMarkerInfo.value = null
}
fun optIntoMyLocation(optIn: Boolean = true) {
}
}
data class MarkerInfo(
val id: String,
val title: String,
val subtitle: String?,
val description: String?,
val iconName: String?
)
【问题讨论】:
标签: android google-maps kotlin