Fix scrolling, add inertial scroll on non-eInk devices

Add a new reader module: ReaderScrolling, that exposes
some Scrolling options to the menu (which are to be used
by and implemented in ReaderPaging and ReaderRolling
themselves) and implement some inertial scrolling logic
used by both of them.
Default to "Classic scrolling" which is the expected
behaviour on phones and tablets.
The old CreDocument buggy behaviour is available as
"Turbo scrolling" for both Paging and Rolling documents.
Added a "On release scrolling" option that might be
useful on eInk to avoid dynamic pan/scrolling.

Try to avoid bad interactions between pan and swipe,
cancelling unwanted panning if we ended up doing a
swipe or multiswipe.
pull/7712/head
poire-z 3 years ago
parent e4fd45ef08
commit d0165f8bd1

@ -130,6 +130,7 @@ function ReaderConfig:onShowConfigMenu()
-- show last used panel when opening config dialog
self.config_dialog:onShowConfigPanel(self.last_panel_index)
UIManager:show(self.config_dialog)
self.ui:handleEvent(Event:new("HandledAsSwipe")) -- cancel any pan scroll made
return true
end

@ -1978,11 +1978,11 @@ function ReaderFooter:_updateFooterText(force_repaint, force_recompute)
UIManager:widgetRepaint(self.view.footer, 0, 0)
-- We've painted it first to ensure self.footer_content.dimen is sane
UIManager:setDirty(self.view.footer, function()
return "ui", self.footer_content.dimen
return self.view.currently_scrolling and "fast" or "ui", self.footer_content.dimen
end)
else
UIManager:setDirty(self.view.dialog, function()
return "ui", refresh_dim
return self.view.currently_scrolling and "fast" or "ui", refresh_dim
end)
end
end

@ -437,6 +437,7 @@ function ReaderMenu:onSwipeShowMenu(ges)
self.ui:handleEvent(Event:new("ShowConfigMenu"))
end
self.ui:handleEvent(Event:new("ShowMenu", self:_getTabIndexFromLocation(ges)))
self.ui:handleEvent(Event:new("HandledAsSwipe")) -- cancel any pan scroll made
return true
end
end

@ -7,6 +7,7 @@ local Math = require("optmath")
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
local Notification = require("ui/widget/notification")
local ReaderZooming = require("apps/reader/modules/readerzooming")
local TimeVal = require("ui/timeval")
local UIManager = require("ui/uimanager")
local bit = require("bit")
local logger = require("logger")
@ -32,7 +33,6 @@ local ReaderPaging = InputContainer:new{
pan_rate = 30, -- default 30 ops, will be adjusted in readerui
current_page = 0,
number_of_pages = 0,
last_pan_relative_y = 0,
visible_area = nil,
page_area = nil,
show_overlap_enable = nil,
@ -41,7 +41,7 @@ local ReaderPaging = InputContainer:new{
inverse_reading_order = nil,
page_flipping_mode = false,
bookmark_flipping_mode = false,
flip_steps = {0,1,2,5,10,20,50,100}
flip_steps = {0,1,2,5,10,20,50,100},
}
function ReaderPaging:init()
@ -101,6 +101,7 @@ function ReaderPaging:init()
{"0"}, doc = "go to end", event = "GotoPercent", args = 100,
}
end
self.pan_interval = TimeVal:new{ usec = 1000000 / self.pan_rate }
self.number_of_pages = self.ui.document.info.number_of_pages
self.ui.menu:registerToMainMenu(self)
end
@ -173,7 +174,6 @@ function ReaderPaging:setupTouchZones()
{
id = "paging_pan",
ges = "pan",
rate = self.pan_rate,
screen_zone = {
ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1,
},
@ -422,7 +422,43 @@ function ReaderPaging:bookmarkFlipping(flipping_page, flipping_ges)
UIManager:setDirty(self.view.dialog, "partial")
end
function ReaderPaging:onScrollSettingsUpdated(scroll_method, inertial_scroll_enabled, scroll_activation_delay)
self.scroll_method = scroll_method
self.scroll_activation_delay = TimeVal:new{ usec = scroll_activation_delay * 1000 }
if inertial_scroll_enabled then
self.ui.scrolling:setInertialScrollCallbacks(
function(distance) -- do_scroll_callback
if not self.ui.document then
return false
end
UIManager.currently_scrolling = true
local top_page, top_position = self:getTopPage(), self:getTopPosition()
self:onPanningRel(distance)
return not (top_page == self:getTopPage() and top_position == self:getTopPosition())
end,
function() -- scroll_done_callback
UIManager.currently_scrolling = false
UIManager:setDirty(self.view.dialog, "partial")
end
)
else
self.ui.scrolling:setInertialScrollCallbacks(nil, nil)
end
end
function ReaderPaging:onSwipe(_, ges)
if self._pan_has_scrolled then
-- We did some panning but released after a short amount of time,
-- so this gesture ended up being a Swipe - and this swipe was
-- not handled by the other modules (so, not opening the menus).
-- Do as :onPanRelese() and ignore this swipe.
self:onPanRelease() -- no arg, so we know there we come from here
return true
else
self._pan_started = false
UIManager.currently_scrolling = false
self._pan_page_states_to_restore = nil
end
local direction = BD.flipDirectionIfMirroredUILayout(ges.direction)
if self.bookmark_flipping_mode then
self:bookmarkFlipping(self.current_page, ges)
@ -461,16 +497,86 @@ function ReaderPaging:onPan(_, ges)
self.view:PanningStart(-ges.relative.x, -ges.relative.y)
end
elseif ges.direction == "north" or ges.direction == "south" then
local relative_type = "relative"
if self.ui.gesture and self.ui.gesture.multiswipes_enabled then
relative_type = "relative_delayed"
end
-- this is only used when mouse wheel is used
if ges.mousewheel_direction and not self.view.page_scroll then
-- Mouse wheel generates a Pan event: in page mode, move one
-- page per event. Scroll mode is handled in the 'else' branch
-- and use the wheeled distance.
self:onGotoViewRel(-1 * ges.mousewheel_direction)
else
self:onPanningRel(self.last_pan_relative_y - ges[relative_type].y)
self.last_pan_relative_y = ges[relative_type].y
elseif self.view.page_scroll then
if not self._pan_started then
self._pan_started = true
-- Re-init state variables
self._pan_has_scrolled = false
self._pan_prev_relative_y = 0
self._pan_to_scroll_later = 0
self._pan_real_last_time = TimeVal.zero
if ges.mousewheel_direction then
self._pan_activation_time = false
else
self._pan_activation_time = ges.time + self.scroll_activation_delay
end
-- We will restore the previous position if this pan
-- ends up being a swipe or a multiswipe
-- Somehow, accumulating the distances scrolled in a self._pan_dist_to_restore
-- so we can scroll these back may not always put us back to the original
-- position (possibly because of these page_states?). It's safer
-- to remember the original page_states and restore that. We can keep
-- a reference to the original table as onPanningRel() will have this
-- table replaced.
self._pan_page_states_to_restore = self.view.page_states
end
local scroll_now = false
if self._pan_activation_time and ges.time >= self._pan_activation_time then
self._pan_activation_time = false -- We can go on, no need to check again
end
if not self._pan_activation_time and ges.time - self._pan_real_last_time >= self.pan_interval then
scroll_now = true
self._pan_real_last_time = ges.time
end
local scroll_dist = 0
if self.scroll_method == self.ui.scrolling.SCROLL_METHOD_CLASSIC then
-- Scroll by the distance the finger moved since last pan event,
-- having the document follows the finger
scroll_dist = self._pan_prev_relative_y - ges.relative.y
self._pan_prev_relative_y = ges.relative.y
if not self._pan_has_scrolled then
-- Avoid checking this for each pan, no need once we have scrolled
if self.ui.scrolling:cancelInertialScroll() or self.ui.scrolling:cancelledByTouch() then
-- If this pan or its initial touch did cancel some inertial scrolling,
-- ignore activation delay to allow continuous scrolling
self._pan_activation_time = false
scroll_now = true
self._pan_real_last_time = ges.time
end
end
self.ui.scrolling:accountManualScroll(scroll_dist, ges.time)
elseif self.scroll_method == self.ui.scrolling.SCROLL_METHOD_TURBO then
-- Legacy scrolling "buggy" behaviour, that can actually be nice
-- Scroll by the distance from the initial finger position, this distance
-- controlling the speed of the scrolling)
if scroll_now then
scroll_dist = -ges.relative.y
end
-- We don't accumulate in _pan_to_scroll_later
elseif self.scroll_method == self.ui.scrolling.SCROLL_METHOD_ON_RELEASE then
self._pan_to_scroll_later = -ges.relative.y
if scroll_now then
self._pan_has_scrolled = true -- so we really apply it later
end
scroll_dist = 0
scroll_now = false
end
if scroll_now then
local dist = self._pan_to_scroll_later + scroll_dist
self._pan_to_scroll_later = 0
if dist ~= 0 then
self._pan_has_scrolled = true
UIManager.currently_scrolling = true
self:onPanningRel(dist)
end
else
self._pan_to_scroll_later = self._pan_to_scroll_later + scroll_dist
end
end
end
return true
@ -484,12 +590,40 @@ function ReaderPaging:onPanRelease(_, ges)
self.view:PanningStop()
end
else
self.last_pan_relative_y = 0
-- trigger full refresh to clear ghosting generated by previous fast refresh
UIManager:setDirty(nil, "full")
if self._pan_has_scrolled and self._pan_to_scroll_later ~= 0 then
self:onPanningRel(self._pan_to_scroll_later)
end
self._pan_started = false
self._pan_page_states_to_restore = nil
UIManager.currently_scrolling = false
if self._pan_has_scrolled then
self._pan_has_scrolled = false
-- Don't do any inertial scrolling if pan events come from
-- a mousewheel (which may have itself some inertia)
if (ges and ges.from_mousewheel) or not self.ui.scrolling:startInertialScroll() then
UIManager:setDirty(self.view.dialog, "partial")
end
end
end
end
function ReaderPaging:onHandledAsSwipe()
if self._pan_started then
-- Restore original position as this pan we've started handling
-- has ended up being a multiswipe or handled as a swipe to open
-- top or bottom menus
if self._pan_has_scrolled then
self.view.page_states = self._pan_page_states_to_restore
self:_gotoPage(self.view.page_states[#self.view.page_states].page, "scrolling")
UIManager:setDirty(self.view.dialog, "ui")
end
self._pan_page_states_to_restore = nil
self._pan_started = false
self._pan_has_scrolled = false
UIManager.currently_scrolling = false
end
return true
end
function ReaderPaging:onZoomModeUpdate(new_mode)
-- we need to remember zoom mode to handle page turn event
self.zoom_mode = new_mode

@ -116,6 +116,7 @@ function ReaderRolling:init()
{"0"}, doc = "go to end", event = "GotoPercent", args = 100,
}
end
self.pan_interval = TimeVal:new{ usec = 1000000 / self.pan_rate }
table.insert(self.ui.postInitCallback, function()
self.rendering_hash = self.ui.document:getDocumentRenderingHash()
@ -377,12 +378,19 @@ function ReaderRolling:setupTouchZones()
{
id = "rolling_pan",
ges = "pan",
rate = self.pan_rate,
screen_zone = {
ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1,
},
handler = function(ges) return self:onPan(nil, ges) end,
},
{
id = "rolling_pan_release",
ges = "pan_release",
screen_zone = {
ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1,
},
handler = function(ges) return self:onPanRelease(nil, ges) end,
},
})
end
@ -515,7 +523,45 @@ function ReaderRolling:getLastPercent()
end
end
function ReaderRolling:onScrollSettingsUpdated(scroll_method, inertial_scroll_enabled, scroll_activation_delay)
self.scroll_method = scroll_method
self.scroll_activation_delay = TimeVal:new{ usec = scroll_activation_delay * 1000 }
if inertial_scroll_enabled then
self.ui.scrolling:setInertialScrollCallbacks(
function(distance) -- do_scroll_callback
if not self.ui.document then
return false
end
UIManager.currently_scrolling = true
local prev_pos = self.current_pos
self:_gotoPos(prev_pos + distance)
return self.current_pos ~= prev_pos
end,
function() -- scroll_done_callback
UIManager.currently_scrolling = false
if self.ui.document then
self.xpointer = self.ui.document:getXPointer()
end
UIManager:setDirty(self.view.dialog, "partial")
end
)
else
self.ui.scrolling:setInertialScrollCallbacks(nil, nil)
end
end
function ReaderRolling:onSwipe(_, ges)
if self._pan_has_scrolled then
-- We did some panning but released after a short amount of time,
-- so this gesture ended up being a Swipe - and this swipe was
-- not handled by the other modules (so, not opening the menus).
-- Do as :onPanRelese() and ignore this swipe.
self:onPanRelease() -- no arg, so we know there we come from here
return true
else
self._pan_started = false
UIManager.currently_scrolling = false
end
local direction = BD.flipDirectionIfMirroredUILayout(ges.direction)
if direction == "west" then
if G_reader_settings:nilOrFalse("page_turns_disable_swipe") then
@ -539,19 +585,116 @@ function ReaderRolling:onSwipe(_, ges)
end
function ReaderRolling:onPan(_, ges)
if self.view.view_mode == "scroll" then
local distance_type = "distance"
if self.ui.gesture and self.ui.gesture.multiswipes_enabled then
distance_type = "distance_delayed"
if ges.direction == "north" or ges.direction == "south" then
if ges.mousewheel_direction and self.view.view_mode == "page" then
-- Mouse wheel generates a Pan event: in page mode, move one
-- page per event. Scroll mode is handled in the 'else' branch
-- and use the wheeled distance.
UIManager:broadcastEvent(Event:new("GotoViewRel", -1 * ges.mousewheel_direction))
elseif self.view.view_mode == "scroll" then
if not self._pan_started then
self._pan_started = true
-- Re-init state variables
self._pan_has_scrolled = false
self._pan_prev_relative_y = 0
self._pan_to_scroll_later = 0
self._pan_real_last_time = TimeVal.zero
if ges.mousewheel_direction then
self._pan_activation_time = false
else
self._pan_activation_time = ges.time + self.scroll_activation_delay
end
-- We will restore the previous position if this pan
-- ends up being a swipe or a multiswipe
self._pan_pos_at_pan_start = self.current_pos
end
local scroll_now = false
if self._pan_activation_time and ges.time >= self._pan_activation_time then
self._pan_activation_time = false -- We can go on, no need to check again
end
if not self._pan_activation_time and ges.time - self._pan_real_last_time >= self.pan_interval then
scroll_now = true
self._pan_real_last_time = ges.time
end
local scroll_dist = 0
if self.scroll_method == self.ui.scrolling.SCROLL_METHOD_CLASSIC then
-- Scroll by the distance the finger moved since last pan event,
-- having the document follows the finger
scroll_dist = self._pan_prev_relative_y - ges.relative.y
self._pan_prev_relative_y = ges.relative.y
if not self._pan_has_scrolled then
-- Avoid checking this for each pan, no need once we have scrolled
if self.ui.scrolling:cancelInertialScroll() or self.ui.scrolling:cancelledByTouch() then
-- If this pan or its initial touch did cancel some inertial scrolling,
-- ignore activation delay to allow continuous scrolling
self._pan_activation_time = false
scroll_now = true
self._pan_real_last_time = ges.time
end
end
self.ui.scrolling:accountManualScroll(scroll_dist, ges.time)
elseif self.scroll_method == self.ui.scrolling.SCROLL_METHOD_TURBO then
-- Legacy scrolling "buggy" behaviour, that can actually be nice
-- Scroll by the distance from the initial finger position, this distance
-- controlling the speed of the scrolling)
if scroll_now then
scroll_dist = -ges.relative.y
end
-- We don't accumulate in _pan_to_scroll_later
elseif self.scroll_method == self.ui.scrolling.SCROLL_METHOD_ON_RELEASE then
self._pan_to_scroll_later = -ges.relative.y
if scroll_now then
self._pan_has_scrolled = true -- so we really apply it later
end
scroll_dist = 0
scroll_now = false
end
if scroll_now then
local dist = self._pan_to_scroll_later + scroll_dist
self._pan_to_scroll_later = 0
if dist ~= 0 then
self._pan_has_scrolled = true
UIManager.currently_scrolling = true
self:_gotoPos(self.current_pos + dist)
-- (We'll update self.xpointer only when done moving, at
-- release/swipe time as it might be expensive)
end
else
self._pan_to_scroll_later = self._pan_to_scroll_later + scroll_dist
end
end
if ges.direction == "north" then
self:_gotoPos(self.current_pos + ges[distance_type])
elseif ges.direction == "south" then
self:_gotoPos(self.current_pos - ges[distance_type])
end
return true
end
function ReaderRolling:onPanRelease(_, ges)
if self._pan_has_scrolled and self._pan_to_scroll_later ~= 0 then
self:_gotoPos(self.current_pos + self._pan_to_scroll_later)
end
self._pan_started = false
UIManager.currently_scrolling = false
if self._pan_has_scrolled then
self._pan_has_scrolled = false
self.xpointer = self.ui.document:getXPointer()
-- Don't do any inertial scrolling if pan events come from
-- a mousewheel (which may have itself some inertia)
if (ges and ges.from_mousewheel) or not self.ui.scrolling:startInertialScroll() then
UIManager:setDirty(self.view.dialog, "partial")
end
--this is only use when mouse wheel is used
elseif ges.mousewheel_direction and self.view.view_mode == "page" then
UIManager:broadcastEvent(Event:new("GotoViewRel", -1 * ges.mousewheel_direction))
end
end
function ReaderRolling:onHandledAsSwipe()
if self._pan_started then
-- Restore original position as this pan we've started handling
-- has ended up being a multiswipe or handled as a swipe to open
-- top or bottom menus
self:_gotoPos(self._pan_pos_at_pan_start)
self._pan_started = false
self._pan_has_scrolled = false
UIManager.currently_scrolling = false
-- No specific refresh: the swipe/multiswipe might show other stuff,
-- and we'd want to avoid a flashing refresh
end
return true
end

