animate marker appearance and disappearance

This commit is contained in:
Johan von Forstner
2020-04-12 19:11:49 +02:00
parent 03b47b6bb9
commit e20dbd6ec1
5 changed files with 200 additions and 74 deletions

View File

@@ -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<Marker, ChargeLocation> = emptyMap()
private var clusterMarkers: List<Marker> = 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<ChargepointListItem>) {
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<ChargeLocationCluster>()
val chargers = chargepoints.filterIsInstance<ChargeLocation>()
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<out String>,

View File

@@ -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)
}
}
}

View File

@@ -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<BitmapData, Bitmap>()
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
}
}

View File

@@ -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()
}

View File

@@ -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)
}