mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-22 10:54:19 -04:00
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:
parent
5fdbb7f188
commit
25c65b7da5
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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) {
|
@ -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() {}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user