Created
August 8, 2012 12:04
-
-
Save yishenggudou/3294612 to your computer and use it in GitHub Desktop.
kivy carousel sample
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
''' | |
Carousel | |
======== | |
.. versionadded:: 1.4.0 | |
The :class:`Carousel` widget provides the classic mobile-friendly carousel view | |
where you can swipe between slides. | |
You can add any content to the carousel and use it horizontally or verticaly. | |
The carousel can display pages in loop or not. | |
Adding content | |
-------------- | |
For the moment you need to add content once with the `slides` ListProperty. | |
''' | |
__all__ = ('Carousel', ) | |
from kivy.animation import Animation | |
from kivy.uix.stencilview import StencilView | |
from kivy.properties import BooleanProperty, ListProperty, OptionProperty, NumericProperty | |
from kivy.logger import Logger | |
# | |
# inspired from @hansent SlideShow | |
# related to GH issue : https://github.com/kivy/kivy/issues/607 | |
# | |
# | |
# Todo : | |
# | |
# - simplify horizontal/vertical computing | |
# - bug if slides set at __init__ (canvas note ready) | |
# - pos/size bug with gridlayout | |
# - buttons inside slides are not clicable | |
# - get/set position based on slides indexes | |
# - dynamically add slides | |
# | |
# | |
class Carousel(StencilView): | |
# | |
# Properties | |
# | |
slides = ListProperty([]) | |
'''Get/Set a list of slides as widgets. You need a minimum of 3 slides. | |
:data:`slides` is a :class:`~kivy.properties.ListProperty` | |
''' | |
position = NumericProperty(0) | |
'''Get/Set the current visible slide based on index. | |
:data:`position` is a :class:`~kivy.properties.NumericProperty`, default | |
to 0 (first item) | |
''' | |
orientation = OptionProperty('horizontal', options=('horizontal', 'vertical')) | |
'''Specifies the orientation in which you can scroll the carousel | |
Can be `horizontal` or `vertical`. | |
:data:`orientation` is a :class:`~kivy.properties.OptionProperty`, | |
default to 'horizontal'. | |
''' | |
min_move = NumericProperty(0.2) | |
'''Defines the minimal distance from the edge where the movement is | |
considered a swipe gesture and the carousel will change his content. | |
This is a percentage of the carousel width. | |
If the movement doesn't reach this minimal value, then the movement is | |
canceled and the content is restored to its preceding position. | |
:data:`min_move` is a :class:`~kivy.properties.NumericProperty`, default | |
to 0.2 | |
''' | |
anim_move_duration = NumericProperty(0.5) | |
'''Defines the duration of the carousel animation between pages. | |
:data:`anim_move_duration` is a :class:`~kivy.properties.NumericProperty`, default | |
to 0.5 | |
''' | |
anim_cancel_duration = NumericProperty(0.3) | |
'''Defines the duration of the animation when a swipe movement is not accepted. | |
This is generally when the user doesnt swipe enough. See :data:`min_move`. | |
:data:`anim_cancel_duration` is a :class:`~kivy.properties.NumericProperty`, default | |
to 0.3 | |
''' | |
loop = BooleanProperty(False) | |
'''Allow carousel to swipe infinitely. When the user reach the last page, he will | |
get the first page when trying to swipe next. Same effect when trying to swipe the | |
first page | |
:data:`loop` is a :class:`~kivy.properties.BooleanProperty`, | |
default to True. | |
''' | |
# private, for internal use only | |
_offset = NumericProperty(0) | |
def __init__(self, *args, **kwargs): | |
super(Carousel, self).__init__(*args, **kwargs) | |
self.slide_touch = (0, 0, 0) | |
self.cards = [] | |
self.animating = False | |
def slide(self, offset, animate=True): | |
if animate: | |
anim = Animation(_offset=offset, t='out_quad', d=self.anim_move_duration) | |
anim.start(self) | |
else: | |
self._offset = offset | |
def on_slides(self, *args, **kwargs): | |
# bug if slides set at __init__ | |
if len(self.slides) < 3: | |
Logger.warning('Carousel: Need at least 3 slides to instantiate') | |
return | |
self.clear_widgets() | |
self.cards = [] | |
# start position for the first slide | |
x, y = (self.x - self.width), (self.y - self.height) | |
ordered_slides = self.slides | |
for index, widget in enumerate(ordered_slides): | |
pos = (x, self.y) if self.orientation == 'horizontal' else (self.x, y) | |
if isinstance(widget, Image): | |
# auto stretch images | |
widget.keep_ratio = False | |
widget.allow_stretch = True | |
widget.size = self.size | |
widget.pos = pos | |
self.add_widget(widget) | |
# we store the original slide index to track current position | |
self.cards.append((index, widget)) | |
x += self.width | |
y += self.height | |
if self.position == 0: | |
if self.orientation == 'horizontal': | |
self.slide(self.width, False) | |
else: | |
self.slide(-self.height, False) | |
def on__offset(self, *args): | |
ref_size = self.width | |
new_pos = (ref_size + self.x, self.y) | |
if self.orientation == 'vertical': | |
ref_size = self.height | |
new_pos = (self.x, ref_size + self.y) | |
moved_next = (self._offset >= ref_size) | |
moved_prev = (self._offset <= -ref_size) | |
if moved_next or moved_prev: | |
# rearrange 3 cards ordering if card need to be changed | |
self._offset = 0 | |
self.cards[0][1].pos = new_pos | |
self.cards[1][1].pos = new_pos | |
self.cards[-1][1].pos = new_pos | |
# add a card for next slide (loop) | |
if moved_next: | |
if self.orientation == 'vertical': | |
self.cards.append(self.cards.pop(0)) | |
else: | |
self.cards.insert(0, self.cards.pop()) | |
else: | |
if self.orientation == 'vertical': | |
self.cards.insert(0, self.cards.pop()) | |
else: | |
self.cards.append(self.cards.pop(0)) | |
# update 3 cards position | |
if self.orientation == 'horizontal': | |
if not (self.is_first and not self.loop): | |
# move prev page only if we want to see it | |
self.cards[0][1].pos = (-ref_size + self._offset + self.x, self.y) | |
self.cards[1][1].pos = (self._offset + self.x, self.y) | |
if not (self.is_last and not self.loop): | |
# move next page only if we want to see it | |
self.cards[2][1].pos = (ref_size + self._offset + self.x, self.y) | |
elif self.orientation == 'vertical': | |
if not (self.is_first and not self.loop): | |
# move prev page only if we want to see it | |
self.cards[0][1].pos = (self.x, ref_size + self._offset + self.y) | |
self.cards[1][1].pos = (self.x, self._offset + self.y) | |
if not (self.is_last and not self.loop): | |
# move next page only if we want to see it | |
self.cards[2][1].pos = (self.x, -ref_size + self._offset + self.y) | |
# record current card index | |
self.position = self.cards[1][0] | |
@property | |
def is_last(self): | |
return (self.position == (len(self.slides) - 1)) | |
@property | |
def is_first(self): | |
return (self.position == 0) | |
def on_touch_down(self, touch): | |
if not self.collide_point(*touch.pos): | |
return | |
if self.animating: | |
return | |
touch.grab(self) | |
self.slide_touch = (touch.uid, touch.x, touch.y) | |
return | |
def on_touch_move(self, touch): | |
if not self.collide_point(*touch.pos): | |
return | |
if self.animating: | |
return | |
tuid, x, y = self.slide_touch | |
if touch.grab_current is self and touch.uid == tuid: | |
self._offset += (touch.dx if self.orientation == 'horizontal' else touch.dy) | |
self.slide_touch = (touch.uid, touch.x, touch.y) | |
self.canvas.ask_update() | |
return True | |
def on_touch_up(self, touch): | |
if not touch.grab_current is self: | |
if not self.collide_point(*touch.pos): | |
return | |
touch.ungrab(self) | |
if self.animating: | |
return | |
tuid, x, y = self.slide_touch | |
if touch.uid == tuid: | |
duration = self.anim_move_duration | |
target = 0 | |
ref_size = (self.width if self.orientation == 'horizontal' else self.height) | |
edge = ref_size * self.min_move | |
if self._offset < -edge: | |
target = -ref_size | |
if self._offset > edge: | |
target = ref_size | |
if self._offset == 0: | |
return | |
if not self.loop: | |
# prevent infinite looping | |
if self.orientation == 'vertical': | |
if target < 0 and self.is_first: | |
target = 0 | |
if target > 0 and self.is_last: | |
target = 0 | |
else: | |
if target > 0 and self.is_first: | |
target = 0 | |
if target < 0 and self.is_last: | |
target = 0 | |
if target == 0: | |
# dont reached the edges, so reset position quickly | |
duration = self.anim_cancel_duration | |
# prevent a bug with double anims :/ | |
self.animating = True | |
anim = Animation(_offset=target, t='out_quad', d=duration) | |
anim.bind(on_complete=self.anim_complete) | |
anim.start(self) | |
return True | |
def anim_complete(self, anim, widget): | |
self.animating = False | |
if __name__ == '__main__': | |
import os | |
import kivy | |
kivy.require('1.4.0') | |
from kivy.app import App | |
from kivy.uix.floatlayout import FloatLayout | |
from kivy.uix.image import Image | |
from kivy.uix.button import Button | |
class SampleCarousel(Carousel): | |
def __init__(self, *args, **kwargs): | |
self.size_hint = (None, None) | |
self.width = 250 | |
self.height = 200 | |
super(SampleCarousel, self).__init__(*args, **kwargs) | |
# get a list of sample images | |
images_dir = os.path.join(os.path.split(os.path.abspath(__file__))[0], 'imgs') | |
imgs = [os.path.join(images_dir, '%s.png' % img) for img in range(1, 6)] | |
# create multiple widgets so they can be use in several places | |
imgs1 = [Image(source=src) for src in imgs] | |
imgs2 = [Image(source=src) for src in imgs] | |
imgs3 = [Image(source=src) for src in imgs] | |
imgs4 = [Image(source=src) for src in imgs] | |
# add labels | |
slides1 = [Button(text='horizontal carousel')] + imgs1 | |
slides2 = [Button(text='Infinite horizontal carousel')] + imgs2 | |
slides3 = [Button(text='vertical carousel')] + imgs3 | |
slides4 = [Button(text='Infinite vertical carousel')] + imgs4 | |
# various carousels | |
slider1 = SampleCarousel(x=50, y=350) | |
slider2 = SampleCarousel(loop=True, x=350, y=350) | |
slider3 = SampleCarousel(orientation='vertical', x=50, y=100) | |
slider4 = SampleCarousel(orientation='vertical', loop=True, x=350, y=100) | |
# add carousels content | |
slider1.slides = slides1 | |
slider2.slides = slides2 | |
slider3.slides = slides3 | |
slider4.slides = slides4 | |
layout = FloatLayout() | |
# add to the layout - bug with GridLayouts | |
layout.add_widget(slider1) | |
layout.add_widget(slider2) | |
layout.add_widget(slider3) | |
layout.add_widget(slider4) | |
class Example1(App): | |
""" images are not aligned vertically """ | |
def build(self): | |
return layout | |
Example1().run() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import random | |
import os | |
import carousel | |
from kivy.base import runTouchApp | |
from kivy.core.window import Window | |
base_dir = os.path.split(os.path.abspath(__file__))[0] | |
imgs = [os.path.join(base_dir, 'imgs', img) for img in os.listdir(os.path.join(base_dir, 'imgs')) if os.path.isfile(os.path.join(base_dir, 'imgs', img))] | |
slider1 = carousel.Carousel(width=800, height=200) | |
slider1.slides = imgs | |
slider2 = carousel.Carousel(width=800, height=200, pos=(0, 300)) | |
random.shuffle(imgs) | |
slider2.slides = imgs | |
Window.add_widget(slider1) | |
Window.add_widget(slider2) | |
# slider3 = carousel.Carousel(width=200, height=600, pos=(0, 0), direction='vertical') | |
# random.shuffle(imgs) | |
# slider3.slides = imgs | |
# slider4 = carousel.Carousel(width=200, height=600, pos=(200, 0), direction='vertical') | |
# random.shuffle(imgs) | |
# slider4.slides = imgs | |
# slider5 = carousel.Carousel(width=200, height=600, pos=(400, 0), direction='vertical') | |
# random.shuffle(imgs) | |
# slider5.slides = imgs | |
# Window.add_widget(slider3) | |
# Window.add_widget(slider4) | |
# Window.add_widget(slider5) | |
runTouchApp() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment