Skip to content
This repository has been archived by the owner on Aug 12, 2022. It is now read-only.

Commit

Permalink
Make toss scrolling time-based not frame based
Browse files Browse the repository at this point in the history
Closes #233
  • Loading branch information
nbilyk committed Dec 16, 2019
1 parent a21eb6c commit 5a17211
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.acornui.component.style.StyleTag
import com.acornui.component.text.text
import com.acornui.di.*
import com.acornui.function.as1
import com.acornui.function.as2
import com.acornui.input.interaction.MouseInteractionRo
import com.acornui.input.interaction.rollOut
import com.acornui.input.interaction.rollOver
Expand Down Expand Up @@ -193,7 +194,7 @@ class TooltipManagerImpl(override val injector: Injector) : TooltipManager, Scop
if (field != value) {
field = value
if (value) {
enterFrameHandle = tick(callback = ::frameHandler.as1)
enterFrameHandle = tick(callback = ::frameHandler.as2)
} else {
enterFrameHandle?.dispose()
enterFrameHandle = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ open class UiComponentImpl(
* @return Returns the [out] vector.
*/
override fun mousePosition(out: Vector2): Vector2 {
canvasToLocal(mouseState.mousePosition(out))
canvasToLocal(out.set(mouseState.canvasX, mouseState.canvasY))
return out
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ import com.acornui.component.createOrReuseAttachment
import com.acornui.config
import com.acornui.input.InteractionType
import com.acornui.input.interaction.*
import com.acornui.math.Vector2
import com.acornui.math.Vector2Ro
import com.acornui.math.*
import com.acornui.math.MathUtils.clamp
import com.acornui.signal.StoppableSignal
import com.acornui.signal.StoppableSignalImpl
import com.acornui.time.nowMs
import com.acornui.time.tick
import kotlin.time.Duration
import kotlin.time.seconds

/**
* A toss scroller lets you grab a target component, and update [ScrollModelRo] objects by dragging it.
Expand All @@ -39,10 +41,11 @@ class TossScroller(
val target: UiComponent,

/**
* Dampening affects how quickly the toss velocity will slow to a stop.
* Make this number 0 < dampening < 1. Where 1 will go forever, and 0 will prevent any momentum.
*
*/
var dampening: Float = DEFAULT_DAMPENING,
var slowTime: Duration = DEFAULT_SLOW_TIME,

val slowEase: Interpolation = DEFAULT_SLOW_EASE,

private val dragAttachment: DragAttachment = target.dragAttachment(minTossDistance)
) : Disposable {
Expand Down Expand Up @@ -73,12 +76,22 @@ class TossScroller(
val tossEnd: StoppableSignal<DragInteractionRo>
get() = _tossEnd

private val _velocity = Vector2()
/**
* The velocity when the drag ended.
*/
private val velocityStart = Vector2()

/**
* The 0-1 range of the toss slow tween.
*/
private var slowAlpha = 0f

private val velocityCurrent = Vector2()

/**
* The current velocity of the toss.
* The current velocity of the toss in points per second.
*/
val velocity: Vector2Ro = _velocity
val velocity: Vector2Ro = velocityCurrent

/**
* Returns true if the user is currently interacting or if there is still momentum.
Expand Down Expand Up @@ -114,53 +127,36 @@ class TossScroller(

private val diff = Vector2()

private val dragStartHandler = { event: DragInteractionRo ->
stop()
startPosition.set(event.startPosition)
position.set(event.position)

clickPreventer = 5
clearHistory()
pushHistory()
startEnterFrame()
dispatchDragEvent(TOSS_START, _tossStart)
Unit
}

private fun pushHistory() {
historyPoints.add(Vector2.obtain().set(position.x, position.y))
historyPoints.add(position.copy())
historyTimes.add(nowMs())
if (historyPoints.size > MAX_HISTORY) {
Vector2.free(historyPoints.poll())
historyPoints.poll()
historyTimes.poll()
}
}

private fun startEnterFrame() {
if (_timer == null) {
_timer = tick {
_timer = tick { dT ->
if (dragAttachment.isDragging) {
// History is also added in an enter frame instead of the drag handler so that if the user stops dragging, the history reflects that.
pushHistory()
} else {
slowAlpha += dT / slowTime.inSeconds.toFloat()
val slowAlphaEased = slowEase.apply(clamp(slowAlpha, 0f, 1f))
if (clickPreventer > 0) clickPreventer--
_velocity.scl(dampening)
position.add(_velocity)
velocityCurrent.set(velocityStart).lerp(0f, 0f, slowAlphaEased)
position.add(velocityCurrent.x * dT, velocityCurrent.y * dT)
dispatchDragEvent(TOSS, _toss)
if (_velocity.isZero(0.1f)) {
if (slowAlpha >= 1f) {
stop()
}
}
}
}
}

private val dragHandler = { event: DragInteractionRo ->
position.set(event.position)
dispatchDragEvent(TOSS, _toss)
Unit
}

private fun dispatchDragEvent(type: InteractionType<DragInteraction>, signal: StoppableSignalImpl<DragInteraction>) {
event.clear()
event.target = target
Expand All @@ -171,38 +167,55 @@ class TossScroller(
signal.dispatch(event)
}

private val dragEndHandler = { event: DragInteractionRo ->
private fun clearHistory() {
historyPoints.clear()
historyTimes.clear()
}

init {
dragAttachment.dragStart.add(::dragStartHandler)
dragAttachment.drag.add(::dragHandler)
dragAttachment.dragEnd.add(::dragEndHandler)
target.click(isCapture = true).add(::clickHandler)
}

private fun dragStartHandler(event: DragInteractionRo) {
stop()
startPosition.set(event.startPosition)
position.set(event.position)

clickPreventer = 5
clearHistory()
pushHistory()
startEnterFrame()
dispatchDragEvent(TOSS_START, _tossStart)
}

private fun dragHandler(event: DragInteractionRo) {
position.set(event.position)
dispatchDragEvent(TOSS, _toss)
}

private fun dragEndHandler(event: DragInteractionRo) {
position.set(event.position)
pushHistory()
// Calculate the velocity.
if (historyPoints.size >= 2) {
diff.set(historyPoints.last()).sub(historyPoints.first())
val time = (historyTimes.last() - historyTimes.first()) * 0.001f
_velocity.set(diff.x / time, diff.y / time).scl(tickTime)
velocityCurrent.set(diff.x / time, diff.y / time)
velocityStart.set(velocityCurrent)
slowAlpha = 0f
}
clearHistory()
}

private fun clearHistory() {
for (i in 0..historyPoints.lastIndex) {
Vector2.free(historyPoints[i])
}
historyPoints.clear()
historyTimes.clear()
}

private val clickHandler = { event: ClickInteractionRo ->
private fun clickHandler(event: ClickInteractionRo) {
if (clickPreventer > 0) {
event.propagation.stopImmediatePropagation()
}
}

init {
dragAttachment.dragStart.add(dragStartHandler)
dragAttachment.drag.add(dragHandler)
dragAttachment.dragEnd.add(dragEndHandler)
target.click(isCapture = true).add(clickHandler)
}

/**
* Enables or disables this toss scroller. If this is set to false in the middle of a toss, the toss will
* still finish as if the drag were immediately released. Use [stop] to halt the current velocity.
Expand All @@ -217,7 +230,7 @@ class TossScroller(
if (_timer != null) {
dispatchDragEvent(TOSS_END, _tossEnd)
clickPreventer = 0
_velocity.clear()
velocityCurrent.clear()
_timer?.dispose()
_timer = null
event.clear()
Expand All @@ -229,10 +242,10 @@ class TossScroller(
_tossStart.dispose()
_toss.dispose()
_tossEnd.dispose()
dragAttachment.dragStart.remove(dragStartHandler)
dragAttachment.drag.remove(dragHandler)
dragAttachment.dragEnd.remove(dragEndHandler)
target.click(isCapture = true).remove(clickHandler)
dragAttachment.dragStart.remove(::dragStartHandler)
dragAttachment.drag.remove(::dragHandler)
dragAttachment.dragEnd.remove(::dragEndHandler)
target.click(isCapture = true).remove(::clickHandler)
}

companion object {
Expand All @@ -241,7 +254,9 @@ class TossScroller(
val TOSS = InteractionType<DragInteraction>("toss")
val TOSS_END = InteractionType<DragInteraction>("tossEnd")

const val DEFAULT_DAMPENING: Float = 0.9f
val DEFAULT_SLOW_TIME = 0.8.seconds
val DEFAULT_SLOW_EASE = Easing.pow3Out

private const val MAX_HISTORY = 10

var minTossDistance: Float = 7f
Expand Down Expand Up @@ -292,8 +307,8 @@ open class TossScrollModelBinding(
}
}

fun UiComponent.enableTossScrolling(dampening: Float = TossScroller.DEFAULT_DAMPENING): TossScroller {
return createOrReuseAttachment(TossScroller) { TossScroller(this, dampening) }
fun UiComponent.enableTossScrolling(slowTime: Duration = TossScroller.DEFAULT_SLOW_TIME, slowEase: Interpolation = TossScroller.DEFAULT_SLOW_EASE): TossScroller {
return createOrReuseAttachment(TossScroller) { TossScroller(this, slowTime, slowEase) }
}

fun UiComponent.disableTossScrolling() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import com.acornui.component.style.Styleable
import com.acornui.di.Owned
import com.acornui.di.inject
import com.acornui.focus.Focusable
import com.acornui.function.as1
import com.acornui.function.as2
import com.acornui.input.*
import com.acornui.input.interaction.KeyInteractionRo
import com.acornui.math.*
Expand Down Expand Up @@ -402,7 +402,7 @@ class TextAreaImpl(owner: Owned) : ContainerImpl(owner), TextArea {
private fun startScrollWatch(event: Any) {
mousePosition(startMouse)
_frameWatch?.dispose()
_frameWatch = tick(-1, callback = ::scrollWatcher.as1)
_frameWatch = tick(-1, callback = ::scrollWatcher.as2)
stage.mouseUp().add(::endScrollWatch)
stage.touchEnd().add(::endScrollWatch)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,6 @@ interface MouseState : Disposable {
*/
val touches: List<TouchRo>

/**
* Sets the [out] vector to the current canvas position.
* @return Returns the [out] vector.
*/
fun mousePosition(out: Vector2): Vector2 {
out.set(canvasX, canvasY)
return out
}

fun mouseIsDown(button: WhichButton): Boolean

companion object : DKey<MouseState>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import com.acornui.component.canvasToLocal
import com.acornui.component.createOrReuseAttachment
import com.acornui.component.stage
import com.acornui.di.inject
import com.acornui.function.as1
import com.acornui.function.as2
import com.acornui.input.*
import com.acornui.math.Vector2
import com.acornui.math.Vector2Ro
Expand Down Expand Up @@ -102,16 +102,11 @@ class DragAttachment(
stop()
}

private val clickBlocker = { event: ClickInteractionRo ->
event.handled = true
event.preventDefault()
}

private fun setIsWatchingMouse(value: Boolean) {
if (watchingMouse == value) return
watchingMouse = value
if (value) {
_enterFrame = tick(-1, callback = ::enterFrameHandler.as1)
_enterFrame = tick(-1, callback = ::enterFrameHandler.as2)
stage.mouseMove().add(::stageMouseMoveHandler)
stage.mouseUp().add(::stageMouseUpHandler)
} else {
Expand Down Expand Up @@ -149,6 +144,7 @@ class DragAttachment(

private fun stageMouseUpHandler(event: MouseInteractionRo) {
event.handled = true
position.set(event.canvasX, event.canvasY)
setIsWatchingMouse(false)
setIsDragging(false)
}
Expand Down Expand Up @@ -208,7 +204,7 @@ class DragAttachment(
if (watchingTouch == value) return
watchingTouch = value
if (value) {
_enterFrame = tick(-1, callback = ::enterFrameHandler.as1)
_enterFrame = tick(-1, callback = ::enterFrameHandler.as2)
stage.touchMove().add(::stageTouchMoveHandler)
stage.touchEnd().add(::stageTouchEndHandler)
} else {
Expand All @@ -228,6 +224,7 @@ class DragAttachment(
if (allowTouchEnd(event)) {
touchId = -1
event.handled = true
position.set(mouse.canvasX, mouse.canvasY)
setIsWatchingTouch(false)
setIsDragging(false)
}
Expand All @@ -238,7 +235,7 @@ class DragAttachment(
//--------------------------------------------------------------

private fun enterFrameHandler() {
mouse.mousePosition(position)
position.set(mouse.canvasX, mouse.canvasY)
if (_isDragging) {
dispatchDragEvent(DragInteraction.DRAG, _drag)
} else {
Expand All @@ -256,7 +253,7 @@ class DragAttachment(
if (dragEvent.defaultPrevented()) {
_isDragging = false
} else {
stage.click(isCapture = true).add(clickBlocker, true) // Set the next click to be marked as handled.
stage.click(isCapture = true).add(::clickBlocker, true) // Set the next click to be marked as handled.
dispatchDragEvent(DragInteraction.DRAG, _drag)
}
} else {
Expand All @@ -265,10 +262,15 @@ class DragAttachment(
}
dispatchDragEvent(DragInteraction.DRAG_END, _dragEnd)

callLater { stage.click(isCapture = true).remove(clickBlocker) }
callLater { stage.click(isCapture = true).remove(::clickBlocker) }
}
}

private fun clickBlocker(event: ClickInteractionRo) {
event.handled = true
event.preventDefault()
}

private fun dispatchDragEvent(type: InteractionType<DragInteractionRo>, signal: Signal1<DragInteractionRo>) {
dragEvent.clear()
dragEvent.target = target
Expand Down
Loading

0 comments on commit 5a17211

Please sign in to comment.