Mercurial > libervia-desktop-kivy
comparison cagou/backport/carousel.py @ 323:5bd583d00594
backport: added a new "backport" module for using unreleased code from Kivy:
Carousel has been backported from Kivy 2.0, because a couple of bugs hitting Cagou are fixed
there (notably https://github.com/kivy/kivy/issues/6370). The issue was specially visible
when sliding chat widgets.
If a version >= 2.0 of kivy is used, a warning will be displayed to indicated that the
backport can be removed.
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 06 Dec 2019 13:23:03 +0100 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
322:e2b51663d8b8 | 323:5bd583d00594 |
---|---|
1 ''' | |
2 Carousel | |
3 ======== | |
4 | |
5 .. image:: images/carousel.gif | |
6 :align: right | |
7 | |
8 .. versionadded:: 1.4.0 | |
9 | |
10 The :class:`Carousel` widget provides the classic mobile-friendly carousel view | |
11 where you can swipe between slides. | |
12 You can add any content to the carousel and have it move horizontally or | |
13 vertically. The carousel can display pages in a sequence or a loop. | |
14 | |
15 Example:: | |
16 | |
17 from kivy.app import App | |
18 from kivy.uix.carousel import Carousel | |
19 from kivy.uix.image import AsyncImage | |
20 | |
21 | |
22 class CarouselApp(App): | |
23 def build(self): | |
24 carousel = Carousel(direction='right') | |
25 for i in range(10): | |
26 src = "http://placehold.it/480x270.png&text=slide-%d&.png" % i | |
27 image = AsyncImage(source=src, allow_stretch=True) | |
28 carousel.add_widget(image) | |
29 return carousel | |
30 | |
31 | |
32 CarouselApp().run() | |
33 | |
34 | |
35 Kv Example:: | |
36 | |
37 Carousel: | |
38 direction: 'right' | |
39 AsyncImage: | |
40 source: 'http://placehold.it/480x270.png&text=slide-1.png' | |
41 AsyncImage: | |
42 source: 'http://placehold.it/480x270.png&text=slide-2.png' | |
43 AsyncImage: | |
44 source: 'http://placehold.it/480x270.png&text=slide-3.png' | |
45 AsyncImage: | |
46 source: 'http://placehold.it/480x270.png&text=slide-4.png' | |
47 | |
48 | |
49 .. versionchanged:: 1.5.0 | |
50 The carousel now supports active children, like the | |
51 :class:`~kivy.uix.scrollview.ScrollView`. It will detect a swipe gesture | |
52 according to the :attr:`Carousel.scroll_timeout` and | |
53 :attr:`Carousel.scroll_distance` properties. | |
54 | |
55 In addition, the slide container is no longer exposed by the API. | |
56 The impacted properties are | |
57 :attr:`Carousel.slides`, :attr:`Carousel.current_slide`, | |
58 :attr:`Carousel.previous_slide` and :attr:`Carousel.next_slide`. | |
59 | |
60 ''' | |
61 | |
62 __all__ = ('Carousel', ) | |
63 | |
64 from functools import partial | |
65 from kivy.clock import Clock | |
66 from kivy.factory import Factory | |
67 from kivy.animation import Animation | |
68 from kivy.uix.stencilview import StencilView | |
69 from kivy.uix.relativelayout import RelativeLayout | |
70 from kivy.properties import BooleanProperty, OptionProperty, AliasProperty, \ | |
71 NumericProperty, ListProperty, ObjectProperty, StringProperty | |
72 | |
73 | |
74 class Carousel(StencilView): | |
75 '''Carousel class. See module documentation for more information. | |
76 ''' | |
77 | |
78 slides = ListProperty([]) | |
79 '''List of slides inside the Carousel. The slides are the | |
80 widgets added to the Carousel using the :attr:`add_widget` method. | |
81 | |
82 :attr:`slides` is a :class:`~kivy.properties.ListProperty` and is | |
83 read-only. | |
84 ''' | |
85 | |
86 def _get_slides_container(self): | |
87 return [x.parent for x in self.slides] | |
88 | |
89 slides_container = AliasProperty(_get_slides_container, bind=('slides',)) | |
90 | |
91 direction = OptionProperty('right', | |
92 options=('right', 'left', 'top', 'bottom')) | |
93 '''Specifies the direction in which the slides are ordered. This | |
94 corresponds to the direction from which the user swipes to go from one | |
95 slide to the next. It | |
96 can be `right`, `left`, `top`, or `bottom`. For example, with | |
97 the default value of `right`, the second slide is to the right | |
98 of the first and the user would swipe from the right towards the | |
99 left to get to the second slide. | |
100 | |
101 :attr:`direction` is an :class:`~kivy.properties.OptionProperty` and | |
102 defaults to 'right'. | |
103 ''' | |
104 | |
105 min_move = NumericProperty(0.2) | |
106 '''Defines the minimum distance to be covered before the touch is | |
107 considered a swipe gesture and the Carousel content changed. | |
108 This is a expressed as a fraction of the Carousel's width. | |
109 If the movement doesn't reach this minimum value, the movement is | |
110 cancelled and the content is restored to its original position. | |
111 | |
112 :attr:`min_move` is a :class:`~kivy.properties.NumericProperty` and | |
113 defaults to 0.2. | |
114 ''' | |
115 | |
116 anim_move_duration = NumericProperty(0.5) | |
117 '''Defines the duration of the Carousel animation between pages. | |
118 | |
119 :attr:`anim_move_duration` is a :class:`~kivy.properties.NumericProperty` | |
120 and defaults to 0.5. | |
121 ''' | |
122 | |
123 anim_cancel_duration = NumericProperty(0.3) | |
124 '''Defines the duration of the animation when a swipe movement is not | |
125 accepted. This is generally when the user does not make a large enough | |
126 swipe. See :attr:`min_move`. | |
127 | |
128 :attr:`anim_cancel_duration` is a :class:`~kivy.properties.NumericProperty` | |
129 and defaults to 0.3. | |
130 ''' | |
131 | |
132 loop = BooleanProperty(False) | |
133 '''Allow the Carousel to loop infinitely. If True, when the user tries to | |
134 swipe beyond last page, it will return to the first. If False, it will | |
135 remain on the last page. | |
136 | |
137 :attr:`loop` is a :class:`~kivy.properties.BooleanProperty` and | |
138 defaults to False. | |
139 ''' | |
140 | |
141 def _get_index(self): | |
142 if self.slides: | |
143 return self._index % len(self.slides) | |
144 return None | |
145 | |
146 def _set_index(self, value): | |
147 if self.slides: | |
148 self._index = value % len(self.slides) | |
149 else: | |
150 self._index = None | |
151 | |
152 index = AliasProperty(_get_index, _set_index, | |
153 bind=('_index', 'slides'), | |
154 cache=True) | |
155 '''Get/Set the current slide based on the index. | |
156 | |
157 :attr:`index` is an :class:`~kivy.properties.AliasProperty` and defaults | |
158 to 0 (the first item). | |
159 ''' | |
160 | |
161 def _prev_slide(self): | |
162 slides = self.slides | |
163 len_slides = len(slides) | |
164 index = self.index | |
165 if len_slides < 2: # None, or 1 slide | |
166 return None | |
167 if self.loop and index == 0: | |
168 return slides[-1] | |
169 if index > 0: | |
170 return slides[index - 1] | |
171 | |
172 previous_slide = AliasProperty(_prev_slide, | |
173 bind=('slides', 'index', 'loop'), | |
174 cache=True) | |
175 '''The previous slide in the Carousel. It is None if the current slide is | |
176 the first slide in the Carousel. This ordering reflects the order in which | |
177 the slides are added: their presentation varies according to the | |
178 :attr:`direction` property. | |
179 | |
180 :attr:`previous_slide` is an :class:`~kivy.properties.AliasProperty`. | |
181 | |
182 .. versionchanged:: 1.5.0 | |
183 This property no longer exposes the slides container. It returns | |
184 the widget you have added. | |
185 ''' | |
186 | |
187 def _curr_slide(self): | |
188 if len(self.slides): | |
189 return self.slides[self.index or 0] | |
190 | |
191 current_slide = AliasProperty(_curr_slide, | |
192 bind=('slides', 'index'), | |
193 cache=True) | |
194 '''The currently shown slide. | |
195 | |
196 :attr:`current_slide` is an :class:`~kivy.properties.AliasProperty`. | |
197 | |
198 .. versionchanged:: 1.5.0 | |
199 The property no longer exposes the slides container. It returns | |
200 the widget you have added. | |
201 ''' | |
202 | |
203 def _next_slide(self): | |
204 if len(self.slides) < 2: # None, or 1 slide | |
205 return None | |
206 if self.loop and self.index == len(self.slides) - 1: | |
207 return self.slides[0] | |
208 if self.index < len(self.slides) - 1: | |
209 return self.slides[self.index + 1] | |
210 | |
211 next_slide = AliasProperty(_next_slide, | |
212 bind=('slides', 'index', 'loop'), | |
213 cache=True) | |
214 '''The next slide in the Carousel. It is None if the current slide is | |
215 the last slide in the Carousel. This ordering reflects the order in which | |
216 the slides are added: their presentation varies according to the | |
217 :attr:`direction` property. | |
218 | |
219 :attr:`next_slide` is an :class:`~kivy.properties.AliasProperty`. | |
220 | |
221 .. versionchanged:: 1.5.0 | |
222 The property no longer exposes the slides container. | |
223 It returns the widget you have added. | |
224 ''' | |
225 | |
226 scroll_timeout = NumericProperty(200) | |
227 '''Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds. | |
228 If the user has not moved :attr:`scroll_distance` within the timeout, | |
229 no scrolling will occur and the touch event will go to the children. | |
230 | |
231 :attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and | |
232 defaults to 200 (milliseconds) | |
233 | |
234 .. versionadded:: 1.5.0 | |
235 ''' | |
236 | |
237 scroll_distance = NumericProperty('20dp') | |
238 '''Distance to move before scrolling the :class:`Carousel` in pixels. As | |
239 soon as the distance has been traveled, the :class:`Carousel` will start | |
240 to scroll, and no touch event will go to children. | |
241 It is advisable that you base this value on the dpi of your target device's | |
242 screen. | |
243 | |
244 :attr:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` and | |
245 defaults to 20dp. | |
246 | |
247 .. versionadded:: 1.5.0 | |
248 ''' | |
249 | |
250 anim_type = StringProperty('out_quad') | |
251 '''Type of animation to use while animating to the next/previous slide. | |
252 This should be the name of an | |
253 :class:`~kivy.animation.AnimationTransition` function. | |
254 | |
255 :attr:`anim_type` is a :class:`~kivy.properties.StringProperty` and | |
256 defaults to 'out_quad'. | |
257 | |
258 .. versionadded:: 1.8.0 | |
259 ''' | |
260 | |
261 ignore_perpendicular_swipes = BooleanProperty(False) | |
262 '''Ignore swipes on axis perpendicular to direction. | |
263 | |
264 :attr:`ignore_perpendicular_swipes` is a | |
265 :class:`~kivy.properties.BooleanProperty` and defaults to False. | |
266 | |
267 .. versionadded:: 1.10.0 | |
268 ''' | |
269 | |
270 # private properties, for internal use only ### | |
271 _index = NumericProperty(0, allownone=True) | |
272 _prev = ObjectProperty(None, allownone=True) | |
273 _current = ObjectProperty(None, allownone=True) | |
274 _next = ObjectProperty(None, allownone=True) | |
275 _offset = NumericProperty(0) | |
276 _touch = ObjectProperty(None, allownone=True) | |
277 | |
278 _change_touch_mode_ev = None | |
279 | |
280 def __init__(self, **kwargs): | |
281 self._trigger_position_visible_slides = Clock.create_trigger( | |
282 self._position_visible_slides, -1) | |
283 super(Carousel, self).__init__(**kwargs) | |
284 self._skip_slide = None | |
285 self.touch_mode_change = False | |
286 self._prioritize_next = False | |
287 self.fbind('loop', lambda *args: self._insert_visible_slides()) | |
288 | |
289 def load_slide(self, slide): | |
290 '''Animate to the slide that is passed as the argument. | |
291 | |
292 .. versionchanged:: 1.8.0 | |
293 ''' | |
294 slides = self.slides | |
295 start, stop = slides.index(self.current_slide), slides.index(slide) | |
296 if start == stop: | |
297 return | |
298 | |
299 self._skip_slide = stop | |
300 if stop > start: | |
301 self._prioritize_next = True | |
302 self._insert_visible_slides(_next_slide=slide) | |
303 self.load_next() | |
304 else: | |
305 self._prioritize_next = False | |
306 self._insert_visible_slides(_prev_slide=slide) | |
307 self.load_previous() | |
308 | |
309 def load_previous(self): | |
310 '''Animate to the previous slide. | |
311 | |
312 .. versionadded:: 1.7.0 | |
313 ''' | |
314 self.load_next(mode='prev') | |
315 | |
316 def load_next(self, mode='next'): | |
317 '''Animate to the next slide. | |
318 | |
319 .. versionadded:: 1.7.0 | |
320 ''' | |
321 if self.index is not None: | |
322 w, h = self.size | |
323 _direction = { | |
324 'top': -h / 2, | |
325 'bottom': h / 2, | |
326 'left': w / 2, | |
327 'right': -w / 2} | |
328 _offset = _direction[self.direction] | |
329 if mode == 'prev': | |
330 _offset = -_offset | |
331 | |
332 self._start_animation(min_move=0, offset=_offset) | |
333 | |
334 def get_slide_container(self, slide): | |
335 return slide.parent | |
336 | |
337 @property | |
338 def _prev_equals_next(self): | |
339 return self.loop and len(self.slides) == 2 | |
340 | |
341 def _insert_visible_slides(self, _next_slide=None, _prev_slide=None): | |
342 get_slide_container = self.get_slide_container | |
343 | |
344 previous_slide = _prev_slide if _prev_slide else self.previous_slide | |
345 if previous_slide: | |
346 self._prev = get_slide_container(previous_slide) | |
347 else: | |
348 self._prev = None | |
349 | |
350 current_slide = self.current_slide | |
351 if current_slide: | |
352 self._current = get_slide_container(current_slide) | |
353 else: | |
354 self._current = None | |
355 | |
356 next_slide = _next_slide if _next_slide else self.next_slide | |
357 if next_slide: | |
358 self._next = get_slide_container(next_slide) | |
359 else: | |
360 self._next = None | |
361 | |
362 if self._prev_equals_next: | |
363 setattr(self, '_prev' if self._prioritize_next else '_next', None) | |
364 | |
365 super_remove = super(Carousel, self).remove_widget | |
366 for container in self.slides_container: | |
367 super_remove(container) | |
368 | |
369 if self._prev and self._prev.parent is not self: | |
370 super(Carousel, self).add_widget(self._prev) | |
371 if self._next and self._next.parent is not self: | |
372 super(Carousel, self).add_widget(self._next) | |
373 if self._current: | |
374 super(Carousel, self).add_widget(self._current) | |
375 | |
376 def _position_visible_slides(self, *args): | |
377 slides, index = self.slides, self.index | |
378 no_of_slides = len(slides) - 1 | |
379 if not slides: | |
380 return | |
381 x, y, width, height = self.x, self.y, self.width, self.height | |
382 _offset, direction = self._offset, self.direction[0] | |
383 _prev, _next, _current = self._prev, self._next, self._current | |
384 get_slide_container = self.get_slide_container | |
385 last_slide = get_slide_container(slides[-1]) | |
386 first_slide = get_slide_container(slides[0]) | |
387 skip_next = False | |
388 _loop = self.loop | |
389 | |
390 if direction in 'rl': | |
391 xoff = x + _offset | |
392 x_prev = {'l': xoff + width, 'r': xoff - width} | |
393 x_next = {'l': xoff - width, 'r': xoff + width} | |
394 if _prev: | |
395 _prev.pos = (x_prev[direction], y) | |
396 elif _loop and _next and index == 0: | |
397 # if first slide is moving to right with direction set to right | |
398 # or toward left with direction set to left | |
399 if ((_offset > 0 and direction == 'r') or | |
400 (_offset < 0 and direction == 'l')): | |
401 # put last_slide before first slide | |
402 last_slide.pos = (x_prev[direction], y) | |
403 skip_next = True | |
404 if _current: | |
405 _current.pos = (xoff, y) | |
406 if skip_next: | |
407 return | |
408 if _next: | |
409 _next.pos = (x_next[direction], y) | |
410 elif _loop and _prev and index == no_of_slides: | |
411 if ((_offset < 0 and direction == 'r') or | |
412 (_offset > 0 and direction == 'l')): | |
413 first_slide.pos = (x_next[direction], y) | |
414 if direction in 'tb': | |
415 yoff = y + _offset | |
416 y_prev = {'t': yoff - height, 'b': yoff + height} | |
417 y_next = {'t': yoff + height, 'b': yoff - height} | |
418 if _prev: | |
419 _prev.pos = (x, y_prev[direction]) | |
420 elif _loop and _next and index == 0: | |
421 if ((_offset > 0 and direction == 't') or | |
422 (_offset < 0 and direction == 'b')): | |
423 last_slide.pos = (x, y_prev[direction]) | |
424 skip_next = True | |
425 if _current: | |
426 _current.pos = (x, yoff) | |
427 if skip_next: | |
428 return | |
429 if _next: | |
430 _next.pos = (x, y_next[direction]) | |
431 elif _loop and _prev and index == no_of_slides: | |
432 if ((_offset < 0 and direction == 't') or | |
433 (_offset > 0 and direction == 'b')): | |
434 first_slide.pos = (x, y_next[direction]) | |
435 | |
436 def on_size(self, *args): | |
437 size = self.size | |
438 for slide in self.slides_container: | |
439 slide.size = size | |
440 self._trigger_position_visible_slides() | |
441 | |
442 def on_pos(self, *args): | |
443 self._trigger_position_visible_slides() | |
444 | |
445 def on_index(self, *args): | |
446 self._insert_visible_slides() | |
447 self._trigger_position_visible_slides() | |
448 self._offset = 0 | |
449 | |
450 def on_slides(self, *args): | |
451 if self.slides: | |
452 self.index = self.index % len(self.slides) | |
453 self._insert_visible_slides() | |
454 self._trigger_position_visible_slides() | |
455 | |
456 def on__offset(self, *args): | |
457 self._trigger_position_visible_slides() | |
458 # if reached full offset, switch index to next or prev | |
459 direction = self.direction[0] | |
460 _offset = self._offset | |
461 width = self.width | |
462 height = self.height | |
463 index = self.index | |
464 if self._skip_slide is not None or index is None: | |
465 return | |
466 | |
467 # Move to next slide? | |
468 if (direction == 'r' and _offset <= -width) or \ | |
469 (direction == 'l' and _offset >= width) or \ | |
470 (direction == 't' and _offset <= - height) or \ | |
471 (direction == 'b' and _offset >= height): | |
472 if self.next_slide: | |
473 self.index += 1 | |
474 | |
475 # Move to previous slide? | |
476 elif (direction == 'r' and _offset >= width) or \ | |
477 (direction == 'l' and _offset <= -width) or \ | |
478 (direction == 't' and _offset >= height) or \ | |
479 (direction == 'b' and _offset <= -height): | |
480 if self.previous_slide: | |
481 self.index -= 1 | |
482 | |
483 elif self._prev_equals_next: | |
484 new_value = (_offset < 0) is (direction in 'rt') | |
485 if self._prioritize_next is not new_value: | |
486 self._prioritize_next = new_value | |
487 if new_value is (self._next is None): | |
488 self._prev, self._next = self._next, self._prev | |
489 | |
490 def _start_animation(self, *args, **kwargs): | |
491 # compute target offset for ease back, next or prev | |
492 new_offset = 0 | |
493 direction = kwargs.get('direction', self.direction)[0] | |
494 is_horizontal = direction in 'rl' | |
495 extent = self.width if is_horizontal else self.height | |
496 min_move = kwargs.get('min_move', self.min_move) | |
497 _offset = kwargs.get('offset', self._offset) | |
498 | |
499 if _offset < min_move * -extent: | |
500 new_offset = -extent | |
501 elif _offset > min_move * extent: | |
502 new_offset = extent | |
503 | |
504 # if new_offset is 0, it wasnt enough to go next/prev | |
505 dur = self.anim_move_duration | |
506 if new_offset == 0: | |
507 dur = self.anim_cancel_duration | |
508 | |
509 # detect edge cases if not looping | |
510 len_slides = len(self.slides) | |
511 index = self.index | |
512 if not self.loop or len_slides == 1: | |
513 is_first = (index == 0) | |
514 is_last = (index == len_slides - 1) | |
515 if direction in 'rt': | |
516 towards_prev = (new_offset > 0) | |
517 towards_next = (new_offset < 0) | |
518 else: | |
519 towards_prev = (new_offset < 0) | |
520 towards_next = (new_offset > 0) | |
521 if (is_first and towards_prev) or (is_last and towards_next): | |
522 new_offset = 0 | |
523 | |
524 anim = Animation(_offset=new_offset, d=dur, t=self.anim_type) | |
525 anim.cancel_all(self) | |
526 | |
527 def _cmp(*l): | |
528 if self._skip_slide is not None: | |
529 self.index = self._skip_slide | |
530 self._skip_slide = None | |
531 | |
532 anim.bind(on_complete=_cmp) | |
533 anim.start(self) | |
534 | |
535 def _get_uid(self, prefix='sv'): | |
536 return '{0}.{1}'.format(prefix, self.uid) | |
537 | |
538 def on_touch_down(self, touch): | |
539 if not self.collide_point(*touch.pos): | |
540 touch.ud[self._get_uid('cavoid')] = True | |
541 return | |
542 if self.disabled: | |
543 return True | |
544 if self._touch: | |
545 return super(Carousel, self).on_touch_down(touch) | |
546 Animation.cancel_all(self) | |
547 self._touch = touch | |
548 uid = self._get_uid() | |
549 touch.grab(self) | |
550 touch.ud[uid] = { | |
551 'mode': 'unknown', | |
552 'time': touch.time_start} | |
553 self._change_touch_mode_ev = Clock.schedule_once( | |
554 self._change_touch_mode, self.scroll_timeout / 1000.) | |
555 self.touch_mode_change = False | |
556 return True | |
557 | |
558 def on_touch_move(self, touch): | |
559 if not self.touch_mode_change: | |
560 if self.ignore_perpendicular_swipes and \ | |
561 self.direction in ('top', 'bottom'): | |
562 if abs(touch.oy - touch.y) < self.scroll_distance: | |
563 if abs(touch.ox - touch.x) > self.scroll_distance: | |
564 self._change_touch_mode() | |
565 self.touch_mode_change = True | |
566 elif self.ignore_perpendicular_swipes and \ | |
567 self.direction in ('right', 'left'): | |
568 if abs(touch.ox - touch.x) < self.scroll_distance: | |
569 if abs(touch.oy - touch.y) > self.scroll_distance: | |
570 self._change_touch_mode() | |
571 self.touch_mode_change = True | |
572 | |
573 if self._get_uid('cavoid') in touch.ud: | |
574 return | |
575 if self._touch is not touch: | |
576 super(Carousel, self).on_touch_move(touch) | |
577 return self._get_uid() in touch.ud | |
578 if touch.grab_current is not self: | |
579 return True | |
580 ud = touch.ud[self._get_uid()] | |
581 direction = self.direction[0] | |
582 if ud['mode'] == 'unknown': | |
583 if direction in 'rl': | |
584 distance = abs(touch.ox - touch.x) | |
585 else: | |
586 distance = abs(touch.oy - touch.y) | |
587 if distance > self.scroll_distance: | |
588 ev = self._change_touch_mode_ev | |
589 if ev is not None: | |
590 ev.cancel() | |
591 ud['mode'] = 'scroll' | |
592 else: | |
593 if direction in 'rl': | |
594 self._offset += touch.dx | |
595 if direction in 'tb': | |
596 self._offset += touch.dy | |
597 return True | |
598 | |
599 def on_touch_up(self, touch): | |
600 if self._get_uid('cavoid') in touch.ud: | |
601 return | |
602 if self in [x() for x in touch.grab_list]: | |
603 touch.ungrab(self) | |
604 self._touch = None | |
605 ud = touch.ud[self._get_uid()] | |
606 if ud['mode'] == 'unknown': | |
607 ev = self._change_touch_mode_ev | |
608 if ev is not None: | |
609 ev.cancel() | |
610 super(Carousel, self).on_touch_down(touch) | |
611 Clock.schedule_once(partial(self._do_touch_up, touch), .1) | |
612 else: | |
613 self._start_animation() | |
614 | |
615 else: | |
616 if self._touch is not touch and self.uid not in touch.ud: | |
617 super(Carousel, self).on_touch_up(touch) | |
618 return self._get_uid() in touch.ud | |
619 | |
620 def _do_touch_up(self, touch, *largs): | |
621 super(Carousel, self).on_touch_up(touch) | |
622 # don't forget about grab event! | |
623 for x in touch.grab_list[:]: | |
624 touch.grab_list.remove(x) | |
625 x = x() | |
626 if not x: | |
627 continue | |
628 touch.grab_current = x | |
629 super(Carousel, self).on_touch_up(touch) | |
630 touch.grab_current = None | |
631 | |
632 def _change_touch_mode(self, *largs): | |
633 if not self._touch: | |
634 return | |
635 self._start_animation() | |
636 uid = self._get_uid() | |
637 touch = self._touch | |
638 ud = touch.ud[uid] | |
639 if ud['mode'] == 'unknown': | |
640 touch.ungrab(self) | |
641 self._touch = None | |
642 super(Carousel, self).on_touch_down(touch) | |
643 return | |
644 | |
645 def add_widget(self, widget, index=0, canvas=None): | |
646 container = RelativeLayout( | |
647 size=self.size, x=self.x - self.width, y=self.y) | |
648 container.add_widget(widget) | |
649 super(Carousel, self).add_widget(container, index, canvas) | |
650 if index != 0: | |
651 self.slides.insert(index - len(self.slides), widget) | |
652 else: | |
653 self.slides.append(widget) | |
654 | |
655 def remove_widget(self, widget, *args, **kwargs): | |
656 # XXX be careful, the widget.parent refer to the RelativeLayout | |
657 # added in add_widget(). But it will break if RelativeLayout | |
658 # implementation change. | |
659 # if we passed the real widget | |
660 slides = self.slides | |
661 if widget in slides: | |
662 if self.index >= slides.index(widget): | |
663 self.index = max(0, self.index - 1) | |
664 container = widget.parent | |
665 slides.remove(widget) | |
666 super(Carousel, self).remove_widget(container) | |
667 return container.remove_widget(widget, *args, **kwargs) | |
668 return super(Carousel, self).remove_widget(widget, *args, **kwargs) | |
669 | |
670 def clear_widgets(self): | |
671 for slide in self.slides[:]: | |
672 self.remove_widget(slide) | |
673 super(Carousel, self).clear_widgets() | |
674 | |
675 | |
676 if __name__ == '__main__': | |
677 from kivy.app import App | |
678 | |
679 class Example1(App): | |
680 | |
681 def build(self): | |
682 carousel = Carousel(direction='left', | |
683 loop=True) | |
684 for i in range(4): | |
685 src = "http://placehold.it/480x270.png&text=slide-%d&.png" % i | |
686 image = Factory.AsyncImage(source=src, allow_stretch=True) | |
687 carousel.add_widget(image) | |
688 return carousel | |
689 | |
690 Example1().run() |