473 lines
12 KiB
Python
473 lines
12 KiB
Python
"""
|
|
Components/Swiper
|
|
=================
|
|
|
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/mdswiper-preview.gif
|
|
:align: center
|
|
|
|
Usage
|
|
=====
|
|
|
|
.. code-block:: kv
|
|
|
|
MDSwiper:
|
|
|
|
MDSwiperItem:
|
|
|
|
MDSwiperItem:
|
|
|
|
MDSwiperItem:
|
|
|
|
Example
|
|
=======
|
|
|
|
.. code-block:: python
|
|
|
|
from kivy.lang.builder import Builder
|
|
|
|
from kivymd.app import MDApp
|
|
|
|
kv = '''
|
|
<MySwiper@MDSwiperItem>
|
|
|
|
FitImage:
|
|
source: "bg.jpg"
|
|
radius: [dp(20),]
|
|
|
|
|
|
MDScreen:
|
|
md_bg_color: self.theme_cls.backgroundColor
|
|
|
|
MDSwiper:
|
|
size_hint_y: None
|
|
height: root.height - dp(40)
|
|
y: root.height - self.height - dp(20)
|
|
|
|
MySwiper:
|
|
|
|
MySwiper:
|
|
|
|
MySwiper:
|
|
|
|
MySwiper:
|
|
|
|
MySwiper:
|
|
'''
|
|
|
|
|
|
class Main(MDApp):
|
|
def build(self):
|
|
self.theme_cls.theme_style = "Dark"
|
|
return Builder.load_string(kv)
|
|
|
|
|
|
Main().run()
|
|
|
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/swiper-example.gif
|
|
:align: center
|
|
|
|
.. warning::
|
|
The width of :class:`MDSwiperItem` is adjusted automatically. Consider changing
|
|
that by :attr:`~MDSwiperItem.width_mult`.
|
|
|
|
.. warning::
|
|
The width of :class:`MDSwiper` is automatically adjusted according to the width of the window.
|
|
|
|
.. rubric:: :class:`~MDSwiper` provides the following events for use:
|
|
|
|
.. code-block:: python
|
|
|
|
__events__ = (
|
|
"on_swipe",
|
|
"on_pre_swipe",
|
|
"on_overswipe_right",
|
|
"on_overswipe_left",
|
|
"on_swipe_left",
|
|
"on_swipe_right"
|
|
)
|
|
|
|
.. code-block:: kv
|
|
|
|
MDSwiper:
|
|
on_swipe: print("on_swipe")
|
|
on_pre_swipe: print("on_pre_swipe")
|
|
on_overswipe_right: print("on_overswipe_right")
|
|
on_overswipe_left: print("on_overswipe_left")
|
|
on_swipe_left: print("on_swipe_left")
|
|
on_swipe_right: print("on_swipe_right")
|
|
"""
|
|
|
|
__all__ = ("MDSwiperItem", "MDSwiper")
|
|
|
|
import os
|
|
|
|
from kivy.animation import Animation
|
|
from kivy.clock import Clock
|
|
from kivy.core.window import Window
|
|
from kivy.effects.dampedscroll import DampedScrollEffect
|
|
from kivy.lang.builder import Builder
|
|
from kivy.properties import (
|
|
BooleanProperty,
|
|
NumericProperty,
|
|
ObjectProperty,
|
|
StringProperty,
|
|
)
|
|
from kivy.uix.anchorlayout import AnchorLayout
|
|
from kivy.uix.scrollview import ScrollView
|
|
from kivy.utils import platform
|
|
|
|
from kivymd import uix_path
|
|
from kivymd.uix.boxlayout import MDBoxLayout
|
|
|
|
with open(
|
|
os.path.join(uix_path, "swiper", "swiper.kv"), encoding="utf-8"
|
|
) as kv_file:
|
|
Builder.load_string(kv_file.read())
|
|
|
|
|
|
# TODO: Redesign this module according to the specification -
|
|
# https://m3.material.io/components/carousel/overview
|
|
class _ScrollViewHardStop(DampedScrollEffect):
|
|
def stop(self, val, t=None):
|
|
return super().stop(val, t=0.01)
|
|
|
|
|
|
class _ItemsBox(AnchorLayout):
|
|
_root = ObjectProperty()
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
Clock.schedule_once(self._update)
|
|
Window.bind(on_resize=self._set_size)
|
|
|
|
def _update(self, *args):
|
|
self._set_size()
|
|
|
|
def _set_size(self, *args):
|
|
window_size = Window.size
|
|
self.size = [
|
|
window_size[0]
|
|
- self._root.items_spacing * self._root.width_mult * 2,
|
|
self._root.height,
|
|
]
|
|
|
|
|
|
class MDSwiperItem(MDBoxLayout):
|
|
"""
|
|
Swiper item class.
|
|
|
|
For more information, see in the
|
|
:class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation.
|
|
"""
|
|
|
|
_root = ObjectProperty()
|
|
_selected = False
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
Clock.schedule_once(self._set_size)
|
|
Window.bind(on_resize=self._set_size)
|
|
|
|
def _set_size(self, *args):
|
|
Clock.schedule_once(lambda x: self._root._reset_size())
|
|
if self._selected:
|
|
self._selected_size()
|
|
else:
|
|
self._dismiss_size()
|
|
|
|
def _selected_size(self):
|
|
size = [
|
|
Window.size[0]
|
|
- self._root.items_spacing * self._root.width_mult * 2,
|
|
self._root.height,
|
|
]
|
|
anim = Animation(
|
|
size=size, d=self._root.size_duration, t=self._root.size_transition
|
|
)
|
|
anim.start(self)
|
|
|
|
def _dismiss_size(self):
|
|
size = [
|
|
Window.size[0]
|
|
- self._root.items_spacing * (1 + self._root.width_mult) * 2,
|
|
self._root.height - self._root.items_spacing * 2,
|
|
]
|
|
anim = Animation(
|
|
size=size, d=self._root.size_duration, t=self._root.size_transition
|
|
)
|
|
anim.start(self)
|
|
|
|
|
|
class MDSwiper(ScrollView):
|
|
"""
|
|
Swiper class.
|
|
|
|
For more information, see in the
|
|
:class:`~kivy.uix.scrollview.ScrollView` class documentation.
|
|
"""
|
|
|
|
items_spacing = NumericProperty("20dp")
|
|
"""
|
|
The space between each :class:`MDSwiperItem`.
|
|
|
|
:attr:`items_spacing` is an :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `20dp`.
|
|
"""
|
|
|
|
transition_duration = NumericProperty(0.2)
|
|
"""
|
|
Duration of switching between :class:`MDSwiperItem`.
|
|
|
|
:attr:`transition_duration` is an :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `0.2`.
|
|
"""
|
|
|
|
size_duration = NumericProperty(0.2)
|
|
"""
|
|
Duration of changing the size of :class:`MDSwiperItem`.
|
|
|
|
:attr:`transition_duration` is an :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `0.2`.
|
|
"""
|
|
|
|
size_transition = StringProperty("out_quad")
|
|
"""
|
|
The type of animation used for changing the size of :class:`MDSwiperItem`.
|
|
|
|
:attr:`size_transition` is an :class:`~kivy.properties.StringProperty`
|
|
and defaults to `out_quad`.
|
|
"""
|
|
|
|
swipe_transition = StringProperty("out_quad")
|
|
"""
|
|
The type of animation used for swiping.
|
|
|
|
:attr:`swipe_transition` is an :class:`~kivy.properties.StringProperty`
|
|
and defaults to `out_quad`.
|
|
"""
|
|
|
|
swipe_distance = NumericProperty("70dp")
|
|
"""
|
|
Distance to move before swiping the :class:`MDSwiperItem`.
|
|
|
|
:attr:`swipe_distance` is an :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `70dp`.
|
|
"""
|
|
|
|
width_mult = NumericProperty(3)
|
|
"""
|
|
This number is multiplied by :attr:`items_spacing` x2 and
|
|
then subtracted from the width of window to specify the width of
|
|
:class:`MDSwiperItem`. So by decreasing the :attr:`width_mult` the width
|
|
of :class:`MDSwiperItem` increases and vice versa.
|
|
|
|
:attr:`width_mult` is an :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `3`.
|
|
"""
|
|
|
|
swipe_on_scroll = BooleanProperty(True)
|
|
"""
|
|
Wheter to swipe on mouse wheel scrolling or not.
|
|
|
|
:attr:`swipe_on_scroll` is an :class:`~kivy.properties.BooleanProperty`
|
|
and defaults to `True`.
|
|
"""
|
|
|
|
_selected = 0
|
|
_start_touch_x = None
|
|
|
|
__events__ = (
|
|
"on_swipe",
|
|
"on_pre_swipe",
|
|
"on_overswipe_right",
|
|
"on_overswipe_left",
|
|
"on_swipe_left",
|
|
"on_swipe_right",
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.register_event_type("on_swipe")
|
|
self.register_event_type("on_pre_swipe")
|
|
self.register_event_type("on_overswipe_right")
|
|
self.register_event_type("on_overswipe_left")
|
|
self.register_event_type("on_swipe_left")
|
|
self.register_event_type("on_swipe_right")
|
|
|
|
self.effect_cls = _ScrollViewHardStop
|
|
|
|
def add_widget(self, widget, index=0):
|
|
if issubclass(widget.__class__, MDSwiperItem):
|
|
widget._root = self
|
|
items_box = _ItemsBox(_root=self)
|
|
items_box.add_widget(widget)
|
|
self.ids.anchor_scroll.add_widget(items_box)
|
|
return
|
|
else:
|
|
return super().add_widget(widget, index=index)
|
|
|
|
def remove_widget(self, widget):
|
|
if not issubclass(widget.__class__, MDSwiperItem):
|
|
return
|
|
|
|
for item_box in self.ids.anchor_scroll.children:
|
|
if widget in item_box.children:
|
|
return self.ids.anchor_scroll.remove_widget(item_box)
|
|
|
|
def set_current(self, index):
|
|
"""Switch to given :class:`MDSwiperItem` index."""
|
|
|
|
self._selected = index
|
|
self.dispatch("on_pre_swipe")
|
|
self._reset_size()
|
|
self.dispatch("on_swipe")
|
|
|
|
def get_current_index(self):
|
|
"""Returns the current :class:`MDSwiperItem` index."""
|
|
|
|
return self._selected
|
|
|
|
def get_current_item(self):
|
|
"""Returns the current :class:`MDSwiperItem` instance."""
|
|
|
|
return list(reversed(self.ids.anchor_scroll.children))[
|
|
self._selected
|
|
].children[0]
|
|
|
|
def get_items(self):
|
|
"""Returns the list of :class:`MDSwiperItem` children.
|
|
|
|
.. note::
|
|
|
|
Use `get_items()` to get the list of children instead of
|
|
`MDSwiper.children`.
|
|
|
|
"""
|
|
|
|
children = list(reversed(self.ids.anchor_scroll.children))
|
|
items = [item.children[0] for item in children]
|
|
return items
|
|
|
|
def _reset_size(self, *args):
|
|
children = list(reversed(self.ids.anchor_scroll.children))
|
|
if not children:
|
|
return
|
|
|
|
child = children[self._selected]
|
|
total_width = self.ids.anchor_scroll.width - Window.width
|
|
|
|
if self.get_current_index() == 0:
|
|
view_x = child.x - self.items_spacing
|
|
elif self.get_current_index() == len(children) - 1:
|
|
view_x = (
|
|
child.x
|
|
- self.items_spacing * self.width_mult
|
|
- self.items_spacing * 2
|
|
)
|
|
else:
|
|
view_x = child.x - self.items_spacing * self.width_mult
|
|
|
|
anim = Animation(
|
|
scroll_x=view_x / total_width,
|
|
d=self.transition_duration,
|
|
t=self.swipe_transition,
|
|
)
|
|
anim.start(self)
|
|
|
|
for widget in children:
|
|
widget.children[0]._dismiss_size()
|
|
widget.children[0]._selected = False
|
|
|
|
child.children[0]._selected_size()
|
|
child.children[0]._selected = True
|
|
|
|
def on_swipe(self):
|
|
pass
|
|
|
|
def on_pre_swipe(self):
|
|
pass
|
|
|
|
def on_overswipe_right(self):
|
|
pass
|
|
|
|
def on_overswipe_left(self):
|
|
pass
|
|
|
|
def on_swipe_left(self):
|
|
pass
|
|
|
|
def on_swipe_right(self):
|
|
pass
|
|
|
|
def swipe_left(self):
|
|
previous_index = self._selected - 1
|
|
if previous_index == -1:
|
|
self.set_current(0)
|
|
self.dispatch("on_overswipe_left")
|
|
else:
|
|
self.set_current(previous_index)
|
|
self.dispatch("on_swipe_left")
|
|
|
|
def swipe_right(self):
|
|
next_index = self._selected + 1
|
|
last_index = len(self.ids.anchor_scroll.children) - 1
|
|
if next_index == last_index + 1:
|
|
self.set_current(last_index)
|
|
self.dispatch("on_overswipe_right")
|
|
else:
|
|
self.set_current(next_index)
|
|
self.dispatch("on_swipe_right")
|
|
|
|
def on_scroll_start(self, touch, check_children=True):
|
|
if platform in ["ios", "android"]:
|
|
return super().on_scroll_start(touch)
|
|
|
|
# on touch pad events
|
|
if touch.button == "scrollright":
|
|
self.swipe_left()
|
|
elif touch.button == "scrollleft":
|
|
self.swipe_right()
|
|
return super().on_scroll_start(touch)
|
|
|
|
def on_touch_down(self, touch):
|
|
super().on_touch_down(touch)
|
|
|
|
if not self.collide_point(touch.pos[0], touch.pos[1]):
|
|
return
|
|
|
|
if platform not in ["ios", "android"] and self.swipe_on_scroll:
|
|
if touch.button == "scrolldown":
|
|
self.swipe_right()
|
|
elif touch.button == "scrollup":
|
|
self.swipe_left()
|
|
else:
|
|
self._start_touch_x = touch.pos[0]
|
|
else:
|
|
self._start_touch_x = touch.pos[0]
|
|
|
|
def on_touch_up(self, touch):
|
|
super().on_touch_up(touch)
|
|
if not self._start_touch_x:
|
|
return
|
|
|
|
if self._start_touch_x:
|
|
touch_x_diff = abs(self._start_touch_x - touch.pos[0])
|
|
else:
|
|
return
|
|
|
|
if touch_x_diff <= self.swipe_distance:
|
|
if touch_x_diff == 0:
|
|
return
|
|
self._reset_size()
|
|
return
|
|
|
|
# swipe to left
|
|
if self._start_touch_x < touch.pos[0]:
|
|
self.swipe_left()
|
|
# swipe to right
|
|
else:
|
|
self.swipe_right()
|
|
|
|
self._start_touch_x = None
|
|
return
|