BoxLayouts'height in RecycleView / minimum_heigh doesn't work

33 views
Skip to first unread message

大橋和季

unread,
Jun 23, 2024, 9:46:48 AM (10 days ago) Jun 23
to Kivy users support

env. : Python==3.11.5, Kivy==2.3.0

I'm trying to make an app with RecycleView. The RecycleBoxLayout has several BoxLayouts and they will have several Labels. All the widgets will be piled vertically.

This is the code:

from kivy.app import App from kivy.uix.label import Label from kivy.uix.boxlayout import BoxLayout from kivy.properties import ListProperty from kivy.lang import Builder from random import randint KV = ''' <RVLabel>: size_hint_y: None text_size: self.width, None height: self.texture_size[1] canvas.before: Color: rgba: .1, .2, .3, 1 Rectangle: pos: self.pos size: self.size <RVBoxLayout>: orientation: 'vertical' # size_hint_y: None height: self.minimum_height RecycleView: viewclass: 'RVBoxLayout' RecycleBoxLayout: orientation: 'vertical' default_size: None, None default_size_hint: 1, None size_hint_y: None height: self.minimum_height ''' class RVBoxLayout(BoxLayout): labels = ListProperty([]) def on_labels(self, instance, data): self.clear_widgets() for label in self.labels: self.add_widget(RVLabel(text=label['text'])) class RVLabel(Label): pass class TestApp(App): def build(self): return Builder.load_string(KV) def on_start(self): rv = self.root rv_data = [] for i in range(10): box_layout = {'labels': [ {'text': f'Toooooooooooooooooooooooooooooooooooo Long Label {j}'} for j in range(randint(1, 5)) ]} rv_data.append(box_layout) rv.data = rv_data if __name__ == '__main__': TestApp().run()

I expected the widgets fit tightly together, but I got a result like this:

image

or in the other case, some Labels show randomly.

When I fix the labels height, height: 30 for example, they are shown correctly but I need the texts to be wrapped.

How can I do that?

Any of your helps will be appreciated!


ElliotG

unread,
Jun 23, 2024, 10:03:40 AM (10 days ago) Jun 23
to Kivy users support
In a RecycleView the only the number of widgets that are visible are created, the data for each widget comes from the RecycleView data list.  The problem in your example is the you are trying to set the size of the RVLabel based on the content of the Label.  Instead you need to set the size (height in this case) of the widget in the data list.  You do this with the key_size attribute of the RecycleBoxLayout, see: https://kivy.org/doc/stable/api-kivy.uix.recyclelayout.html#kivy.uix.recyclelayout.RecycleLayout.key_size

The key_size attribute sets the name of the key that will specify the size of the widget in the data list.  Below is an example that is similar to your issue, and uses key_size.  There are a few things to highlight.  An instance of the ScrollLabel is used to calculate the texture_size to set the appropriate value for the size in the data list.  The size must also be update if the width of the window changes.   I've attached to code as well as pasted it below.

Hope this helps.  Happy to answer any questions.

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.label import Label
from kivy.uix.recycleview import RecycleView

kv = """
<ScrollLabel>:
    size_hint: 1, None
    text_size: self.width, None  # height must be calculated based on texture_size
    font_size: 30

<ConsoleRV>:
    viewclass: 'ScrollLabel'
    RecycleBoxLayout:
        id: scroll_box
        orientation: 'vertical'
        key_size: 'ks'
        default_size_hint: 1, None
        size_hint: 1, None
        height: self.minimum_height
        on_width: root.update_text() # when width changes, update sizes in rv data list


BoxLayout:
    orientation: 'vertical'
    Label:
        text: 'Test long Scroll'
        size_hint_y: None
        height: 30
    ConsoleRV:
        id: console_rv
"""


class ScrollLabel(Label):
    pass


class ConsoleRV(RecycleView):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.size_label = ScrollLabel()  # used to calculate texture size, never added to widget tree

    def add_text(self, text):
        st = [x + '\n' for x in text.split('\n')]  # assuming there are some \n in the text, split into separate labels
        sl = self.size_label  # label to use for calculating texture size
        for t in st:
            sl.text = t
            sl.width = self.width
            sl.texture_update()
            self.data.append({'text': t, 'ks': sl.texture_size})

    def update_text(self):
        sl = self.size_label
        for i, entry in enumerate(self.data):
            sl.text = entry['text']
            sl.width = self.width
            sl.texture_update()
            self.data[i]['ks'] = sl.texture_size
        self.refresh_from_data()


sample_text = \
"""In general, the worst-case time complexity of QuickSort is O(n^2), which occurs when the array is already sorted or almost sorted in the reverse order. The best-case time complexity is O(n log n), which occurs when the pivot element is always chosen as the middle element or when the array is already sorted. The average-case time complexity of QuickSort is O(n log n), which makes it a good sorting algorithm for most practical purposes.
However, the actual compute efficiency of QuickSort can be affected by a variety of factors, including the choice of pivot element, the size of the input array, and the presence of duplicate elements. For example, if the pivot element is always chosen as the first or last element in the array, the time complexity can degrade to O(n^2) in the worst case. Similarly, if the input array contains a large number of duplicate elements, the time complexity can also degrade to O(n^2) in the worst case.
In general, QuickSort is a fast and efficient sorting algorithm that is well-suited for many practical applications. Its average-case time complexity of O(n log n) makes it a good choice for sorting large arrays, and it can be implemented in a variety of programming languages.
The time complexity of a bubble sort algorithm is typically O(n^2), which means that the algorithm's performance is proportional to the square of the size of the input array. This is because a bubble sort algorithm compares adjacent elements and swaps them if they are out of order, which means that it has to perform a number of comparisons and swaps that is proportional to the size of the input array.
For example, if the input array contains n elements, the bubble sort algorithm will need to perform n-1 comparisons on the first pass, n-2 comparisons on the second pass, and so on, until it reaches the final pass, which will only require one comparison. This gives us a total of (n-1) + (n-2) + ... + 2 + 1 = (n^2 - n)/2 comparisons, which is O(n^2).
In addition to the time complexity, the compute efficiency of a bubble sort algorithm can also be affected by factors such as the choice of data structures and the presence of optimized code. However, in general, bubble sort is not considered to be a very efficient sorting algorithm, especially for large input arrays. There are many other sorting algorithms that have a better time complexity and are more efficient in practice, such as QuickSort and MergeSort.
There are many different sorting algorithms that have been developed over the years, and the most efficient algorithm for a given situation can depend on a variety of factors. Some of the factors that can affect the efficiency of a sorting algorithm include the size of the input array, the type of data being sorted, the presence of certain patterns or distributions in the data, and the hardware and software environment in which the algorithm is being implemented.
In general, the most efficient sorting algorithms have a time complexity of O(n log n), which means that their performance is proportional to the size of the input array multiplied by the logarithm of the array size. Some examples of sorting algorithms that have a time complexity of O(n log n) include Quicksort, MergeSort, and HeapSort. These algorithms are generally considered to be the most efficient for sorting large arrays.
However, there are also other sorting algorithms that can be more efficient in certain situations. For example, if the input array is already partially sorted or has a limited number of possible values, certain algorithms, such as Insertion Sort and Selection Sort, can be more efficient. In addition, some algorithms, such as Radix Sort and Counting Sort, can be more efficient for sorting data with a limited range of values.
In general, it is important to consider the specific requirements and constraints of a sorting problem when selecting an algorithm, as the most efficient algorithm can vary depending on the situation.
"""


class LongScrollApp(App):

    def build(self):
        return Builder.load_string(kv)

    def on_start(self):
        self.root.ids.console_rv.add_text(sample_text * 10)


LongScrollApp().run()

rv_console.py

大橋和季

unread,
Jun 25, 2024, 10:09:21 AM (8 days ago) Jun 25
to Kivy users support
Dear Elliot,

I'm so movig for your kindness and your reply is so helpful!

I've tried to correct my code and this is it:

from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ListProperty
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView

from random import randint

KV = '''
<RVLabel>:
    size_hint_y: None
    text_size: self.width, None
    # height: self.texture_size[1] # height must be calculated based on texture_size

    canvas.before:
        Color:
            rgba: .1, .2, .3, 1
        Rectangle:
            pos: self.pos
            size: self.size

<RVBoxLayout>:
    orientation: 'vertical'
    # size_hint_y: None
    height: self.minimum_height
    on_width: app.update_text() # when width changes, update sizes in rv data list

RecycleView:
    viewclass: 'RVBoxLayout'
    RecycleBoxLayout:
        id: rb
        orientation: 'vertical'
        key_size: 'ks' # new property for me!
        default_size: None, None
        default_size_hint: 1, None
        size_hint_y: None
        height: self.minimum_height
'''

class RVBoxLayout(BoxLayout):
    labels = ListProperty([])

    def on_labels(self, instance, data):
        self.clear_widgets()
        for label in self.labels:
            self.add_widget(RVLabel(text=label['text'], size=label['ks']))

class RVLabel(Label):
    pass

class TestApp(App):
    def build(self):
        return Builder.load_string(KV)

    def on_start(self):
        self.rv_label = RVLabel()  # used to calculate texture size, never added to widget tree

        rv = self.root
        rl = self.rv_label
        rv_data = []

        for i in range(10):
            rl.width = rv.width
            rl.texture_update()
            box_layout_data = {'labels': [
                {'text': f'Much Longerrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr Label {j}', 'ks': rl.texture_size}
                for j in range(randint(1, 5)) # <- A
            ]}
            rv_data.append(box_layout_data)

        rv.data = rv_data
   
    def update_text(self):
        rv = self.root
        rl = self.rv_label
        for i, entry in enumerate(rv.data):
            for j in range(len(entry['labels'])):
                rl.text = entry['labels'][j]['text']
                rl.width = rv.width
                rl.texture_update()
                rv.data[i]['labels'][j]['ks'] = rl.texture_size
        rv.refresh_from_data()

if __name__ == '__main__':
    TestApp().run()


First I thought this was good, but repeating execution, the Label widgets showed randomly like the former program. When I changed the window width the labels correctly got shown.

I guessed this was for my pc performance so I tried it after a small change. Instead of the point A's range(randint(1, 5)), I changed it to range(1).

This is the result.
 TestApp Screen Shot.png
It looks something went wrong. What do you think about this?

Thank you so much for your kindness!
2024年6月23日日曜日 23:03:40 UTC+9 ElliotG:

ELLIOT GARBUS

unread,
Jun 26, 2024, 11:47:28 AM (7 days ago) Jun 26
to Kivy users support
I'm having trouble understanding what you are trying to accomplish.  

The key_size in the data list must specify the size of the widget.  In this case it would be the size of the RVBoxLayout (The viewclass).

In your code you are dynamically changing the number of widgets in the viewclass widgt.  This is causing problems.  If you want you could create a number of different viewclass widgets using key_viewclass and specifying the viewclass in the data list.

I have modified your code below, removing the BoxLayout and putting a varying number of lines in the RVLabel.

from kivy.app import App
from kivy.uix.label import Label
from kivy.lang import Builder

from random import randint

KV = '''
<RVLabel>:
    size_hint: 1, None
    text_size: self.width, None
    canvas.before:
        Color:
            rgba: .1, .2, .3, 1
        Rectangle:
            pos: self.pos
            size: self.size

RecycleView:
    viewclass: 'RVLabel'
    RecycleBoxLayout:
        id: rb
        orientation: 'vertical'
        key_size: 'ks' # new property for me!
        default_size: None, None
        default_size_hint: 1, None
        size_hint_y: None
        height: self.minimum_height
        spacing: dp(10)              # added spacing to show individual viewclass instances
        on_width: app.update_text()
'''


class RVLabel(Label):
    pass


class TestApp(App):
    def build(self):
        return Builder.load_string(KV)

    def on_start(self):
        self.size_label = RVLabel()  # used to calculate texture size, never added to widget tree
        self.size_label.width = self.root.width # set the Label Width
        for i in range(20):
            # create the text, a list of strings then join them
            text = [f'Much Longerrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr Label {j}'
                    for j in range(randint(1, 5))]
            text = '\n'.join(text)
            # assign the text to the size Label, and update the texture to calculate the height
            self.size_label.text = text
            self.size_label.texture_update()
            self.root.data.append({'text': text, 'ks': self.size_label.texture_size})

    def update_text(self):
        rv = self.root
        sl = self.size_label
        for i, entry in enumerate(rv.data):
            sl.text = entry['text']
            sl.width = rv.width
            sl.texture_update()
            rv.data[i]['ks'] = sl.texture_size
        rv.refresh_from_data()


if __name__ == '__main__':
    TestApp().run()


From: kivy-...@googlegroups.com <kivy-...@googlegroups.com> on behalf of 大橋和季 <0cd3882...@gmail.com>
Sent: Tuesday, June 25, 2024 7:09 AM
To: Kivy users support <kivy-...@googlegroups.com>
Subject: [kivy-users] Re: BoxLayouts'height in RecycleView / minimum_heigh doesn't work
 
--
You received this message because you are subscribed to the Google Groups "Kivy users support" group.
To unsubscribe from this group and stop receiving emails from it, send an email to kivy-users+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/kivy-users/e7eadd7f-d2d4-4da7-a547-2fb140f3054fn%40googlegroups.com.

大橋和季

unread,
Jun 27, 2024, 1:34:47 AM (6 days ago) Jun 27
to Kivy users support
Thank you for your proposal. I know what you say.

Actually, I'd made an app that includes BoxLayout nested structures in a ScrollView. The number of children Labels changes dynamically, so I tried this and to modify my old app to have a RecycleView. This is why.

Let me attatch the py file and kv file for the old version. They include Japanese characters but not so many.
2024年6月27日木曜日 0:47:28 UTC+9 ElliotG:
cal4.kv
cal4.py

ElliotG

