2
0
mirror of https://github.com/koreader/koreader synced 2024-11-13 19:11:25 +00:00
koreader/frontend/device/gesturedetector.lua
poire-z 74c1813a82 GestureDetector: add Tap interval on keyboard setting
Follow up to b90f6db8: allow specifying an other
value for tap interval when the keyboard is shown
(a good value for tap interval on reader and UI
elements might be too long on the keyboard, and
prevent typing fast).
2020-10-31 09:36:24 +01:00

944 lines
33 KiB
Lua

--[[--
This module detects gestures.
Current detectable gestures:
* `touch` (user touched screen)
* `tap` (touch action detected as single tap)
* `pan`
* `hold`
* `swipe`
* `pinch`
* `spread`
* `rotate`
* `hold_pan`
* `double_tap`
* `inward_pan`
* `outward_pan`
* `pan_release`
* `hold_release`
* `two_finger_tap`
* `two_finger_pan`
* `two_finger_swipe`
* `two_finger_pan_release`
You change the state machine by feeding it touch events, i.e. calling
@{GestureDetector:feedEvent|GestureDetector:feedEvent(tev)}.
a touch event should have following format:
tev = {
slot = 1,
id = 46,
x = 0,
y = 1,
timev = TimeVal:new{...},
}
Don't confuse `tev` with raw evs from kernel, `tev` is build according to ev.
@{GestureDetector:feedEvent|GestureDetector:feedEvent(tev)} will return a
detection result when you feed a touch release event to it.
--]]
local Geom = require("ui/geometry")
local TimeVal = require("ui/timeval")
local logger = require("logger")
local util = require("util")
-- default values (all the time parameters are in microseconds)
local TAP_INTERVAL = 0 * 1000
local DOUBLE_TAP_INTERVAL = 300 * 1000
local TWO_FINGER_TAP_DURATION = 300 * 1000
local HOLD_INTERVAL = 500 * 1000
local PAN_DELAYED_INTERVAL = 500 * 1000
local SWIPE_INTERVAL = 900 * 1000
-- current values
local ges_tap_interval = G_reader_settings:readSetting("ges_tap_interval") or TAP_INTERVAL
local ges_double_tap_interval = G_reader_settings:readSetting("ges_double_tap_interval") or DOUBLE_TAP_INTERVAL
local ges_two_finger_tap_duration = G_reader_settings:readSetting("ges_two_finger_tap_duration") or TWO_FINGER_TAP_DURATION
local ges_hold_interval = G_reader_settings:readSetting("ges_hold_interval") or HOLD_INTERVAL
local ges_pan_delayed_interval = G_reader_settings:readSetting("ges_pan_delayed_interval") or PAN_DELAYED_INTERVAL
local ges_swipe_interval = G_reader_settings:readSetting("ges_swipe_interval") or SWIPE_INTERVAL
local GestureDetector = {
-- must be initialized with the Input singleton class
input = nil,
-- default values (accessed for display by plugins/gestures.koplugin)
TAP_INTERVAL = TAP_INTERVAL,
DOUBLE_TAP_INTERVAL = DOUBLE_TAP_INTERVAL,
TWO_FINGER_TAP_DURATION = TWO_FINGER_TAP_DURATION,
HOLD_INTERVAL = HOLD_INTERVAL,
PAN_DELAYED_INTERVAL = PAN_DELAYED_INTERVAL,
SWIPE_INTERVAL = SWIPE_INTERVAL,
-- pinch/spread direction table
DIRECTION_TABLE = {
east = "horizontal",
west = "horizontal",
north = "vertical",
south = "vertical",
northeast = "diagonal",
northwest = "diagonal",
southeast = "diagonal",
southwest = "diagonal",
},
-- states are stored in separated slots
states = {},
hold_timer_id = {},
track_ids = {},
tev_stacks = {},
-- latest feeded touch event in each slots
last_tevs = {},
first_tevs = {},
-- for multiswipe gestures
multiswipe_directions = {},
-- detecting status on each slots
detectings = {},
-- for single/double tap
last_taps = {},
}
function GestureDetector:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
if o.init then o:init() end
return o
end
function GestureDetector:init()
local scaler = self.screen:getDPI() / 167
-- distance parameters
self.TWO_FINGER_TAP_REGION = 20 * scaler
self.DOUBLE_TAP_DISTANCE = 50 * scaler
self.SINGLE_TAP_BOUNCE_DISTANCE = self.DOUBLE_TAP_DISTANCE
self.PAN_THRESHOLD = self.DOUBLE_TAP_DISTANCE
self.MULTISWIPE_THRESHOLD = self.DOUBLE_TAP_DISTANCE
end
--[[--
Feeds touch events to state machine.
--]]
function GestureDetector:feedEvent(tevs)
repeat
local tev = table.remove(tevs)
if tev then
local slot = tev.slot
if not self.states[slot] then
self:clearState(slot) -- initiate state
end
local ges = self.states[slot](self, tev)
if tev.id ~= -1 then
self.last_tevs[slot] = tev
end
-- return no more than one gesture
if ges then return ges end
end
until tev == nil
end
function GestureDetector:deepCopyEv(tev)
return {
x = tev.x,
y = tev.y,
id = tev.id,
slot = tev.slot,
timev = TimeVal:new{
sec = tev.timev.sec,
usec = tev.timev.usec,
}
}
end
--[[
tap2 is the later tap
--]]
function GestureDetector:isTapBounce(tap1, tap2, interval)
local tv_diff = tap2.timev - tap1.timev
return (
math.abs(tap1.x - tap2.x) < self.SINGLE_TAP_BOUNCE_DISTANCE and
math.abs(tap1.y - tap2.y) < self.SINGLE_TAP_BOUNCE_DISTANCE and
(tv_diff.sec == 0 and (tv_diff.usec) < interval)
)
end
function GestureDetector:isDoubleTap(tap1, tap2)
local tv_diff = tap2.timev - tap1.timev
return (
math.abs(tap1.x - tap2.x) < self.DOUBLE_TAP_DISTANCE and
math.abs(tap1.y - tap2.y) < self.DOUBLE_TAP_DISTANCE and
(tv_diff.sec == 0 and (tv_diff.usec) < ges_double_tap_interval)
)
end
function GestureDetector:isTwoFingerTap()
if self.last_tevs[0] == nil or self.last_tevs[1] == nil then
return false
end
local x_diff0 = math.abs(self.last_tevs[0].x - self.first_tevs[0].x)
local x_diff1 = math.abs(self.last_tevs[1].x - self.first_tevs[1].x)
local y_diff0 = math.abs(self.last_tevs[0].y - self.first_tevs[0].y)
local y_diff1 = math.abs(self.last_tevs[1].y - self.first_tevs[1].y)
local tv_diff0 = self.last_tevs[0].timev - self.first_tevs[0].timev
local tv_diff1 = self.last_tevs[1].timev - self.first_tevs[1].timev
return (
x_diff0 < self.TWO_FINGER_TAP_REGION and
x_diff1 < self.TWO_FINGER_TAP_REGION and
y_diff0 < self.TWO_FINGER_TAP_REGION and
y_diff1 < self.TWO_FINGER_TAP_REGION and
tv_diff0.sec == 0 and tv_diff0.usec < ges_two_finger_tap_duration and
tv_diff1.sec == 0 and tv_diff1.usec < ges_two_finger_tap_duration
)
end
--[[--
Compares `last_pan` with `first_tev` in this slot.
The second boolean argument `simple` results in only four directions if true.
@return (direction, distance) pan direction and distance
--]]
function GestureDetector:getPath(slot, simple, diagonal, first_tev)
first_tev = first_tev or self.first_tevs
local x_diff = self.last_tevs[slot].x - first_tev[slot].x
local y_diff = self.last_tevs[slot].y - first_tev[slot].y
local direction = nil
local distance = math.sqrt(x_diff*x_diff + y_diff*y_diff)
if x_diff ~= 0 or y_diff ~= 0 then
local v_direction = y_diff < 0 and "north" or "south"
local h_direction = x_diff < 0 and "west" or "east"
if (not simple
and math.abs(y_diff) > 0.577*math.abs(x_diff)
and math.abs(y_diff) < 1.732*math.abs(x_diff))
or (simple and diagonal)
then
direction = v_direction..h_direction
elseif (math.abs(x_diff) > math.abs(y_diff)) then
direction = h_direction
else
direction = v_direction
end
end
return direction, distance
end
function GestureDetector:isSwipe(slot)
if not self.first_tevs[slot] or not self.last_tevs[slot] then return end
local tv_diff = self.last_tevs[slot].timev - self.first_tevs[slot].timev
if (tv_diff.sec == 0) and (tv_diff.usec < ges_swipe_interval) then
local x_diff = self.last_tevs[slot].x - self.first_tevs[slot].x
local y_diff = self.last_tevs[slot].y - self.first_tevs[slot].y
if x_diff ~= 0 or y_diff ~= 0 then
return true
end
end
end
function GestureDetector:getRotate(orig_point, start_point, end_point)
local a = orig_point:distance(start_point)
local b = orig_point:distance(end_point)
local c = start_point:distance(end_point)
return math.acos((a*a + b*b - c*c)/(2*a*b))*180/math.pi
end
--[[
Warning! this method won't update self.state, you need to do it
in each state method!
--]]
function GestureDetector:switchState(state_new, tev, param)
--- @todo Do we need to check whether state is valid? (houqp)
return self[state_new](self, tev, param)
end
function GestureDetector:clearState(slot)
self.states[slot] = self.initialState
self.hold_timer_id[slot] = nil
self.detectings[slot] = false
self.first_tevs[slot] = nil
self.last_tevs[slot] = nil
self.multiswipe_directions = {}
self.multiswipe_type = nil
end
function GestureDetector:setNewInterval(type, interval)
if type == "ges_tap_interval" then
ges_tap_interval = interval
elseif type == "ges_double_tap_interval" then
ges_double_tap_interval = interval
elseif type == "ges_two_finger_tap_duration" then
ges_two_finger_tap_duration = interval
elseif type == "ges_hold_interval" then
ges_hold_interval = interval
elseif type == "ges_pan_delayed_interval" then
ges_pan_delayed_interval = interval
elseif type == "ges_swipe_interval" then
ges_swipe_interval = interval
end
end
function GestureDetector:getInterval(type)
if type == "ges_tap_interval" then
return ges_tap_interval
elseif type == "ges_double_tap_interval" then
return ges_double_tap_interval
elseif type == "ges_two_finger_tap_duration" then
return ges_two_finger_tap_duration
elseif type == "ges_hold_interval" then
return ges_hold_interval
elseif type == "ges_pan_delayed_interval" then
return ges_pan_delayed_interval
elseif type == "ges_swipe_interval" then
return ges_swipe_interval
end
end
function GestureDetector:clearStates()
self:clearState(0)
self:clearState(1)
end
function GestureDetector:initialState(tev)
local slot = tev.slot
if tev.id then
-- an event ends
if tev.id == -1 then
self.detectings[slot] = false
else
self.track_ids[slot] = tev.id
if tev.x and tev.y then
-- user starts a new touch motion
if not self.detectings[slot] then
self.detectings[slot] = true
self.first_tevs[slot] = self:deepCopyEv(tev)
-- default to tap state
return self:switchState("tapState", tev)
end
end
end
end
end
--[[--
Handles both single and double tap.
--]]
function GestureDetector:tapState(tev)
logger.dbg("in tap state...")
local slot = tev.slot
if tev.id == -1 then
-- end of tap event
if self.detectings[0] and self.detectings[1] then
if self:isTwoFingerTap() then
local pos0 = Geom:new{
x = self.last_tevs[0].x,
y = self.last_tevs[0].y,
w = 0, h = 0,
}
local pos1 = Geom:new{
x = self.last_tevs[1].x,
y = self.last_tevs[1].y,
w = 0, h = 0,
}
local tap_span = pos0:distance(pos1)
logger.dbg("two-finger tap detected with span", tap_span)
self:clearStates()
return {
ges = "two_finger_tap",
pos = pos0:midpoint(pos1),
span = tap_span,
time = tev.timev,
}
else
self:clearState(slot)
end
elseif self.last_tevs[slot] ~= nil then
-- Normal single tap seems to always go thru here
-- (the next 'else' might be there for edge cases)
return self:handleDoubleTap(tev)
else
-- last tev in this slot is cleared by last two finger tap
self:clearState(slot)
return {
ges = "tap",
pos = Geom:new{
x = tev.x,
y = tev.y,
w = 0, h = 0,
},
time = tev.timev,
}
end
else
return self:handleNonTap(tev)
end
end
function GestureDetector:handleDoubleTap(tev)
local slot = tev.slot
local ges_ev = {
-- default to single tap
ges = "tap",
pos = Geom:new{
x = self.last_tevs[slot].x,
y = self.last_tevs[slot].y,
w = 0, h = 0,
},
time = tev.timev,
}
-- cur_tap is used for double tap detection
local cur_tap = {
x = tev.x,
y = tev.y,
timev = tev.timev,
}
-- Tap interval / bounce detection may be tweaked by widget (i.e. VirtualKeyboard)
local tap_interval = self.input.tap_interval_override or ges_tap_interval
-- We do tap bounce detection even when double tap is enabled (so, double tap
-- is triggered when: ges_tap_interval <= delay < ges_double_tap_interval)
if tap_interval > 0 and self.last_taps[slot] ~= nil and self:isTapBounce(self.last_taps[slot], cur_tap, tap_interval) then
logger.dbg("tap bounce detected in slot", slot, ": ignored")
-- Simply ignore it, and clear state as this is the end of a touch event
-- (this doesn't clear self.last_taps[slot], so a 3rd tap can be detected
-- as a double tap)
self:clearState(slot)
return
end
if not self.input.disable_double_tap and self.last_taps[slot] ~= nil and
self:isDoubleTap(self.last_taps[slot], cur_tap) then
-- it is a double tap
self:clearState(slot)
ges_ev.ges = "double_tap"
self.last_taps[slot] = nil
logger.dbg("double tap detected in slot", slot)
return ges_ev
end
-- set current tap to last tap
self.last_taps[slot] = cur_tap
if self.input.disable_double_tap then
-- We can send the event immediately (no need for the
-- timer stuff needed for double tap support)
logger.dbg("single tap detected in slot", slot, ges_ev.pos)
self:clearState(slot)
return ges_ev
end
-- Double tap enabled: we can't send this single tap immediately as it
-- may be the start of a double tap. We'll send it as a single tap after
-- a timer if no second tap happened in the double tap delay.
logger.dbg("set up single/double tap timer")
-- deadline should be calculated by adding current tap time and the interval
-- (No need to compute self._has_real_clock_time_ev_time here, we should always
-- have been thru handleNonTap() where it is computed, before getting here)
local ref_time = self._has_real_clock_time_ev_time and cur_tap.timev or TimeVal:now()
local deadline = ref_time + TimeVal:new{
sec = 0,
usec = not self.input.disable_double_tap and ges_double_tap_interval or 0,
}
self.input:setTimeout(function()
logger.dbg("in single/double tap timer", self.last_taps[slot] ~= nil)
-- double tap will set last_tap to nil so if it is not, then
-- user has not double-tap'ed: it's a single tap
if self.last_taps[slot] ~= nil then
self.last_taps[slot] = nil
-- we are using closure here
logger.dbg("single tap detected in slot", slot, ges_ev.pos)
return ges_ev
end
end, deadline)
-- we are already at the end of touch event
-- so reset the state
self:clearState(slot)
end
function GestureDetector:handleNonTap(tev)
local slot = tev.slot
if self.states[slot] ~= self.tapState then
-- switched from other state, probably from initialState
-- we return nil in this case
self.states[slot] = self.tapState
if self._has_real_clock_time_ev_time == nil then
if tev.timev.sec < TimeVal:now().sec - 600 then
-- ev.timev is probably the uptime since device boot
-- (which might pause on suspend) that we can't use
-- with setTimeout(): we'll use TimeVal:now()
self._has_real_clock_time_ev_time = false
logger.info("event times are not real clock time: some adjustments will be made")
else
-- assume they are real clock time
self._has_real_clock_time_ev_time = true
logger.info("event times are real clock time: no adjustment needed")
end
end
logger.dbg("set up hold timer")
local ref_time = self._has_real_clock_time_ev_time and tev.timev or TimeVal:now()
local deadline = ref_time + TimeVal:new{
sec = 0, usec = ges_hold_interval
}
-- Be sure the following setTimeout only react to this tapState
local hold_timer_id = tev.timev
self.hold_timer_id[slot] = hold_timer_id
self.input:setTimeout(function()
if self.states[slot] == self.tapState and self.hold_timer_id[slot] == hold_timer_id then
-- timer set in tapState, so we switch to hold
logger.dbg("hold gesture detected in slot", slot)
return self:switchState("holdState", tev, true)
end
end, deadline)
return {
ges = "touch",
pos = Geom:new{
x = tev.x,
y = tev.y,
w = 0, h = 0,
},
time = tev.timev,
}
else
-- it is not end of touch event, see if we need to switch to
-- other states
if (tev.x and math.abs(tev.x - self.first_tevs[slot].x) >= self.PAN_THRESHOLD) or
(tev.y and math.abs(tev.y - self.first_tevs[slot].y) >= self.PAN_THRESHOLD) then
-- if user's finger moved long enough in X or
-- Y distance, we switch to pan state
return self:switchState("panState", tev)
end
end
end
function GestureDetector:panState(tev)
logger.dbg("in pan state...")
local slot = tev.slot
if tev.id == -1 then
-- end of pan, signal swipe gesture if necessary
if self:isSwipe(slot) then
if self.detectings[0] and self.detectings[1] then
local ges_ev = self:handleTwoFingerPan(tev)
self:clearStates()
if ges_ev then
if ges_ev.ges == "two_finger_pan" then
ges_ev.ges = "two_finger_swipe"
elseif ges_ev.ges == "inward_pan" then
ges_ev.ges = "pinch"
elseif ges_ev.ges == "outward_pan" then
ges_ev.ges = "spread"
end
logger.dbg(ges_ev.ges, ges_ev.direction, ges_ev.distance, "detected")
end
return ges_ev
else
return self:handleSwipe(tev)
end
else -- if end of pan is not swipe then it must be pan release.
return self:handlePanRelease(tev)
end
else
if self.states[slot] ~= self.panState then
self.states[slot] = self.panState
end
return self:handlePan(tev)
end
end
function GestureDetector:handleSwipe(tev)
local slot = tev.slot
local swipe_direction, swipe_distance = self:getPath(slot)
local start_pos = Geom:new{
x = self.first_tevs[slot].x,
y = self.first_tevs[slot].y,
w = 0, h = 0,
}
local ges = "swipe"
local multiswipe_directions
if #self.multiswipe_directions > 1 then
ges = "multiswipe"
multiswipe_directions = ""
for k, v in pairs(self.multiswipe_directions) do
local sep = ""
if k > 1 then
sep = " "
end
multiswipe_directions = multiswipe_directions .. sep .. v[1]
end
logger.dbg("multiswipe", multiswipe_directions)
end
--- @todo dirty hack for some weird devices, replace it with better solution
if swipe_direction == "west" and DCHANGE_WEST_SWIPE_TO_EAST then
swipe_direction = "east"
elseif swipe_direction == "east" and DCHANGE_EAST_SWIPE_TO_WEST then
swipe_direction = "west"
end
logger.dbg("swipe", swipe_direction, swipe_distance, "detected in slot", slot)
self:clearState(slot)
return {
ges = ges,
-- use first pan tev coordination as swipe start point
pos = start_pos,
direction = swipe_direction,
multiswipe_directions = multiswipe_directions,
distance = swipe_distance,
time = tev.timev,
}
end
function GestureDetector:handlePan(tev)
local slot = tev.slot
if self.detectings[0] and self.detectings[1] then
return self:handleTwoFingerPan(tev)
else
local pan_direction, pan_distance = self:getPath(slot)
local tv_diff = self.last_tevs[slot].timev - self.first_tevs[slot].timev
local pan_ev = {
ges = "pan",
relative = {
-- default to pan 0
x = 0,
y = 0,
},
relative_delayed = {
-- default to pan 0
x = 0,
y = 0,
},
pos = nil,
direction = pan_direction,
distance = pan_distance,
distance_delayed = 0,
time = tev.timev,
}
-- regular pan
pan_ev.relative.x = tev.x - self.first_tevs[slot].x
pan_ev.relative.y = tev.y - self.first_tevs[slot].y
-- delayed pan, used where necessary to reduce potential activation of panning
-- when swiping is intended (e.g., for the menu or for multiswipe)
if not ((tv_diff.sec == 0) and (tv_diff.usec < ges_pan_delayed_interval)) then
pan_ev.relative_delayed.x = tev.x - self.first_tevs[slot].x
pan_ev.relative_delayed.y = tev.y - self.first_tevs[slot].y
pan_ev.distance_delayed = pan_distance
end
pan_ev.pos = Geom:new{
x = self.last_tevs[slot].x,
y = self.last_tevs[slot].y,
w = 0, h = 0,
}
local msd_cnt = #self.multiswipe_directions
local msd_direction_prev = (msd_cnt > 0) and self.multiswipe_directions[msd_cnt][1] or ""
local prev_ms_ev, fake_first_tev
if msd_cnt == 0 then
-- determine whether to initiate a straight or diagonal multiswipe
self.multiswipe_type = "straight"
if pan_direction ~= "north" and pan_direction ~= "south"
and pan_direction ~= "east" and pan_direction ~= "west" then
self.multiswipe_type = "diagonal"
end
-- recompute a more accurate direction and distance in a multiswipe context
elseif msd_cnt > 0 then
prev_ms_ev = self.multiswipe_directions[msd_cnt][2]
fake_first_tev = {
[slot] = {
["x"] = prev_ms_ev.pos.x,
["y"] = prev_ms_ev.pos.y,
["slot"] = slot,
},
}
end
-- the first time fake_first_tev is nil, so self.first_tevs is automatically used instead
local msd_direction, msd_distance
if self.multiswipe_type == "straight" then
msd_direction, msd_distance = self:getPath(slot, true, false, fake_first_tev)
else
msd_direction, msd_distance = self:getPath(slot, true, true, fake_first_tev)
end
if msd_distance > self.MULTISWIPE_THRESHOLD then
local pan_ev_multiswipe = pan_ev
-- store a copy of pan_ev without rotation adjustment
-- for multiswipe calculations when rotated
if self.screen:getTouchRotation() > self.screen.ORIENTATION_PORTRAIT then
pan_ev_multiswipe = util.tableDeepCopy(pan_ev)
end
if msd_direction ~= msd_direction_prev then
self.multiswipe_directions[msd_cnt+1] = {
[1] = msd_direction,
[2] = pan_ev_multiswipe,
}
-- update ongoing swipe direction to the new maximum
else
self.multiswipe_directions[msd_cnt] = {
[1] = msd_direction,
[2] = pan_ev_multiswipe,
}
end
end
return pan_ev
end
end
function GestureDetector:handleTwoFingerPan(tev)
-- triggering slot
local tslot = tev.slot
-- reference slot
local rslot = tslot == 1 and 0 or 1
local tpan_dir, tpan_dis = self:getPath(tslot)
local tstart_pos = Geom:new{
x = self.first_tevs[tslot].x,
y = self.first_tevs[tslot].y,
w = 0, h = 0,
}
local tend_pos = Geom:new{
x = self.last_tevs[tslot].x,
y = self.last_tevs[tslot].y,
w = 0, h = 0,
}
local rstart_pos = Geom:new{
x = self.first_tevs[rslot].x,
y = self.first_tevs[rslot].y,
w = 0, h = 0,
}
if self.states[rslot] == self.panState then
local rpan_dir, rpan_dis = self:getPath(rslot)
local rend_pos = Geom:new{
x = self.last_tevs[rslot].x,
y = self.last_tevs[rslot].y,
w = 0, h = 0,
}
local start_distance = tstart_pos:distance(rstart_pos)
local end_distance = tend_pos:distance(rend_pos)
local ges_ev = {
ges = "two_finger_pan",
-- use midpoint of tstart and rstart as swipe start point
pos = tstart_pos:midpoint(rstart_pos),
distance = tpan_dis + rpan_dis,
direction = tpan_dir,
time = tev.timev,
}
if tpan_dir ~= rpan_dir then
if start_distance > end_distance then
ges_ev.ges = "inward_pan"
else
ges_ev.ges = "outward_pan"
end
ges_ev.direction = self.DIRECTION_TABLE[tpan_dir]
end
logger.dbg(ges_ev.ges, ges_ev.direction, ges_ev.distance, "detected")
return ges_ev
elseif self.states[rslot] == self.holdState then
local angle = self:getRotate(rstart_pos, tstart_pos, tend_pos)
logger.dbg("rotate", angle, "detected")
return {
ges = "rotate",
pos = rstart_pos,
angle = angle,
time = tev.timev,
}
end
end
function GestureDetector:handlePanRelease(tev)
local slot = tev.slot
local release_pos = Geom:new{
x = self.last_tevs[slot].x,
y = self.last_tevs[slot].y,
w = 0, h = 0,
}
local pan_ev = {
ges = "pan_release",
pos = release_pos,
time = tev.timev,
}
if self.detectings[0] and self.detectings[1] then
logger.dbg("two finger pan release detected")
pan_ev.ges = "two_finger_pan_release"
self:clearStates()
else
logger.dbg("pan release detected in slot", slot)
self:clearState(slot)
end
return pan_ev
end
function GestureDetector:holdState(tev, hold)
logger.dbg("in hold state...")
local slot = tev.slot
-- when we switch to hold state, we pass additional param "hold"
if tev.id ~= -1 and hold and self.last_tevs[slot].x and self.last_tevs[slot].y then
self.states[slot] = self.holdState
return {
ges = "hold",
pos = Geom:new{
x = self.last_tevs[slot].x,
y = self.last_tevs[slot].y,
w = 0, h = 0,
},
time = tev.timev,
}
elseif tev.id == -1 and self.last_tevs[slot] ~= nil then
-- end of hold, signal hold release
logger.dbg("hold_release detected in slot", slot)
local last_x = self.last_tevs[slot].x
local last_y = self.last_tevs[slot].y
self:clearState(slot)
return {
ges = "hold_release",
pos = Geom:new{
x = last_x,
y = last_y,
w = 0, h = 0,
},
time = tev.timev,
}
elseif (tev.x and math.abs(tev.x - self.first_tevs[slot].x) >= self.PAN_THRESHOLD) or
(tev.y and math.abs(tev.y - self.first_tevs[slot].y) >= self.PAN_THRESHOLD) then
local ges_ev = self:handlePan(tev)
if ges_ev ~= nil then ges_ev.ges = "hold_pan" end
return ges_ev
end
end
local ges_coordinate_translation_270 = {
north = "west",
south = "east",
east = "north",
west = "south",
northeast = "northwest",
northwest = "southwest",
southeast = "northeast",
southwest = "southeast",
}
local ges_coordinate_translation_180 = {
north = "south",
south = "north",
east = "west",
west = "east",
northeast = "southwest",
northwest = "southeast",
southeast = "northwest",
southwest = "northeast",
}
local ges_coordinate_translation_90 = {
north = "east",
south = "west",
east = "south",
west = "north",
northeast = "southeast",
northwest = "northeast",
southeast = "southwest",
southwest = "northwest",
}
local function translateGesDirCoordinate(direction, translation_table)
return translation_table[direction]
end
local function translateMultiswipeGesDirCoordinate(multiswipe_directions, translation_table)
return multiswipe_directions:gsub("%S+", translation_table)
end
--[[--
Changes gesture's `x` and `y` coordinates according to screen view mode.
@param ges gesture that you want to adjust
@return adjusted gesture.
--]]
function GestureDetector:adjustGesCoordinate(ges)
local mode = self.screen:getTouchRotation()
if mode == self.screen.ORIENTATION_LANDSCAPE then
-- in landscape mode rotated 90
if ges.pos then
ges.pos.x, ges.pos.y = (self.screen:getWidth() - ges.pos.y), (ges.pos.x)
end
if ges.ges == "swipe" or ges.ges == "pan"
or ges.ges == "multiswipe"
or ges.ges == "two_finger_swipe"
or ges.ges == "two_finger_pan"
then
ges.direction = translateGesDirCoordinate(ges.direction, ges_coordinate_translation_90)
if ges.ges == "multiswipe" then
ges.multiswipe_directions = translateMultiswipeGesDirCoordinate(ges.multiswipe_directions, ges_coordinate_translation_90)
end
if ges.relative then
ges.relative.x, ges.relative.y = -ges.relative.y, ges.relative.x
ges.relative_delayed.x, ges.relative_delayed.y = -ges.relative_delayed.y, ges.relative_delayed.x
end
elseif ges.ges == "pinch" or ges.ges == "spread"
or ges.ges == "inward_pan"
or ges.ges == "outward_pan" then
if ges.direction == "horizontal" then
ges.direction = "vertical"
elseif ges.direction == "vertical" then
ges.direction = "horizontal"
end
end
elseif mode == self.screen.ORIENTATION_LANDSCAPE_ROTATED then
-- in landscape mode rotated 270
if ges.pos then
ges.pos.x, ges.pos.y = (ges.pos.y), (self.screen:getHeight() - ges.pos.x)
end
if ges.ges == "swipe" or ges.ges == "pan"
or ges.ges == "multiswipe"
or ges.ges == "two_finger_swipe"
or ges.ges == "two_finger_pan"
then
ges.direction = translateGesDirCoordinate(ges.direction, ges_coordinate_translation_270)
if ges.ges == "multiswipe" then
ges.multiswipe_directions = translateMultiswipeGesDirCoordinate(ges.multiswipe_directions, ges_coordinate_translation_270)
end
if ges.relative then
ges.relative.x, ges.relative.y = ges.relative.y, -ges.relative.x
ges.relative_delayed.x, ges.relative_delayed.y = ges.relative_delayed.y, -ges.relative_delayed.x
end
elseif ges.ges == "pinch" or ges.ges == "spread"
or ges.ges == "inward_pan"
or ges.ges == "outward_pan" then
if ges.direction == "horizontal" then
ges.direction = "vertical"
elseif ges.direction == "vertical" then
ges.direction = "horizontal"
end
end
elseif mode == self.screen.ORIENTATION_PORTRAIT_ROTATED then
-- in portrait mode rotated 180
if ges.pos then
ges.pos.x, ges.pos.y = (self.screen:getWidth() - ges.pos.x), (self.screen:getHeight() - ges.pos.y)
end
if ges.ges == "swipe" or ges.ges == "pan"
or ges.ges == "multiswipe"
or ges.ges == "two_finger_swipe"
or ges.ges == "two_finger_pan"
then
ges.direction = translateGesDirCoordinate(ges.direction, ges_coordinate_translation_180)
if ges.ges == "multiswipe" then
ges.multiswipe_directions = translateMultiswipeGesDirCoordinate(ges.multiswipe_directions, ges_coordinate_translation_180)
end
if ges.relative then
ges.relative.x, ges.relative.y = -ges.relative.x, -ges.relative.y
ges.relative_delayed.x, ges.relative_delayed.y = -ges.relative_delayed.x, -ges.relative_delayed.y
end
elseif ges.ges == "pinch" or ges.ges == "spread"
or ges.ges == "inward_pan"
or ges.ges == "outward_pan" then
if ges.direction == "horizontal" then
ges.direction = "horizontal"
elseif ges.direction == "vertical" then
ges.direction = "vertical"
end
end
end
logger.dbg("adjusted ges:", ges.ges, ges.multiswipe_directions or ges.direction)
return ges
end
return GestureDetector