Last active
October 12, 2019 20:13
-
-
Save jbasko/a87685ccf0b6f253c935f8cfea6036fc to your computer and use it in GitHub Desktop.
Kivy's RecycleView "metrics" - helpers to scroll based on item index
This file contains 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 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