Skip to content

Instantly share code, notes, and snippets.

@jbasko
Last active October 12, 2019 20:13
Show Gist options
  • Save jbasko/a87685ccf0b6f253c935f8cfea6036fc to your computer and use it in GitHub Desktop.
Save jbasko/a87685ccf0b6f253c935f8cfea6036fc to your computer and use it in GitHub Desktop.
Kivy's RecycleView "metrics" - helpers to scroll based on item index
import math
from kivy.base import runTouchApp
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.properties import ObjectProperty, StringProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.recycleview import RecycleView
class RecycleViewMetrics:
def __init__(self, recycle_view: RecycleView):
self._view = recycle_view
@property
def scroll_y(self) -> float:
return self._view.scroll_y
@property
def num_total(self) -> int:
return len(self._view.data)
@property
def num_visible(self) -> int:
return len(self._view.children[0].children) - 1
@property
def first_visible_index(self) -> int:
"""
Returns the index of the first item that is definitely fully visible.
It could be that the previous item is also visible.
With scroll_y = 0.0 we can see the last num_visible items so the index
of the first definitely visible item is num_total - num_visible.
scroll_y [1.0 .. 0.0] then maps to [0 .. num_total - num_visible].
"""
return int(math.ceil((1.0 - self.scroll_y) * max(0, self.num_total - self.num_visible)))
@property
def last_visible_index(self) -> int:
"""
Returns the index of the last item that is definitely visible.
It could be that the next item is also visible.
With scroll_y = 1.0 the last visible item's index is num_visible - 1.
With scroll_y = 0.0 the last visible item's index is num_total - 1.
So scroll_y [1.0 .. 0.0] maps to [num_visible - 1 .. num_total - 1]
"""
return int(math.floor(
self.num_visible - 1 + (1.0 - self.scroll_y) * (self.num_total - 1 - self.num_visible - 1)
))
def is_visible(self, index: int) -> bool:
return self.first_visible_index <= index <= self.last_visible_index
def scroll_to(self, index: int):
"""
Set recycle view's scroll_y so that the item at the specified index is definitely visible.
"""
if self.is_visible(index):
return
scroll_y = 1.0 - (
min(
self.num_total - self.num_visible,
max(0, index - (self.num_visible / 2)),
)
) / (self.num_total - self.num_visible)
self._view.scroll_y = scroll_y
Builder.load_string(f"""\
<ScrollExplorer>:
orientation: "vertical"
RecycleView:
size_hint_y: .8
id: list_view
scroll_type: ["bars", "content"]
bar_width: sp(20)
viewclass: "Button"
RecycleBoxLayout:
id: list_items_view
orientation: "vertical"
default_size: None, sp(42)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
spacing: 1
BoxLayout:
size_hint_y: .2
BoxLayout:
orientation: "vertical"
Label:
text: "scroll_y:"
Label:
text: root.scroll_y
BoxLayout:
orientation: "vertical"
Label:
text: "items in total:"
Label:
text: root.num_total
BoxLayout:
orientation: "vertical"
Label:
text: "items reliably visible:"
Label:
text: root.num_visible
BoxLayout:
orientation: "vertical"
Label:
text: "first visible index:"
Label:
text: root.first_visible_index
BoxLayout:
orientation: "vertical"
Label:
text: "last visible index:"
Label:
text: root.last_visible_index
BoxLayout:
orientation: "vertical"
TextInput:
id: item_index_input
hint_text: "index of item"
Button:
text: "Scroll to"
on_release: root.metrics.scroll_to(int(item_index_input.text)) if item_index_input.text else None
""")
class ScrollExplorer(BoxLayout):
metrics: RecycleViewMetrics = ObjectProperty()
scroll_y: str = StringProperty()
num_total: str = StringProperty()
num_visible: str = StringProperty()
first_visible_index: str = StringProperty()
last_visible_index: str = StringProperty()
def __init__(self, **kwargs):
super().__init__(**kwargs)
Clock.schedule_once(self.build)
Clock.schedule_interval(self.update, .3)
def build(self, *args):
for i in range(42):
self.ids.list_view.data.append({"text": f"Item index={i}"})
self.metrics = RecycleViewMetrics(recycle_view=self.ids.list_view)
def update(self, *args):
if not self.metrics:
return
self.scroll_y = f"{self.metrics.scroll_y:.4f}"
self.num_total = str(self.metrics.num_total)
self.num_visible = str(self.metrics.num_visible)
self.first_visible_index = str(self.metrics.first_visible_index)
self.last_visible_index = str(self.metrics.last_visible_index)
if __name__ == "__main__":
runTouchApp(ScrollExplorer())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment