diff --git a/app/src/main/java/com/johan/evmap/fragment/MapFragment.kt b/app/src/main/java/com/johan/evmap/fragment/MapFragment.kt index 6756e1ce..c57a250f 100644 --- a/app/src/main/java/com/johan/evmap/fragment/MapFragment.kt +++ b/app/src/main/java/com/johan/evmap/fragment/MapFragment.kt @@ -38,8 +38,7 @@ import com.johan.evmap.api.ChargeLocationCluster import com.johan.evmap.api.ChargepointListItem import com.johan.evmap.api.ChargerPhoto import com.johan.evmap.databinding.FragmentMapBinding -import com.johan.evmap.ui.ClusterIconGenerator -import com.johan.evmap.ui.getBitmapDescriptor +import com.johan.evmap.ui.* import com.johan.evmap.viewmodel.MapPosition import com.johan.evmap.viewmodel.MapViewModel import com.johan.evmap.viewmodel.viewModelFactory @@ -57,6 +56,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac private var markers: Map = emptyMap() private var clusterMarkers: List = emptyList() + private lateinit var clusterIconGenerator: ClusterIconGenerator + private lateinit var chargerIconGenerator: ChargerIconGenerator + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -67,6 +69,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac binding.vm = vm fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext()) + clusterIconGenerator = ClusterIconGenerator(requireContext()) + chargerIconGenerator = ChargerIconGenerator(requireContext()) setHasOptionsMenu(true) @@ -214,6 +218,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac } else -> false } + } map.setOnMapClickListener { vm.chargerSparse.value = null @@ -270,40 +275,51 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac private fun updateMap(chargepoints: List) { val map = this.map ?: return - markers.keys.forEach { it.remove() } clusterMarkers.forEach { it.remove() } - val context = context ?: return - val iconGenerator = ClusterIconGenerator(context) val clusters = chargepoints.filterIsInstance() val chargers = chargepoints.filterIsInstance() - markers = chargers.map { charger -> - map.addMarker( + val chargepointIds = chargers.map { it.id }.toSet() + markers = markers.filter { + if (!chargepointIds.contains(it.value.id)) { + val tint = getMarkerTint(it.value) + if (it.key.isVisible) { + animateMarkerDisappear(it.key, tint, chargerIconGenerator) + } else { + it.key.remove() + } + false + } else { + true + } + } + markers = markers + chargers.filter { + !markers.containsValue(it) + }.map { charger -> + val tint = getMarkerTint(charger) + val marker = map.addMarker( MarkerOptions() .position(LatLng(charger.coordinates.lat, charger.coordinates.lng)) .icon( - getBitmapDescriptor( - R.drawable.ic_map_marker_charging, when { - charger.maxPower >= 100 -> R.color.charger_100kw - charger.maxPower >= 43 -> R.color.charger_43kw - charger.maxPower >= 20 -> R.color.charger_20kw - charger.maxPower >= 11 -> R.color.charger_11kw - else -> R.color.charger_low - }, context + chargerIconGenerator.getBitmapDescriptor( + R.drawable.ic_map_marker_charging, + tint ) ) - ) to charger + ) + animateMarkerAppear(marker, tint, chargerIconGenerator) + + marker to charger }.toMap() clusterMarkers = clusters.map { cluster -> map.addMarker( MarkerOptions() .position(LatLng(cluster.coordinates.lat, cluster.coordinates.lng)) - .icon(BitmapDescriptorFactory.fromBitmap(iconGenerator.makeIcon(cluster.clusterCount.toString()))) + .icon(BitmapDescriptorFactory.fromBitmap(clusterIconGenerator.makeIcon(cluster.clusterCount.toString()))) ) } } - override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, diff --git a/app/src/main/java/com/johan/evmap/ui/ClusterIconGenerator.kt b/app/src/main/java/com/johan/evmap/ui/ClusterIconGenerator.kt deleted file mode 100644 index 357390b8..00000000 --- a/app/src/main/java/com/johan/evmap/ui/ClusterIconGenerator.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.johan.evmap.ui - -import android.content.Context -import android.view.ViewGroup -import com.google.maps.android.ui.IconGenerator -import com.google.maps.android.ui.SquareTextView -import com.johan.evmap.R - -class ClusterIconGenerator(context: Context) : IconGenerator(context) { - init { - setBackground(context.getDrawable(R.drawable.marker_cluster_bg)) - setContentView(makeSquareTextView(context)) - setTextAppearance(R.style.TextAppearance_AppCompat_Inverse) - } - - private fun makeSquareTextView(context: Context): SquareTextView? { - val density = context.resources.displayMetrics.density - val twelveDpi = (12.0f * density).toInt() - - return SquareTextView(context).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - id = com.google.maps.android.R.id.amu_text - setPadding(twelveDpi, twelveDpi, twelveDpi, twelveDpi) - } - } -} diff --git a/app/src/main/java/com/johan/evmap/ui/IconGenerators.kt b/app/src/main/java/com/johan/evmap/ui/IconGenerators.kt new file mode 100644 index 00000000..8aa0c40b --- /dev/null +++ b/app/src/main/java/com/johan/evmap/ui/IconGenerators.kt @@ -0,0 +1,96 @@ +package com.johan.evmap.ui + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.PorterDuff +import android.graphics.drawable.Drawable +import android.view.ViewGroup +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import com.google.android.gms.maps.model.BitmapDescriptor +import com.google.android.gms.maps.model.BitmapDescriptorFactory +import com.google.maps.android.ui.IconGenerator +import com.google.maps.android.ui.SquareTextView +import com.johan.evmap.R + +class ClusterIconGenerator(context: Context) : IconGenerator(context) { + init { + setBackground(context.getDrawable(R.drawable.marker_cluster_bg)) + setContentView(makeSquareTextView(context)) + setTextAppearance(R.style.TextAppearance_AppCompat_Inverse) + } + + private fun makeSquareTextView(context: Context): SquareTextView? { + val density = context.resources.displayMetrics.density + val twelveDpi = (12.0f * density).toInt() + + return SquareTextView(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + id = com.google.maps.android.R.id.amu_text + setPadding(twelveDpi, twelveDpi, twelveDpi, twelveDpi) + } + } +} + + +class ChargerIconGenerator(val context: Context) { + data class BitmapData(val id: Int, val tint: Int, val scale: Int, val alpha: Int) + + val cache = hashMapOf() + val oversize = 1.5f + + fun getBitmapDescriptor( + @DrawableRes id: Int, + @ColorRes tint: Int, + scale: Int = 255, + alpha: Int = 255 + ): BitmapDescriptor? { + val data = BitmapData(id, tint, scale, alpha) + if (cache.containsKey(data)) { + return BitmapDescriptorFactory.fromBitmap(cache[data]) + } else { + val bitmap = generateBitmap(data) + cache[data] = bitmap + return BitmapDescriptorFactory.fromBitmap(bitmap) + } + } + + private fun generateBitmap(data: BitmapData): Bitmap { + val vd: Drawable = context.getDrawable(data.id)!! + + DrawableCompat.setTint(vd, ContextCompat.getColor(context, data.tint)); + DrawableCompat.setTintMode(vd, PorterDuff.Mode.MULTIPLY); + + val leftPadding = vd.intrinsicWidth * (oversize - 1) / 2 + val topPadding = vd.intrinsicWidth * (oversize - 1) + vd.setBounds( + leftPadding.toInt(), topPadding.toInt(), + leftPadding.toInt() + vd.intrinsicWidth, + topPadding.toInt() + vd.intrinsicHeight + ) + vd.alpha = data.alpha + + val bm = Bitmap.createBitmap( + (vd.intrinsicWidth * oversize).toInt(), (vd.intrinsicHeight * oversize).toInt(), + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bm) + + val scale = data.scale / 255f + canvas.scale( + scale, + scale, + leftPadding + vd.intrinsicWidth / 2f, + topPadding + vd.intrinsicHeight.toFloat() + ) + + vd.draw(canvas) + return bm + } +} \ No newline at end of file diff --git a/app/src/main/java/com/johan/evmap/ui/MarkerUtils.kt b/app/src/main/java/com/johan/evmap/ui/MarkerUtils.kt new file mode 100644 index 00000000..23dfcbaf --- /dev/null +++ b/app/src/main/java/com/johan/evmap/ui/MarkerUtils.kt @@ -0,0 +1,70 @@ +package com.johan.evmap.ui + +import android.animation.ValueAnimator +import androidx.core.animation.addListener +import androidx.interpolator.view.animation.FastOutLinearInInterpolator +import androidx.interpolator.view.animation.LinearOutSlowInInterpolator +import com.google.android.gms.maps.model.Marker +import com.johan.evmap.R +import com.johan.evmap.api.ChargeLocation + +fun getMarkerTint(charger: ChargeLocation): Int = when { + charger.maxPower >= 100 -> R.color.charger_100kw + charger.maxPower >= 43 -> R.color.charger_43kw + charger.maxPower >= 20 -> R.color.charger_20kw + charger.maxPower >= 11 -> R.color.charger_11kw + else -> R.color.charger_low +} + +fun animateMarkerAppear( + marker: Marker, + tint: Int, + gen: ChargerIconGenerator +) { + ValueAnimator.ofInt(0, 255).apply { + duration = 250 + interpolator = LinearOutSlowInInterpolator() + addUpdateListener { animationState -> + if (!marker.isVisible) { + cancel() + return@addUpdateListener + } + val scale = animationState.animatedValue as Int + marker.setIcon( + gen.getBitmapDescriptor( + R.drawable.ic_map_marker_charging, + tint, + scale = scale + ) + ) + } + }.start() +} + +fun animateMarkerDisappear( + marker: Marker, + tint: Int, + gen: ChargerIconGenerator +) { + ValueAnimator.ofInt(255, 0).apply { + duration = 200 + interpolator = FastOutLinearInInterpolator() + addUpdateListener { animationState -> + if (!marker.isVisible) { + cancel() + return@addUpdateListener + } + val scale = animationState.animatedValue as Int + marker.setIcon( + gen.getBitmapDescriptor( + R.drawable.ic_map_marker_charging, + tint, + scale = scale + ) + ) + } + addListener(onEnd = { + marker.remove() + }) + }.start() +} \ No newline at end of file diff --git a/app/src/main/java/com/johan/evmap/ui/Utils.kt b/app/src/main/java/com/johan/evmap/ui/Utils.kt deleted file mode 100644 index 434af6de..00000000 --- a/app/src/main/java/com/johan/evmap/ui/Utils.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.johan.evmap.ui - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.PorterDuff -import android.graphics.drawable.Drawable -import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.DrawableCompat -import com.google.android.gms.maps.model.BitmapDescriptor -import com.google.android.gms.maps.model.BitmapDescriptorFactory - - -fun getBitmapDescriptor(@DrawableRes id: Int, @ColorRes tint: Int, context: Context): BitmapDescriptor? { - val vd: Drawable = context.getDrawable(id)!! - - DrawableCompat.setTint(vd, ContextCompat.getColor(context, tint)); - DrawableCompat.setTintMode(vd, PorterDuff.Mode.MULTIPLY); - - vd.setBounds(0, 0, vd.intrinsicWidth, vd.intrinsicHeight) - val bm = Bitmap.createBitmap(vd.intrinsicWidth, vd.intrinsicHeight, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bm) - vd.draw(canvas) - return BitmapDescriptorFactory.fromBitmap(bm) -} \ No newline at end of file