unread,
Jun 27, 2024, 9:43:40 PM (5 days ago) Jun 27
to Kivy users support
I could not run your code - I didn't have the required json file, but I got an idea of what you want to do.

With a ScrollView, each widget is instanced.  You can store state in the widget.  In a RecycleView, only the visible widgets are instanced the state come from the RecycleView data  list (or can come from another list outside of the widget).  This puts some restrictions on how the viewclass is designed. 

In the example below, I use the key_viewclass attribute and multiple widgets, based on the number of items in each widget.  For this example there can be 1, 2 or 3 Labels in the BoxLayout.  The can be extended.  This implementation is a little tedious - but I can not thing of a better way to make this work with a RecycleView.

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import StringProperty
from kivy.uix.label import Label
from kivy.clock import Clock


kv = """
<ListHolderBase>:
    size_hint_y: None
    orientation: 'vertical'
    spacing: dp(2)
    padding: dp(2)
    canvas:
        Color:
            rgb: .7, .7, .7

        Rectangle:
            pos: self.pos
            size: self.size

<ListHolder1>:  # a BoxLayout that holds a number of list items.
    ListItem:
        text: root.text
       
<ListHolder2>:
    ListItem:
        text: root.text_0
    ListItem:
        text: root.text_1
       
<ListHolder3>:
    ListItem:
        text: root.text_0
    ListItem:
        text: root.text_1
    ListItem:
        text: root.text_2

<ListItem>: # A label

    size_hint: 1, None
    text_size: self.width, None
    height: self.texture_size[1]
    font_size: dp(100)

    canvas.before:
        Color:
            rgba: .1, .2, .3, 1
        RoundedRectangle:
            pos: self.pos
            size: self.size

BoxLayout:
    orientation: 'vertical'
    Label:
        size_hint_y: None
        height: dp(30)
        text: 'Labels in a BoxLayout under a RecycleView'
    RecycleView:
        id: rv
        key_viewclass: 'widget'  # multiple viewclass widgets in the data list with the key 'widget'

        RecycleBoxLayout:
            id: rb
            orientation: 'vertical'
            key_size: 'ks' # new property for me!
            default_size: None, None
            default_size_hint: 1, None
            size_hint_y: None
            height: self.minimum_height
            spacing: dp(15)              # added spacing to show individual viewclass instances
"""


class ListHolderBase(BoxLayout):
    pass


class ListHolder1(ListHolderBase):
    text = StringProperty()


class ListHolder2(ListHolderBase):
    text_0 = StringProperty()  # using a list property did not work to hold text
    text_1 = StringProperty()


class ListHolder3(ListHolderBase):
    text_0 = StringProperty()  # using a list property did not work to hold text
    text_1 = StringProperty()
    text_2 = StringProperty()


class ListItem(Label):
    pass


class RecycleBoxApp(App):


    def build(self):
        return Builder.load_string(kv)

    def on_start(self):
        Clock.schedule_once(self.create_content)  # enter kivy loop to allow widgets to size

    def create_content(self, *args):
        contents = [['One'], ['Two', 'Three'], ['Four'], ['Five', 'Six', 'Seven'], ['Eight'], ['Nine', 'Ten']]
        self.list_item = ListItem()  # used to size Labels

        width = self.list_item.width = self.root.ids.rb.width
        for content in contents:
            height = 0  # used to calculate the height of the ListHolder
            for item in content:
                self.list_item.text = item
                self.list_item.texture_update()
                height += self.list_item.texture_size[1]
            if len(content) == 1:
                self.root.ids.rv.data.append({'text': content[0],
                                              'widget': f'ListHolder{len(content)}',
                                              'ks': (width, height + 4)})  # + padding
            elif len(content) == 2:
                self.root.ids.rv.data.append({'text_0': content[0], 'text_1': content[1],
                                              'widget': f'ListHolder{len(content)}',
                                              'ks': (width, height + 6)}) # + padding and spacing
            elif len(content) == 3:
                self.root.ids.rv.data.append({'text_0': content[0], 'text_1': content[1], 'text_2': content[2],
                                              'widget': f'ListHolder{len(content)}',
                                              'ks': (width, height + 8)})  # + padding and spacing
        print(self.root.ids.rv.data)


RecycleBoxApp().run()

rv_nested_box.py

大橋和季

unread,
Jun 28, 2024, 10:55:09 AM (5 days ago) Jun 28
to Kivy users support
I'm sorry to have confused you when running the app. It automatically makes folders required, so I didn't attatch the json files. I'm relieved when I heard you had understood what I wanted to say.

I understood your code. Now I know thanks to you that RecycleView can do not all of what ScrollView can do.  

I'd be happy if you helped me in the next question!
2024年6月28日金曜日 10:43:40 UTC+9 ElliotG:
Reply all
Reply to author
Forward
0 new messages