4
.. versionadded:: 1.0.4
6
The :class:`ScrollView` widget provides a scrollable/pannable viewport that is
7
clipped at the scrollview's bounding box.
13
The ScrollView accepts only one child and applies a viewport/window to
14
it according to the :attr:`ScrollView.scroll_x` and
15
:attr:`ScrollView.scroll_y` properties. Touches are analyzed to
16
determine if the user wants to scroll or control the child in some
17
other manner - you cannot do both at the same time. To determine if
18
interaction is a scrolling gesture, these properties are used:
20
- :attr:`ScrollView.scroll_distance`: the minimum distance to travel,
21
defaults to 20 pixels.
22
- :attr:`ScrollView.scroll_timeout`: the maximum time period, defaults
25
If a touch travels :attr:`~ScrollView.scroll_distance` pixels within the
26
:attr:`~ScrollView.scroll_timeout` period, it is recognized as a scrolling
27
gesture and translation (scroll/pan) will begin. If the timeout occurs, the
28
touch down event is dispatched to the child instead (no translation).
30
The default value for those settings can be changed in the configuration file::
36
.. versionadded:: 1.1.1
38
ScrollView now animates scrolling in Y when a mousewheel is used.
41
Limiting to the X or Y Axis
42
---------------------------
44
By default, the ScrollView allows scrolling in both the X and Y axes. You can
45
explicitly disable scrolling on an axis by setting
46
:attr:`ScrollView.do_scroll_x` or :attr:`ScrollView.do_scroll_y` to False.
49
Managing the Content Size and Position
50
--------------------------------------
52
ScrollView manages the position of its children similarly to a
53
RelativeLayout (see :mod:`~kivy.uix.relativelayout`) but not the size. You must
54
carefully specify the `size_hint` of your content to get the desired
57
By default, size_hint is (1, 1), so the content size will fit your ScrollView
58
exactly (you will have nothing to scroll). You must deactivate at least one of
59
the size_hint instructions (x or y) of the child to enable scrolling.
61
To scroll a :class:`GridLayout` on Y-axis/vertically, set the child's width
62
identical to that of the ScrollView (size_hint_x=1, default), and set the
63
size_hint_y property to None::
65
layout = GridLayout(cols=1, spacing=10, size_hint_y=None)
66
# Make sure the height is such that there is something to scroll.
67
layout.bind(minimum_height=layout.setter('height'))
69
btn = Button(text=str(i), size_hint_y=None, height=40)
70
layout.add_widget(btn)
71
root = ScrollView(size_hint=(None, None), size=(400, 400))
72
root.add_widget(layout)
78
.. versionadded:: 1.7.0
80
When scrolling would exceed the bounds of the :class:`ScrollView`, it
81
uses a :class:`~kivy.effects.scroll.ScrollEffect` to handle the
82
overscroll. These effects can perform actions like bouncing back,
83
changing opacity, or simply preventing scrolling beyond the normal
84
boundaries. Note that complex effects may perform many computations,
85
which can be slow on weaker hardware.
87
You can change what effect is being used by setting
88
:attr:`ScrollView.effect_cls` to any effect class. Current options
91
- :class:`~kivy.effects.scroll.ScrollEffect`: Does not allow
92
scrolling beyond the :class:`ScrollView` boundaries.
93
- :class:`~kivy.effects.dampedscroll.DampedScrollEffect`: The
94
current default. Allows the user to scroll beyond the normal
95
boundaries, but has the content spring back once the
96
touch/click is released.
97
- :class:`~kivy.effects.opacityscroll.OpacityScrollEffect`: Similar
98
to the :class:`~kivy.effect.dampedscroll.DampedScrollEffect`, but
99
also reduces opacity during overscroll.
101
You can also create your own scroll effect by subclassing one of these,
102
then pass it as the :attr:`~ScrollView.effect_cls` in the same way.
104
Alternatively, you can set :attr:`ScrollView.effect_x` and/or
105
:attr:`ScrollView.effect_y` to an *instance* of the effect you want to
106
use. This will override the default effect set in
107
:attr:`ScrollView.effect_cls`.
109
All the effects are located in the :mod:`kivy.effects`.
113
__all__ = ('ScrollView', )
115
from functools import partial
116
from kivy.animation import Animation
117
from kivy.compat import string_types
118
from kivy.config import Config
119
from kivy.clock import Clock
120
from kivy.factory import Factory
121
from kivy.uix.stencilview import StencilView
122
from kivy.metrics import sp, dp
123
from kivy.effects.dampedscroll import DampedScrollEffect
124
from kivy.properties import NumericProperty, BooleanProperty, AliasProperty, \
125
ObjectProperty, ListProperty, ReferenceListProperty, OptionProperty
126
from kivy.uix.behaviors import FocusBehavior
129
# When we are generating documentation, Config doesn't exist
130
_scroll_timeout = _scroll_distance = 0
132
_scroll_timeout = Config.getint('widgets', 'scroll_timeout')
133
_scroll_distance = sp(Config.getint('widgets', 'scroll_distance'))
136
class ScrollView(StencilView):
137
'''ScrollView class. See module documentation for more information.
141
Generic event fired when scrolling starts from touch.
143
Generic event fired when scrolling move from touch.
145
Generic event fired when scrolling stops from touch.
147
.. versionchanged:: 1.9.0
148
`on_scroll_start`, `on_scroll_move` and `on_scroll_stop` events are
149
now dispatched when scrolling to handle nested ScrollViews.
151
.. versionchanged:: 1.7.0
152
`auto_scroll`, `scroll_friction`, `scroll_moves`, `scroll_stoptime' has
153
been deprecated, use :attr:`effect_cls` instead.
156
scroll_distance = NumericProperty(_scroll_distance)
157
'''Distance to move before scrolling the :class:`ScrollView`, in pixels. As
158
soon as the distance has been traveled, the :class:`ScrollView` will start
159
to scroll, and no touch event will go to children.
160
It is advisable that you base this value on the dpi of your target device's
163
:attr:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` and
164
defaults to 20 (pixels), according to the default value in user
168
scroll_wheel_distance = NumericProperty(20)
169
'''Distance to move when scrolling with a mouse wheel.
170
It is advisable that you base this value on the dpi of your target device's
173
.. versionadded:: 1.8.0
175
:attr:`scroll_wheel_distance` is a
176
:class:`~kivy.properties.NumericProperty` , defaults to 20 pixels.
179
scroll_timeout = NumericProperty(_scroll_timeout)
180
'''Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds.
181
If the user has not moved :attr:`scroll_distance` within the timeout,
182
the scrolling will be disabled, and the touch event will go to the
185
:attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and
186
defaults to 55 (milliseconds) according to the default value in user
189
.. versionchanged:: 1.5.0
190
Default value changed from 250 to 55.
193
scroll_x = NumericProperty(0.)
194
'''X scrolling value, between 0 and 1. If 0, the content's left side will
195
touch the left side of the ScrollView. If 1, the content's right side will
196
touch the right side.
198
This property is controled by :class:`ScrollView` only if
199
:attr:`do_scroll_x` is True.
201
:attr:`scroll_x` is a :class:`~kivy.properties.NumericProperty` and
205
scroll_y = NumericProperty(1.)
206
'''Y scrolling value, between 0 and 1. If 0, the content's bottom side will
207
touch the bottom side of the ScrollView. If 1, the content's top side will
210
This property is controled by :class:`ScrollView` only if
211
:attr:`do_scroll_y` is True.
213
:attr:`scroll_y` is a :class:`~kivy.properties.NumericProperty` and
217
do_scroll_x = BooleanProperty(True)
218
'''Allow scroll on X axis.
220
:attr:`do_scroll_x` is a :class:`~kivy.properties.BooleanProperty` and
224
do_scroll_y = BooleanProperty(True)
225
'''Allow scroll on Y axis.
227
:attr:`do_scroll_y` is a :class:`~kivy.properties.BooleanProperty` and
231
def _get_do_scroll(self):
232
return (self.do_scroll_x, self.do_scroll_y)
234
def _set_do_scroll(self, value):
235
if type(value) in (list, tuple):
236
self.do_scroll_x, self.do_scroll_y = value
238
self.do_scroll_x = self.do_scroll_y = bool(value)
239
do_scroll = AliasProperty(_get_do_scroll, _set_do_scroll,
240
bind=('do_scroll_x', 'do_scroll_y'))
241
'''Allow scroll on X or Y axis.
243
:attr:`do_scroll` is a :class:`~kivy.properties.AliasProperty` of
244
(:attr:`do_scroll_x` + :attr:`do_scroll_y`)
248
# must return (y, height) in %
249
# calculate the viewport size / scrollview size %
250
if self._viewport is None:
252
vh = self._viewport.height
254
if vh < h or vh == 0:
256
ph = max(0.01, h / float(vh))
257
sy = min(1.0, max(0.0, self.scroll_y))
261
vbar = AliasProperty(_get_vbar, None, bind=(
262
'scroll_y', '_viewport', 'viewport_size'))
263
'''Return a tuple of (position, size) of the vertical scrolling bar.
265
.. versionadded:: 1.2.0
267
The position and size are normalized between 0-1, and represent a
268
percentage of the current scrollview height. This property is used
269
internally for drawing the little vertical bar when you're scrolling.
271
:attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly.
275
# must return (x, width) in %
276
# calculate the viewport size / scrollview size %
277
if self._viewport is None:
279
vw = self._viewport.width
281
if vw < w or vw == 0:
283
pw = max(0.01, w / float(vw))
284
sx = min(1.0, max(0.0, self.scroll_x))
288
hbar = AliasProperty(_get_hbar, None, bind=(
289
'scroll_x', '_viewport', 'viewport_size'))
290
'''Return a tuple of (position, size) of the horizontal scrolling bar.
292
.. versionadded:: 1.2.0
294
The position and size are normalized between 0-1, and represent a
295
percentage of the current scrollview height. This property is used
296
internally for drawing the little horizontal bar when you're scrolling.
298
:attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly.
301
bar_color = ListProperty([.7, .7, .7, .9])
302
'''Color of horizontal / vertical scroll bar, in RGBA format.
304
.. versionadded:: 1.2.0
306
:attr:`bar_color` is a :class:`~kivy.properties.ListProperty` and defaults
310
bar_inactive_color = ListProperty([.7, .7, .7, .2])
311
'''Color of horizontal / vertical scroll bar (in RGBA format), when no
314
.. versionadded:: 1.9.0
316
:attr:`bar_inactive_color` is a
317
:class:`~kivy.properties.ListProperty` and defaults to [.7, .7, .7, .2].
320
bar_width = NumericProperty('2dp')
321
'''Width of the horizontal / vertical scroll bar. The width is interpreted
322
as a height for the horizontal bar.
324
.. versionadded:: 1.2.0
326
:attr:`bar_width` is a :class:`~kivy.properties.NumericProperty` and
330
bar_pos_x = OptionProperty('bottom', options=('top', 'bottom'))
331
'''Which side of the ScrollView the horizontal scroll bar should go
332
on. Possible values are 'top' and 'bottom'.
334
.. versionadded:: 1.8.0
336
:attr:`bar_pos_x` is an :class:`~kivy.properties.OptionProperty`,
337
defaults to 'bottom'.
341
bar_pos_y = OptionProperty('right', options=('left', 'right'))
342
'''Which side of the ScrollView the vertical scroll bar should go
343
on. Possible values are 'left' and 'right'.
345
.. versionadded:: 1.8.0
347
:attr:`bar_pos_y` is an :class:`~kivy.properties.OptionProperty` and
352
bar_pos = ReferenceListProperty(bar_pos_x, bar_pos_y)
353
'''Which side of the scroll view to place each of the bars on.
355
:attr:`bar_pos` is a :class:`~kivy.properties.ReferenceListProperty` of
356
(:attr:`bar_pos_x`, :attr:`bar_pos_y`)
359
bar_margin = NumericProperty(0)
360
'''Margin between the bottom / right side of the scrollview when drawing
361
the horizontal / vertical scroll bar.
363
.. versionadded:: 1.2.0
365
:attr:`bar_margin` is a :class:`~kivy.properties.NumericProperty`, default
369
effect_cls = ObjectProperty(DampedScrollEffect, allownone=True)
370
'''Class effect to instanciate for X and Y axis.
372
.. versionadded:: 1.7.0
374
:attr:`effect_cls` is an :class:`~kivy.properties.ObjectProperty` and
375
defaults to :class:`DampedScrollEffect`.
377
.. versionchanged:: 1.8.0
378
If you set a string, the :class:`~kivy.factory.Factory` will be used to
383
effect_x = ObjectProperty(None, allownone=True)
384
'''Effect to apply for the X axis. If None is set, an instance of
385
:attr:`effect_cls` will be created.
387
.. versionadded:: 1.7.0
389
:attr:`effect_x` is an :class:`~kivy.properties.ObjectProperty` and
393
effect_y = ObjectProperty(None, allownone=True)
394
'''Effect to apply for the Y axis. If None is set, an instance of
395
:attr:`effect_cls` will be created.
397
.. versionadded:: 1.7.0
399
:attr:`effect_y` is an :class:`~kivy.properties.ObjectProperty` and
400
defaults to None, read-only.
403
viewport_size = ListProperty([0, 0])
404
'''(internal) Size of the internal viewport. This is the size of your only
405
child in the scrollview.
408
scroll_type = OptionProperty(['content'], options=(['content'], ['bars'],
409
['bars', 'content'], ['content', 'bars']))
410
'''Sets the type of scrolling to use for the content of the scrollview.
411
Available options are: ['content'], ['bars'], ['bars', 'content'].
413
.. versionadded:: 1.8.0
415
:attr:`scroll_type` is a :class:`~kivy.properties.OptionProperty`, defaults
419
# private, for internal use only
421
_viewport = ObjectProperty(None, allownone=True)
422
_bar_color = ListProperty([0, 0, 0, 0])
424
def _set_viewport_size(self, instance, value):
425
self.viewport_size = value
427
def on__viewport(self, instance, value):
429
value.bind(size=self._set_viewport_size)
430
self.viewport_size = value.size
432
__events__ = ('on_scroll_start', 'on_scroll_move', 'on_scroll_stop')
434
def __init__(self, **kwargs):
436
self._trigger_update_from_scroll = Clock.create_trigger(
437
self.update_from_scroll, -1)
438
# create a specific canvas for the viewport
439
from kivy.graphics import PushMatrix, Translate, PopMatrix, Canvas
440
self.canvas_viewport = Canvas()
441
self.canvas = Canvas()
442
with self.canvas_viewport.before:
444
self.g_translate = Translate(0, 0)
445
with self.canvas_viewport.after:
448
super(ScrollView, self).__init__(**kwargs)
450
self.register_event_type('on_scroll_start')
451
self.register_event_type('on_scroll_move')
452
self.register_event_type('on_scroll_stop')
454
# now add the viewport canvas to our canvas
455
self.canvas.add(self.canvas_viewport)
457
effect_cls = self.effect_cls
458
if isinstance(effect_cls, string_types):
459
effect_cls = Factory.get(effect_cls)
460
if self.effect_x is None and effect_cls is not None:
461
self.effect_x = effect_cls(target_widget=self._viewport)
462
if self.effect_y is None and effect_cls is not None:
463
self.effect_y = effect_cls(target_widget=self._viewport)
465
trigger_update_from_scroll = self._trigger_update_from_scroll
466
update_effect_widget = self._update_effect_widget
467
update_effect_x_bounds = self._update_effect_x_bounds
468
update_effect_y_bounds = self._update_effect_y_bounds
470
fbind('width', update_effect_x_bounds)
471
fbind('height', update_effect_y_bounds)
472
fbind('viewport_size', self._update_effect_bounds)
473
fbind('_viewport', update_effect_widget)
474
fbind('scroll_x', trigger_update_from_scroll)
475
fbind('scroll_y', trigger_update_from_scroll)
476
fbind('pos', trigger_update_from_scroll)
477
fbind('size', trigger_update_from_scroll)
479
update_effect_widget()
480
update_effect_x_bounds()
481
update_effect_y_bounds()
483
def on_effect_x(self, instance, value):
485
value.bind(scroll=self._update_effect_x)
486
value.target_widget = self._viewport
488
def on_effect_y(self, instance, value):
490
value.bind(scroll=self._update_effect_y)
491
value.target_widget = self._viewport
493
def on_effect_cls(self, instance, cls):
494
if isinstance(cls, string_types):
495
cls = Factory.get(cls)
496
self.effect_x = cls(target_widget=self._viewport)
497
self.effect_x.bind(scroll=self._update_effect_x)
498
self.effect_y = cls(target_widget=self._viewport)
499
self.effect_y.bind(scroll=self._update_effect_y)
501
def _update_effect_widget(self, *args):
503
self.effect_x.target_widget = self._viewport
505
self.effect_y.target_widget = self._viewport
507
def _update_effect_x_bounds(self, *args):
508
if not self._viewport or not self.effect_x:
510
self.effect_x.min = -(self.viewport_size[0] - self.width)
511
self.effect_x.max = 0
512
self.effect_x.value = self.effect_x.min * self.scroll_x
514
def _update_effect_y_bounds(self, *args):
515
if not self._viewport or not self.effect_y:
517
self.effect_y.min = -(self.viewport_size[1] - self.height)
518
self.effect_y.max = 0
519
self.effect_y.value = self.effect_y.min * self.scroll_y
521
def _update_effect_bounds(self, *args):
522
if not self._viewport:
525
self._update_effect_x_bounds()
527
self._update_effect_y_bounds()
529
def _update_effect_x(self, *args):
531
if not vp or not self.effect_x:
533
sw = vp.width - self.width
536
sx = self.effect_x.scroll / float(sw)
538
self._trigger_update_from_scroll()
540
def _update_effect_y(self, *args):
542
if not vp or not self.effect_y:
544
sh = vp.height - self.height
547
sy = self.effect_y.scroll / float(sh)
549
self._trigger_update_from_scroll()
551
def to_local(self, x, y, **k):
552
tx, ty = self.g_translate.xy
553
return x - tx, y - ty
555
def to_parent(self, x, y, **k):
556
tx, ty = self.g_translate.xy
557
return x + tx, y + ty
559
def _apply_transform(self, m, pos=None):
560
tx, ty = self.g_translate.xy
561
m.translate(tx, ty, 0)
562
return super(ScrollView, self)._apply_transform(m, (0, 0))
564
def simulate_touch_down(self, touch):
565
# at this point the touch is in parent coords
567
touch.apply_transform_2d(self.to_local)
568
ret = super(ScrollView, self).on_touch_down(touch)
572
def on_touch_down(self, touch):
573
if self.dispatch('on_scroll_start', touch):
578
def on_scroll_start(self, touch, check_children=True):
581
touch.apply_transform_2d(self.to_local)
582
if self.dispatch_children('on_scroll_start', touch):
586
if not self.collide_point(*touch.pos):
587
touch.ud[self._get_uid('svavoid')] = True
591
if self._touch or (not (self.do_scroll_x or self.do_scroll_y)):
592
return self.simulate_touch_down(touch)
594
# handle mouse scrolling, only if the viewport size is bigger than the
595
# scrollview size, and if the user allowed to do it
599
scroll_type = self.scroll_type
601
scroll_bar = 'bars' in scroll_type
603
# check if touch is in bar_x(horizontal) or bay_y(bertical)
604
ud['in_bar_x'] = ud['in_bar_y'] = False
605
width_scrollable = vp.width > self.width
606
height_scrollable = vp.height > self.height
607
bar_pos_x = self.bar_pos_x[0]
608
bar_pos_y = self.bar_pos_y[0]
610
d = {'b': True if touch.y < self.y + self.bar_width else False,
611
't': True if touch.y > self.top - self.bar_width else False,
612
'l': True if touch.x < self.x + self.bar_width else False,
613
'r': True if touch.x > self.right - self.bar_width else False}
615
if (width_scrollable and d[bar_pos_x]):
616
ud['in_bar_x'] = True
617
if (height_scrollable and d[bar_pos_y]):
618
ud['in_bar_y'] = True
620
if vp and 'button' in touch.profile and \
621
touch.button.startswith('scroll'):
623
m = sp(self.scroll_wheel_distance)
626
if ((btn == 'scrolldown' and self.scroll_y >= 1) or
627
(btn == 'scrollup' and self.scroll_y <= 0) or
628
(btn == 'scrollleft' and self.scroll_x >= 1) or
629
(btn == 'scrollright' and self.scroll_x <= 0)):
632
if (self.effect_x and self.do_scroll_y and height_scrollable
633
and btn in ('scrolldown', 'scrollup')):
634
e = self.effect_x if ud['in_bar_x'] else self.effect_y
636
elif (self.effect_y and self.do_scroll_x and width_scrollable
637
and btn in ('scrollleft', 'scrollright')):
638
e = self.effect_y if ud['in_bar_y'] else self.effect_x
641
if btn in ('scrolldown', 'scrollleft'):
642
e.value = max(e.value - m, e.min)
644
elif btn in ('scrollup', 'scrollright'):
645
e.value = min(e.value + m, e.max)
647
touch.ud[self._get_uid('svavoid')] = True
648
e.trigger_velocity_update()
651
# no mouse scrolling, so the user is going to drag the scrollview with
654
uid = self._get_uid()
655
FocusBehavior.ignored_touch.append(touch)
661
'user_stopped': False,
662
'frames': Clock.frames,
663
'time': touch.time_start}
665
if self.do_scroll_x and self.effect_x and not ud['in_bar_x']:
666
self.effect_x.start(touch.x)
667
self._scroll_x_mouse = self.scroll_x
668
if self.do_scroll_y and self.effect_y and not ud['in_bar_y']:
669
self.effect_y.start(touch.y)
670
self._scroll_y_mouse = self.scroll_y
672
if (ud.get('in_bar_x', False) or ud.get('in_bar_y', False)):
675
Clock.schedule_once(self._change_touch_mode,
676
self.scroll_timeout / 1000.)
677
if scroll_type == ['bars']:
682
def on_touch_move(self, touch):
683
if self._touch is not touch:
686
touch.apply_transform_2d(self.to_local)
687
super(ScrollView, self).on_touch_move(touch)
689
return self._get_uid() in touch.ud
690
if touch.grab_current is not self:
693
if not (self.do_scroll_y or self.do_scroll_x):
694
return super(ScrollView, self).on_touch_move(touch)
696
touch.ud['sv.handled'] = {'x': False, 'y': False}
697
if self.dispatch('on_scroll_move', touch):
700
def on_scroll_move(self, touch):
701
if self._get_uid('svavoid') in touch.ud:
705
touch.apply_transform_2d(self.to_local)
706
if self.dispatch_children('on_scroll_move', touch):
712
uid = self._get_uid()
713
if not uid in touch.ud:
715
return self.on_scroll_start(touch, False)
719
# check if the minimum distance has been travelled
720
if mode == 'unknown' or mode == 'scroll':
721
if not touch.ud['sv.handled']['x'] and self.do_scroll_x \
724
if touch.ud.get('in_bar_x', False):
725
dx = touch.dx / float(width - width * self.hbar[1])
726
self.scroll_x = min(max(self.scroll_x + dx, 0.), 1.)
727
self._trigger_update_from_scroll()
729
if self.scroll_type != ['bars']:
730
self.effect_x.update(touch.x)
731
if self.scroll_x < 0 or self.scroll_x > 1:
734
touch.ud['sv.handled']['x'] = True
735
if not touch.ud['sv.handled']['y'] and self.do_scroll_y \
738
if touch.ud.get('in_bar_y', False):
739
dy = touch.dy / float(height - height * self.vbar[1])
740
self.scroll_y = min(max(self.scroll_y + dy, 0.), 1.)
741
self._trigger_update_from_scroll()
743
if self.scroll_type != ['bars']:
744
self.effect_y.update(touch.y)
745
if self.scroll_y < 0 or self.scroll_y > 1:
748
touch.ud['sv.handled']['y'] = True
750
if mode == 'unknown':
751
ud['dx'] += abs(touch.dx)
752
ud['dy'] += abs(touch.dy)
753
if ud['dx'] or ud['dy'] > self.scroll_distance:
754
if not self.do_scroll_x and not self.do_scroll_y:
755
# touch is in parent, but _change expects window coords
757
touch.apply_transform_2d(self.to_local)
758
touch.apply_transform_2d(self.to_window)
759
self._change_touch_mode()
766
ud['dt'] = touch.time_update - ud['time']
767
ud['time'] = touch.time_update
768
ud['user_stopped'] = True
772
def on_touch_up(self, touch):
773
if self._touch is not touch and self.uid not in touch.ud:
774
# touch is in parents
776
touch.apply_transform_2d(self.to_local)
777
if super(ScrollView, self).on_touch_up(touch):
782
if self.dispatch('on_scroll_stop', touch):
786
def on_scroll_stop(self, touch, check_children=True):
791
touch.apply_transform_2d(self.to_local)
792
if self.dispatch_children('on_scroll_stop', touch):
796
if self._get_uid('svavoid') in touch.ud:
798
if self._get_uid() not in touch.ud:
802
uid = self._get_uid()
804
if self.do_scroll_x and self.effect_x:
805
if not touch.ud.get('in_bar_x', False) and\
806
self.scroll_type != ['bars']:
807
self.effect_x.stop(touch.x)
808
if self.do_scroll_y and self.effect_y and\
809
self.scroll_type != ['bars']:
810
if not touch.ud.get('in_bar_y', False):
811
self.effect_y.stop(touch.y)
812
if ud['mode'] == 'unknown':
813
# we must do the click at least..
814
# only send the click if it was not a click to stop
816
if not ud['user_stopped']:
817
self.simulate_touch_down(touch)
818
Clock.schedule_once(partial(self._do_touch_up, touch), .2)
819
Clock.unschedule(self._update_effect_bounds)
820
Clock.schedule_once(self._update_effect_bounds)
822
# if we do mouse scrolling, always accept it
823
if 'button' in touch.profile and touch.button.startswith('scroll'):
826
return self._get_uid() in touch.ud
828
def scroll_to(self, widget, padding=10, animate=True):
829
'''Scrolls the viewport to ensure that the given widget is visible,
830
optionally with padding and animation. If animate is True (the
831
default), then the default animation parameters will be used.
832
Otherwise, it should be a dict containing arguments to pass to
833
:class:`~kivy.animation.Animation` constructor.
835
.. versionadded:: 1.9.1
840
if isinstance(padding, (int, float)):
841
padding = (padding, padding)
843
pos = self.parent.to_widget(*widget.to_window(*widget.pos))
844
cor = self.parent.to_widget(*widget.to_window(widget.right,
850
dy = self.y - pos[1] + dp(padding[1])
851
elif cor[1] > self.top:
852
dy = self.top - cor[1] - dp(padding[1])
855
dx = self.x - pos[0] + dp(padding[0])
856
elif cor[0] > self.right:
857
dx = self.right - cor[0] - dp(padding[0])
859
dsx, dsy = self.convert_distance_to_scroll(dx, dy)
860
sxp = min(1, max(0, self.scroll_x - dsx))
861
syp = min(1, max(0, self.scroll_y - dsy))
865
animate = {'d': 0.2, 't': 'out_quad'}
866
Animation.stop_all(self, 'scroll_x', 'scroll_y')
867
Animation(scroll_x=sxp, scroll_y=syp, **animate).start(self)
872
def convert_distance_to_scroll(self, dx, dy):
873
'''Convert a distance in pixels to a scroll distance, depending on the
874
content size and the scrollview size.
876
The result will be a tuple of scroll distance that can be added to
877
:data:`scroll_x` and :data:`scroll_y`
879
if not self._viewport:
882
if vp.width > self.width:
883
sw = vp.width - self.width
887
if vp.height > self.height:
888
sh = vp.height - self.height
894
def update_from_scroll(self, *largs):
895
'''Force the reposition of the content, according to current value of
896
:attr:`scroll_x` and :attr:`scroll_y`.
898
This method is automatically called when one of the :attr:`scroll_x`,
899
:attr:`scroll_y`, :attr:`pos` or :attr:`size` properties change, or
900
if the size of the content changes.
902
if not self._viewport:
906
# update from size_hint
907
if vp.size_hint_x is not None:
908
vp.width = vp.size_hint_x * self.width
909
if vp.size_hint_y is not None:
910
vp.height = vp.size_hint_y * self.height
912
if vp.width > self.width:
913
sw = vp.width - self.width
914
x = self.x - self.scroll_x * sw
917
if vp.height > self.height:
918
sh = vp.height - self.height
919
y = self.y - self.scroll_y * sh
921
y = self.top - vp.height
923
# from 1.8.0, we now use a matrix by default, instead of moving the
924
# widget position behind. We set it here, but it will be a no-op most of
927
self.g_translate.xy = x, y
929
# New in 1.2.0, show bar when scrolling happens and (changed in 1.9.0)
930
# fade to bar_inactive_color when no scroll is happening.
931
Clock.unschedule(self._bind_inactive_bar_color)
932
self.funbind('bar_inactive_color', self._change_bar_color)
933
Animation.stop_all(self, '_bar_color')
934
self.fbind('bar_color', self._change_bar_color)
935
self._bar_color = self.bar_color
936
Clock.schedule_once(self._bind_inactive_bar_color, .5)
938
def _bind_inactive_bar_color(self, *l):
939
self.funbind('bar_color', self._change_bar_color)
940
self.fbind('bar_inactive_color', self._change_bar_color)
942
_bar_color=self.bar_inactive_color, d=.5, t='out_quart').start(self)
944
def _change_bar_color(self, inst, value):
945
self._bar_color = value
950
def add_widget(self, widget, index=0):
952
raise Exception('ScrollView accept only one widget')
954
self.canvas = self.canvas_viewport
955
super(ScrollView, self).add_widget(widget, index)
957
self._viewport = widget
958
widget.bind(size=self._trigger_update_from_scroll)
959
self._trigger_update_from_scroll()
961
def remove_widget(self, widget):
963
self.canvas = self.canvas_viewport
964
super(ScrollView, self).remove_widget(widget)
966
if widget is self._viewport:
967
self._viewport = None
969
def _get_uid(self, prefix='sv'):
970
return '{0}.{1}'.format(prefix, self.uid)
972
def _change_touch_mode(self, *largs):
975
uid = self._get_uid()
977
if uid not in touch.ud:
981
if ud['mode'] != 'unknown' or ud['user_stopped']:
983
diff_frames = Clock.frames - ud['frames']
985
# in order to be able to scroll on very slow devices, let at least 3
986
# frames displayed to accumulate some velocity. And then, change the
987
# touch mode. Otherwise, we might never be able to compute velocity, and
988
# no way to scroll it. See #1464 and #1499
990
Clock.schedule_once(self._change_touch_mode, 0)
993
if self.do_scroll_x and self.effect_x:
994
self.effect_x.cancel()
995
if self.do_scroll_y and self.effect_y:
996
self.effect_y.cancel()
997
# XXX the next line was in the condition. But this stop
998
# the possibily to "drag" an object out of the scrollview in the
999
# non-used direction: if you have an horizontal scrollview, a
1000
# vertical gesture will not "stop" the scroll view to look for an
1001
# horizontal gesture, until the timeout is done.
1002
# and touch.dx + touch.dy == 0:
1005
# touch is in window coords
1007
touch.apply_transform_2d(self.to_widget)
1008
touch.apply_transform_2d(self.to_parent)
1009
self.simulate_touch_down(touch)
1013
def _do_touch_up(self, touch, *largs):
1014
# touch is in window coords
1016
touch.apply_transform_2d(self.to_widget)
1017
super(ScrollView, self).on_touch_up(touch)
1019
# don't forget about grab event!
1020
for x in touch.grab_list[:]:
1021
touch.grab_list.remove(x)
1025
touch.grab_current = x
1026
# touch is in window coords
1028
touch.apply_transform_2d(self.to_widget)
1029
super(ScrollView, self).on_touch_up(touch)
1031
touch.grab_current = None
1034
if __name__ == '__main__':
1035
from kivy.app import App
1037
from kivy.uix.gridlayout import GridLayout
1038
from kivy.uix.button import Button
1040
class ScrollViewApp(App):
1043
layout1 = GridLayout(cols=4, spacing=10, size_hint=(None, None))
1044
layout1.bind(minimum_height=layout1.setter('height'),
1045
minimum_width=layout1.setter('width'))
1047
btn = Button(text=str(i), size_hint=(None, None),
1049
layout1.add_widget(btn)
1050
scrollview1 = ScrollView(bar_width='2dp')
1051
scrollview1.add_widget(layout1)
1053
layout2 = GridLayout(cols=4, spacing=10, size_hint=(None, None))
1054
layout2.bind(minimum_height=layout2.setter('height'),
1055
minimum_width=layout2.setter('width'))
1057
btn = Button(text=str(i), size_hint=(None, None),
1059
layout2.add_widget(btn)
1060
scrollview2 = ScrollView(scroll_type=['bars'],
1062
scroll_wheel_distance=100)
1063
scrollview2.add_widget(layout2)
1065
root = GridLayout(cols=2)
1066
root.add_widget(scrollview1)
1067
root.add_widget(scrollview2)
1070
ScrollViewApp().run()