How can I make a RecycleView in a Kivy python app display all its labels without truncating the text contents of the label nor adding huge spaces in-between the labels?
I'm trying to display a very large amount of text in a Kivy (5+ MB) without causing it to lock-up. I think objectively the best solution here is to use a RecycleView with each line of the text in its own Label.
Official Documentation Demo
The example given in the official Kivy documentation about RecycleView is fine because the amount of text in the label is extremely short.
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
Builder.load_string('''
<RV>:
viewclass: 'Label'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
''')
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(x)} for x in range(100)]
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()
Demo with content
But if we update the example above so that the text in the label is actually substantial, mimicking real-world text, then the contents of the label's text gets truncated. And there's a huge space in-between each label.
import random
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
Builder.load_string('''
<RV>:
viewclass: 'Label'
scroll_type: ['bars','content']
bar_width: dp(25)
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
''')
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(self.get_random())} for x in range(100)]
def get_random(self):
# generate some random ASCII content
random_ascii = ''.join( [random.choice('0123456789abcdefghijklnmnoqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') for i in range(0,900)] )
random_ascii = 'START|' + random_ascii + '|END'
print( random_ascii)
return random_ascii
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()
Demo with content and 'text_size'
I've tried setting the text_size of the Label. That certainly displays much more of the text, but it's still not showing all of the text in each Label.
In this example, the gap between each label is now gone.
import random
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
Builder.load_string('''
<MyLabel#Label>:
text_size: self.size
<RV>:
viewclass: 'MyLabel'
scroll_type: ['bars','content']
bar_width: dp(25)
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
''')
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(self.get_random())} for x in range(100)]
def get_random(self):
# generate some random ASCII content
random_ascii = ''.join( [random.choice('0123456789abcdefghijklnmnoqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') for i in range(0,900)] )
random_ascii = 'START|' + random_ascii + '|END'
print( random_ascii)
return random_ascii
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()
How can I display a vertical RecycleView of Labels such that the text contents of the Labels is not truncated, and there is no extra padding/margin between each row of Labels?
If you want the Label to stretch up as its text content, you can bind its width to its texture width. This will enable you to scroll horizontally within RecycleView. Again if you want to scroll vertically, you need to explicitly specify the height of each content (here Label).
Here's a modified version (of the last one) of your kvlang,
<MyLabel#Label>:
size_hint_x: None
width: self.texture_size[0]
# Canvas added for visual purpose.
canvas.before:
Color:
rgb: 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
<RV>:
viewclass: 'MyLabel'
scroll_type: ['bars','content']
bar_width: dp(25)
RecycleBoxLayout:
spacing: dp(1) # Adjust to your need (atyn).
padding: dp(2) # atyn.
default_size: None, dp(20) # atyn.
default_size_hint: None, None
size_hint: None, None
size: self.minimum_size
orientation: 'vertical'
Depending on the sample size (due to hardware) it may or may not be able to render the text. If so, try with smaller sample size (like in your examples 500/600 instead of 900).
Related
I need to create an autofill app, I have a label and text input on the top, and recycle view on the bottom. However, when I run the program, the recycle view disappears, even though I have set in the string. This app will facilitate searching content by typing the name in the text input and the relevant content will appear in the recycle view, so the user is not required to view through the long list of content.
from kivy.app import App
from kivy.lang import Builder
from kivy.core.window import Window
from kivy.uix.recycleview import RecycleView
from kivy.uix.boxlayout import BoxLayout
Window.size = (350, 600)
Builder.load_string('''
<MyLayout>:
BoxLayout:
orientation: "vertical"
spacing: 10
padding: 10
Label :
text : 'Favourite Pizza'
TextInput :
font_size: 30
focus: True
multiline : False
<RV>:
RecycleBoxLayout:
viewclass: 'TextInput'
default_size: None, 30
default_size_hint: 1, None
size_hint_y: .8
height: self.minimum_height
orientation : 'vertical'
''')
class MyLayout(BoxLayout):
pass
class RV(RecycleView):
def __init__(self, **kwrgs):
super(RV, self).__init__(**kwrgs)
content = ["Pepperoni", "Cheese","Papper", "Hawaii", "Seafood",
"Ham", "Taco", "Onion"]
self.data = [{'text':item} for item in content]
print(content)
class MainApp(App):
title='Search App'
def build(self):
Window.clearcolor = (51/255, 153/255, 1, 1)
return MyLayout()
MainApp().run()
What should I do in order to get a complete view (label, text input & recycle view)? I want to type an input text, the relevant content will appear in the recycle view, can I use recycle view to achieve this purpose? Can I use both BoxLayout and the RecycleBoxLayout at the same time, since it refers to the different widgets?
First of all, dynamic classes must be in the same level as root.
Secondly in order to make RecycleView grow vertically, here, you have to set size_hint_y of RecycleBoxLayout to None. Thus your kvlang should now look like,
<MyLayout>:
BoxLayout:
orientation: "vertical"
spacing: 10
padding: 10
Label :
text : 'Favourite Pizza'
TextInput :
font_size: 30
focus: True
multiline : False
RV:
<RV>:
viewclass: 'TextInput'
RecycleBoxLayout:
default_size: None, 30
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation : 'vertical'
I am trying to get a background around a label that fits the number of lines in the texted in it. For example, a label with text 'Line1\nLine1\nLine3' would have a larger Y dimension that just 'line1'
What currently happens is all of the labels are the same size and clip-off text that doesn't fit within them, the labels are also inside a recycleview layout because I would like to be able to scroll and update large amount of the labels often.
I have tried a few things but have had no luck, and am struggling to get a variable to be understood where I have added # HERE in the .kv file
from kivy.app import App
from kivy.properties import NumericProperty, Clock, ObjectProperty, StringProperty
from kivy.uix.widget import Widget
from kivy.uix.recycleview import RecycleView
from kivy.uix.button import Button
from kivy.uix.label import Label
class TopPostsTest(RecycleView):
def __init__(self, **kwargs):
super().__init__(**kwargs)
message_height = NumericProperty(20)
items = ["hello\ntest\ntest", "test, gghgjhgjhgjhgjhghjghjgjhgjhgjhgjhgjhgjhgjhgjhg", "cheese"]
self.data = [{'text':str(p)} for p in items]
class LabelColor(Label):
pass
class TruthApp(App):
# def build(self):
# return super().build()
pass
if __name__ == "__main__":
TruthApp().run()
<MainControl#PageLayout>:
border: "25dp"
swipe_threshold: 0.4
TopPostsTest:
Settings:
<LabelColor>:
color: 0,0,0,1
text_size: self.size
halign: 'left'
valign: 'top'
font_name: "Assets/Fonts/Nunito-Bold.ttf"
font_size: "12dp"
multiline: True
canvas.before:
Color:
rgba: (0.8, 0.8, 0.8, 1)
RoundedRectangle:
pos: self.pos
size: self.size
radius: [5, 5, 5, 5]
canvas:
Color:
rgba:0,0.9,0.9,1
Line:
width:0.8
rounded_rectangle:(self.x,self.y,self.width,root.height, 5) # HERE
<TopPostsTest>:
viewclass: 'LabelColor'
scroll_y: 1
RecycleBoxLayout:
id: message_view
default_size: None, dp(40) # NewHERE
default_size_hint: 1, None
size_hint_y: None
padding: ["10dp", "16dp"]
spacing: "8dp"
height: self.minimum_height
orientation: 'vertical'
Thank you for any help :)
Edit:
I have found that I have been changing the wrong value and that the variable that needs changing has been marker with # NewHERE, however I am still unable to get it to work or get a variable from the py file into the kv
In order to get a Label that expands vertically as it's text-content grows you can set its height to its texture height.
Also, to fit the text within available space (width) you can change text_size.
Thus you can modify the kvlang for LabelColor as,
<LabelColor>:
color: 0,0,0,1
text_size: self.width, None
size_hint_y: None
height: self.texture_size[1]
I'm trying to build a log viewer on kivy using recycleview since logs can be pretty large. I'm assigning one label widget per line so I can have more control over the text in the future. Some lines will have more text than others so adapted the Label widget to resize according, but when putting that inside recycleview can't seem to be able to control the height of the widget per line anymore, it stays at the same size. What I expect is the label to wrap on the text and adjust height since don't need the extra space between lines. If there's to little text a lot of free space is shown, if I put to much text in the label it floods and label doesn't grow.
One workaround that I tried with different code was to assign at least a 200 lines per label, that seems to work, but I do need more control over each line of text.
This is the example code:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ObjectProperty
Builder.load_string('''
<Row#BoxLayout>:
canvas:
Color:
rgba: 1, 0.1, 0.1, 0.5 #Red Marker
Rectangle:
size: self.size
pos: self.pos
value: ''
orientation: 'vertical'
Label:
text: root.value
text_size: self.width, None
size_hint_y: None
height: self.texture_size[1]
font_size: 20
<LogDisplayWidget>:
rv: rv
orientation: 'vertical'
RecycleView:
id: rv
scroll_type: ['bars', 'content']
scroll_wheel_distance: dp(114)
bar_width: dp(10)
viewclass: 'Row'
RecycleBoxLayout:
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
''')
class LogDisplayWidget(BoxLayout):
rv = ObjectProperty()
def __init__(self):
super(LogDisplayWidget, self).__init__()
self.load_text()
def load_text(self):
for i in range(10):
line = str(i) + 'This is a test of a bunch of text'
self.rv.data.append({'value': line})
class TestApp(App):
def build(self):
return LogDisplayWidget()
if __name__ == "__main__":
TestApp().run()
enter image description here
enter image description here
Did a code rewrite, the labels appear resized correctly in first page, but getting jerky unexpected results after scrolling, it shows correct label size sometimes then some are to big, and the scroll skips like trying to adjust itself and it fixes size again. Does anyone have a better way to implement this or I'm missing something? I'm suspecting it has something to do with the way the view refreshes
This is the new code:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
import random
Builder.load_string('''
<Row#Label>:
canvas.before:
Color:
rgba: 0.8, 0.1, 0.1, 0.5 #Red Marker
Rectangle:
size: self.size
pos: self.pos
text_size: self.width, None
size_hint_y: None
height: self.texture_size[1]
font_size: dp(20)
<RV>:
viewclass: 'Row'
RecycleBoxLayout:
default_size: None, dp(20)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(3)
''')
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
line = ''
for i in range(50):
n = random.randint(0, 1)
if n:
j = random.randint(5, 30)
line = 'Line: ' + str(i+1) + ' This is a test of a bunch of text' * j
else:
line = 'Line: ' + str(i+1) + ' This is a test of a bunch of text'
self.data.append({'text': line})
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()
I'm trying to better understand how the RecycleView functions. Seems like only practical examples will teach me. Docs aren't helping. Kindly have a look at my current screen in the pic below.
Now following are what I'm trying to achieve.
Add/Remove new rows to/from the list.
The first column sl/no should maintain the order even after deleting from in between.
How to achieve 'sort' function? Is it by taking the data into python and do the sort and then update that to the RV again ?
Please find the both .py and .kv codes below.
rv_main.py
import os
os.environ['KIVY_GL_BACKEND'] = 'gl'
import kivy
kivy.require('1.11.0')
from kivy.uix.boxlayout import BoxLayout
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.recyclegridlayout import RecycleGridLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.recyclegridlayout import RecycleGridLayout
from kivy.uix.label import Label
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.properties import StringProperty
from kivy.properties import ObjectProperty
from kivy.properties import ListProperty, BooleanProperty
from kivy.properties import NumericProperty
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior, RecycleBoxLayout):
''' Adds selection and focus behaviour to the view. '''
#-----------------------------------------------------------------------
class RecycleViewRow(RecycleDataViewBehavior, BoxLayout):
''' Add selection support to the Label '''
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
slno = StringProperty('')
typ = StringProperty('')
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
return super(RecycleViewRow, self).refresh_view_attrs(
rv, index, data)
def on_touch_down(self, touch):
''' Add selection on touch down '''
if super(RecycleViewRow, self).on_touch_down(touch):
return True
if self.collide_point(*touch.pos) and self.selectable:
return self.parent.select_with_touch(self.index, touch)
def apply_selection(self, rv, index, is_selected):
''' Respond to the selection of items in the view. '''
self.selected = is_selected
if is_selected:
pass
else:
pass
def delete_row(self,rv, index, is_selected):
if is_selected:
rv.data.pop(index)
rv.layout_manager.clear_selection()
#-----------------------------------------------------------------------
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
#fetch data from the database
self.data = [{'slno': str(x+1),'typ': 'default'} for x in range(4)]
#-----------------------------------------------------------------------
class DataTable(BoxLayout):
def addRow(self):
pass
def removeRow(self):
pass
#-----------------------------------------------------------------------
class RvMainApp(App):
def build(self):
return DataTable()
if __name__ == '__main__':
RvMainApp().run()
rvmain.kv
#: kivy 1.11.0
<RecycleViewRow>:
id: rv
slno: ""
typ: ""
canvas.before:
Color:
rgba: (.0, 0.9, .1, .3) if self.selected else (0.4, 0.4, 0.4, 1)
Rectangle:
pos: self.pos
size: self.size
orientation: 'horizontal'
size_hint: 1.0, 1.0
Label:
text: root.slno
size_hint_x : 1.0
Label:
text: root.typ
size_hint_x : 1.0
#----------------------------------------------------------------
<RV>:
id : rv
viewclass: 'RecycleViewRow'
SelectableRecycleBoxLayout:
default_size: None, dp(40)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
#----------------------------------------------------------------
<DataTable>:
orientation : 'vertical'
Button:
BoxLayout:
Button:
RV:
Button:
BoxLayout:
Button:
text: "Add"
on_release: rv.data.append({"slno": "?", "typ": 'default'})
Button:
text: "Remove"
on_release:
You edit the data list of the recycleview. The data will be sorted as it is sorted in that list.
So here is an example of the add remove feature:
from kivy.app import App
from kivy.lang import Builder
KV = '''
<Row#BoxLayout>:
ind: 1
Button:
text: str(root.ind)
Button:
text: "default"
BoxLayout:
ind: 1
orientation: "vertical"
Button:
BoxLayout:
Button:
RecycleView:
id: rv
data: [{"text":"first","ind":1}]
viewclass: 'Row'
RecycleBoxLayout:
default_size_hint: 1, None
default_size: None, dp(56)
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
Button
BoxLayout:
Button:
text: "Add"
on_release:
root.ind += 1
rv.data.append({"ind": root.ind})
Button:
text: "Remove"
on_release:
root.ind = root.ind - 1 if root.ind > 0 else 0
if len(rv.data): rv.data.pop(-1)
'''
class Test(App):
def build(self):
self.root = Builder.load_string(KV)
return self.root
Test().run()
And here is an example on how to sort the data list by some key. In this case ind.
from kivy.app import App
from kivy.lang import Builder
KV = '''
RecycleView:
viewclass: 'Label'
data: sorted([{"text":"!", "ind":3},{"text":"world", "ind":2},{"text":"hello", "ind":1}], key=lambda k: k["ind"])
RecycleBoxLayout:
id: layout
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
'''
class TestApp(App):
def build(self):
return Builder.load_string(KV)
if __name__ == '__main__':
TestApp().run()
How to make press method on this code working? When I press a button the list populates, but when I call it from Clock then not. I can see populate print on the console but the list does not appear in the view. I mean simply: how to stimulate pressing the button in the code?
from kivy.config import Config
Config.set('graphics', 'multisamples', '0')
from random import sample
from string import ascii_lowercase
import pyrebase
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock
kv = """
<Row#BoxLayout>:
canvas.before:
Color:
rgba: 0.5, 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
value: ''
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:
id: populate_btn
text: 'Populate list'
on_press: root.populate()
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 __init__(self, **kwargs):
super().__init__(**kwargs)
def populate(self):
print("populate")
self.rv.data = [{'value': ''.join(sample(ascii_lowercase, 6))}
for x in range(50)]
def press(self):
self.ids.populate_btn.dispatch('on_press')
def interval(dt):
x = Test()
x.press()
Clock.schedule_interval(interval, 3)
class TestApp(App):
def build(self):
return Test()
if __name__ == '__main__':
TestApp().run()
The error is caused because the Test object created in interval function is different from the Test object that returns build method, besides that the Test object created in interval is eliminated since it is a local variable. So the solution is to use the same reference by passing it to the interval function for it I will use functools.partial() function.
# ...
from functools import partial
# ...
def interval(x, dt):
x.press()
class TestApp(App):
def build(self):
t = Test()
Clock.schedule_interval(partial(interval, t), 3)
return t
# ...