So far I made GestureListener, which handles zoom and pan, making sure you never zoom out too far and you can never pan out of map, works pretty smooth, but probably doesn't cover all the possibilities like if y axis is rotated.
If you can call fitBounds separately after resize so that world clips in case it needs. I will try to update class as I get it cleaner and more flexible. Pretty sure this scale() method has to have build-in alternative in engine.
Another thing I added is ScrollInputListener, which can listen to mouse scroll and perform zoom.
Just add this class wrapped into GestureDetector and add to your input multiplexer. I am using it with FillViewport.
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.OrthographicCamera
import com.badlogic.gdx.input.GestureDetector
import com.badlogic.gdx.utils.viewport.Viewport
class ScrollInputListener(
val camera: OrthographicCamera,
val panGestureListener: PanGestureListener
) : InputAdapter() {
fun amountToZoom(amount: Int): Float {
return (1.0f + amount.toFloat() / 10f)
}
override fun scrolled(amount: Int): Boolean {
val newZoom = camera.zoom * amountToZoom(amount)
return panGestureListener.doZoomIfAllowed(newZoom)
}
}
class CameraMovement(val x: Float, val y: Float) {
private fun shouldMove(): Boolean {
return x!= 0f || y != 0f
}
fun move(camera: OrthographicCamera, scale: Float): Boolean {
if (shouldMove()) {
camera.translate(-x / scale, y / scale)
return true
}
return false
}
}
val Viewport.targetRatio: Float
get() {
return Gdx.graphics.height.toFloat() / this.worldHeight.toFloat()
}
val Viewport.sourceRatio: Float
get() {
return Gdx.graphics.width.toFloat() / this.worldWidth.toFloat()
}
fun Viewport.scale(): Float {
return if (targetRatio < sourceRatio) Gdx.graphics.width.toFloat() / this.worldWidth.toFloat() else Gdx.graphics.height.toFloat() / this.worldHeight.toFloat()
}
class PanGestureListener(
val camera: OrthographicCamera,
val viewport: Viewport,
val minWorldRatio: Double = 0.5
) : GestureDetector.GestureAdapter() {
override fun pan(x: Float, y: Float, deltaX: Float, deltaY: Float): Boolean {
val scale = viewport.scale()
return fitBounds(scale, deltaX, deltaY).move(camera, scale)
}
fun fitBounds(scale: Float = viewport.scale(), deltaX: Float = 0f, deltaY: Float = 0f): CameraMovement {
val mapLeft = 0
val mapRight = viewport.worldWidth / camera.zoom
val mapTop = 0
val mapBottom = viewport.worldHeight / camera.zoom
val cameraHalfWidth = if (viewport.targetRatio < viewport.sourceRatio) {
camera.viewportWidth / 2f
} else {
camera.viewportWidth / 2f * (viewport.sourceRatio / viewport.targetRatio)
}
val cameraHalfHeight = if (viewport.targetRatio < viewport.sourceRatio) {
camera.viewportHeight / 2f * (viewport.targetRatio / viewport.sourceRatio)
} else {
camera.viewportHeight / 2f
}
var xMovement = deltaX
var yMovement = deltaY
if ((camera.position.x - deltaX / scale) / camera.zoom - cameraHalfWidth < mapLeft) {
camera.position.x = (mapLeft + cameraHalfWidth) * camera.zoom
xMovement = 0f
}
if ((camera.position.x - deltaX / scale) / camera.zoom + cameraHalfWidth > mapRight) {
camera.position.x = (mapRight - cameraHalfWidth) * camera.zoom
xMovement = 0f
}
if ((camera.position.y + deltaY / scale) / camera.zoom - cameraHalfHeight < mapTop) {
camera.position.y = (mapTop + cameraHalfHeight) * camera.zoom
yMovement = 0f
}
if ((camera.position.y + deltaY / scale) / camera.zoom + cameraHalfHeight > mapBottom) {
camera.position.y = (mapBottom - cameraHalfHeight) * camera.zoom
yMovement = 0f
}
return CameraMovement(xMovement, yMovement)
}
var initialZoom = camera.zoom
internal fun checkZoom(newZoom: Float, scale: Float = viewport.scale()): Boolean {
if (
viewport.worldWidth * scale * newZoom > Gdx.graphics.width &&
viewport.worldHeight * scale * newZoom > Gdx.graphics.height
) {
return false
}
if (
viewport.worldWidth * scale * newZoom < Gdx.graphics.width * minWorldRatio &&
viewport.worldHeight * scale * newZoom < Gdx.graphics.height * minWorldRatio
) {
return false
}
return true
}
override fun zoom(initialDistance: Float, distance: Float): Boolean {
val newZoom = initialZoom * initialDistance / distance
val scale = viewport.scale()
return doZoomIfAllowed(newZoom, scale)
}
fun doZoomIfAllowed(newZoom: Float, scale: Float = viewport.scale()): Boolean {
if (checkZoom(newZoom = newZoom, scale = scale)) {
camera.zoom = newZoom
fitBounds(scale = scale)
return true
}
return false
}
override fun touchDown(x: Float, y: Float, pointer: Int, button: Int): Boolean {
initialZoom = camera.zoom
return super.touchDown(x, y, pointer, button)
}
}
Stage? What's alternative? I am also posting class that is handling pan/zoom. - K.H.