Skip to content

Instantly share code, notes, and snippets.

@kengoon
Last active November 27, 2023 11:57
Show Gist options
  • Save kengoon/06732861afef5b8c4254d2d1a4982150 to your computer and use it in GitHub Desktop.
Save kengoon/06732861afef5b8c4254d2d1a4982150 to your computer and use it in GitHub Desktop.
Kivy RecycleView with extra features
from kivy.uix.recycleview import RecycleView
from kivy.clock import Clock
from kivy.properties import OptionProperty, BooleanProperty, ObjectProperty, NumericProperty, ListProperty
from kivy.animation import Animation
from kivymd.uix.behaviors import StencilBehavior, SpecificBackgroundColorBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivy.uix.widget import Widget
from Components.effects import LowerScrollEffect, LowerDampedScrollEffect
LEFT = 1 # finger and content moves right (scroll bar moves left) to see the far left of the entire content
RIGHT = 2 # finger and content moves left (scroll bar moves right) to see the far right of the entire content
DOWN = 3 # finger and content moves up (scroll bar moves down) to see the very bottom of the entire content
UP = 4 # finger and content moves down (scroll bar moves up) to see the very top of the entire content
NULL = 0 # finger or scroll bar or content is not moving
class RealRecycleView(RecycleView, StencilBehavior):
do_swipe = BooleanProperty(False)
swipe_direction = OptionProperty("horizontal", options=["horizontal", "vertical"], allownone=False)
effect_cls = ObjectProperty(LowerDampedScrollEffect, allownone=True)
scroll_distance_traveled = ListProperty([0, 0]) # read only
scroll_direction = NumericProperty(NULL) # read only
_swipe_right_listeners = []
_swipe_left_listeners = []
_swipe_down_listeners = []
_swipe_up_listeners = []
def __init__(self, **kwargs):
self.register_event_type('on_real_scroll_stop') # type: ignore
self.register_event_type("on_real_scroll_start")
self.register_event_type("on_swipe_up")
self.register_event_type("on_swipe_down")
self.register_event_type("on_swipe_left")
self.register_event_type("on_swipe_right")
self.register_event_type("on_overscroll")
self.register_event_type("on_overscroll_down")
self.register_event_type("on_overscroll_up")
super().__init__(**kwargs)
self.scroll_index = 0
self._scrolling = False
self._clock = Clock.create_trigger(self.check_scrolling, 1, True)
self._start_touch = None
self._is_touch_move = False
self.__scroll_y = self.scroll_y
self.__scroll_x = self.scroll_x
Clock.schedule_once(self.on_data)
def on_scroll_move(self, touch):
if supra := super().on_scroll_move(touch):
self._is_touch_move = True
self._clock()
return supra
def on_scroll_start(self, touch, check_children=True):
if supra := super().on_scroll_start(touch, check_children):
self.dispatch("on_real_scroll_start")
self._start_touch = touch.pos
return supra
def on_scroll_stop(self, touch, check_children=True):
if supra := super().on_scroll_stop(touch, check_children):
self.get_swipe_direction(touch)
return supra
def get_swipe_direction(self, touch):
checks = all([self.do_swipe, self._start_touch, self._is_touch_move])
if checks:
if self.swipe_direction == "horizontal":
if self._start_touch[0] < touch.pos[0]:
self.swipe_right()
self.dispatch("on_swipe_right")
else:
self.swipe_left()
self.dispatch("on_swipe_left")
elif self._start_touch[1] < touch.pos[1]:
self.swipe_up()
self.dispatch("on_swipe_up")
else:
self.swipe_down()
self.dispatch("on_swipe_down")
self._start_touch = None
self._is_touch_move = False
def on_data(self, *_):
if self.swipe_direction == "vertical":
self.scroll_index = len(self.data) - 1
def swipe_up(self):
if not self.children:
return
if self.scroll_index > 0 < len(self.data) - 1:
self.scroll_index -= 1
child_height = self.children[0].default_height
scroll = self.convert_distance_to_scroll(0, child_height * self.scroll_index)[1]
anim = Animation(scroll_y=max(scroll, 0.0), t='out_quad', d=.3)
anim.bind(on_complete=lambda *_: self.dispatch_listeners(direction="up"))
anim.start(self)
def swipe_down(self):
if not self.children:
return
if self.scroll_index < len(self.data) - 1:
self.scroll_index += 1
child_height = self.children[0].default_height
scroll = self.convert_distance_to_scroll(0, child_height * self.scroll_index)[1]
anim = Animation(scroll_y=min(scroll, 1.0), t='out_quad', d=.3)
anim.bind(on_complete=lambda *_: self.dispatch_listeners(direction="down"))
anim.start(self)
def swipe_left(self):
if not self.children:
return
if self.scroll_index < len(self.data) - 1:
self.scroll_index += 1
child_width = self.children[0].default_width
scroll = self.convert_distance_to_scroll(child_width * self.scroll_index, 0)[0]
anim = Animation(scroll_x=min(scroll, 1.0), t='out_quad', d=.3)
anim.bind(on_complete=lambda *_: self.dispatch_listeners(direction="left"))
anim.start(self)
def swipe_right(self):
if not self.children:
return
if self.scroll_index > 0 < len(self.data):
self.scroll_index -= 1
child_width = self.children[0].default_width
scroll = self.convert_distance_to_scroll(child_width * self.scroll_index, 0)[0]
anim = Animation(scroll_x=max(scroll, 0.0), t='out_quad', d=.3)
anim.bind(on_complete=lambda *_: self.dispatch_listeners(direction="right"))
anim.start(self)
def on_scroll_y(self, *_):
if self.__scroll_y > self.scroll_y:
self.scroll_direction = DOWN
else:
self.scroll_direction = UP
self.__scroll_y = self.scroll_y
self._scrolling = True
viewport = self.get_viewport()
viewport_scroll_height = self.viewport_size[1] - viewport[-1]
self.scroll_distance_traveled = viewport[0], viewport_scroll_height - viewport[1]
def on_scroll_x(self, *_):
if self.__scroll_x < self.scroll_x:
self.scroll_direction = RIGHT
else:
self.scroll_direction = LEFT
self.__scroll_x = self.scroll_x
self._scrolling = True
viewport = self.get_viewport()
self.scroll_distance_traveled = viewport[0], viewport[1]
def check_scrolling(self, *_):
if not self._scrolling:
self._clock.cancel()
self.dispatch("on_real_scroll_stop")
self.scroll_direction = NULL
self._scrolling = False
@classmethod
def register_swipe_listener(cls, **kwargs):
if func := kwargs.get("up"):
cls._swipe_up_listeners.append(func)
if func := kwargs.get("down"):
cls._swipe_down_listeners.append(func)
if func := kwargs.get("left"):
cls._swipe_left_listeners.append(func)
if func := kwargs.get("right"):
cls._swipe_right_listeners.append(func)
else:
raise AttributeError(f"Unknown argument. Argument must be any or both of [up, down, right, left]")
@classmethod
def unregister_swipe_listener(cls, **kwargs):
if func := kwargs.get("up"):
cls._swipe_up_listeners.remove(func)
if func := kwargs.get("down"):
cls._swipe_down_listeners.remove(func)
if func := kwargs.get("left"):
cls._swipe_left_listeners.remove(func)
if func := kwargs.get("right"):
cls._swipe_right_listeners.remove(func)
else:
raise AttributeError(f"Unknown argument. Argument must be any or both of [up, down, right, left]")
def dispatch_listeners(self, direction):
if direction == "up":
for func in self._swipe_up_listeners:
func()
elif direction == "down":
for func in self._swipe_down_listeners:
func()
elif direction == "left":
for func in self._swipe_left_listeners:
func()
else:
for func in self._swipe_right_listeners:
func()
def on_real_scroll_stop(self):
pass
def on_real_scroll_start(self):
pass
def on_swipe_up(self):
pass
def on_swipe_down(self):
pass
def on_swipe_left(self):
pass
def on_swipe_right(self):
pass
def on_overscroll(self, *args):
pass
def on_overscroll_down(self):
pass
def on_overscroll_up(self):
pass
@kengoon
Copy link
Author

kengoon commented Nov 21, 2023

Kivy Custom RecycleView with Extra Features like

  • swiping (like TikTok swiping). make sure to set effect_cls to HardStopScrollEffect or HardStopDampedScrollEffect and set do_swipe to True. The default swipe_direction is set to horizontal, which means swiping will be left right, set to vertical for up down
  • on_swipe_[direction] event
  • getting exact on_scroll_stop event (as in knowing when the scrollbar or the widget stops moving)
  • listening for swipe directions by registering a function with register_swipe_listener
  • on_overscroll event
  • on_overscroll_[direction] event
  • scroll_distance_traveled gets the actual distance traveled while scrolling, is read only and should not be tampered with. This is good for designing sliver bars or tracking the distance to perform other forms of actions.
  • scroll_direction gets the scroll direction be it up, down, left, right. Read the source code to see the flags used

PS: check here for source code of HardStopScrollEffect or HardStopDampedScrollEffect

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment