Sideband/sbapp/kivymd/uix/swiper/swiper.py

561 lines
14 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 kivymd.app import MDApp
from kivy.lang.builder import Builder
kv = '''
<MySwiper@MDSwiperItem>
FitImage:
source: "guitar.png"
radius: [20,]
MDScreen:
MDTopAppBar:
id: toolbar
title: "MDSwiper"
elevation: 4
pos_hint: {"top": 1}
MDSwiper:
size_hint_y: None
height: root.height - toolbar.height - dp(40)
y: root.height - self.height - toolbar.height - dp(20)
MySwiper:
MySwiper:
MySwiper:
MySwiper:
MySwiper:
'''
class Main(MDApp):
def build(self):
return Builder.load_string(kv)
Main().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/mdswiper-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")
Example
=======
.. code-block:: python
from kivy.lang.builder import Builder
from kivymd.app import MDApp
kv = '''
<MagicButton@MagicBehavior+MDIconButton>
<MySwiper@MDSwiperItem>
RelativeLayout:
FitImage:
source: "guitar.png"
radius: [20,]
MDBoxLayout:
adaptive_height: True
spacing: "12dp"
MagicButton:
id: icon
icon: "weather-sunny"
user_font_size: "56sp"
opposite_colors: True
MDLabel:
text: "MDLabel"
font_style: "H5"
size_hint_y: None
height: self.texture_size[1]
pos_hint: {"center_y": .5}
opposite_colors: True
MDScreen:
MDTopAppBar:
id: toolbar
title: "MDSwiper"
elevation: 4
pos_hint: {"top": 1}
MDSwiper:
size_hint_y: None
height: root.height - toolbar.height - dp(40)
y: root.height - self.height - toolbar.height - dp(20)
on_swipe: self.get_current_item().ids.icon.shake()
MySwiper:
MySwiper:
MySwiper:
MySwiper:
MySwiper:
'''
class Main(MDApp):
def build(self):
return Builder.load_string(kv)
Main().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/mdswiper-on-swipe.gif
:align: center
How to automatically switch a SwiperItem?
=========================================
Use method :attr:`~MDSwiper.set_current` which takes the index of :class:`MDSwiperItem` as argument.
Example
=======
.. code-block:: kv
MDSwiper:
id: swiper
MDSwiperItem: # First widget with index 0
MDSwiperItem: # Second widget with index 1
MDRaisedButton:
text: "Go to Second"
on_release: swiper.set_current(1)
"""
__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.utils import platform
from kivymd import uix_path
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.scrollview import MDScrollView
with open(
os.path.join(uix_path, "swiper", "swiper.kv"), encoding="utf-8"
) as kv_file:
Builder.load_string(kv_file.read())
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):
"""
:class:`MDSwiperItem` is a :class:`BoxLayout` but it's size is adjusted
automatically.
"""
_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(MDScrollView):
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