-
Notifications
You must be signed in to change notification settings - Fork 211
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
shiny/gesture: new package for gesture events.
Change-Id: I4848263cbdf1da41d3b0dc5b13ca718a4c77c5ee Reviewed-on: https://go-review.googlesource.com/24201 Reviewed-by: David Crawshaw <[email protected]>
- Loading branch information
Showing
4 changed files
with
369 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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 | ||
} |
Oops, something went wrong.