1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
|
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package gesture provides gesture events such as long presses and drags.
// These are higher level than underlying mouse and touch events.
package gesture
import (
"fmt"
"time"
"golang.org/x/exp/shiny/screen"
"golang.org/x/mobile/event/mouse"
)
// TODO: handle touch events, not just mouse events.
//
// TODO: multi-button / multi-touch gestures such as pinch, rotate and tilt?
const (
// TODO: use a resolution-independent unit such as DIPs or Millimetres?
dragThreshold = 10 // Pixels.
doublePressThreshold = 300 * time.Millisecond
longPressThreshold = 500 * time.Millisecond
)
// Type describes the type of a touch event.
type Type uint8
const (
// TypeStart and TypeEnd are the start and end of a gesture. A gesture
// spans multiple events.
TypeStart Type = 0
TypeEnd Type = 1
// TypeIsXxx is when the gesture is recognized as a long press, double
// press or drag. For example, a mouse button press won't generate a
// TypeIsLongPress immediately, but if a threshold duration passes without
// the corresponding mouse button release, a TypeIsLongPress event is sent.
//
// Once a TypeIsXxx event is sent, the corresponding Event.Xxx bool field
// is set for this and subsequent events. For example, a TypeTap event by
// itself doesn't say whether or not it is a single tap or the first tap of
// a double tap. If the app needs to distinguish these two sorts of taps,
// it can wait until a TypeEnd or TypeIsDoublePress event is seen. If a
// TypeEnd is seen before TypeIsDoublePress, or equivalently, if the
// TypeEnd event's DoublePress field is false, the gesture is a single tap.
//
// These attributes aren't exclusive. A long press drag is perfectly valid.
//
// The uncommon "double press" instead of "double tap" terminology is
// because, in this package, taps are associated with button releases, not
// button presses. Note also that "double" really means "at least two".
TypeIsLongPress Type = 10
TypeIsDoublePress Type = 11
TypeIsDrag Type = 12
// TypeTap and TypeDrag are tap and drag events.
//
// For 'flinging' drags, to simulate inertia, look to the Velocity field of
// the TypeEnd event.
//
// TODO: implement velocity.
TypeTap Type = 20
TypeDrag Type = 21
// All internal types are >= typeInternal.
typeInternal Type = 100
// The typeXxxSchedule and typeXxxResolve constants are used for the two
// step process for sending an event after a timeout, in a separate
// goroutine. There are two steps so that the spawned goroutine is
// guaranteed to execute only after any other EventDeque.SendFirst calls
// are made for the one underlying mouse or touch event.
typeDoublePressSchedule Type = 100
typeDoublePressResolve Type = 101
typeLongPressSchedule Type = 110
typeLongPressResolve Type = 111
)
func (t Type) String() string {
switch t {
case TypeStart:
return "Start"
case TypeEnd:
return "End"
case TypeIsLongPress:
return "IsLongPress"
case TypeIsDoublePress:
return "IsDoublePress"
case TypeIsDrag:
return "IsDrag"
case TypeTap:
return "Tap"
case TypeDrag:
return "Drag"
default:
return fmt.Sprintf("gesture.Type(%d)", t)
}
}
// Point is a mouse or touch location, in pixels.
type Point struct {
X, Y float32
}
// Event is a gesture event.
type Event struct {
// Type is the gesture type.
Type Type
// Drag, LongPress and DoublePress are set when the gesture is recognized as
// a drag, etc.
//
// Note that these status fields can be lost during a gesture's events over
// time: LongPress can be set for the first press of a double press, but
// unset on the second press.
Drag bool
LongPress bool
DoublePress bool
// InitialPos is the initial position of the button press or touch that
// started this gesture.
InitialPos Point
// CurrentPos is the current position of the button or touch event.
CurrentPos Point
// TODO: a "Velocity Point" field. See
// - frameworks/native/libs/input/VelocityTracker.cpp in AOSP, or
// - https://chromium.googlesource.com/chromium/src/+/master/ui/events/gesture_detection/velocity_tracker.cc in Chromium,
// for some velocity tracking implementations.
// Time is the event's time.
Time time.Time
// TODO: include the mouse Button and key Modifiers?
}
type internalEvent struct {
eventFilter *EventFilter
typ Type
x, y float32
// pressCounter is the EventFilter.pressCounter value at the time this
// internal event was scheduled to be delivered after a timeout. It detects
// whether there have been other button presses and releases during that
// timeout, and hence whether this internalEvent is obsolete.
pressCounter uint32
}
// EventFilter generates gesture events from lower level mouse and touch
// events.
type EventFilter struct {
EventDeque screen.EventDeque
inProgress bool
drag bool
longPress bool
doublePress bool
// initialPos is the initial position of the button press or touch that
// started this gesture.
initialPos Point
// pressButton is the initial button that started this gesture. If
// button.None, no gesture is in progress.
pressButton mouse.Button
// pressCounter is incremented on every button press and release.
pressCounter uint32
}
func (f *EventFilter) sendFirst(t Type, x, y float32, now time.Time) {
if t >= typeInternal {
f.EventDeque.SendFirst(internalEvent{
eventFilter: f,
typ: t,
x: x,
y: y,
pressCounter: f.pressCounter,
})
return
}
f.EventDeque.SendFirst(Event{
Type: t,
Drag: f.drag,
LongPress: f.longPress,
DoublePress: f.doublePress,
InitialPos: f.initialPos,
CurrentPos: Point{
X: x,
Y: y,
},
// TODO: Velocity.
Time: now,
})
}
func (f *EventFilter) sendAfter(e internalEvent, sleep time.Duration) {
time.Sleep(sleep)
f.EventDeque.SendFirst(e)
}
func (f *EventFilter) end(x, y float32, now time.Time) {
f.sendFirst(TypeEnd, x, y, now)
f.inProgress = false
f.drag = false
f.longPress = false
f.doublePress = false
f.initialPos = Point{}
f.pressButton = mouse.ButtonNone
}
// Filter filters the event. It can return e, a different event, or nil to
// consume the event. It can also trigger side effects such as pushing new
// events onto its EventDeque.
func (f *EventFilter) Filter(e interface{}) interface{} {
switch e := e.(type) {
case internalEvent:
if e.eventFilter != f {
break
}
now := time.Now()
switch e.typ {
case typeDoublePressSchedule:
e.typ = typeDoublePressResolve
go f.sendAfter(e, doublePressThreshold)
case typeDoublePressResolve:
if e.pressCounter == f.pressCounter {
// It's a single press only.
f.end(e.x, e.y, now)
}
case typeLongPressSchedule:
e.typ = typeLongPressResolve
go f.sendAfter(e, longPressThreshold)
case typeLongPressResolve:
if e.pressCounter == f.pressCounter && !f.drag {
f.longPress = true
f.sendFirst(TypeIsLongPress, e.x, e.y, now)
}
}
return nil
case mouse.Event:
now := time.Now()
switch e.Direction {
case mouse.DirNone:
if f.pressButton == mouse.ButtonNone {
break
}
startDrag := false
if !f.drag &&
(abs(e.X-f.initialPos.X) > dragThreshold || abs(e.Y-f.initialPos.Y) > dragThreshold) {
f.drag = true
startDrag = true
}
if f.drag {
f.sendFirst(TypeDrag, e.X, e.Y, now)
}
if startDrag {
f.sendFirst(TypeIsDrag, e.X, e.Y, now)
}
case mouse.DirPress:
if f.pressButton != mouse.ButtonNone {
break
}
oldInProgress := f.inProgress
oldDoublePress := f.doublePress
f.drag = false
f.longPress = false
f.doublePress = f.inProgress
f.initialPos = Point{e.X, e.Y}
f.pressButton = e.Button
f.pressCounter++
f.inProgress = true
f.sendFirst(typeLongPressSchedule, e.X, e.Y, now)
if !oldDoublePress && f.doublePress {
f.sendFirst(TypeIsDoublePress, e.X, e.Y, now)
}
if !oldInProgress {
f.sendFirst(TypeStart, e.X, e.Y, now)
}
case mouse.DirRelease:
if f.pressButton != e.Button {
break
}
f.pressButton = mouse.ButtonNone
f.pressCounter++
if f.drag {
f.end(e.X, e.Y, now)
break
}
f.sendFirst(typeDoublePressSchedule, e.X, e.Y, now)
f.sendFirst(TypeTap, e.X, e.Y, now)
}
}
return e
}
func abs(x float32) float32 {
if x < 0 {
return -x
} else if x == 0 {
return 0 // Handle floating point negative zero.
}
return x
}
|