From 25c65b7da59f5c9119ec9513f8e073aac379a509 Mon Sep 17 00:00:00 2001 From: vegeta1k95 <32207817+vegeta1k95@users.noreply.github.com> Date: Sat, 4 Feb 2023 21:51:42 +0100 Subject: [PATCH] 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 --- core/src/com/unciv/MainMenuScreen.kt | 2 +- .../src/com/unciv/ui/cityscreen/CityScreen.kt | 2 +- .../com/unciv/ui/mapeditor/EditorMapHolder.kt | 3 +- core/src/com/unciv/ui/options/AdvancedTab.kt | 2 +- core/src/com/unciv/ui/tilegroups/TileGroup.kt | 1 - .../ui/{map => tilegroups}/TileGroupMap.kt | 19 +- .../com/unciv/ui/utils/ZoomGestureListener.kt | 10 +- .../com/unciv/ui/utils/ZoomableScrollPane.kt | 163 +++++++++++++----- .../unciv/ui/worldscreen/WorldMapHolder.kt | 40 ++--- 9 files changed, 159 insertions(+), 83 deletions(-) rename core/src/com/unciv/ui/{map => tilegroups}/TileGroupMap.kt (95%) diff --git a/core/src/com/unciv/MainMenuScreen.kt b/core/src/com/unciv/MainMenuScreen.kt index c1ae9dbe7d..68d89c3e1e 100644 --- a/core/src/com/unciv/MainMenuScreen.kt +++ b/core/src/com/unciv/MainMenuScreen.kt @@ -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 diff --git a/core/src/com/unciv/ui/cityscreen/CityScreen.kt b/core/src/com/unciv/ui/cityscreen/CityScreen.kt index d51443374f..4c6c9e2a6a 100644 --- a/core/src/com/unciv/ui/cityscreen/CityScreen.kt +++ b/core/src/com/unciv/ui/cityscreen/CityScreen.kt @@ -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 diff --git a/core/src/com/unciv/ui/mapeditor/EditorMapHolder.kt b/core/src/com/unciv/ui/mapeditor/EditorMapHolder.kt index d67321bfac..976c922f34 100644 --- a/core/src/com/unciv/ui/mapeditor/EditorMapHolder.kt +++ b/core/src/com/unciv/ui/mapeditor/EditorMapHolder.kt @@ -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 diff --git a/core/src/com/unciv/ui/options/AdvancedTab.kt b/core/src/com/unciv/ui/options/AdvancedTab.kt index 57a624dc5d..8f8514da39 100644 --- a/core/src/com/unciv/ui/options/AdvancedTab.kt +++ b/core/src/com/unciv/ui/options/AdvancedTab.kt @@ -241,7 +241,7 @@ private fun CoroutineScope.generateScreenshots(configs:ArrayList( * 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( // 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( } 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) { diff --git a/core/src/com/unciv/ui/utils/ZoomGestureListener.kt b/core/src/com/unciv/ui/utils/ZoomGestureListener.kt index 4e729f455f..0b37e24b6c 100644 --- a/core/src/com/unciv/ui/utils/ZoomGestureListener.kt +++ b/core/src/com/unciv/ui/utils/ZoomGestureListener.kt @@ -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() {} } diff --git a/core/src/com/unciv/ui/utils/ZoomableScrollPane.kt b/core/src/com/unciv/ui/utils/ZoomableScrollPane.kt index 4258f4901f..a47335f971 100644 --- a/core/src/com/unciv/ui/utils/ZoomableScrollPane.kt +++ b/core/src/com/unciv/ui/utils/ZoomableScrollPane.kt @@ -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() { - zoom(scaleX / 0.8f) + fun zoomIn(immediate: Boolean = false) { + if (immediate) + zoom(scaleX / 0.8f) + else + zoomListener.zoomIn(0.8f) } - fun zoomOut() { - zoom(scaleX * 0.8f) + 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 + } + + 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 isZooming = false + 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 diff --git a/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt b/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt index 5e4d6690c1..e7960a92dc 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt @@ -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