@ -0,0 +1,419 @@
local Device = require("device")
local Event = require("ui/event")
local InputContainer = require("ui/widget/container/inputcontainer")
local TimeVal = require("ui/timeval")
local UIManager = require("ui/uimanager")
local logger = require("logger")
local _ = require("gettext")
local T = require("ffi/util").template
local Screen = Device.screen
-- This module exposes Scrolling settings, and additionnally
-- handles inertial scrolling on non-eInk devices.
local SCROLL_METHOD_CLASSIC = "classic"
local SCROLL_METHOD_TURBO = "turbo"
local SCROLL_METHOD_ON_RELEASE = "on_release"
local ReaderScrolling = InputContainer:new{
-- Available scrolling methods (make them available to other reader modules)
SCROLL_METHOD_CLASSIC = SCROLL_METHOD_CLASSIC,
SCROLL_METHOD_TURBO = SCROLL_METHOD_TURBO,
SCROLL_METHOD_ON_RELEASE = SCROLL_METHOD_ON_RELEASE,
scroll_method = SCROLL_METHOD_CLASSIC,
scroll_activation_delay = 0, -- 0 ms
inertial_scroll = false,
pan_rate = 30, -- default 30 ops, will be adjusted in readerui
scroll_friction = 0.2, -- the lower, the sooner inertial scrolling stops
-- go at ending scrolling soon when we reach steps smaller than this
end_scroll_dist = Screen:scaleBySize(10),
-- no inertial scrolling if 300ms pause without any movement before release
pause_before_release_cancel_duration = TimeVal:new{ sec = 0, usec = 300000 },
-- Callbacks to be updated by readerrolling or readerpaging
_do_scroll_callback = function(distance) return false end,
_scroll_done_callback = function() end,
_inertial_scroll_supported = false,
_inertial_scroll_enabled = false,
_inertial_scroll_interval = 1 / 30,
_inertial_scroll_action_scheduled = false,
_just_reschedule = false,
_last_manual_scroll_dy = 0,
_velocity = 0,
}
function ReaderScrolling:init()
if not Device:isTouchDevice() then
-- No scroll support, no menu
return
end
-- The different scrolling methods are handled directly by readerpaging/readerrolling
self.scroll_method = G_reader_settings:readSetting("scroll_method")
-- Keep inertial scrolling available on the emulator (which advertizes itself as eInk)
if not Device:hasEinkScreen() or Device:isEmulator() then
self._inertial_scroll_supported = true
end
if self._inertial_scroll_supported then
self.inertial_scroll = G_reader_settings:nilOrTrue("inertial_scroll")
self._inertial_scroll_interval = 1 / self.pan_rate
-- Set this so we don't have to check for nil, and in case
-- we miss a first touch event.
-- We can keep it obsolete, which will result in a long
-- duration and a small/zero velocity that won't hurt.
self._last_manual_scroll_timev = TimeVal.zero
self:_setupAction()
end
self.ui.menu:registerToMainMenu(self)
end
function ReaderScrolling:getDefaultScrollActivationDelay()
if (self.ui.gestures and self.ui.gestures.multiswipes_enabled)
or G_reader_settings:readSetting("activate_menu") ~= "tap" then
-- If swipes to show menu or multiswipes are enabled, higher default
-- scroll activation delay to avoid scrolling and restoring when
-- doing swipes
return 500 -- 500ms
end
-- Otherwise, no need for any delay
return 0
end
function ReaderScrolling:addToMainMenu(menu_items)
menu_items.scrolling = {
text = _("Scrolling"),
enabled_func = function()
-- Make it only enabled when in continuous/scroll mode
-- (different setting in self.view whether rolling or paging document)
if self.view and (self.view.page_scroll or self.view.view_mode == "scroll") then
return true
end
return false
end,
sub_item_table = {
{
text = _("Classic scrolling"),
help_text = _([[Classic scrolling will move the document with your finger.]]),
checked_func = function()
return self.scroll_method == self.SCROLL_METHOD_CLASSIC
end,
callback = function()
if self.scroll_method ~= self.SCROLL_METHOD_CLASSIC then
self.scroll_method = self.SCROLL_METHOD_CLASSIC
self:applyScrollSettings()
end
end,
},
{
text = _("Turbo scrolling"),
help_text = _([[
Turbo scrolling will scroll the document, at each step, by the distance from your initial finger position (rather than by the distance from your previous finger position).
It allows for faster scrolling without the need to lift and reposition your finger.]]),
checked_func = function()
return self.scroll_method == self.SCROLL_METHOD_TURBO
end,
callback = function()
if self.scroll_method ~= self.SCROLL_METHOD_TURBO then
self.scroll_method = self.SCROLL_METHOD_TURBO
self:applyScrollSettings()
end
end,
},
{
text = _("On-release scrolling"),
help_text = _([[
On-release scrolling will scroll the document by the panned distance only on finger up.
This is interesting on eInk if you only pan to better adjust page vertical position.]]),
checked_func = function()
return self.scroll_method == self.SCROLL_METHOD_ON_RELEASE
end,
callback = function()
if self.scroll_method ~= self.SCROLL_METHOD_ON_RELEASE then
self.scroll_method = self.SCROLL_METHOD_ON_RELEASE
self:applyScrollSettings()
end
end,
separator = true,
},
{
text_func = function()
return T(_("Activation delay: %1 ms"), self.scroll_activation_delay)
end,
keep_menu_open = true,
callback = function(touchmenu_instance)
local scroll_activation_delay_default = self:getDefaultScrollActivationDelay()
local SpinWidget = require("ui/widget/spinwidget")
local widget = SpinWidget:new{
title_text = _("Scroll activation delay"),
info_text = T(_([[
A delay can be used to avoid scrolling when swipes or multiswipes are intended.
The delay value is in milliseconds and can range from 0 to 2000 (2 seconds).
Default value: %1 ms]]), scroll_activation_delay_default),
width = math.floor(Screen:getWidth() * 0.75),
value = self.scroll_activation_delay,
value_min = 0,
value_max = 2000,
value_step = 100,
value_hold_step = 500,
ok_text = _("Set delay"),
default_value = scroll_activation_delay_default,
callback = function(spin)
self.scroll_activation_delay = spin.value
self:applyScrollSettings()
if touchmenu_instance then touchmenu_instance:updateItems() end
end
}
UIManager:show(widget)
end,
},
}
}
if self._inertial_scroll_supported then
-- Add it before "Activation delay" to keep checkboxes together
table.insert(menu_items.scrolling.sub_item_table, 4, {
text = _("Allow inertial scrolling"),
enabled_func = function()
return self.scroll_method == self.SCROLL_METHOD_CLASSIC
end,
checked_func = function()
return self.scroll_method == self.SCROLL_METHOD_CLASSIC and self.inertial_scroll
end,
callback = function()
self.inertial_scroll = not self.inertial_scroll
self:applyScrollSettings()
end,
})
end
end
function ReaderScrolling:onReaderReady()
-- We don't know if the gestures plugin is loaded in :init(), but we know it here
self.scroll_activation_delay = G_reader_settings:readSetting("scroll_activation_delay")
or self:getDefaultScrollActivationDelay()
self:applyScrollSettings()
end
function ReaderScrolling:applyScrollSettings()
G_reader_settings:saveSetting("scroll_method", self.scroll_method)
G_reader_settings:saveSetting("inertial_scroll", self.inertial_scroll)
if self.scroll_activation_delay == self:getDefaultScrollActivationDelay() then
G_reader_settings:delSetting("scroll_activation_delay")
else
G_reader_settings:saveSetting("scroll_activation_delay", self.scroll_activation_delay)
end
if self.scroll_method == self.SCROLL_METHOD_CLASSIC then
self._inertial_scroll_enabled = self.inertial_scroll
else
self._inertial_scroll_enabled = false
end
self:setupTouchZones()
self.ui:handleEvent(Event:new("ScrollSettingsUpdated", self.scroll_method,
self._inertial_scroll_enabled, self.scroll_activation_delay))
end
function ReaderScrolling:setupTouchZones()
self.ges_events = {}
self.onGesture = nil
local zones = {
{
id = "inertial_scrolling_touch",
ges = "touch",
screen_zone = {
ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1,
},
handler = function(ges)
-- A touch might set the start of the first pan event,
-- that we need to compute its duration
self._last_manual_scroll_timev = ges.time
-- If we are scrolling, a touch cancels it.
-- We want its release (which will trigger a tap) to not change pages.
-- This also allows a pan following this touch to skip any scroll
-- activation delay
self._cancelled_by_touch = self._inertial_scroll_action
and self._inertial_scroll_action(false)
or false
end,
},
{
id = "inertial_scrolling_tap",
ges = "tap",
screen_zone = {
ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1,
},
overrides = {
"tap_forward",
"tap_backward",
"readermenu_tap",
"readermenu_ext_tap",
"readerconfigmenu_tap",
"readerconfigmenu_ext_tap",
"readerfooter_tap",
"readerhighlight_tap",
"tap_link",
},
handler = function()
-- Ignore tap if cancelled by its initial touch
if self._cancelled_by_touch then
self._cancelled_by_touch = false
return true
end
-- Otherwise, let it be handled by other tap handlers
end,
},
}
if self._inertial_scroll_enabled then
self.ui:registerTouchZones(zones)
else
self.ui:unRegisterTouchZones(zones)
end
end
function ReaderScrolling:isInertialScrollingEnabled()
return self._inertial_scroll_enabled
end
function ReaderScrolling:setInertialScrollCallbacks(do_scroll_callback, scroll_done_callback)
self._do_scroll_callback = do_scroll_callback
self._scroll_done_callback = scroll_done_callback
end
function ReaderScrolling:startInertialScroll()
if not self._inertial_scroll_enabled then
return false
end
return self._inertial_scroll_action(true)
end
function ReaderScrolling:cancelInertialScroll()
if not self._inertial_scroll_enabled then
return
end
return self._inertial_scroll_action(false)
end
function ReaderScrolling:cancelledByTouch()
return self._cancelled_by_touch
end
function ReaderScrolling:accountManualScroll(dy, timev)
if not self._inertial_scroll_enabled then
return
end
self._last_manual_scroll_dy = dy
self._last_manual_scroll_duration = timev - self._last_manual_scroll_timev
self._last_manual_scroll_timev = timev
end
function ReaderScrolling:_setupAction()
self._inertial_scroll_action = function(action)
-- action can be:
-- - true: stop any previous ongoing inertial scroll, then start a new one
-- (returns true if we started one)
-- - false: just stop any previous ongoing inertial scroll
-- (returns true if we did cancel one)
if action ~= nil then
local cancelled = false
if self._inertial_scroll_action_scheduled then
UIManager:unschedule(self._inertial_scroll_action)
self._inertial_scroll_action_scheduled = false
cancelled = true
self._scroll_done_callback()
logger.dbg("inertial scrolling cancelled")
end
if action == false then
self._last_manual_scroll_dy = 0
return cancelled
end
-- Initiate inertial scrolling (action=true), unless we should not
if UIManager:getTime() - self._last_manual_scroll_timev >= self.pause_before_release_cancel_duration then
-- but not if no finger move for 0.3s before finger up
self._last_manual_scroll_dy = 0
return false
end
if self._last_manual_scroll_duration:isZero() or self._last_manual_scroll_dy == 0 then
return false
end
-- Initial velocity is the one of the last pan scroll given to accountManualScroll()
local delay = self._last_manual_scroll_duration:tousecs()
if delay < 1 then delay = 1 end -- safety check
self._velocity = self._last_manual_scroll_dy * 1000000 / delay
self._last_manual_scroll_dy = 0
self._inertial_scroll_action_scheduled = true
-- We'll keep re-scheduling this same action, which will do
-- alternatively thanks to the _just_reschedule flag:
-- * either, in _inertial_scroll_interval, do a scroll
-- * or, then, at next tick, reschedule 1)
-- This is needed as the first one will cause a repaint that
-- may take more than _inertial_scroll_interval, which if we
-- didn't do that could be run before we process any input,
-- not allowing us to interrupt this inertial scrolling.
self._just_reschedule = false
UIManager:scheduleIn(self._inertial_scroll_interval, self._inertial_scroll_action)
-- self._stats_scroll_iterations = 0
-- self._stats_scroll_distance = 0
logger.dbg("inertial scrolling started")
return true
end
if not self._inertial_scroll_action_scheduled then
-- Safety check, shouldn't happen
return
end
if not self.ui.document then
-- might happen if scheduled and run after document is closed
return
end
if self._just_reschedule then
-- just re-schedule this, so a real scrolling is done after the delay
self._just_reschedule = false
UIManager:scheduleIn(self._inertial_scroll_interval, self._inertial_scroll_action)
return
end
-- Decrease velocity at each step
self._velocity = self._velocity * math.pow(self.scroll_friction, self._inertial_scroll_interval)
local dist = math.floor(self._velocity * self._inertial_scroll_interval)
if math.abs(dist) < self.end_scroll_dist then
-- Decrease it even more so scrolling stops sooner
self._velocity = self._velocity / 1.5
end
-- self._stats_scroll_iterations = self._stats_scroll_iterations + 1
-- self._stats_scroll_distance = self._stats_scroll_distance + dist
logger.dbg("inertial scrolling by", dist)
local did_scroll = self._do_scroll_callback(dist)
if did_scroll and math.abs(dist) > 2 then
-- Schedule at next tick the real re-scheduling
self._just_reschedule = true
UIManager:nextTick(self._inertial_scroll_action)
return
end
-- We're done
self._inertial_scroll_action_scheduled = false
self._scroll_done_callback()
logger.dbg("inertial scrolling ended")
--[[
local Notification = require("ui/widget/notification")
UIManager:show(Notification:new{
text = string.format("%d iterations, %d px scrolled",
self._stats_scroll_iterations, self._stats_scroll_distance),
})
]]--
end
end
return ReaderScrolling

