I have custom renderer for clusters using on GoogleMap in my app. It has custom drawable background and even number textview is custom layout with my own font and size.
It is working perfectly but sometimes map will not render cluster at all and instead of my custom layout its some weird white-colored circle with semitransparent background and default font. I don't use that kind of layout or drawable for clusters or markers so I don't know from where it is coming from. When I zoom in and then zoom out again, sometimes it is corrected and re-rendered to my drawable.
Here is code:
private val stationMarkers = mutableListOf<StationMarkerSettings>()
inner class StationMarkerRender(private val context: Context, map: GoogleMap, clusterManager: ClusterManager<StationMarker>)
: DefaultClusterRenderer<StationMarker>(context, map, clusterManager) {
private val mClusterIconGenerator = IconGenerator(context)
//TextView displaying number - centered
private fun makeSquareTextView(context: Context): ViewGroup {
return (CustomResources.inflateLayout(
LayoutInflater.from(context),
R.layout.station_marker_cluster_text,
null
) as ViewGroup)
}
override fun onBeforeClusterRendered(
cluster: Cluster<StationMarker>,
markerOptions: MarkerOptions
) {
super.onBeforeClusterRendered(cluster, markerOptions)
mClusterIconGenerator.setBackground(ContextCompat.getDrawable(context, R.drawable.station_pin_cluster))
mClusterIconGenerator.setContentView(makeSquareTextView(context))
val icon = mClusterIconGenerator.makeIcon(java.lang.String.valueOf(cluster.size))
markerOptions.icon(BitmapDescriptorFactory.fromBitmap(icon))
}
override fun onBeforeClusterItemRendered(item: StationMarker, mo: MarkerOptions) {
stationMarkers.add(StationMarkerSettings(item, mo))
mo.icon(bitmapDescriptorFromVector(activity, R.drawable.station_pin_unselected)).anchor(anchorU, 1f)
}
override fun shouldRenderAsCluster(cluster: Cluster<StationMarker>): Boolean {
return cluster.size > 1
}
override fun getColor(clusterSize: Int): Int {
return ContextCompat.getColor(context, R.color.white)
}
}
Result:
Related
I'm building my first game in Android Studio. Right now, dots fall from the top of the screen down to the bottom. For some reason, in Layout Inspector the view of each dot is the entire screen even though the dots are comparatively small. This negatively affects the game since when a user presses anywhere on the screen, it deletes the most recently created dot rather than the one pressed. I want to get the dot's view to match the size of the actual dots without effecting other functionality.
Dot.kt
class Dot(context: Context, attrs: AttributeSet?, private var dotColor: Int, private var xPos: Int, private var yPos: Int) : View(context, attrs) {
private var isMatching: Boolean = false
private var dotIsPressed: Boolean = false
private var isDestroyed: Boolean = false
private lateinit var mHandler: Handler
private lateinit var runnable: Runnable
init {
this.isPressed = false
this.isDestroyed = false
mHandler = Handler()
runnable = object : Runnable {
override fun run() {
moveDown()
invalidate()
mHandler.postDelayed(this, 20)
}
}
val random = Random()
xPos = random.nextInt(context.resources.displayMetrics.widthPixels)
startFalling()
startDrawing()
}
// other methods
fun getDotColor() = dotColor
fun getXPos() = xPos
fun getYPos() = yPos
fun isMatching() = isMatching
fun setMatching(matching: Boolean) {
this.isMatching = matching
}
fun dotIsPressed() = dotIsPressed
override fun setPressed(pressed: Boolean) {
this.dotIsPressed = pressed
}
fun isDestroyed() = isDestroyed
fun setDestroyed(destroyed: Boolean) {
this.isDestroyed = destroyed
}
fun moveDown() {
// code to move the dot down the screen
yPos += 10
}
fun checkCollision(line: Line) {
// check if dot is colliding with line
// if yes, check if dot is matching or not
// update the dot state accordingly
}
fun startFalling() {
mHandler.post(runnable)
}
fun startDrawing() {
mHandler.postDelayed(object : Runnable {
override fun run() {
invalidate()
mHandler.postDelayed(this, 500)
}
}, 500)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (!isDestroyed) {
val paint = Paint().apply {
color = dotColor
}
canvas?.drawCircle(xPos.toFloat(), yPos.toFloat(), 30f, paint)
}
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private var score = 0
private lateinit var scoreCounter: TextView
private val dots = mutableListOf<Dot>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
createLine(Color.RED, 5000)
scoreCounter = TextView(this)
scoreCounter.text = score.toString()
scoreCounter.setTextColor(Color.WHITE)
val layout = findViewById<ConstraintLayout>(R.id.layout)
layout.setBackgroundColor(Color.BLACK)
val params = ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.WRAP_CONTENT,
ConstraintLayout.LayoutParams.WRAP_CONTENT
)
params.topToTop = ConstraintLayout.LayoutParams.PARENT_ID
params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID
scoreCounter.layoutParams = params
layout.addView(scoreCounter)
val dotColors = intArrayOf(Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW)
val random = Random()
val handler = Handler()
val runnable = object : Runnable {
override fun run() {
val dotColor = dotColors[random.nextInt(dotColors.size)]
createAndAddDot(0, 0, dotColor)
handler.postDelayed(this, 500)
}
}
handler.post(runnable)
}
fun updateScore(increment: Int) {
score += increment
scoreCounter.text = score.toString()
}
fun createAndAddDot(x: Int, y: Int, color: Int) {
Log.d("Dot", "createAndAddDot called")
val dot = Dot(this, null, color, x, y)
val layout = findViewById<ConstraintLayout>(R.id.layout)
layout.addView(dot)
dots.add(dot)
dot.setOnTouchListener { view, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
val dotToRemove = dots.find { it == view }
dotToRemove?.let {
layout.removeView(it)
dots.remove(it)
updateScore(1)
view.performClick()
}
}
true
}
}
fun createLine(color: Int, interval: Int) {
Log.d("Line", "createLine called")
val line = Line(color, interval)
val lineView = Line.LineView(this, null, line)
val layout = findViewById<ConstraintLayout>(R.id.layout)
if (layout == null) {
throw IllegalStateException("Layout not found")
}
layout.addView(lineView)
val params = ConstraintLayout.LayoutParams(2000, 350)
lineView.layoutParams = params
params.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID
params.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
params.bottomMargin = (0.1 * layout.height).toInt()
}
}
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Your view here -->
<View
android:id="#+id/view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<!-- Guideline set to 10% from the bottom -->
<androidx.constraintlayout.widget.Guideline
android:id="#+id/bottom_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.1" />
</androidx.constraintlayout.widget.ConstraintLayout>
I tried changing the view size with
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) val diameter = 40 // or any other desired diameter for the dots setMeasuredDimension(diameter, diameter) }
That made the view size a square stuck in the top left corner. As I played around with it, I could only get dots to show in that small window in the top corner rather than moving down the screen from different starting x-positions
Your custom view isn't a dot, it's a large display area that draws a dot somewhere inside it and animates its position. In onDraw you're drawing a circle at xPos (a random point on the screen width via displayMetrics.widthPixels) and yPos (an increasing value which moves the dot down the view).
There are two typical approaches to things like this:
use simple views like ImageViews. Let the containing Activity or Fragment add them to a container and control their position, maybe using the View Animation system. Handle player interaction by giving them click listeners and let the view system work out what's been clicked.
create a custom view that acts as the game area. Let that custom view control the game state (what dots exist, where they currently are) and draw that state in onDraw. Handle touch events on the view, and work out if those touches coincide with a dot (by comparing to the current game state).
What you're doing is sort of a combination of the two with none of the advantages that either approach gives on its own. You have multiple equally-sized "game field" views stacked on top of each other, so any clicks will be consumed by the top one, because you're clicking the entire view itself. And because your custom view fills the whole area, you can't move it around with basic view properties to control where the dot is - you have to write the logic to draw the view and animate its contents.
You could implement some code that handles the clicks and decides whether the view consumes it (because it intersects a dot) or passes it on to the next view in the stack, but that's a lot of work and you still have all your logic split between the Activity/Fragment and the custom view itself.
I think it would be way easier to just pick one approach - either use ImageViews sized to the dot you want and let the view system handle the interaction, or make a view that runs the game internally. Personally I'd go with the latter (you'll find it a lot easier to handle dots going out of bounds, get better performance, more control over the look and interaction etc, no need to cancel Runnables) but it's up to you!
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
}
Following up with my problem I describe here i'm trying to draw some shapes to a canvas that isn't associated with any view/layout (it's totally in memory & never drawn to the screen) and then use the bitmap I drew in my activity.
The problem:
It seems like my draw method never gets called. I have some log messages within it and I never see them print, and when I view the bitmap during runtime in Android Studio's debugger its always blank.
I've read various posts about having to call setWillNotDraw(false) to get the onDraw() method in a custom view to trigger, but because i'm never going to render this canvas to the screen my custom class extends Drawable() instead of View(context which doesn't include that method. This seems like a good choice since View includes lots of logic for user touches and other actions that my no-UI, background-generated drawable won't use.
That being said I still only saw blank bitmaps and no log messages from onDraw() when I extended View instead of Drawable and called setWillNowDraw(false) in the classes' constructor.
What's causing my canvas to always be blank?
class CustomImage : Drawable() {
var bitmap: Bitmap
private var barcodeText = Paint(ANTI_ALIAS_FLAG).apply {
color = Color.BLACK
textSize = 24f
}
private var circlePainter = Paint(ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
}
init {
bitmap = Bitmap.createBitmap(LABEL_SIDE_LENGTH, LABEL_SIDE_LENGTH, Bitmap.Config.ARGB_8888)
}
override fun draw(canvas: Canvas) {
Timber.d("canvas: In draw")
canvas.setBitmap(bitmap)
canvas.apply {
drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
drawText("002098613", LABEL_SIDE_LENGTH - 50f, 50f, barcodeText)
drawCircle(200f, 200f, 100f, circlePainter)
}
Timber.d("canvas: $canvas")
}
override fun setAlpha(alpha: Int) {
}
override fun getOpacity(): Int {
return PixelFormat.OPAQUE
}
override fun setColorFilter(colorFilter: ColorFilter?) {
}
companion object {
const val LABEL_SIDE_LENGTH = 1160
}
}
class MyActivity: AppCompatActivity(){
private lateinit var customImgBitmap: Bitmap
override fun onCreate(savedInstanceState: Bundle?) {
customImgBitmap = createImgBitmap()
val test = POLabelGenerator
}
...
private fun createImgBitmap(): Bitmap {
return CustomImage.bitmap
}
}
I have a list of places which are marked in google maps using Markers. I want to select a Marker so that it will highlight with a different color. When I click on the same marker or any other marker I want remove the selection made in the first marker and set it back to the default color.
This is my onClusterItemClick method
override fun onClusterItemClick(p0: Station?): Boolean {
dragView.visibility = View.VISIBLE
viewModel.loadStation(p0?.id!!)
val marker = renderer.getMarker(p0)
//save previous merker here
marker?.setIcon(BitmapDescriptorFactory.fromResource(R.drawable.ic_map_pin_selected))
return true
}
This is my Station Renderer
/**
* Class to design the pin point into the map
*/
inner class StationRenderer(context: Context, map: GoogleMap,
clusterManager: ClusterManager<Station>) : DefaultClusterRenderer<Station>(context, map, clusterManager) {
override fun onBeforeClusterRendered(cluster: Cluster<Station>?, markerOptions: MarkerOptions?) {
markerOptions?.icon(BitmapDescriptorFactory.fromBitmap(createStoreMarker(cluster?.size.toString())))
}
override fun onBeforeClusterItemRendered(item: Station?, markerOptions: MarkerOptions?) {
markerOptions?.icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_map_pin))
}
private fun createStoreMarker(stationsCount:String): Bitmap {
val markerLayout = layoutInflater.inflate(R.layout.marker_item, null)
val markerImage = markerLayout.findViewById(R.id.marker_image) as ImageView
val markerRating = markerLayout.findViewById(R.id.marker_text) as TextView
markerImage.setImageResource(R.drawable.ic_map_pin)
markerRating.text = stationsCount
markerLayout.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED))
markerLayout.layout(0, 0, markerLayout.getMeasuredWidth(), markerLayout.getMeasuredHeight())
val bitmap = Bitmap.createBitmap(markerLayout.getMeasuredWidth(), markerLayout.getMeasuredHeight(), Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
markerLayout.draw(canvas)
return bitmap
}
override fun shouldRenderAsCluster(cluster: Cluster<Station>?): Boolean {
return cluster?.size !!> 1
}
}
In googleMaps these is no such thing as selected or deselected or some kind of listeners specifically for that but you have onMarkerClick(); you can use this Listener and add some logic to achieve that thing.
googleMap.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener(){
#Override
public boolean onMarkerClick(Marker marker){
return false;
}
});
You can get the Idea from this: How to select and deselect a marker in google maps in android?
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))
}
}
}
}