Smooth zoom when scrolling + cleanup code for listeners (#8569)

* Smooth zoom when scrolling + cleanups of listeners

* Remove debug leftovers

* Remove debug leftovers

---------

Co-authored-by: tunerzinc@gmail.com <vfylfhby>
This commit is contained in:
vegeta1k95 2023-02-04 21:51:42 +01:00 committed by GitHub
parent 5fdbb7f188
commit 25c65b7da5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 159 additions and 83 deletions

View File

@ -21,7 +21,7 @@ import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
import com.unciv.ui.civilopedia.CivilopediaScreen import com.unciv.ui.civilopedia.CivilopediaScreen
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.map.TileGroupMap import com.unciv.ui.tilegroups.TileGroupMap
import com.unciv.ui.mapeditor.EditorMapHolder import com.unciv.ui.mapeditor.EditorMapHolder
import com.unciv.ui.mapeditor.MapEditorScreen import com.unciv.ui.mapeditor.MapEditorScreen
import com.unciv.ui.multiplayer.MultiplayerScreen import com.unciv.ui.multiplayer.MultiplayerScreen

View File

@ -19,7 +19,7 @@ import com.unciv.models.stats.Stat
import com.unciv.ui.audio.CityAmbiencePlayer import com.unciv.ui.audio.CityAmbiencePlayer
import com.unciv.ui.audio.SoundPlayer import com.unciv.ui.audio.SoundPlayer
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.map.TileGroupMap import com.unciv.ui.tilegroups.TileGroupMap
import com.unciv.ui.popup.ToastPopup import com.unciv.ui.popup.ToastPopup
import com.unciv.ui.tilegroups.CityTileGroup import com.unciv.ui.tilegroups.CityTileGroup
import com.unciv.ui.tilegroups.TileSetStrings import com.unciv.ui.tilegroups.TileSetStrings

View File

@ -8,11 +8,10 @@ import com.badlogic.gdx.scenes.scene2d.InputListener
import com.badlogic.gdx.scenes.scene2d.Stage import com.badlogic.gdx.scenes.scene2d.Stage
import com.badlogic.gdx.scenes.scene2d.Touchable import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.actions.Actions import com.badlogic.gdx.scenes.scene2d.actions.Actions
import com.unciv.UncivGame
import com.unciv.logic.map.HexMath import com.unciv.logic.map.HexMath
import com.unciv.logic.map.tile.Tile import com.unciv.logic.map.tile.Tile
import com.unciv.logic.map.TileMap import com.unciv.logic.map.TileMap
import com.unciv.ui.map.TileGroupMap import com.unciv.ui.tilegroups.TileGroupMap
import com.unciv.ui.tilegroups.TileGroup import com.unciv.ui.tilegroups.TileGroup
import com.unciv.ui.tilegroups.TileSetStrings import com.unciv.ui.tilegroups.TileSetStrings
import com.unciv.ui.utils.BaseScreen import com.unciv.ui.utils.BaseScreen

View File

@ -241,7 +241,7 @@ private fun CoroutineScope.generateScreenshots(configs:ArrayList<ScreenshotConfi
newScreen.mapHolder.onTileClicked(newScreen.mapHolder.tileMap[-2, 3]) // Then click on Keshik newScreen.mapHolder.onTileClicked(newScreen.mapHolder.tileMap[-2, 3]) // Then click on Keshik
if (currentConfig.attackCity) if (currentConfig.attackCity)
newScreen.mapHolder.onTileClicked(newScreen.mapHolder.tileMap[-2, 2]) // Then click city again for attack table newScreen.mapHolder.onTileClicked(newScreen.mapHolder.tileMap[-2, 2]) // Then click city again for attack table
newScreen.mapHolder.zoomIn() newScreen.mapHolder.zoomIn(true)
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Thread.sleep(300) Thread.sleep(300)
launchOnGLThread { launchOnGLThread {

View File

@ -5,7 +5,6 @@ import com.badlogic.gdx.scenes.scene2d.Group
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.tile.Tile import com.unciv.logic.map.tile.Tile
import com.unciv.ui.map.TileGroupMap
import com.unciv.ui.tilegroups.layers.TileLayerBorders import com.unciv.ui.tilegroups.layers.TileLayerBorders
import com.unciv.ui.tilegroups.layers.TileLayerCityButton import com.unciv.ui.tilegroups.layers.TileLayerCityButton
import com.unciv.ui.tilegroups.layers.TileLayerFeatures import com.unciv.ui.tilegroups.layers.TileLayerFeatures

View File

@ -1,13 +1,12 @@
package com.unciv.ui.map package com.unciv.ui.tilegroups
import com.badlogic.gdx.graphics.g2d.Batch import com.badlogic.gdx.graphics.g2d.Batch
import com.badlogic.gdx.math.Rectangle
import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Group import com.badlogic.gdx.scenes.scene2d.Group
import com.unciv.logic.map.HexMath import com.unciv.logic.map.HexMath
import com.unciv.logic.map.TileMap import com.unciv.logic.map.TileMap
import com.unciv.ui.tilegroups.CityTileGroup
import com.unciv.ui.tilegroups.TileGroup
import com.unciv.ui.tilegroups.WorldTileGroup
import com.unciv.ui.tilegroups.layers.TileLayerBorders import com.unciv.ui.tilegroups.layers.TileLayerBorders
import com.unciv.ui.tilegroups.layers.TileLayerCityButton import com.unciv.ui.tilegroups.layers.TileLayerCityButton
import com.unciv.ui.tilegroups.layers.TileLayerFeatures import com.unciv.ui.tilegroups.layers.TileLayerFeatures
@ -52,6 +51,7 @@ class TileGroupMap<T: TileGroup>(
* and thus not perform any [com.badlogic.gdx.scenes.scene2d.Action]s. * and thus not perform any [com.badlogic.gdx.scenes.scene2d.Action]s.
* Most children here already do not do anything in their [act] methods. However, even iterating through all of them */ * Most children here already do not do anything in their [act] methods. However, even iterating through all of them */
var shouldAct = true var shouldAct = true
var shouldHit = true
private var topX = -Float.MAX_VALUE private var topX = -Float.MAX_VALUE
private var topY = -Float.MAX_VALUE private var topY = -Float.MAX_VALUE
@ -132,6 +132,8 @@ class TileGroupMap<T: TileGroup>(
// If map is not wrapped, Map's width doesn't need to be reduce by groupSize // If map is not wrapped, Map's width doesn't need to be reduce by groupSize
if (worldWrap) setSize(topX - bottomX - groupSize, topY - bottomY) if (worldWrap) setSize(topX - bottomX - groupSize, topY - bottomY)
else setSize(topX - bottomX, topY - bottomY) else setSize(topX - bottomX, topY - bottomY)
cullingArea = Rectangle(0f, 0f, width, height)
} }
/** /**
@ -146,9 +148,14 @@ class TileGroupMap<T: TileGroup>(
} }
override fun act(delta: Float) { override fun act(delta: Float) {
if(shouldAct) { if (shouldAct)
super.act(delta) super.act(delta)
} }
override fun hit(x: Float, y: Float, touchable: Boolean): Actor? {
if (shouldHit)
return super.hit(x, y, touchable)
return null
} }
override fun draw(batch: Batch?, parentAlpha: Float) { override fun draw(batch: Batch?, parentAlpha: Float) {

View File

@ -9,7 +9,6 @@ import com.badlogic.gdx.scenes.scene2d.InputEvent
open class ZoomGestureListener( open class ZoomGestureListener(
halfTapSquareSize: Float, tapCountInterval: Float, longPressDuration: Float, maxFlingDelay: Float halfTapSquareSize: Float, tapCountInterval: Float, longPressDuration: Float, maxFlingDelay: Float
) : EventListener { ) : EventListener {
val detector: GestureDetector val detector: GestureDetector
var event: InputEvent? = null var event: InputEvent? = null
@ -24,7 +23,7 @@ open class ZoomGestureListener(
object : GestureDetector.GestureAdapter() { object : GestureDetector.GestureAdapter() {
override fun zoom(initialDistance: Float, distance: Float): Boolean { override fun zoom(initialDistance: Float, distance: Float): Boolean {
this@ZoomGestureListener.zoom(event, initialDistance, distance) this@ZoomGestureListener.zoom(initialDistance, distance)
return true return true
} }
@ -71,10 +70,15 @@ open class ZoomGestureListener(
detector.touchDragged(event.stageX, event.stageY, event.pointer) detector.touchDragged(event.stageX, event.stageY, event.pointer)
return true return true
} }
InputEvent.Type.scrolled -> {
return scrolled(event.scrollAmountX, event.scrollAmountY)
}
else -> return false else -> return false
} }
} }
open fun zoom(event: InputEvent?, initialDistance: Float, distance: Float) {}
open fun scrolled(amountX: Float, amountY: Float): Boolean { return false }
open fun zoom(initialDistance: Float, distance: Float) {}
open fun pinch() {} open fun pinch() {}
open fun pinchStop() {} open fun pinchStop() {}
} }

View File

@ -1,14 +1,15 @@
package com.unciv.ui.utils package com.unciv.ui.utils
import com.badlogic.gdx.math.Interpolation import com.badlogic.gdx.math.Interpolation
import com.badlogic.gdx.math.MathUtils
import com.badlogic.gdx.math.Rectangle import com.badlogic.gdx.math.Rectangle
import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.Action import com.badlogic.gdx.scenes.scene2d.Action
import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Group import com.badlogic.gdx.scenes.scene2d.Group
import com.badlogic.gdx.scenes.scene2d.InputEvent import com.badlogic.gdx.scenes.scene2d.InputEvent
import com.badlogic.gdx.scenes.scene2d.InputListener
import com.badlogic.gdx.scenes.scene2d.actions.FloatAction import com.badlogic.gdx.scenes.scene2d.actions.FloatAction
import com.badlogic.gdx.scenes.scene2d.actions.TemporalAction
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
import com.badlogic.gdx.scenes.scene2d.utils.ActorGestureListener import com.badlogic.gdx.scenes.scene2d.utils.ActorGestureListener
import com.badlogic.gdx.scenes.scene2d.utils.Cullable import com.badlogic.gdx.scenes.scene2d.utils.Cullable
@ -31,12 +32,13 @@ open class ZoomableScrollPane(
var onPanStartListener: (() -> Unit)? = null var onPanStartListener: (() -> Unit)? = null
var onZoomStopListener: (() -> Unit)? = null var onZoomStopListener: (() -> Unit)? = null
var onZoomStartListener: (() -> Unit)? = null var onZoomStartListener: (() -> Unit)? = null
private val zoomListener = ZoomListener()
private val horizontalPadding get() = width / 2 private val horizontalPadding get() = width / 2
private val verticalPadding get() = height / 2 private val verticalPadding get() = height / 2
init { init {
addZoomListeners() this.addListener(zoomListener)
} }
fun reloadMaxZoom() { fun reloadMaxZoom() {
@ -49,6 +51,10 @@ open class ZoomableScrollPane(
zoom(1f) zoom(1f)
} }
// We don't want default scroll listener
// which defines that mouse scroll = vertical movement
override fun addScrollListener() {}
override fun getActor() : Actor? { override fun getActor() : Actor? {
val group: Group = super.getActor() as Group val group: Group = super.getActor() as Group
return if (group.hasChildren()) group.children[0] else null return if (group.hasChildren()) group.children[0] else null
@ -141,93 +147,160 @@ open class ZoomableScrollPane(
// by half (i.e. middle) of what our size changed. // by half (i.e. middle) of what our size changed.
// However, we also changed the padding, which is exactly equal to half of our size change, so we actually don't need to move our center at all. // However, we also changed the padding, which is exactly equal to half of our size change, so we actually don't need to move our center at all.
} }
fun zoomIn() { fun zoomIn(immediate: Boolean = false) {
zoom(scaleX / 0.8f) if (immediate)
zoom(scaleX / 0.8f)
else
zoomListener.zoomIn(0.8f)
} }
fun zoomOut() { fun zoomOut(immediate: Boolean = false) {
zoom(scaleX * 0.8f) if (immediate)
zoom(scaleX * 0.8f)
else
zoomListener.zoomOut(0.8f)
} }
class ScrollZoomListener(private val zoomableScrollPane: ZoomableScrollPane):InputListener(){ fun isZooming(): Boolean {
override fun scrolled(event: InputEvent?, x: Float, y: Float, amountX: Float, amountY: Float): Boolean { return zoomListener.isZooming
if (amountX > 0 || amountY > 0) zoomableScrollPane.zoomOut() }
else zoomableScrollPane.zoomIn()
return false inner class ZoomListener : ZoomGestureListener() {
inner class ZoomAction : TemporalAction() {
var startingZoom: Float = 1f
var finishingZoom: Float = 1f
var currentZoom: Float = 1f
init {
duration = 0.3f
interpolation = Interpolation.fastSlow
}
override fun begin() {
isZooming = true
}
override fun end() {
zoomAction = null
isZooming = false
}
override fun update(percent: Float) {
currentZoom = MathUtils.lerp(startingZoom, finishingZoom, percent)
zoom(currentZoom)
}
} }
}
class ZoomListener(private val zoomableScrollPane: ZoomableScrollPane): ZoomGestureListener(){ private var zoomAction: ZoomAction? = null
private var isZooming = false
private var lastInitialDistance = 0f private var lastInitialDistance = 0f
var lastScale = 1f var lastScale = 1f
var isZooming = false
fun zoomOut(zoomMultiplier: Float = 0.82f) {
if (scaleX <= minZoom) {
if (zoomAction != null)
zoomAction!!.finish()
return
}
if (zoomAction != null) {
zoomAction!!.startingZoom = zoomAction!!.currentZoom
zoomAction!!.finishingZoom *= zoomMultiplier
zoomAction!!.restart()
} else {
zoomAction = ZoomAction()
zoomAction!!.startingZoom = scaleX
zoomAction!!.finishingZoom = scaleX * zoomMultiplier
addAction(zoomAction)
}
}
fun zoomIn(zoomMultiplier: Float = 0.82f) {
if (scaleX >= maxZoom) {
if (zoomAction != null)
zoomAction!!.finish()
return
}
if (zoomAction != null) {
zoomAction!!.startingZoom = zoomAction!!.currentZoom
zoomAction!!.finishingZoom /= zoomMultiplier
zoomAction!!.restart()
} else {
zoomAction = ZoomAction()
zoomAction!!.startingZoom = scaleX
zoomAction!!.finishingZoom = scaleX / zoomMultiplier
addAction(zoomAction)
}
}
override fun pinch() { override fun pinch() {
if (!isZooming) { if (!isZooming) {
isZooming = true isZooming = true
zoomableScrollPane.onZoomStartListener?.invoke() onZoomStartListener?.invoke()
} }
} }
override fun pinchStop() { override fun pinchStop() {
isZooming = false isZooming = false
zoomableScrollPane.onZoomStopListener?.invoke() onZoomStopListener?.invoke()
} }
override fun zoom(event: InputEvent?, initialDistance: Float, distance: Float) { override fun zoom(initialDistance: Float, distance: Float) {
if (lastInitialDistance != initialDistance) { if (lastInitialDistance != initialDistance) {
lastInitialDistance = initialDistance lastInitialDistance = initialDistance
lastScale = zoomableScrollPane.scaleX lastScale = scaleX
} }
val scale: Float = sqrt((distance / initialDistance).toDouble()).toFloat() * lastScale val scale: Float = sqrt((distance / initialDistance).toDouble()).toFloat() * lastScale
zoomableScrollPane.zoom(scale) zoom(scale)
} }
override fun scrolled(amountX: Float, amountY: Float): Boolean {
if (amountX > 0 || amountY > 0)
zoomOut()
else
zoomIn()
return true
}
} }
private fun addZoomListeners() { inner class FlickScrollListener : ActorGestureListener() {
// At first, Remove the existing inputListener private var isPanning = false
// which defines that mouse scroll = vertical movement
val zoomListener = listeners.last { it is InputListener && it !in captureListeners }
removeListener(zoomListener)
addListener(ScrollZoomListener(this))
addListener(ZoomListener(this))
}
class FlickScrollListener(private val zoomableScrollPane: ZoomableScrollPane): ActorGestureListener(){
private var wasPanning = false
override fun pan(event: InputEvent, x: Float, y: Float, deltaX: Float, deltaY: Float) { override fun pan(event: InputEvent, x: Float, y: Float, deltaX: Float, deltaY: Float) {
if (!wasPanning) { if (!isPanning) {
wasPanning = true isPanning = true
zoomableScrollPane.onPanStartListener?.invoke() onPanStartListener?.invoke()
} }
zoomableScrollPane.setScrollbarsVisible(true) setScrollbarsVisible(true)
zoomableScrollPane.scrollX -= deltaX scrollX -= deltaX
zoomableScrollPane.scrollY += deltaY scrollY += deltaY
//this is the new feature to fake an infinite scroll //this is the new feature to fake an infinite scroll
when { when {
zoomableScrollPane.continuousScrollingX && zoomableScrollPane.scrollPercentX >= 1 && deltaX < 0 -> { continuousScrollingX && scrollPercentX >= 1 && deltaX < 0 -> {
zoomableScrollPane.scrollPercentX = 0f scrollPercentX = 0f
} }
zoomableScrollPane.continuousScrollingX && zoomableScrollPane.scrollPercentX <= 0 && deltaX > 0-> { continuousScrollingX && scrollPercentX <= 0 && deltaX > 0-> {
zoomableScrollPane.scrollPercentX = 1f scrollPercentX = 1f
} }
} }
//clamp() call is missing here but it doesn't seem to make any big difference in this case //clamp() call is missing here but it doesn't seem to make any big difference in this case
if ((zoomableScrollPane.isScrollX && deltaX != 0f || zoomableScrollPane.isScrollY && deltaY != 0f)) zoomableScrollPane.cancelTouchFocus() if ((isScrollX && deltaX != 0f || isScrollY && deltaY != 0f))
cancelTouchFocus()
} }
override fun panStop(event: InputEvent?, x: Float, y: Float, pointer: Int, button: Int) { override fun panStop(event: InputEvent?, x: Float, y: Float, pointer: Int, button: Int) {
wasPanning = false isPanning = false
zoomableScrollPane.onPanStopListener?.invoke() onPanStopListener?.invoke()
} }
} }
override fun getFlickScrollListener(): ActorGestureListener { override fun getFlickScrollListener(): ActorGestureListener {
return FlickScrollListener(this) return FlickScrollListener()
} }
private var scrollingTo: Vector2? = null private var scrollingTo: Vector2? = null

View File

@ -34,7 +34,7 @@ import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.UncivStage import com.unciv.ui.UncivStage
import com.unciv.ui.audio.SoundPlayer import com.unciv.ui.audio.SoundPlayer
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.map.TileGroupMap import com.unciv.ui.tilegroups.TileGroupMap
import com.unciv.ui.tilegroups.TileGroup import com.unciv.ui.tilegroups.TileGroup
import com.unciv.ui.tilegroups.TileSetStrings import com.unciv.ui.tilegroups.TileSetStrings
import com.unciv.ui.tilegroups.WorldTileGroup import com.unciv.ui.tilegroups.WorldTileGroup
@ -72,37 +72,31 @@ class WorldMapHolder(
if (Gdx.app.type == Application.ApplicationType.Desktop) this.setFlingTime(0f) if (Gdx.app.type == Application.ApplicationType.Desktop) this.setFlingTime(0f)
continuousScrollingX = tileMap.mapParameters.worldWrap continuousScrollingX = tileMap.mapParameters.worldWrap
reloadMaxZoom() reloadMaxZoom()
disablePointerEventsAndActionsOnPan() setupZoomPanListeners()
} }
/** /**
* When scrolling or zooming the world map, there are two unnecessary (at least currently) things happening that take a decent amount of time: * When scrolling or zooming the world map, there are three unnecessary (at least currently) things happening that take a decent amount of time:
* *
* 1. Checking which [Actor]'s bounds the pointer (mouse/finger) entered+exited and sending appropriate events to these actors * 1. Checking which [Actor]'s bounds the pointer (mouse/finger) entered+exited and sending appropriate events to these actors
* 2. Running all [Actor.act] methods of all child [Actor]s * 2. Running all [Actor.act] methods of all child [Actor]s
* 3. Running all [Actor.hit] methode of all chikld [Actor]s * 3. Running all [Actor.hit] methods of all child [Actor]s
* *
* Disabling them while panning increases the frame rate while panning by approximately 100%. * Disabling them while panning/zooming increases the frame rate by approximately 100%.
*/ */
private fun disablePointerEventsAndActionsOnPan() { private fun setupZoomPanListeners() {
onPanStartListener = {
(stage as UncivStage).performPointerEnterExitEvents = false fun setActHit() {
tileGroupMap.shouldAct = false val isEnabled = !isZooming() && !isPanning
} (stage as UncivStage).performPointerEnterExitEvents = isEnabled
onPanStopListener = { tileGroupMap.shouldAct = isEnabled
(stage as UncivStage).performPointerEnterExitEvents = true tileGroupMap.shouldHit = isEnabled
tileGroupMap.shouldAct = true
}
onZoomStartListener = {
(stage as UncivStage).performPointerEnterExitEvents = false
tileGroupMap.shouldAct = false
tileGroupMap.touchable = Touchable.disabled
}
onZoomStopListener = {
(stage as UncivStage).performPointerEnterExitEvents = true
tileGroupMap.shouldAct = true
tileGroupMap.touchable = Touchable.enabled
} }
onPanStartListener = { setActHit() }
onPanStopListener = { setActHit() }
onZoomStartListener = { setActHit() }
onZoomStopListener = { setActHit() }
} }
// Interface for classes that contain the data required to draw a button // Interface for classes that contain the data required to draw a button