I use MapView inside my recyclerview items to display info about a city which the item is about, and I can't figure out how to fix binding issues.
When current item is at the top of the viewport, the map is correct:
But when another item is at the top of the viewport, that is what happens:
As you can see, map displays Salzburg instead of Edinburgh because Salzburg is at the top of recyclerview viewport.
My ViewHolder class:
inner class WeatherViewHolder(
private val binding: ItemWeatherBinding
) : RecyclerView.ViewHolder(
binding.root
), OnMapReadyCallback {
lateinit var gMap: GoogleMap
init {
binding.apply {
// Other binging here...
with(map) {
onCreate(null)
getMapAsync(this#WeatherViewHolder)
}
}
}
fun bind(weather: Weather) {
binding.apply {
//Other binding here...
setMapLocation()
}
}
override fun onMapReady(googleMap: GoogleMap) {
Log.v("Maps", "OnReady")
MapsInitializer.initialize(superFragment.activity)
gMap = googleMap
with(gMap.uiSettings) {
isZoomControlsEnabled = false
isCompassEnabled = false
isMapToolbarEnabled = false
isMyLocationButtonEnabled = false
isRotateGesturesEnabled = false
isScrollGesturesEnabled = false
isTiltGesturesEnabled = false
isZoomGesturesEnabled = false
}
binding.map.onResume()
setMapLocation()
}
private fun setMapLocation() {
if (!::gMap.isInitialized) return
Log.v("Maps", "setting location")
val weather = getItem(adapterPosition)
with(gMap) {
moveCamera(CameraUpdateFactory.newLatLngZoom(LatLng(weather.latitude, weather.longitude), 10f))
mapType = GoogleMap.MAP_TYPE_NORMAL
}
superFragment.viewLifecycleOwner.lifecycleScope.launch {
repository.getInfo { response ->
gMap.addTileOverlay(/*setting my tiles here*/)
}
}
}
}
I tried calling setMapLocation() only to an item which is currently expanded (as I use expandable layout here), but it only led to more confusing behavior.
The question here is: how can I fix this behavior and display correct map regardless of recyclerview's scroll position?
Related
inside an activity I have an observer I am calling a function to setup adapter. After that I am setting list to my adapter. And doing How to know when the RecyclerView has finished laying down the items?. And setting layout manager on that. But it's not working.
activity.kt
class activity : BaseActivity() {
private var cAdapter: cdAdapter? = null
fun setupObserver(){
viewModel.list.observe(this, { value ->
if (cAdapter == null) {
setupAdapter()
}
submitList(value)
binding.cRecyclerView.afterMeasured {
layoutManager = LinearLayoutManager(this.context).apply {
direction(this)
}
}
})
}
private fun setupAdapter() {
cAdapter = cdAdapter()
binding.cRecyclerView.adapter = cAdapter
}
private fun direction(linearLayoutManager: LinearLayoutManager) {
linearLayoutManager.apply {
// if (binding.cRecyclerView.canScrollVertically(1)) {
// logE("Yes can scroll")
//} else {
// logE("No scroll")
// }
if (viewModel.itemsAreMorethanTwo()) {
stackFromEnd = true
reverseLayout = true
val index = viewModel.findLastBiggerValue()
if (index != null && index >= 0) {
scrollToPosition(index)
}
} else {
stackFromEnd = false
reverseLayout = true
}
}
}
inline fun <T : View> T.afterMeasured(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
})
}
}
Basically inside direction function I am checking that if item has more than two value, i'm using reverse layout as well as stackFromEnd as true. As you see inside direction function some lines are commented and I need to check that my items are less than the whole height, i.e. I don't want to scroll up or down and the whole items consist in a single screen
For the commented section, I want to know if the view is scrollable or not.
the code below is working and above code is not.
If I move this code from observer
binding.cRecyclerView.afterMeasured {
layoutManager = LinearLayoutManager(this.context).apply {
direction(this)
}
}
inside like this
private fun setupAdapter() {
cAdapter = cdAdapter()
binding.cRecyclerView.apply {
adapter = cAdapter
layoutManager = LinearLayoutManager(this.context).apply {
setLayoutDirection(this)
}
}
}
When I move this piece of code in setupAdapter the commented section does not work though it sets the layout manager correctly.
What I am doing wrong in above code? Please suggest me something.
My problem is that live data observer is triggered Observer<T> { state.value = it } with the correct data but compose doesn't kick on recompose. Only when I add an item all changes are propagated. There must some checking on the list itself if it has changed. I guess it doens't compare list items.
#Composable
fun <R, T : R> LiveData<T>.observeAsState(initial: R): State<R> {
val lifecycleOwner = LifecycleOwnerAmbient.current
val state = remember { mutableStateOf(initial) }
onCommit(this, lifecycleOwner) {
val observer = Observer<T> { state.value = it }
observe(lifecycleOwner, observer)
onDispose { removeObserver(observer) }
}
return state
}
val items: List<TrackedActivityWithMetric> by vm.activities.observeAsState(mutableListOf())
LazyColumnForIndexed(
items = items,
Modifier.padding(8.dp)
) { index, item ->
....
MetricBlock(item.past[1], item.activity.id )
}
So behind the scenes there must be some kind hash comparing mechanism preventing rendering same item twice (More elabored answer wanted). The incorrect rendering was caused by property which was not in TrackedActivityWithMetric data class constructor.
Jetpack Compose does not work well with MutableList, you need to use a List and do something like this:
var myList: List<MyItem> by mutableStateOf(listOf())
private set
for adding an item:
fun addItem(item: MyItem) {
myList = myList + listOf(myItem)
}
for editing an item:
fun editItem(item: MyItem) {
val index = myList.indexOf(myItem)
myList = myList.toMutableList().also {
it[index] = myItem
}
}
I expect my 'heart' icon to change when my ViewHolder item is clicked.
Fortunately, it does this. However, an issue arises as multiple items seems to replicate the button click.
What I mean is:
If I tap the heart on item number 1. Other items throughout the list replicate also change the heart.
Why is this happening and what is a potential fix? I am confused why this issue is occuring as I am referencing the ViewHolder item. Thus, shouldn't it only affect the item I am clicking?
View Holder
fun bind(item: Location) {
heart.setOnClickListener {
item.fav = item.fav != true
heart.setImageDrawable(
when (item.fav) {
false -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_border_heart))
else -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_whole_heart))
})
}
}
onBindViewHolder you need to save list of fave in change item image base on that list otherwise it changes randomly as view recreates
fun bind(item: Location) {
heart.setImageDrawable(
when (item.fav) {
false -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_border_heart))
else -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_whole_heart))
})
heart.setOnClickListener {
item.fav = item.fav != true
heart.setImageDrawable(
when (item.fav) {
false -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_border_heart))
else -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_whole_heart))
})
}
}
You didn't check the view ID in the onClick method. You can set onClick directly on the views as below.
class LocationViewHolder(v: View): RecyclerView.ViewHolder(v), View.OnClickListener {
private val actLoc: TextView = v.findViewById(R.id.location_main)
private val genLoc: TextView = v.findViewById(R.id.location_subtitle)
private val heart: ImageView = v.findViewById(R.id.heart)
private lateinit var item: Location
fun bind(item: Location) {
this.item = item
actLoc.setText(item.actualLocation)
actLoc.setOnClickListener {
Toast.makeText(itemView.context, "${item.cords}", Toast.LENGTH_SHORT).show()
}
genLoc.setText(item.genLocation)
genLoc.setOnClickListener {
Toast.makeText(itemView.context, "${item.cords}", Toast.LENGTH_SHORT).show()
}
heart.setOnClickListener {
item.fav = item.fav != true
heart.setImageDrawable(
when (item.fav) {
false -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_border_heart))
else -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_whole_heart))
})
}
}
However, an issue arises as multiple items seems to replicate the
button click.
it is because of the cell recycling mechanism
heart.setImageDrawable(
when (item.fav) {
false -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_border_heart))
else -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_whole_heart))
})
should be part of the bind function in the viewholder and not part of the onClick function. What I would expect is
Click informs the viewmodel
Viewmodel update the dataset
Viewmodel informs the recyclerview
I am using Kotlin in my Android project. MapView is in fragment.
When user selects polygon I change its stroke color. Problem is that sometimes onPolygonClick method detects different polygon. You can see it in this GIF: https://gyazo.com/167aed90529031df01c07d7f583f790e
onPolygonClick method:
override fun onPolygonClick(polygon: Polygon?) {
polygons.forEach {
it.strokeColor = Color.BLACK
}
polygon?.strokeColor = Color.WHITE
}
At first, I fetch hexagons from server and then I draw it on map. This is method which is called after data are fetched to add polygons to the map:
private fun drawRegion(regions: Array<Kraj>) {
//reset map
googleMap.clear()
polygons = ArrayList()
setMapViewBounds(regions)
for (region in regions) {
val rnd = Random()
val color = Color.argb(255, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256))
for (hexagon in region.hexagons) {
val options = PolygonOptions()
for (point in hexagon) {
options.add(point)
}
options.strokeColor(Color.BLACK)
options.fillColor(color)
options.strokeWidth(2.5.toFloat())
options.clickable(true)
val pol = googleMap.addPolygon(options)
pol.tag = region.id
polygons.add(pol)
}
}
}
As you can see I also save all polygons to polygons array so I can access all of them in onPolygonClick method.
onMapReady method:
override fun onMapReady(map: GoogleMap?) {
map?.let {
googleMap = it
googleMap.setMapStyle(
MapStyleOptions.loadRawResourceStyle(
activity?.applicationContext, R.raw.empty_map_style
)
)
}
map?.setOnMapClickListener(this)
map?.setOnPolygonClickListener(this)
addObservers()
}
I didn't figure out why MapView detects different polygon, but I did a workaround.
In case you have same problem as me, just set your polygon options clickable to false (otherwise onMapClick is not working if you tap on polygon), setup onMapClickListener and detect which polygon was clicked:
override fun onMapClick(coords: LatLng?) {
val polygon = polygons.first {PolyUtil.containsLocation(coords!!, it.points, true)}
val id = polygon?.tag.let { it } as Int
polygons.forEach {
it.strokeColor = Color.BLACK
val itId = it?.tag.let { it } as Int
if (it == polygon) {
it.strokeColor = Color.BLUE
}
}
viewModel.userClickedOnPolygonWithId(id)
polygon?.strokeColor = Color.WHITE
}
On my map I use the Marker Cluster Utility to group the markers. All the markers when first put on the map have the same icon, then, when I move close to one of the markers, its icon must change. I've read other discussions about this, but as far as I've understood, I'd need to remove the marker and generate it again with the new icon.
My markers belong to a cluster, so I should remove the marker from the cluster, generate a new marker and add it to the cluster manager object.
The problem is that the cluster manager object has a renderer attached to it which also defines the marker's icon and it would use the same icon as for the removed marker.
Some code:
the renderer class
class VenueMarkerRender(private val context: Context, map: GoogleMap, clusterManager: ClusterManager<Venue>)
: DefaultClusterRenderer<Venue>(context, map, clusterManager) {
override fun onBeforeClusterItemRendered(item: Venue?, markerOptions: MarkerOptions?) {
super.onBeforeClusterItemRendered(item, markerOptions)
markerOptions!!.icon(bitmapDescriptorFromVector(context, R.drawable.ic_map_black))
}
override fun onBeforeClusterRendered(cluster: Cluster<Venue>?, markerOptions: MarkerOptions?) {
super.onBeforeClusterRendered(cluster, markerOptions)
markerOptions!!.icon(bitmapDescriptorFromVector(context, R.drawable.ic_home_black_24dp))
}
override fun shouldRenderAsCluster(cluster: Cluster<Venue>?): Boolean {
return cluster!!.size > 1
}
/**
* Takes a vector image and make it available to use as a marker's icon
*/
private fun bitmapDescriptorFromVector(context: Context, #DrawableRes vectorDrawableResourceId: Int): BitmapDescriptor {
// ...
return BitmapDescriptorFactory.fromBitmap(bitmap)
}
}
the Venue class
class Venue : ClusterItem {
private var mPosition: LatLng
private var mTitle: String? = null
private var mSnippet: String? = null
constructor(lat: Double, lng: Double, title: String, snippet: String) {
mPosition = LatLng(lat, lng)
mTitle = title
mSnippet = snippet
}
override fun getPosition(): LatLng {
return mPosition
}
override fun getTitle(): String {
return mTitle!!
}
override fun getSnippet(): String? {
return mSnippet
}
}
finally how the cluster manager is created and how a venue is added to it
mClusterManager = ClusterManager(this, map)
val renderer = VenueMarkerRender(this, map, mClusterManager!!)
mClusterManager!!.renderer = renderer
// other code
for (i in 0 until markers.length()) {
val marker = JSONObject(markers.getJSONObject(i).toString())
val venue = Venue(
marker.getDouble("lat"),
marker.getDouble("lng"),
marker.getString("title"),
marker.getString("snippet"),
)
mClusterManager!!.addItem(venue)
}
mClusterManager!!.cluster()
Is it possible to generate a new Venue object with its own icon and to add it to the cluster manager object? Or is there a better way to obtain what I need?
I've just found the solution, I hope this will help someone else.
I've declared the renderer as a class attribute to make it available everywhere inside the activity
private var renderer: VenueMarkerRender? = null
before it was a private variable inside the method which sets up the Cluster Manager. Then it is initialized as already shown in the previous message
renderer = VenueMarkerRender(this, map, mClusterManager!!)
Now to change the marker when I get close to it, it is enough to call this method each time that the location changes
private fun markerProximity() {
// get the venues' list from the cluster
val venues = mClusterManager!!.algorithm.items
// if the cluster was not empty
if (venues.isNotEmpty()) {
// initialize the array which will contain the distance
val distance: FloatArray = floatArrayOf(0f,0f,0f)
// loop through all the venues
for (venue:Venue in venues) {
// get the distance in meters between the current position and the venue location
Location.distanceBetween(
venue.position.latitude,
venue.position.longitude,
lastLocation.latitude,
lastLocation.longitude,
distance)
// if closer than 3 meters
if ( distance[0] < 3 ) {
// change this marker's icon
renderer!!.getMarker(venue)
.setIcon(BitmapDescriptorFactory
.fromResource(R.drawable.my_location))
}
}
}
}