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.ui.civilopedia.CivilopediaScreen
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.MapEditorScreen
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.SoundPlayer
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.tilegroups.CityTileGroup
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.Touchable
import com.badlogic.gdx.scenes.scene2d.actions.Actions
import com.unciv.UncivGame
import com.unciv.logic.map.HexMath
import com.unciv.logic.map.tile.Tile
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.TileSetStrings
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
if (currentConfig.attackCity)
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) {
Thread.sleep(300)
launchOnGLThread {

View File

@ -5,7 +5,6 @@ import com.badlogic.gdx.scenes.scene2d.Group
import com.unciv.UncivGame
import com.unciv.logic.civilization.Civilization
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.TileLayerCityButton
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.math.Rectangle
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Group
import com.unciv.logic.map.HexMath
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.TileLayerCityButton
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.
* Most children here already do not do anything in their [act] methods. However, even iterating through all of them */
var shouldAct = true
var shouldHit = true
private var topX = -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 (worldWrap) setSize(topX - bottomX - groupSize, 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) {
if(shouldAct) {
if (shouldAct)
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) {

View File

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

View File

@ -1,14 +1,15 @@
package com.unciv.ui.utils
import com.badlogic.gdx.math.Interpolation
import com.badlogic.gdx.math.MathUtils
import com.badlogic.gdx.math.Rectangle
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.Action
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Group
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.TemporalAction
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
import com.badlogic.gdx.scenes.scene2d.utils.ActorGestureListener
import com.badlogic.gdx.scenes.scene2d.utils.Cullable
@ -31,12 +32,13 @@ open class ZoomableScrollPane(
var onPanStartListener: (() -> Unit)? = null
var onZoomStopListener: (() -> Unit)? = null
var onZoomStartListener: (() -> Unit)? = null
private val zoomListener = ZoomListener()
private val horizontalPadding get() = width / 2
private val verticalPadding get() = height / 2
init {
addZoomListeners()
this.addListener(zoomListener)
}
fun reloadMaxZoom() {
@ -49,6 +51,10 @@ open class ZoomableScrollPane(
zoom(1f)
}
// We don't want default scroll listener
// which defines that mouse scroll = vertical movement
override fun addScrollListener() {}
override fun getActor() : Actor? {
val group: Group = super.getActor() as Group
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.
// 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) {
if (immediate)
zoom(scaleX / 0.8f)
else
zoomListener.zoomIn(0.8f)
}
fun zoomOut() {
fun zoomOut(immediate: Boolean = false) {
if (immediate)
zoom(scaleX * 0.8f)
else
zoomListener.zoomOut(0.8f)
}
class ScrollZoomListener(private val zoomableScrollPane: ZoomableScrollPane):InputListener(){
override fun scrolled(event: InputEvent?, x: Float, y: Float, amountX: Float, amountY: Float): Boolean {
if (amountX > 0 || amountY > 0) zoomableScrollPane.zoomOut()
else zoomableScrollPane.zoomIn()
return false
}
fun isZooming(): Boolean {
return zoomListener.isZooming
}
class ZoomListener(private val zoomableScrollPane: ZoomableScrollPane): ZoomGestureListener(){
inner class ZoomListener : ZoomGestureListener() {
private var isZooming = false
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)
}
}
private var zoomAction: ZoomAction? = null
private var lastInitialDistance = 0f
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() {
if (!isZooming) {
isZooming = true
zoomableScrollPane.onZoomStartListener?.invoke()
onZoomStartListener?.invoke()
}
}
override fun pinchStop() {
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) {
lastInitialDistance = initialDistance
lastScale = zoomableScrollPane.scaleX
lastScale = scaleX
}
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() {
// At first, Remove the existing inputListener
// 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
inner class FlickScrollListener : ActorGestureListener() {
private var isPanning = false
override fun pan(event: InputEvent, x: Float, y: Float, deltaX: Float, deltaY: Float) {
if (!wasPanning) {
wasPanning = true
zoomableScrollPane.onPanStartListener?.invoke()
if (!isPanning) {
isPanning = true
onPanStartListener?.invoke()
}
zoomableScrollPane.setScrollbarsVisible(true)
zoomableScrollPane.scrollX -= deltaX
zoomableScrollPane.scrollY += deltaY
setScrollbarsVisible(true)
scrollX -= deltaX
scrollY += deltaY
//this is the new feature to fake an infinite scroll
when {
zoomableScrollPane.continuousScrollingX && zoomableScrollPane.scrollPercentX >= 1 && deltaX < 0 -> {
zoomableScrollPane.scrollPercentX = 0f
continuousScrollingX && scrollPercentX >= 1 && deltaX < 0 -> {
scrollPercentX = 0f
}
zoomableScrollPane.continuousScrollingX && zoomableScrollPane.scrollPercentX <= 0 && deltaX > 0-> {
zoomableScrollPane.scrollPercentX = 1f
continuousScrollingX && scrollPercentX <= 0 && deltaX > 0-> {
scrollPercentX = 1f
}
}
//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) {
wasPanning = false
zoomableScrollPane.onPanStopListener?.invoke()
isPanning = false
onPanStopListener?.invoke()
}
}
override fun getFlickScrollListener(): ActorGestureListener {
return FlickScrollListener(this)
return FlickScrollListener()
}
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.audio.SoundPlayer
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.TileSetStrings
import com.unciv.ui.tilegroups.WorldTileGroup
@ -72,37 +72,31 @@ class WorldMapHolder(
if (Gdx.app.type == Application.ApplicationType.Desktop) this.setFlingTime(0f)
continuousScrollingX = tileMap.mapParameters.worldWrap
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
* 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() {
onPanStartListener = {
(stage as UncivStage).performPointerEnterExitEvents = false
tileGroupMap.shouldAct = false
}
onPanStopListener = {
(stage as UncivStage).performPointerEnterExitEvents = true
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
private fun setupZoomPanListeners() {
fun setActHit() {
val isEnabled = !isZooming() && !isPanning
(stage as UncivStage).performPointerEnterExitEvents = isEnabled
tileGroupMap.shouldAct = isEnabled
tileGroupMap.shouldHit = isEnabled
}
onPanStartListener = { setActHit() }
onPanStopListener = { setActHit() }
onZoomStartListener = { setActHit() }
onZoomStopListener = { setActHit() }
}
// Interface for classes that contain the data required to draw a button