ui/gesture,ui/internal/fling: extract fling logic into its own package

We're going to re-use fling extrapolation and animation for Wayland touchpads.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2019-09-13 18:04:10 +02:00
parent a68d97f947
commit e6f0e0582d
4 changed files with 127 additions and 98 deletions
+8 -85
View File
@@ -11,12 +11,11 @@ package gesture
import (
"math"
"runtime"
"time"
"gioui.org/ui"
"gioui.org/ui/f32"
"gioui.org/ui/input"
"gioui.org/ui/internal/fling"
"gioui.org/ui/pointer"
)
@@ -46,8 +45,8 @@ type ClickType uint8
type Scroll struct {
dragging bool
axis Axis
estimator estimator
flinger flinger
estimator fling.Extrapolation
flinger fling.Animation
pid pointer.ID
grab bool
last int
@@ -57,15 +56,6 @@ type Scroll struct {
type ScrollState uint8
type flinger struct {
// Current offset in pixels.
x float32
// Initial time.
t0 time.Time
// Initial velocity in pixels pr second.
v0 float32
}
type Axis uint8
const (
@@ -102,16 +92,7 @@ const (
StateFlinging
)
var (
touchSlop = ui.Dp(3)
// Pixels/second.
minFlingVelocity = ui.Dp(50)
maxFlingVelocity = ui.Dp(8000)
)
const (
thresholdVelocity = 1
)
var touchSlop = ui.Dp(3)
// Add the handler to the operation list to receive click events.
func (c *Click) Add(ops *ui.Ops) {
@@ -168,7 +149,7 @@ func (s *Scroll) Add(ops *ui.Ops) {
// Stop any remaining fling movement.
func (s *Scroll) Stop() {
s.flinger = flinger{}
s.flinger = fling.Animation{}
}
// Scroll detects the scrolling distance from the available events and
@@ -190,7 +171,7 @@ func (s *Scroll) Scroll(cfg ui.Config, q input.Queue, axis Axis) int {
break
}
s.Stop()
s.estimator = estimator{}
s.estimator = fling.Extrapolation{}
v := s.val(e.Position)
s.last = int(math.Round(float64(v)))
s.estimator.Sample(e.Time, v)
@@ -201,16 +182,8 @@ func (s *Scroll) Scroll(cfg ui.Config, q input.Queue, axis Axis) int {
break
}
fling := s.estimator.Estimate()
if slop, d := float32(cfg.Px(touchSlop)), fling.Distance; d >= slop || -slop >= d {
if min, v := float32(cfg.Px(minFlingVelocity)), fling.Velocity; v >= min || -min >= v {
max := float32(cfg.Px(maxFlingVelocity))
if v > max {
v = max
} else if v < -max {
v = -max
}
s.flinger.Init(cfg.Now(), v)
}
if slop, d := float32(cfg.Px(touchSlop)), fling.Distance; d < -slop || d > slop {
s.flinger.Start(cfg, fling.Velocity)
}
fallthrough
case pointer.Cancel:
@@ -270,56 +243,6 @@ func (s *Scroll) State() ScrollState {
}
}
func (f *flinger) Init(now time.Time, v0 float32) {
f.t0 = now
f.v0 = v0
f.x = 0
}
func (f *flinger) Active() bool {
return f.v0 != 0
}
// Tick computes and returns a fling distance since
// the last time Tick was called.
func (f *flinger) Tick(now time.Time) int {
if !f.Active() {
return 0
}
var k float32
if runtime.GOOS == "darwin" {
k = -2 // iOS
} else {
k = -4.2 // Android and default
}
t := now.Sub(f.t0)
// The acceleration x''(t) of a point mass with a drag
// force, f, proportional with velocity, x'(t), is
// governed by the equation
//
// x''(t) = kx'(t)
//
// Given the starting position x(0) = 0, the starting
// velocity x'(0) = v0, the position is then
// given by
//
// x(t) = v0*e^(k*t)/k - v0/k
//
ekt := float32(math.Exp(float64(k) * t.Seconds()))
x := f.v0*ekt/k - f.v0/k
dist := x - f.x
idist := int(math.Round(float64(dist)))
f.x += float32(idist)
// Solving for the velocity x'(t) gives us
//
// x'(t) = v0*e^(k*t)
v := f.v0 * ekt
if v < thresholdVelocity && v > -thresholdVelocity {
f.v0 = 0
}
return idist
}
func (a Axis) String() string {
switch a {
case Horizontal:
+98
View File
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: Unlicense OR MIT
package fling
import (
"math"
"runtime"
"time"
"gioui.org/ui"
)
type Animation struct {
// Current offset in pixels.
x float32
// Initial time.
t0 time.Time
// Initial velocity in pixels pr second.
v0 float32
}
var (
// Pixels/second.
minFlingVelocity = ui.Dp(50)
maxFlingVelocity = ui.Dp(8000)
)
const (
thresholdVelocity = 1
)
// Start a fling given a starting velocity. Returns whether a
// fling was started.
func (f *Animation) Start(c ui.Config, velocity float32) bool {
min := float32(c.Px(minFlingVelocity))
v := velocity
if -min <= v && v <= min {
return false
}
max := float32(c.Px(maxFlingVelocity))
if v > max {
v = max
} else if v < -max {
v = -max
}
f.init(c.Now(), v)
return true
}
func (f *Animation) init(now time.Time, v0 float32) {
f.t0 = now
f.v0 = v0
f.x = 0
}
func (f *Animation) Active() bool {
return f.v0 != 0
}
// Tick computes and returns a fling distance since
// the last time Tick was called.
func (f *Animation) Tick(now time.Time) int {
if !f.Active() {
return 0
}
var k float32
if runtime.GOOS == "darwin" {
k = -2 // iOS
} else {
k = -4.2 // Android and default
}
t := now.Sub(f.t0)
// The acceleration x''(t) of a point mass with a drag
// force, f, proportional with velocity, x'(t), is
// governed by the equation
//
// x''(t) = kx'(t)
//
// Given the starting position x(0) = 0, the starting
// velocity x'(0) = v0, the position is then
// given by
//
// x(t) = v0*e^(k*t)/k - v0/k
//
ekt := float32(math.Exp(float64(k) * t.Seconds()))
x := f.v0*ekt/k - f.v0/k
dist := x - f.x
idist := int(math.Round(float64(dist)))
f.x += float32(idist)
// Solving for the velocity x'(t) gives us
//
// x'(t) = v0*e^(k*t)
v := f.v0 * ekt
if -thresholdVelocity < v && v < thresholdVelocity {
f.v0 = 0
}
return idist
}
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
package gesture
package fling
import (
"math"
@@ -9,15 +9,16 @@ import (
"time"
)
// Estimator computes a 1-dimensional velocity estimate
// Extrapolation computes a 1-dimensional velocity estimate
// for a set of timestamped points using the least squares
// fit of a 2nd order polynomial. The same method is used
// by Android.
type estimator struct {
type Extrapolation struct {
// Index into points.
idx int
// Circular buffer of samples.
samples []sample
samples []sample
lastValue float32
// Pre-allocated cache for samples.
cache [historySize]sample
@@ -36,7 +37,7 @@ type matrix struct {
data []float32
}
type estimate struct {
type Estimate struct {
Velocity float32
Distance float32
}
@@ -50,8 +51,15 @@ const (
maxSampleGap = 40 * time.Millisecond
)
// Sample adds a sample to the estimation.
func (e *estimator) Sample(t time.Duration, val float32) {
// SampleDelta adds a relative sample to the estimation.
func (e *Extrapolation) SampleDelta(t time.Duration, delta float32) {
val := delta + e.lastValue
e.Sample(t, val)
}
// Sample adds an absolute sample to the estimation.
func (e *Extrapolation) Sample(t time.Duration, val float32) {
e.lastValue = val
if e.samples == nil {
e.samples = e.cache[:0]
}
@@ -73,9 +81,9 @@ func (e *estimator) Sample(t time.Duration, val float32) {
// Velocity returns an estimate of the implied velocity and
// distance for the points sampled, or zero if the estimation method
// failed.
func (e *estimator) Estimate() estimate {
func (e *Extrapolation) Estimate() Estimate {
if len(e.samples) == 0 {
return estimate{}
return Estimate{}
}
values := e.values[:0]
times := e.times[:0]
@@ -97,16 +105,16 @@ func (e *estimator) Estimate() estimate {
}
coef, ok := polyFit(times, values)
if !ok {
return estimate{}
return Estimate{}
}
dist := values[len(values)-1] - values[0]
return estimate{
return Estimate{
Velocity: coef[1],
Distance: dist,
}
}
func (e *estimator) get(i int) sample {
func (e *Extrapolation) get(i int) sample {
idx := (e.idx + i - 1 + len(e.samples)) % len(e.samples)
return e.samples[idx]
}
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
package gesture
package fling
import "testing"