@ -73,6 +73,9 @@ local ReaderView = OverlapGroup:extend{
flipping_visible = false,
-- to ensure periodic flush of settings
settings_last_save_tv = nil,
-- might be directly updated by readerpaging/readerrolling when
-- they handle some panning/scrolling, to request "fast" refreshes
currently_scrolling = false,
}
function ReaderView:init()
@ -614,7 +617,7 @@ function ReaderView:recalculate()
end
-- Flag a repaint so self:paintTo will be called
-- NOTE: This is also unfortunately called during panning, essentially making sure we'll never be using "fast" for pans ;).
UIManager:setDirty(self.dialog, "partial")
UIManager:setDirty(self.dialog, self.currently_scrolling and "fast" or "partial")
end
function ReaderView:PanningUpdate(dx, dy)

@ -33,6 +33,7 @@ local ReaderFont = require("apps/reader/modules/readerfont")
local ReaderGoto = require("apps/reader/modules/readergoto")
local ReaderHinting = require("apps/reader/modules/readerhinting")
local ReaderHighlight = require("apps/reader/modules/readerhighlight")
local ReaderScrolling = require("apps/reader/modules/readerscrolling")
local ReaderKoptListener = require("apps/reader/modules/readerkoptlistener")
local ReaderLink = require("apps/reader/modules/readerlink")
local ReaderMenu = require("apps/reader/modules/readermenu")
@ -334,6 +335,13 @@ function ReaderUI:init()
})
end
self.disable_double_tap = G_reader_settings:nilOrTrue("disable_double_tap")
-- scrolling (scroll settings + inertial scrolling)
self:registerModule("scrolling", ReaderScrolling:new{
pan_rate = pan_rate,
dialog = self.dialog,
ui = self,
view = self.view,
})
-- back location stack
self:registerModule("back", ReaderBack:new{
ui = self,

@ -217,6 +217,7 @@ function Device:init()
relative_delayed = fake_ges.relative_delayed,
pos = pos,
time = ev.time,
from_mousewheel = true,
}
local fake_pan_ev = Event:new("Pan", nil, fake_ges)
local fake_release_ev = Event:new("Gesture", fake_ges_release)

@ -134,6 +134,7 @@ local order = {
"----------------------------",
"menu_activate",
"page_turns",
"scrolling",
"ignore_hold_corners",
"screen_disable_double_tab",
},

@ -22,6 +22,7 @@ local UIManager = {
FULL_REFRESH_COUNT =
G_reader_settings:isTrue("night_mode") and G_reader_settings:readSetting("night_full_refresh_count") or G_reader_settings:readSetting("full_refresh_count") or DEFAULT_FULL_REFRESH_COUNT,
refresh_count = 0,
currently_scrolling = false,
-- How long to wait between ZMQ wakeups: 50ms.
ZMQ_TIMEOUT = 50 * 1000,
@ -1293,6 +1294,10 @@ function UIManager:_refresh(mode, region, dither)
return
end
end
-- Downgrade all refreshes to "fast" when ReaderPaging or ReaderScrolling have set this flag
if self.currently_scrolling then
mode = "fast"
end
if not region and mode == "full" then
self.refresh_count = 0 -- reset counter on explicit full refresh
end

@ -1103,7 +1103,8 @@ function Gestures:gestureAction(action, ges)
or (ges.ges == "hold" and self.ignore_hold_corners) then
return
else
Dispatcher:execute(self.ui, action_list, ges)
self.ui:handleEvent(Event:new("HandledAsSwipe"))
Dispatcher:execute(self.ui, action_list, ges)
end
return true
end

Loading…
Cancel
Save