Fix draggable

This commit is contained in:
Alexander Nozik 2022-09-14 12:11:15 +03:00
parent 693f608b14
commit 8c79c913e6
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
4 changed files with 60 additions and 62 deletions

View File

@ -5,8 +5,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.PointMode import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.VectorPainter
import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
@ -136,23 +136,18 @@ public class MapBitmapImageFeature(
public class MapVectorImageFeature( public class MapVectorImageFeature(
public val position: GeodeticMapCoordinates, public val position: GeodeticMapCoordinates,
public val painter: Painter, public val image: ImageVector,
public val size: DpSize, public val size: DpSize = DpSize(20.dp, 20.dp),
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
) : DraggableMapFeature { ) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position) override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapVectorImageFeature(newCoordinates, painter, size, zoomRange) MapVectorImageFeature(newCoordinates, image, size, zoomRange)
}
@Composable @Composable
public fun MapVectorImageFeature( public fun painter(): VectorPainter = rememberVectorPainter(image)
position: GeodeticMapCoordinates, }
image: ImageVector,
size: DpSize = DpSize(20.dp, 20.dp),
zoomRange: IntRange = defaultZoomRange,
): MapVectorImageFeature = MapVectorImageFeature(position, rememberVectorPainter(image), size, zoomRange)
/** /**
* A group of other features * A group of other features

View File

@ -38,16 +38,16 @@ public class MapFeaturesState internal constructor(
return safeId return safeId
} }
public fun <T> setAttribute(id: FeatureId, key: MapFeaturesState.Attribute<T>, value: T) { public fun <T> setAttribute(id: FeatureId, key: Attribute<T>, value: T) {
attributes.getOrPut(id) { mutableStateMapOf() }[key] = value attributes.getOrPut(id) { mutableStateMapOf() }[key] = value
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
public fun <T> getAttribute(id: FeatureId, key: MapFeaturesState.Attribute<T>): T? = public fun <T> getAttribute(id: FeatureId, key: Attribute<T>): T? =
attributes[id]?.get(key)?.let { it as T } attributes[id]?.get(key)?.let { it as T }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
public fun <T> findAllWithAttribute(key: MapFeaturesState.Attribute<T>, condition: (T) -> Boolean): Set<FeatureId> { public fun <T> findAllWithAttribute(key: Attribute<T>, condition: (T) -> Boolean): Set<FeatureId> {
return attributes.filterValues { return attributes.filterValues {
condition(it[key] as T) condition(it[key] as T)
}.keys }.keys
@ -56,13 +56,13 @@ public class MapFeaturesState internal constructor(
@Composable @Composable
public fun rememberMapFeatureState( public fun rememberMapFeatureState(
builder: @Composable MapFeaturesState.() -> Unit = {}, builder: MapFeaturesState.() -> Unit = {},
): MapFeaturesState = remember(builder) { ): MapFeaturesState = remember(builder) {
MapFeaturesState( MapFeaturesState(
mutableStateMapOf(), mutableStateMapOf(),
mutableStateMapOf() mutableStateMapOf()
) ).apply(builder)
}.apply { builder() } }
public fun MapFeaturesState.circle( public fun MapFeaturesState.circle(
center: GeodeticMapCoordinates, center: GeodeticMapCoordinates,
@ -183,7 +183,6 @@ public fun MapFeaturesState.points(
id: FeatureId? = null, id: FeatureId? = null,
): FeatureId = addFeature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode)) ): FeatureId = addFeature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode))
@Composable
public fun MapFeaturesState.image( public fun MapFeaturesState.image(
position: Pair<Double, Double>, position: Pair<Double, Double>,
image: ImageVector, image: ImageVector,
@ -192,13 +191,15 @@ public fun MapFeaturesState.image(
id: FeatureId? = null, id: FeatureId? = null,
): FeatureId = addFeature(id, MapVectorImageFeature(position.toCoordinates(), image, size, zoomRange)) ): FeatureId = addFeature(id, MapVectorImageFeature(position.toCoordinates(), image, size, zoomRange))
@Composable
public fun MapFeaturesState.group( public fun MapFeaturesState.group(
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
id: FeatureId? = null, id: FeatureId? = null,
builder: @Composable MapFeaturesState.() -> Unit, builder: MapFeaturesState.() -> Unit,
): FeatureId { ): FeatureId {
val map = rememberMapFeatureState(builder).features() val map = MapFeaturesState(
mutableStateMapOf(),
mutableStateMapOf()
).apply(builder).features()
val feature = MapFeatureGroup(map, zoomRange) val feature = MapFeatureGroup(map, zoomRange)
return addFeature(id, feature) return addFeature(id, feature)
} }

View File

@ -87,7 +87,7 @@ internal fun GmcRectangle.computeViewPoint(
return MapViewPoint(center, zoom) return MapViewPoint(center, zoom)
} }
private val defaultCanvasSize = DpSize(512.dp, 512.dp) internal val defaultCanvasSize = DpSize(512.dp, 512.dp)
/** /**
* Draw a map using convenient parameters. If neither [initialViewPoint], noe [initialRectangle] is defined, * Draw a map using convenient parameters. If neither [initialViewPoint], noe [initialRectangle] is defined,
@ -103,46 +103,44 @@ public fun MapView(
initialRectangle: GmcRectangle? = null, initialRectangle: GmcRectangle? = null,
config: MapViewConfig = MapViewConfig(), config: MapViewConfig = MapViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: @Composable (MapFeaturesState.() -> Unit) = {}, buildFeatures: MapFeaturesState.() -> Unit = {},
): Unit = key(buildFeatures) { ): Unit = key(buildFeatures) {
val featureState = rememberMapFeatureState(buildFeatures) val featureState = rememberMapFeatureState(buildFeatures)
val features = featureState.features() val features = featureState.features()
var cachedCanvasSize: DpSize by remember { mutableStateOf(defaultCanvasSize) } val viewPointOverride: MapViewPoint = remember(initialViewPoint, initialRectangle) {
val viewPointOverride: MapViewPoint = remember(initialViewPoint, initialRectangle, cachedCanvasSize) {
initialViewPoint initialViewPoint
?: initialRectangle?.computeViewPoint(mapTileProvider, cachedCanvasSize) ?: initialRectangle?.computeViewPoint(mapTileProvider, defaultCanvasSize)
?: features.values.computeBoundingBox(1.0)?.computeViewPoint(mapTileProvider, cachedCanvasSize) ?: features.values.computeBoundingBox(1.0)?.computeViewPoint(mapTileProvider, defaultCanvasSize)
?: MapViewPoint.globe ?: MapViewPoint.globe
} }
val featureDrag by derivedStateOf { val featureDrag by remember {
DragHandle.withPrimaryButton { _, start, end -> derivedStateOf {
val zoom = start.zoom DragHandle.withPrimaryButton { _, start, end ->
featureState.findAllWithAttribute(DraggableAttribute) { it }.forEach { id -> val zoom = start.zoom
val feature = features[id] as? DraggableMapFeature ?: return@forEach featureState.findAllWithAttribute(DraggableAttribute) { it }.forEach { id ->
val boundingBox = feature.getBoundingBox(zoom) ?: return@forEach val feature = features[id] as? DraggableMapFeature ?: return@forEach
if (start.focus in boundingBox) { val boundingBox = feature.getBoundingBox(zoom) ?: return@forEach
featureState.addFeature(id, feature.withCoordinates(end.focus)) if (start.focus in boundingBox) {
return@withPrimaryButton false featureState.addFeature(id, feature.withCoordinates(end.focus))
return@withPrimaryButton false
}
} }
return@withPrimaryButton true
} }
return@withPrimaryButton true
} }
} }
val newConfig by derivedStateOf { val newConfig by remember {
config.copy( derivedStateOf {
dragHandle = DragHandle.combine(featureDrag, config.dragHandle), config.copy(
onCanvasSizeChange = { canvasSize -> dragHandle = DragHandle.combine(featureDrag, config.dragHandle)
config.onCanvasSizeChange(canvasSize) )
cachedCanvasSize = canvasSize }
}
)
} }

View File

@ -45,7 +45,6 @@ private val logger = KotlinLogging.logger("MapView")
/** /**
* A component that renders map and provides basic map manipulation capabilities * A component that renders map and provides basic map manipulation capabilities
*/ */
@Composable @Composable
public actual fun MapView( public actual fun MapView(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
@ -54,10 +53,12 @@ public actual fun MapView(
config: MapViewConfig, config: MapViewConfig,
modifier: Modifier, modifier: Modifier,
): Unit = key(initialViewPoint) { ): Unit = key(initialViewPoint) {
var canvasSize by remember { mutableStateOf(DpSize(512.dp, 512.dp)) } var canvasSize by remember { mutableStateOf(defaultCanvasSize) }
var viewPoint by remember { mutableStateOf(initialViewPoint) } var viewPoint by remember { mutableStateOf(initialViewPoint) }
require(viewPoint.zoom in 1.0..18.0) { "Zoom value of ${viewPoint.zoom} is not valid" }
fun setViewPoint(newViewPoint: MapViewPoint) { fun setViewPoint(newViewPoint: MapViewPoint) {
config.onViewChange(newViewPoint) config.onViewChange(newViewPoint)
viewPoint = newViewPoint viewPoint = newViewPoint
@ -68,9 +69,7 @@ public actual fun MapView(
floor(viewPoint.zoom).toInt() floor(viewPoint.zoom).toInt()
} }
val tileScale: Double by derivedStateOf { val tileScale: Double by derivedStateOf { 2.0.pow(viewPoint.zoom - zoom) }
2.0.pow(viewPoint.zoom - zoom)
}
val mapTiles = remember(mapTileProvider) { mutableStateListOf<MapTile>() } val mapTiles = remember(mapTileProvider) { mutableStateListOf<MapTile>() }
@ -103,9 +102,12 @@ public actual fun MapView(
val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp()) val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp())
//start selection //start selection
if (event.buttons.isPrimaryPressed && event.keyboardModifiers.isShiftPressed) { var selectionStart: Offset? =
selectRect = Rect(change.position, change.position) if (event.buttons.isPrimaryPressed && event.keyboardModifiers.isShiftPressed) {
} change.position
} else {
null
}
drag(change.id) { dragChange -> drag(change.id) { dragChange ->
val dragAmount = dragChange.position - dragChange.previousPosition val dragAmount = dragChange.position - dragChange.previousPosition
@ -124,19 +126,19 @@ public actual fun MapView(
) )
) { ) {
//clear selection just in case //clear selection just in case
selectRect = null selectionStart = null
return@drag return@drag
} }
if (event.buttons.isPrimaryPressed) { if (event.buttons.isPrimaryPressed) {
//Evaluating selection frame //Evaluating selection frame
selectRect?.let { rect -> selectionStart?.let { start ->
val offset = dragChange.position val offset = dragChange.position
selectRect = Rect( selectRect = Rect(
min(offset.x, rect.left), min(offset.x, start.x),
min(offset.y, rect.top), min(offset.y, start.y),
max(offset.x, rect.right), max(offset.x, start.x),
max(offset.y, rect.bottom) max(offset.y, start.y)
) )
return@drag return@drag
} }
@ -214,6 +216,8 @@ public actual fun MapView(
} }
} }
val painterCache = features.features().values.filterIsInstance<MapVectorImageFeature>().associateWith { it.painter() }
Canvas(canvasModifier) { Canvas(canvasModifier) {
fun WebMercatorCoordinates.toOffset(): Offset = Offset( fun WebMercatorCoordinates.toOffset(): Offset = Offset(
@ -265,7 +269,7 @@ public actual fun MapView(
val offset = feature.position.toOffset() val offset = feature.position.toOffset()
val size = feature.size.toSize() val size = feature.size.toSize()
translate(offset.x - size.width / 2, offset.y - size.height / 2) { translate(offset.x - size.width / 2, offset.y - size.height / 2) {
with(feature.painter) { with(painterCache[feature]!!) {
draw(size) draw(size)
} }
} }