Created
June 28, 2012 10:34
-
-
Save tito/3010607 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
from kivy.clock import Clock | |
from kivy.event import EventDispatcher | |
from kivy.uix.floatlayout import FloatLayout | |
from kivy.uix.label import Label | |
from kivy.uix.widget import Widget | |
from kivy.properties import ObjectProperty, DictProperty, NumericProperty | |
from kivy.lang import Builder | |
from math import ceil, floor | |
Builder.load_string(''' | |
<ListView>: | |
container: container | |
ScrollView: | |
pos: root.pos | |
on_scroll_y: root._scroll(args[1]) | |
do_scroll_x: False | |
GridLayout: | |
cols: 1 | |
id: container | |
size_hint_y: None | |
''') | |
class Adapter(EventDispatcher): | |
'''Adapter is a bridge between an AbstractView and the data. | |
''' | |
cls = ObjectProperty(None) | |
template = ObjectProperty(None) | |
converter = ObjectProperty(None) | |
def __init__(self, **kwargs): | |
super(Adapter, self).__init__(**kwargs) | |
if self.cls is None and self.template is None: | |
raise Exception('A cls or template must be defined') | |
if self.cls is not None and self.template is not None: | |
raise Exception('Cannot use cls and template at the same time') | |
def get_count(self): | |
raise NotImplementedError() | |
def get_item(self, index): | |
raise NotImplementedError() | |
def get_view(self, index): | |
item = self.get_item(index) | |
if item is None: | |
return None | |
if self.converter: | |
item = self.converter(item) | |
if self.cls: | |
print 'CREATE VIEW FOR', index | |
return self.cls(**item) | |
return Builder.template(self.template, **item) | |
class ListAdapter(Adapter): | |
'''Adapter around a simple Python list | |
''' | |
def __init__(self, data, **kwargs): | |
super(ListAdapter, self).__init__(**kwargs) | |
if type(data) not in (tuple, list): | |
raise Exception('ListAdapter: data must be a tuple or a list') | |
self.data = data | |
def get_count(self): | |
return len(self.data) | |
def get_item(self, index): | |
if index < 0 or index >= len(self.data): | |
return None | |
return self.data[index] | |
class AbstractView(FloatLayout): | |
'''View using an Adapter as a data provider | |
''' | |
adapter = ObjectProperty(None) | |
items = DictProperty({}) | |
def set_item(self, index, item): | |
pass | |
def get_item(self, index): | |
items = self.items | |
if index in items: | |
return items[index] | |
item = self.adapter.get_view(index) | |
if item: | |
items[index] = item | |
return item | |
class ListView(AbstractView): | |
'''Implementation of an Abstract View as a vertical scrollable list. | |
''' | |
divider = ObjectProperty(None) | |
divider_height = NumericProperty(2) | |
container = ObjectProperty(None) | |
row_height = NumericProperty(None) | |
_index = NumericProperty(0) | |
_sizes = DictProperty({}) | |
_count = NumericProperty(0) | |
_wstart = NumericProperty(0) | |
_wend = NumericProperty(None) | |
def __init__(self, **kwargs): | |
super(ListView, self).__init__(**kwargs) | |
self._trigger_populate = Clock.create_trigger(self._spopulate, -1) | |
self.bind(size=self._trigger_populate, | |
pos=self._trigger_populate) | |
self.populate() | |
def _scroll(self, scroll_y): | |
if self.row_height is None: | |
return | |
scroll_y = 1 - min(1, max(scroll_y, 0)) | |
container = self.container | |
mstart = (container.height - self.height) * scroll_y | |
mend = mstart + self.height | |
# convert distance to index | |
rh = self.row_height | |
istart = int(ceil(mstart / rh)) | |
iend = int(floor(mend / rh)) | |
istart = max(0, istart - 1) | |
iend = max(0, iend - 1) | |
if istart < self._wstart: | |
rstart = max(0, istart - 10) | |
self.populate(rstart, iend) | |
self._wstart = rstart | |
self._wend = iend | |
elif iend > self._wend: | |
self.populate(istart, iend + 10) | |
self._wstart = istart | |
self._wend = iend + 10 | |
def _spopulate(self, *dt): | |
self.populate() | |
def populate(self, istart=None, iend=None): | |
print 'populate', istart, iend | |
container = self.container | |
sizes = self._sizes | |
rh = self.row_height | |
# ensure we know what we want to show | |
if istart is None: | |
istart = self._wstart | |
iend = self._wend | |
# clear the view | |
container.clear_widgets() | |
# guess only ? | |
if iend is not None: | |
# fill with a "padding" | |
fh = 0 | |
for x in xrange(istart): | |
fh += sizes[x] if x in sizes else rh | |
container.add_widget(Widget(size_hint_y=None, height=fh)) | |
# now fill with real item | |
index = istart | |
while index <= iend: | |
item = self.get_item(index) | |
index += 1 | |
if item is None: | |
continue | |
sizes[index] = item.height | |
container.add_widget(item) | |
else: | |
available_height = self.height | |
real_height = 0 | |
index = self._index | |
count = 0 | |
while available_height > 0: | |
item = self.get_item(index) | |
sizes[index] = item.height | |
index += 1 | |
count += 1 | |
container.add_widget(item) | |
available_height -= item.height | |
real_height += item.height | |
self._count = count | |
# extrapolate the full size of the container from the size of items | |
if count: | |
container.height = real_height / count * self.adapter.get_count() | |
if self.row_height is None: | |
self.row_height = real_height / count | |
if __name__ == '__main__': | |
from kivy.base import runTouchApp | |
''' | |
from glob import glob | |
from kivy.uix.image import AsyncImage | |
adapter = ListAdapter(glob('/home/tito/Images/*.jpg'), | |
converter=lambda x: {'source': x, 'size_hint_y': None, 'height': 640}, | |
cls=AsyncImage) | |
''' | |
adapter = ListAdapter(['start'] + [ | |
'The ScrollView accepts only one child, and controls a viewport/window to it', | |
'according to the scroll_x and scroll_y properties. Touches are analyzed to', | |
'determine if the user wants to scroll or control the child - you cannot do', | |
'both at the same time. To determine if interaction is a scrolling gesture,', | |
'these properties are used', | |
] * 100 + ['end'], | |
converter=lambda x: {'text': x, 'size_hint_y': None, 'height': 25}, | |
cls=Label) | |
view = ListView(adapter=adapter) | |
runTouchApp(view) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment