first commit
This commit is contained in:
commit
417e54da96
5696 changed files with 900003 additions and 0 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
124
kivy_venv/share/kivy-examples/widgets/recycleview/basic_data.py
Normal file
124
kivy_venv/share/kivy-examples/widgets/recycleview/basic_data.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
from random import sample, randint
|
||||
from string import ascii_lowercase
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
|
||||
|
||||
kv = """
|
||||
<Row@RecycleKVIDsDataViewBehavior+BoxLayout>:
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: 0.5, 0.5, 0.5, 1
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
value: ''
|
||||
Label:
|
||||
id: name
|
||||
Label:
|
||||
text: root.value
|
||||
|
||||
<Test>:
|
||||
canvas:
|
||||
Color:
|
||||
rgba: 0.3, 0.3, 0.3, 1
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
rv: rv
|
||||
orientation: 'vertical'
|
||||
GridLayout:
|
||||
cols: 3
|
||||
rows: 2
|
||||
size_hint_y: None
|
||||
height: dp(108)
|
||||
padding: dp(8)
|
||||
spacing: dp(16)
|
||||
Button:
|
||||
text: 'Populate list'
|
||||
on_press: root.populate()
|
||||
Button:
|
||||
text: 'Sort list'
|
||||
on_press: root.sort()
|
||||
Button:
|
||||
text: 'Clear list'
|
||||
on_press: root.clear()
|
||||
BoxLayout:
|
||||
spacing: dp(8)
|
||||
Button:
|
||||
text: 'Insert new item'
|
||||
on_press: root.insert(new_item_input.text)
|
||||
TextInput:
|
||||
id: new_item_input
|
||||
size_hint_x: 0.6
|
||||
hint_text: 'value'
|
||||
padding: dp(10), dp(10), 0, 0
|
||||
BoxLayout:
|
||||
spacing: dp(8)
|
||||
Button:
|
||||
text: 'Update first item'
|
||||
on_press: root.update(update_item_input.text)
|
||||
TextInput:
|
||||
id: update_item_input
|
||||
size_hint_x: 0.6
|
||||
hint_text: 'new value'
|
||||
padding: dp(10), dp(10), 0, 0
|
||||
Button:
|
||||
text: 'Remove first item'
|
||||
on_press: root.remove()
|
||||
|
||||
RecycleView:
|
||||
id: rv
|
||||
scroll_type: ['bars', 'content']
|
||||
scroll_wheel_distance: dp(114)
|
||||
bar_width: dp(10)
|
||||
viewclass: 'Row'
|
||||
RecycleBoxLayout:
|
||||
default_size: None, dp(56)
|
||||
default_size_hint: 1, None
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
orientation: 'vertical'
|
||||
spacing: dp(2)
|
||||
"""
|
||||
|
||||
Builder.load_string(kv)
|
||||
|
||||
|
||||
class Test(BoxLayout):
|
||||
|
||||
def populate(self):
|
||||
self.rv.data = [
|
||||
{'name.text': ''.join(sample(ascii_lowercase, 6)),
|
||||
'value': str(randint(0, 2000))}
|
||||
for x in range(50)]
|
||||
|
||||
def sort(self):
|
||||
self.rv.data = sorted(self.rv.data, key=lambda x: x['name.text'])
|
||||
|
||||
def clear(self):
|
||||
self.rv.data = []
|
||||
|
||||
def insert(self, value):
|
||||
self.rv.data.insert(0, {
|
||||
'name.text': value or 'default value', 'value': 'unknown'})
|
||||
|
||||
def update(self, value):
|
||||
if self.rv.data:
|
||||
self.rv.data[0]['name.text'] = value or 'default new value'
|
||||
self.rv.refresh_from_data()
|
||||
|
||||
def remove(self):
|
||||
if self.rv.data:
|
||||
self.rv.data.pop(0)
|
||||
|
||||
|
||||
class TestApp(App):
|
||||
def build(self):
|
||||
return Test()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
TestApp().run()
|
|
@ -0,0 +1,143 @@
|
|||
"""
|
||||
A constantly appending log, using recycleview.
|
||||
- use variable size widgets using the key_size property to cache texture_size
|
||||
- keeps current position in scroll when new data is happened, unless the view
|
||||
is at the very bottom, in which case it follows the log
|
||||
- works well with mouse scrolling, but less nicely when using swipes,
|
||||
improvements welcome.
|
||||
"""
|
||||
|
||||
from random import sample
|
||||
from string import printable
|
||||
from time import asctime
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.recycleview import RecycleView
|
||||
from kivy.lang import Builder
|
||||
from kivy.properties import NumericProperty, ListProperty
|
||||
from kivy.clock import Clock
|
||||
|
||||
|
||||
KV = """
|
||||
#:import rgba kivy.utils.rgba
|
||||
|
||||
<LogLabel@RelativeLayout>:
|
||||
# using a boxlayout here allows us to have better control of the text
|
||||
# position
|
||||
text: ''
|
||||
index: None
|
||||
Label:
|
||||
y: 0
|
||||
x: 5
|
||||
size_hint: None, None
|
||||
size: self.texture_size
|
||||
padding: dp(5), dp(5)
|
||||
color: rgba("#3f3e36")
|
||||
text: root.text
|
||||
on_texture_size: app.update_size(root.index, self.texture_size)
|
||||
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: rgba("#dbeeff")
|
||||
RoundedRectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
radius: dp(5), dp(5)
|
||||
|
||||
BoxLayout:
|
||||
orientation: 'vertical'
|
||||
spacing: dp(2)
|
||||
|
||||
# a label to help understand what's happening with the scrolling
|
||||
Label:
|
||||
size_hint_y: None
|
||||
height: self.texture_size[1]
|
||||
text:
|
||||
'''height: {height}
|
||||
scrollable_distance: {scrollable_distance}
|
||||
distance_to_top: {distance_to_top}
|
||||
scroll_y: {scroll_y}
|
||||
'''.format(
|
||||
height=rv.height,
|
||||
scrollable_distance=rv.scrollable_distance,
|
||||
distance_to_top=rv.distance_to_top,
|
||||
scroll_y=rv.scroll_y,
|
||||
)
|
||||
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: rgba("#77b4ff")
|
||||
RoundedRectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
radius: dp(5), dp(5)
|
||||
|
||||
FixedRecycleView:
|
||||
id: rv
|
||||
data: app.data
|
||||
viewclass: 'LogLabel'
|
||||
scrollable_distance: box.height - self.height
|
||||
|
||||
RecycleBoxLayout:
|
||||
id: box
|
||||
orientation: 'vertical'
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
default_size: 0, 48
|
||||
default_size_hint: 1, None
|
||||
spacing: dp(1)
|
||||
key_size: 'cached_size'
|
||||
"""
|
||||
|
||||
|
||||
class FixedRecycleView(RecycleView):
|
||||
distance_to_top = NumericProperty()
|
||||
scrollable_distance = NumericProperty()
|
||||
|
||||
def on_scrollable_distance(self, *args):
|
||||
"""This method maintains the position in scroll, by using the saved
|
||||
distance_to_top property to adjust the scroll_y property. Only if we
|
||||
are currently scrolled back.
|
||||
"""
|
||||
if self.scroll_y > 0:
|
||||
self.scroll_y = (
|
||||
(self.scrollable_distance - self.distance_to_top)
|
||||
/ self.scrollable_distance
|
||||
)
|
||||
|
||||
def on_scroll_y(self, *args):
|
||||
"""Save the distance_to_top everytime we scroll.
|
||||
"""
|
||||
self.distance_to_top = (1 - self.scroll_y) * self.scrollable_distance
|
||||
|
||||
|
||||
class Application(App):
|
||||
data = ListProperty()
|
||||
|
||||
def build(self):
|
||||
Clock.schedule_interval(self.add_log, .1)
|
||||
return Builder.load_string(KV)
|
||||
|
||||
def add_log(self, dt):
|
||||
"""Produce random text to append in the log, with the date, we don't
|
||||
want to forget when we babbled incoherently.
|
||||
"""
|
||||
self.data.append({
|
||||
'index': len(self.data),
|
||||
'text': f"[{asctime()}]: {''.join(sample(printable, 50))}",
|
||||
'cached_size': (0, 0)
|
||||
})
|
||||
|
||||
def update_size(self, index, size):
|
||||
"""Maintain the size data for a log entry, so recycleview can adjust
|
||||
the size computation.
|
||||
As a log entry needs to be displayed to compute its size, it's by
|
||||
default considered to be (0, 0) which is a good enough approximation
|
||||
for such a small widget, but you might want do give a better default
|
||||
value if that doesn't fit your needs.
|
||||
"""
|
||||
self.data[index]['cached_size'] = size
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
Application().run()
|
|
@ -0,0 +1,149 @@
|
|||
'''
|
||||
A form generator, using random data, but can be data driven (json or whatever)
|
||||
|
||||
Shows that you can use the key_viewclass attribute of RecycleView to select a
|
||||
different Widget for each item.
|
||||
'''
|
||||
|
||||
from random import choice, choices
|
||||
from string import ascii_lowercase
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.lang import Builder
|
||||
from kivy import properties as P
|
||||
|
||||
|
||||
KV = r'''
|
||||
<RVTextInput,RVCheckBox,RVSpinner>:
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
index: None
|
||||
title: ''
|
||||
|
||||
|
||||
<RVTextInput@BoxLayout>:
|
||||
value: ''
|
||||
Label:
|
||||
text: root.title
|
||||
size_hint_y: None
|
||||
height: self.texture_size[1]
|
||||
TextInput:
|
||||
text: root.value
|
||||
on_text: app.handle_update(self.text, root.index)
|
||||
size_hint_y: None
|
||||
height: dp(40)
|
||||
multiline: False
|
||||
|
||||
|
||||
<RVCheckBox@BoxLayout>:
|
||||
value: False
|
||||
Label:
|
||||
text: root.title
|
||||
size_hint_y: None
|
||||
height: self.texture_size[1]
|
||||
CheckBox:
|
||||
active: root.value
|
||||
on_active: app.handle_update(self.active, root.index)
|
||||
size_hint_y: None
|
||||
height: dp(40)
|
||||
|
||||
|
||||
<RVSpinner@BoxLayout>:
|
||||
value: ''
|
||||
values: []
|
||||
Label:
|
||||
text: root.title
|
||||
size_hint_y: None
|
||||
height: self.texture_size[1]
|
||||
Spinner:
|
||||
text: root.value
|
||||
values: root.values
|
||||
size_hint_y: None
|
||||
height: dp(40)
|
||||
on_text: app.handle_update(self.text, root.index)
|
||||
|
||||
|
||||
FloatLayout:
|
||||
RecycleView:
|
||||
id: rv
|
||||
data: app.data
|
||||
key_viewclass: 'widget'
|
||||
size_hint_x: 1
|
||||
RecycleBoxLayout:
|
||||
orientation: 'vertical'
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
default_size_hint: 1, None
|
||||
|
||||
'''
|
||||
|
||||
|
||||
class Application(App):
|
||||
'''A form manager demonstrating the power of RecycleView's key_viewclass
|
||||
property.
|
||||
'''
|
||||
data = P.ListProperty()
|
||||
|
||||
def build(self):
|
||||
root = Builder.load_string(KV)
|
||||
rv = root.ids.rv
|
||||
self.data = [
|
||||
self.create_random_input(rv, index)
|
||||
for index in range(20)
|
||||
]
|
||||
|
||||
return root
|
||||
|
||||
def handle_update(self, value, index):
|
||||
if None not in (index, value):
|
||||
self.data[index]['value'] = value
|
||||
|
||||
def create_random_input(self, rv, index):
|
||||
return choice((
|
||||
self.create_textinput,
|
||||
self.create_checkbox,
|
||||
self.create_spinner
|
||||
))(rv, index)
|
||||
|
||||
def create_spinner(self, rv, index):
|
||||
"""
|
||||
create a dict of data for a spinner
|
||||
"""
|
||||
return {
|
||||
'index': index,
|
||||
'widget': 'RVSpinner',
|
||||
'value': '',
|
||||
'values': [
|
||||
letter * 5
|
||||
for letter in ascii_lowercase[:5]
|
||||
],
|
||||
'ready': True,
|
||||
}
|
||||
|
||||
def create_checkbox(self, rv, index):
|
||||
"""
|
||||
create a dict of data for a checkbox
|
||||
"""
|
||||
return {
|
||||
'index': index,
|
||||
'widget': 'RVCheckBox',
|
||||
'value': choice((True, False)),
|
||||
'title': ''.join(choices(ascii_lowercase, k=10)),
|
||||
'ready': True,
|
||||
}
|
||||
|
||||
def create_textinput(self, rv, index):
|
||||
"""
|
||||
create a dict of data for a textinput
|
||||
"""
|
||||
return {
|
||||
'index': index,
|
||||
'widget': 'RVTextInput',
|
||||
'value': ''.join(choices(ascii_lowercase, k=10)),
|
||||
'title': ''.join(choices(ascii_lowercase, k=10)),
|
||||
'ready': True,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
Application().run()
|
196
kivy_venv/share/kivy-examples/widgets/recycleview/messenger.py
Normal file
196
kivy_venv/share/kivy-examples/widgets/recycleview/messenger.py
Normal file
|
@ -0,0 +1,196 @@
|
|||
from kivy.app import App
|
||||
from kivy.lang import Builder
|
||||
from kivy.clock import Clock
|
||||
from kivy.properties import ListProperty
|
||||
from kivy.animation import Animation
|
||||
from kivy.metrics import dp
|
||||
|
||||
KV = '''
|
||||
#:import RGBA kivy.utils.rgba
|
||||
|
||||
<ImageButton@ButtonBehavior+Image>:
|
||||
size_hint: None, None
|
||||
size: self.texture_size
|
||||
|
||||
canvas.before:
|
||||
PushMatrix
|
||||
Scale:
|
||||
origin: self.center
|
||||
x: .75 if self.state == 'down' else 1
|
||||
y: .75 if self.state == 'down' else 1
|
||||
|
||||
canvas.after:
|
||||
PopMatrix
|
||||
|
||||
BoxLayout:
|
||||
orientation: 'vertical'
|
||||
padding: dp(5), dp(5)
|
||||
RecycleView:
|
||||
id: rv
|
||||
data: app.messages
|
||||
viewclass: 'Message'
|
||||
do_scroll_x: False
|
||||
|
||||
RecycleBoxLayout:
|
||||
id: box
|
||||
orientation: 'vertical'
|
||||
size_hint_y: None
|
||||
size: self.minimum_size
|
||||
default_size_hint: 1, None
|
||||
# magic value for the default height of the message
|
||||
default_size: 0, 38
|
||||
key_size: '_size'
|
||||
|
||||
FloatLayout:
|
||||
size_hint_y: None
|
||||
height: 0
|
||||
Button:
|
||||
size_hint_y: None
|
||||
height: self.texture_size[1]
|
||||
opacity: 0 if not self.height else 1
|
||||
text:
|
||||
(
|
||||
'go to last message'
|
||||
if rv.height < box.height and rv.scroll_y > 0 else
|
||||
''
|
||||
)
|
||||
pos_hint: {'pos': (0, 0)}
|
||||
on_release: app.scroll_bottom()
|
||||
|
||||
BoxLayout:
|
||||
size_hint: 1, None
|
||||
size: self.minimum_size
|
||||
TextInput:
|
||||
id: ti
|
||||
size_hint: 1, None
|
||||
height: min(max(self.line_height, self.minimum_height), 150)
|
||||
multiline: False
|
||||
|
||||
on_text_validate:
|
||||
app.send_message(self)
|
||||
|
||||
ImageButton:
|
||||
source: 'data/logo/kivy-icon-48.png'
|
||||
on_release:
|
||||
app.send_message(ti)
|
||||
|
||||
<Message@FloatLayout>:
|
||||
message_id: -1
|
||||
bg_color: '#223344'
|
||||
side: 'left'
|
||||
text: ''
|
||||
size_hint_y: None
|
||||
_size: 0, 0
|
||||
size: self._size
|
||||
text_size: None, None
|
||||
opacity: min(1, self._size[0])
|
||||
|
||||
Label:
|
||||
text: root.text
|
||||
padding: 10, 10
|
||||
size_hint: None, 1
|
||||
size: self.texture_size
|
||||
text_size: root.text_size
|
||||
|
||||
on_texture_size:
|
||||
app.update_message_size(
|
||||
root.message_id,
|
||||
self.texture_size,
|
||||
root.width,
|
||||
)
|
||||
|
||||
pos_hint:
|
||||
(
|
||||
{'x': 0, 'center_y': .5}
|
||||
if root.side == 'left' else
|
||||
{'right': 1, 'center_y': .5}
|
||||
)
|
||||
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: RGBA(root.bg_color)
|
||||
RoundedRectangle:
|
||||
size: self.texture_size
|
||||
radius: dp(5), dp(5), dp(5), dp(5)
|
||||
pos: self.pos
|
||||
|
||||
canvas.after:
|
||||
Color:
|
||||
Line:
|
||||
rounded_rectangle: self.pos + self.texture_size + [dp(5)]
|
||||
width: 1.01
|
||||
'''
|
||||
|
||||
|
||||
class MessengerApp(App):
|
||||
messages = ListProperty()
|
||||
|
||||
def build(self):
|
||||
return Builder.load_string(KV)
|
||||
|
||||
def add_message(self, text, side, color):
|
||||
# create a message for the recycleview
|
||||
self.messages.append({
|
||||
'message_id': len(self.messages),
|
||||
'text': text,
|
||||
'side': side,
|
||||
'bg_color': color,
|
||||
'text_size': [None, None],
|
||||
})
|
||||
|
||||
def update_message_size(self, message_id, texture_size, max_width):
|
||||
# when the label is updated, we want to make sure the displayed size is
|
||||
# proper
|
||||
if max_width == 0:
|
||||
return
|
||||
|
||||
one_line = dp(50) # a bit of hack, YMMV
|
||||
|
||||
# if the texture is too big, limit its size
|
||||
if texture_size[0] >= max_width * 2 / 3:
|
||||
self.messages[message_id] = {
|
||||
**self.messages[message_id],
|
||||
'text_size': (max_width * 2 / 3, None),
|
||||
}
|
||||
|
||||
# if it was limited, but is now too small to be limited, raise the limit
|
||||
elif texture_size[0] < max_width * 2 / 3 and \
|
||||
texture_size[1] > one_line:
|
||||
self.messages[message_id] = {
|
||||
**self.messages[message_id],
|
||||
'text_size': (max_width * 2 / 3, None),
|
||||
'_size': texture_size,
|
||||
}
|
||||
|
||||
# just set the size
|
||||
else:
|
||||
self.messages[message_id] = {
|
||||
**self.messages[message_id],
|
||||
'_size': texture_size,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def focus_textinput(textinput):
|
||||
textinput.focus = True
|
||||
|
||||
def send_message(self, textinput):
|
||||
text = textinput.text
|
||||
textinput.text = ''
|
||||
self.add_message(text, 'right', '#223344')
|
||||
self.focus_textinput(textinput)
|
||||
Clock.schedule_once(lambda *args: self.answer(text), 1)
|
||||
self.scroll_bottom()
|
||||
|
||||
def answer(self, text, *args):
|
||||
self.add_message('do you really think so?', 'left', '#332211')
|
||||
|
||||
def scroll_bottom(self):
|
||||
rv = self.root.ids.rv
|
||||
box = self.root.ids.box
|
||||
if rv.height < box.height:
|
||||
Animation.cancel_all(rv, 'scroll_y')
|
||||
Animation(scroll_y=0, t='out_quad', d=.5).start(rv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
MessengerApp().run()
|
|
@ -0,0 +1,95 @@
|
|||
"""Detecting and acting upon "Pull down actions" in a RecycleView
|
||||
- When using overscroll or being at the to, a "pull down to refresh" message
|
||||
appears
|
||||
- if the user pulls down far enough, then a refresh is triggered, which adds
|
||||
new elements at the top of the list.
|
||||
|
||||
"""
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
from datetime import datetime
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.lang import Builder
|
||||
from kivy.properties import ListProperty, BooleanProperty
|
||||
from kivy.metrics import dp
|
||||
from kivy.clock import mainthread
|
||||
|
||||
|
||||
KV = r'''
|
||||
FloatLayout:
|
||||
Label:
|
||||
opacity: 1 if app.refreshing or rv.scroll_y > 1 else 0
|
||||
size_hint_y: None
|
||||
pos_hint: {'top': 1}
|
||||
text: 'Refreshing…' if app.refreshing else 'Pull down to refresh'
|
||||
|
||||
RecycleView:
|
||||
id: rv
|
||||
data: app.data
|
||||
viewclass: 'Row'
|
||||
do_scroll_y: True
|
||||
do_scroll_x: False
|
||||
on_scroll_y: app.check_pull_refresh(self, grid)
|
||||
|
||||
RecycleGridLayout:
|
||||
id: grid
|
||||
cols: 1
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
default_size: 0, 36
|
||||
default_size_hint: 1, None
|
||||
|
||||
|
||||
<Row@Label>:
|
||||
_id: 0
|
||||
text: ''
|
||||
canvas:
|
||||
Line:
|
||||
rectangle: self.pos + self.size
|
||||
width: 0.6
|
||||
'''
|
||||
|
||||
|
||||
class Application(App):
|
||||
data = ListProperty([])
|
||||
refreshing = BooleanProperty()
|
||||
|
||||
def build(self):
|
||||
self.refresh_data()
|
||||
return Builder.load_string(KV)
|
||||
|
||||
def check_pull_refresh(self, view, grid):
|
||||
"""Check the amount of overscroll to decide if we want to trigger the
|
||||
refresh or not.
|
||||
"""
|
||||
max_pixel = dp(200)
|
||||
to_relative = max_pixel / (grid.height - view.height)
|
||||
if view.scroll_y <= 1.0 + to_relative or self.refreshing:
|
||||
return
|
||||
|
||||
self.refresh_data()
|
||||
|
||||
def refresh_data(self):
|
||||
# using a Thread to do a potentially long operation without blocking
|
||||
# the UI.
|
||||
self.refreshing = True
|
||||
Thread(target=self._refresh_data).start()
|
||||
|
||||
def _refresh_data(self):
|
||||
sleep(2)
|
||||
update_time = datetime.now().strftime("%H:%M:%S")
|
||||
|
||||
self.prepend_data([
|
||||
{'_id': i, 'text': '[{}] hello {}'.format(update_time, i)}
|
||||
for i in range(len(self.data) + 10, len(self.data), -1)
|
||||
])
|
||||
|
||||
@mainthread
|
||||
def prepend_data(self, data):
|
||||
self.data = data + self.data
|
||||
self.refreshing = False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
Application().run()
|
|
@ -0,0 +1,109 @@
|
|||
'''How to use Animation with RecycleView items?
|
||||
|
||||
In case you really want to use the Animation class with RecycleView, you'll
|
||||
likely encounter an issue, as widgets are moved around, they are used to
|
||||
represent different items, so an animation on a specific item is going to
|
||||
affect others, and this will lead to really confusing results.
|
||||
|
||||
This example works around that by creating a "proxy" widget for the animation,
|
||||
and, by putting it in the data, allowing the displayed widget to mimic the
|
||||
animation. As the item always refers to its proxy, whichever widget is used to
|
||||
display the item will keep in sync with the animation.
|
||||
|
||||
'''
|
||||
from copy import copy
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.clock import triggered
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.button import Button
|
||||
from kivy.properties import (
|
||||
ObjectProperty, ListProperty
|
||||
)
|
||||
|
||||
|
||||
KV = '''
|
||||
<Item>:
|
||||
index: None
|
||||
animation_proxy: None
|
||||
on_release: app.animate_item(self.index)
|
||||
|
||||
|
||||
RecycleView:
|
||||
data: app.data
|
||||
viewclass: 'Item'
|
||||
RecycleBoxLayout:
|
||||
orientation: 'vertical'
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
default_size_hint: 1, None
|
||||
default_size: 0, dp(40)
|
||||
'''
|
||||
|
||||
|
||||
class Item(Button):
|
||||
animation_proxy = ObjectProperty(allownone=True)
|
||||
_animation_proxy = None
|
||||
|
||||
def update_opacity(self, proxy, opacity):
|
||||
# sync one animated property to the value in the proxy
|
||||
self.opacity = opacity
|
||||
|
||||
def on_animation_proxy(self, *args):
|
||||
"""When we create an animation proxy for an item, we need to bind to
|
||||
the animated property to update our own.
|
||||
"""
|
||||
if self._animation_proxy:
|
||||
self._animation_proxy.unbind(opacity=self.update_opacity)
|
||||
|
||||
self._animation_proxy = self.animation_proxy
|
||||
if self.animation_proxy:
|
||||
# when we are assigned an animation_proxy, sync our properties to
|
||||
# the animated version.
|
||||
self.opacity = self.animation_proxy.opacity
|
||||
self.animation_proxy.bind(opacity=self.update_opacity)
|
||||
else:
|
||||
# if we lose our animation proxy, we need to reset the animated
|
||||
# property to their default values.
|
||||
self.opacity = 1
|
||||
|
||||
|
||||
class Application(App):
|
||||
data = ListProperty()
|
||||
|
||||
def build(self):
|
||||
self.data = [
|
||||
{'index': i, 'text': 'hello {}'.format(i), 'animation_proxy': None}
|
||||
for i in range(1000)
|
||||
]
|
||||
return Builder.load_string(KV)
|
||||
|
||||
# the triggered decorator allows delaying the animation until after the
|
||||
# blue effect on the button is removed, to avoid a flash as widgets gets
|
||||
# reordered when that happens
|
||||
@triggered(timeout=0.05)
|
||||
def animate_item(self, index):
|
||||
# the animation we actually want to do on the item, note that any
|
||||
# property animated here needs to be synchronized from the proxy to the
|
||||
# animated widget (in on_animation_proxy and using methods for each
|
||||
# animation)
|
||||
proxy = Widget(opacity=1)
|
||||
item = copy(self.data[index])
|
||||
animation = (
|
||||
Animation(opacity=0, d=.1, t='out_quad')
|
||||
+ Animation(opacity=1, d=5, t='out_quad')
|
||||
)
|
||||
animation.bind(on_complete=lambda *x: self.reset_animation(item))
|
||||
item['animation_proxy'] = proxy
|
||||
self.data[index] = item
|
||||
animation.start(proxy)
|
||||
|
||||
def reset_animation(self, item):
|
||||
# animation is complete, widget should be garbage collected
|
||||
item['animation_proxy'] = None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
Application().run()
|
Loading…
Add table
Add a link
Reference in a new issue