From f42f05f743c7df0115e6267297abc8e682ce6e92 Mon Sep 17 00:00:00 2001 From: milanboers Date: Mon, 22 Oct 2012 19:20:48 +0200 Subject: [PATCH 1/3] Made scrolling more intuitive. Tapping/clicking is now immediately (within 55ms by default) being passed to the children if not scrolling instead of a delay, except for when there was a scroll move within this time. Also takes into account more than one move for a better and more intuitive experience on touch devices. Tested on desktop and phone. --- kivy/config.py | 4 +- kivy/uix/scrollview.py | 148 ++++++++++++++++++++++++++++++----------- 2 files changed, 114 insertions(+), 38 deletions(-) diff --git a/kivy/config.py b/kivy/config.py index 7be5a88e8..c3f1fb98d 100644 --- a/kivy/config.py +++ b/kivy/config.py @@ -388,9 +388,11 @@ if not environ.get('KIVY_DOC_INCLUDE'): elif version == 3: # add token for scrollview - Config.setdefault('widgets', 'scroll_timeout', '250') + Config.setdefault('widgets', 'scroll_timeout', '55') Config.setdefault('widgets', 'scroll_distance', '20') Config.setdefault('widgets', 'scroll_friction', '1.') + Config.setdefualt('widgets', 'scroll_stoptime', '300') + Config.setdefault('widgets', 'scroll_moves', '5') # remove old list_* token Config.remove_option('widgets', 'list_friction') diff --git a/kivy/uix/scrollview.py b/kivy/uix/scrollview.py index 8293bde23..0c6c9ba9c 100644 --- a/kivy/uix/scrollview.py +++ b/kivy/uix/scrollview.py @@ -82,23 +82,47 @@ If you want to reduce the default timeout, you can set:: __all__ = ('ScrollView', ) +from copy import copy from functools import partial from kivy.animation import Animation from kivy.config import Config from kivy.clock import Clock from kivy.uix.stencilview import StencilView from kivy.properties import NumericProperty, BooleanProperty, AliasProperty, \ - ObjectProperty, ListProperty + ObjectProperty, ListProperty # When we are generating documentation, Config doesn't exist -_scroll_timeout = _scroll_distance = _scroll_friction = 0 +_scroll_timeout = _scroll_stoptime = _scroll_distance = _scroll_friction = 0 if Config: _scroll_timeout = Config.getint('widgets', 'scroll_timeout') + _scroll_stoptime = Config.getint('widgets', 'scroll_stoptime') _scroll_distance = Config.getint('widgets', 'scroll_distance') + _scroll_moves = Config.getint('widgets', 'scroll_moves') _scroll_friction = Config.getfloat('widgets', 'scroll_friction') +class FixedList(list): + '''A list. In addition, you can specify the maximum length. + This will save memory. + ''' + def __init__(self, maxlength=0, *args, **kwargs): + super(FixedList, self).__init__(*args, **kwargs) + self.maxlength = maxlength + + def append(self, x): + super(FixedList, self).append(x) + self._cut() + + def extend(self, L): + super(FixedList, self).append(L) + self._cut() + + def _cut(self): + while len(self) > self.maxlength: + self.pop(0) + + class ScrollView(StencilView): '''ScrollView class. See module documentation for more information. ''' @@ -206,10 +230,12 @@ class ScrollView(StencilView): return uid = self._get_uid() touch = self._touch - mode = touch.ud[uid]['mode'] - if mode == 'unknown': - touch.ungrab(self) - self._touch = None + ud = touch.ud[uid] + if ud['mode'] == 'unknown' and \ + not ud['user_stopped'] and \ + touch.dx + touch.dy == 0: + #touch.ungrab(self) + #self._touch = None # correctly calculate the position of the touch inside the # scrollview touch.push() @@ -231,31 +257,40 @@ class ScrollView(StencilView): super(ScrollView, self).on_touch_up(touch) touch.grab_current = None - def _do_animation(self, touch): + def _do_animation(self, touch, *largs): uid = self._get_uid() ud = touch.ud[uid] dt = touch.time_end - ud['time'] - if dt > self.scroll_timeout / 1000.: - self._tdx = self._tdy = self._ts = 0 + avgdx = sum([move.dx for move in ud['moves']]) / len(ud['moves']) + avgdy = sum([move.dy for move in ud['moves']]) / len(ud['moves']) + if ud['user_stopped'] and \ + abs(avgdy) < self.scroll_distance and \ + abs(avgdx) < self.scroll_distance: + return + if ud['same'] > self.scroll_stoptime / 1000.: return dt = ud['dt'] if dt == 0: self._tdx = self._tdy = self._ts = 0 return - dx = touch.dx - dy = touch.dy + dx = avgdx + dy = avgdy self._sx = ud['sx'] self._sy = ud['sy'] self._tdx = dx = dx / dt self._tdy = dy = dy / dt - if abs(dx) < 10 and abs(dy) < 10: - return self._ts = self._tsn = touch.time_update + Clock.unschedule(self._update_animation) Clock.schedule_interval(self._update_animation, 0) def _update_animation(self, dt): if self._touch is not None or self._ts == 0: + touch = self._touch + uid = self._get_uid() + ud = touch.ud[uid] + # scrolling stopped by user input + ud['user_stopped'] = True return False self._tsn += dt global_dt = self._tsn - self._ts @@ -268,6 +303,7 @@ class ScrollView(StencilView): (self.do_scroll_y and not self.do_scroll_x and test_dy) or\ (self.do_scroll_x and self.do_scroll_y and test_dx and test_dy): self._ts = 0 + # scrolling stopped by friction return False dx *= dt dy *= dt @@ -284,8 +320,18 @@ class ScrollView(StencilView): self.scroll_y -= sy self._scroll_y_mouse = self.scroll_y if ssx == self.scroll_x and ssy == self.scroll_y: + # scrolling stopped by end of box return False + def _update_delta(self, dt): + touch = self._touch + uid = self._get_uid() + ud = touch.ud[uid] + if touch.dx + touch.dy != 0: + ud['same'] += dt + else: + ud['same'] = 0 + def on_touch_down(self, touch): if not self.collide_point(*touch.pos): touch.ud[self._get_uid('svavoid')] = True @@ -299,12 +345,10 @@ class ScrollView(StencilView): vp = self._viewport if vp.height > self.height: # let's say we want to move over 40 pixels each scroll - d = (vp.height - self.height) - d = self.scroll_distance / float(d) if touch.button == 'scrollup': - syd = self._scroll_y_mouse - d + syd = self._scroll_y_mouse elif touch.button == 'scrolldown': - syd = self._scroll_y_mouse + d + syd = self._scroll_y_mouse self._scroll_y_mouse = scroll_y = min(max(syd, 0), 1) Animation.stop_all(self, 'scroll_y') Animation(scroll_y=scroll_y, d=.3, t='out_quart').start(self) @@ -319,9 +363,14 @@ class ScrollView(StencilView): 'sx': self.scroll_x, 'sy': self.scroll_y, 'dt': None, - 'time': touch.time_start} + 'time': touch.time_start, + 'user_stopped': False, + 'same': 0, + 'moves': FixedList(self.scroll_moves)} + + Clock.schedule_interval(self._update_delta, 0) Clock.schedule_once(self._change_touch_mode, - self.scroll_timeout / 1000.) + self.scroll_timeout / 1000.) return True def on_touch_move(self, touch): @@ -335,23 +384,22 @@ class ScrollView(StencilView): uid = self._get_uid() ud = touch.ud[uid] mode = ud['mode'] + ud['moves'].append(copy(touch)) # seperate the distance to both X and Y axis. # if a distance is reach, but on the inverse axis, stop scroll mode ! if mode == 'unknown': distance = abs(touch.ox - touch.x) - if distance > self.scroll_distance: - if not self.do_scroll_x: - self._change_touch_mode() - return - mode = 'scroll' + if not self.do_scroll_x: + self._change_touch_mode() + return + mode = 'scroll' distance = abs(touch.oy - touch.y) - if distance > self.scroll_distance: - if not self.do_scroll_y: - self._change_touch_mode() - return - mode = 'scroll' + if not self.do_scroll_y: + self._change_touch_mode() + return + mode = 'scroll' if mode == 'scroll': ud['mode'] = mode @@ -374,7 +422,7 @@ class ScrollView(StencilView): # never ungrabed, cause their on_touch_up will be never called. # base.py: the me.grab_list[:] => it's a copy, and we are already # iterate on it. - + Clock.unschedule(self._update_delta) if self._get_uid('svavoid') in touch.ud: return @@ -385,10 +433,12 @@ class ScrollView(StencilView): touch.ungrab(self) self._touch = None uid = self._get_uid() - mode = touch.ud[uid]['mode'] - if mode == 'unknown': + ud = touch.ud[uid] + if ud['mode'] == 'unknown': # we must do the click at least.. - super(ScrollView, self).on_touch_down(touch) + # only send the click if it was not a click to stop autoscrolling + if not ud['user_stopped']: + super(ScrollView, self).on_touch_down(touch) Clock.schedule_once(partial(self._do_touch_up, touch), .1) elif self.auto_scroll: self._do_animation(touch) @@ -433,10 +483,35 @@ class ScrollView(StencilView): default to 1, according to the default value in user configuration. ''' + scroll_moves = NumericProperty(_scroll_moves) + '''The speed of automatic scrolling is based on previous touch moves. This + is to prevent accidental slowing down by the user at the end of the swipe + to slow down the automatic scrolling. + The moves property specifies the amount of previous scrollmoves that + should be taken into consideration when calculating the automatic scrolling + speed. + + :data:`scroll_moves` is a :class:`~kivy.properties.NumericProperty`, + default to 5. + ''' + + scroll_stoptime = NumericProperty(_scroll_stoptime) + '''Time after which user input not moving will disable autoscroll for that + move. If the user has not moved within the stoptime, autoscroll will not + start. + This is to prevent autoscroll to trigger while the user has slowed down + on purpose to prevent this. + + :data:`scroll_stoptime` is a :class:`~kivy.properties.NumericProperty`, + default to 300 (milliseconds) + ''' + scroll_distance = NumericProperty(_scroll_distance) '''Distance to move before scrolling the :class:`ScrollView`, in pixels. As soon as the distance has been traveled, the :class:`ScrollView` will start to scroll, and no touch event will go to children. + It is advisable that you base this value on the dpi of your target device's + screen. :data:`scroll_distance` is a :class:`~kivy.properties.NumericProperty`, default to 20 (pixels), according to the default value in user @@ -445,11 +520,11 @@ class ScrollView(StencilView): scroll_timeout = NumericProperty(_scroll_timeout) '''Timeout allowed to trigger the :data:`scroll_distance`, in milliseconds. - If the timeout is reached, the scrolling will be disabled, and the touch - event will go to the children. + If the user has not moved :data:`scroll_distance` within the timeout, + the scrolling will be disabled, and the touch event will go to the children. :data:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty`, - default to 250 (milliseconds), according to the default value in user + default to 55 (milliseconds), according to the default value in user configuration. ''' @@ -601,4 +676,3 @@ class ScrollView(StencilView): if value: value.bind(size=self._set_viewport_size) self._viewport_size = value.size - From c7d5e98632c6f06a6a90bfa759f3d90a45981bad Mon Sep 17 00:00:00 2001 From: milanboers Date: Mon, 22 Oct 2012 19:22:23 +0200 Subject: [PATCH 2/3] Config documentation update. --- kivy/config.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/kivy/config.py b/kivy/config.py index c3f1fb98d..50100f42b 100644 --- a/kivy/config.py +++ b/kivy/config.py @@ -138,6 +138,16 @@ Available configuration tokens property in :class:`~kivy.uix.scrollview.Scrollview` widget. Check the widget documentation for more information. + `scroll_stoptime`: int + Default value of :data:`~kivy.uix.scrollview.Scrollview.scroll_stoptime` + property in :class:`~kivy.uix.scrollview.Scrollview` widget. + Check the widget documentation for more information. + + `scroll_moves`: int + Default value of :data:`~kivy.uix.scrollview.Scrollview.scroll_moves` + property in :class:`~kivy.uix.scrollview.Scrollview` widget. + Check the widget documentation for more information. + :modules: You can activate modules with this syntax:: From b42ddc4222c4035e742fabb6e21f558bb7c952ad Mon Sep 17 00:00:00 2001 From: milanboers Date: Tue, 23 Oct 2012 07:18:17 +0200 Subject: [PATCH 3/3] Fixed typo. --- kivy/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kivy/config.py b/kivy/config.py index 50100f42b..c198f0376 100644 --- a/kivy/config.py +++ b/kivy/config.py @@ -401,7 +401,7 @@ if not environ.get('KIVY_DOC_INCLUDE'): Config.setdefault('widgets', 'scroll_timeout', '55') Config.setdefault('widgets', 'scroll_distance', '20') Config.setdefault('widgets', 'scroll_friction', '1.') - Config.setdefualt('widgets', 'scroll_stoptime', '300') + Config.setdefault('widgets', 'scroll_stoptime', '300') Config.setdefault('widgets', 'scroll_moves', '5') # remove old list_* token