Search code examples
pythonkivy

Recycle view with variable widget size not working (Kivy)


So I have a program that receives a string from a server and formats it before adding it to a list used by a recycleview. There is a function that is called when the texture size of one of the widgets in the list changes as they can be different sizes depending on the content. However, whenever more items are added the recycleview goes completely wrong and all of the items overlap.

The behavior varies a lot so I cannot include every example of the issue but I have taken out the relevant code from my program and made a small example program which I will include below. I have also added some images of the issue.

I have been unable to get this to work or find anyone with similar a similar issue. Any help or links will be greatly appreciated, thanks in advance

main.py

from kivy.app import App
from kivy.properties import ListProperty
from kivy.uix.boxlayout import BoxLayout

example_string = 'test)£++£(0)+££+(test)£++£(0)+££+(test)£++£(0)+££+(test)£++£(0)+££+(test)£++£(0'

class MASTER(BoxLayout):
    def example_button(self, Button): # Demo of list updating
        global example_string # I know
        example_string += ')+££+(test)£++£(0'
        temp = []
        id_num = 0
        for post in example_string.split(')+££+('): # The string is an example of the input data
            temp.append({'message_id':id_num, 'text':('[font=Nunito-Bold.ttf][color=161616]Someone:[/color][/font]\n' + post.split(')£++£(')[0]), '_size':[0,0], '_group':str(id_num), '_score':int(post.split(')£++£(')[1])})
            id_num = id_num + 1
        App.get_running_app().posts = temp

class DemoApp(App):
    # One post format = {'message_id':0, 'text':'post_test_here','_size':[0,0], '_group':str(0), '_score':20}
    # Text fromat string = [font=Nunito-Bold.ttf][color=161616]Someone:[/color][/font]\n
    posts = ListProperty([{'message_id':0, 'text':'[font=Nunito-Bold.ttf][color=161616]Someone:[/color][/font]\nHello, this is a test of a post jhghjgjhgh','_size':[0,0], '_group':str(0), '_score':20}, {'message_id':1, 'text':'[font=Nunito-Bold.ttf][color=161616]Someone:[/color][/font]\nHello, this is a test of a post hghghghghghghg', '_size':[0,0], '_group':str(1), '_score':100}, {'message_id':2, 'text':'post_test_her\n\ne','_size':[0,0], '_group':str(2), '_score':20}])

    def update_message_size(self, message_id, texture_size):
        self.posts[message_id] = {**self.posts[message_id], '_size':[1, texture_size[1]]}
        print('Message ID = ' + str(message_id))
        print('Texture size = ' + str(texture_size))

    def up_vote(self, button, mode): # Not part of the problem
        if button.state == 'down':
            if mode == 'all':
                print("+1 upvote for message index:" + str(button.parent.parent.message_id) + ' in all posts')
            else:
                print("+1 upvote for message index:" + str(button.parent.parent.message_id) + ' in top posts')
    
    def down_vote(self, button, mode): # Not part of the problem
        if button.state == 'down':
            if mode == 'all':
                print("-1 upvote for message index:" + str(button.parent.parent.message_id) + ' in all posts')
            else:
                print("-1 upvote for message index:" + str(button.parent.parent.message_id) + ' in top posts')





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

demo.kv

MASTER:

<MASTER>:
    Button:
        text: 'Add items'
        on_press: root.example_button(self)
    RecycleView:
        viewclass: 'PostGrid'
        scroll_y: 1
        id: rv
        data: app.posts
        canvas.before:
            Color:
                rgba: 0, 0, 0, 1
            Rectangle:
                pos: self.pos
                size: self.size
        RecycleBoxLayout:
            id: box
            default_size_hint: 1, None
            size_hint_y: None
            padding: ["10dp", "16dp"]
            spacing: "20dp"
            height: self.minimum_height
            orientation: 'vertical'
            key_size: '_size'

<PostGrid@BoxLayout>:
    message_id: -1
    orientation: "horizontal"
    text: ''
    _group: ''
    _score: 0
    spacing: "6dp"
    text_size: None, None
    BoxLayout:
        id: voting_menu
        orientation: "vertical"
        spacing: "2dp"
        size_hint: .2, 1
        size: self.size
        ToggleButton:
            id: button_up
            on_state: app.up_vote(self, 'all')
            group: str(root._group)
            text: "UP"
            color: (1,1,1,1) if self.state=='normal' else (.8,0,0,1)
            font_size: "10dp"
            size_hint: 1, .3
            background_color: .2, .2, .2, 0
            canvas.before:
                Color:
                    rgba: (.1,.1,.1,1)
                RoundedRectangle:
                    pos: self.pos
                    size: self.size
                    radius: [6,]
            canvas:
                Color:
                    rgba: .2,.2,.2,1
                Line:
                    width: 1.4
                    rounded_rectangle:(self.x,self.y,self.width,self.height, 5)
        Label:
            id: vote_count
            text: str(root._score)
            size_hint: 1, .4
            multiline: False

        ToggleButton:
            id: button_down
            on_state: app.down_vote(self, 'all')
            group: str(root._group)
            text: "DOWN"
            color: (1,1,1,1) if self.state=='normal' else (.8,0,0,1)
            font_size: "10dp"
            size_hint: 1, .3
            background_color: .2, .2, .2, 0
            canvas.before:
                Color:
                    rgba: (.1,.1,.1,1)
                RoundedRectangle:
                    pos: self.pos
                    size: self.size
                    radius: [6,]
            canvas:
                Color:
                    rgba: (.2,.2,.2,1)
                Line:
                    width: 1.4
                    rounded_rectangle:(self.x,self.y,self.width,self.height, 5)
    Label:
        text: root.text
        padding: "10dp", "12dp"
        size_hint: .9, 1
        height: self.texture_size[1]
        font_size: "12dp"
        text_size: self.width, None
        color: 0,0,0,1
        multiline: True
        markup: True
        
        on_texture_size: app.update_message_size(root.message_id, self.texture_size)

        pos: self.pos

        canvas.before:
            Color:
                rgba: (0.8, 0.8, 0.8, 1)
            RoundedRectangle:
                size: self.texture_size
                radius: [5, 5, 5, 5]
                pos: self.x, self.y
        canvas:
            Color:
                rgba:0.8,0,0,1
            Line:
                width: 1.4
                rounded_rectangle:(self.x,self.y,self.width,self.height, 5)

Image of unexpected behavior enter image description here Image of different unexpected behavior enter image description here Video of problem https://youtu.be/F_2TLh-cFYA

Note I would encourage anyone to run this code to fully understand the problem since the code actually behaives as expected the first time I change what is stored in the list 'posts'


Solution

  • How about setting size of the labels from inside instead of setting it from outside using key_size.

    You might keep the viewclass widget of fixed height using the minimum_height and control its child widget by the label's texture height.

    Thus your modified PostGrid and associated code in kvlang will look something like this,

            RecycleBoxLayout:
                id: box
                default_size_hint: 1, None
                default_size: None, dp(50) #
                size_hint_y: None
                padding: ["10dp", "16dp"]
                spacing: "20dp"
                height: self.minimum_height
                orientation: 'vertical'
    #            key_size: '_size'
    # Remove the associated prop. from python (i.e. from method 'example_button' etc.) as well.
    
    <PostGrid@BoxLayout>:
        message_id: -1
        orientation: "horizontal"
        text: ''
        _group: ''
        _score: 0
        spacing: "6dp"
        text_size: None, None
        size_hint_y: None
        height: self.minimum_height
        BoxLayout:
            id: voting_menu
            orientation: "vertical"
            spacing: "2dp"
            size_hint: .2, None
            height: label.height # This binding will force voting_menu to resize.
    #        size: self.size # I don't think it has any effect.
            ToggleButton:
                id: button_up
                on_state: app.up_vote(self, 'all')
                group: str(root._group)
                text: "UP"
                color: (1,1,1,1) if self.state=='normal' else (.8,0,0,1)
                font_size: "10dp"
                size_hint: 1, .3
                background_color: .2, .2, .2, 0
                canvas.before:
                    Color:
                        rgba: (.1,.1,.1,1)
                    RoundedRectangle:
                        pos: self.pos
                        size: self.size
                        radius: [6,]
                canvas:
                    Color:
                        rgba: .2,.2,.2,1
                    Line:
                        width: 1.4
                        rounded_rectangle:(self.x,self.y,self.width,self.height, 5)
            Label:
                id: vote_count
                text: str(root._score)
                size_hint: 1, .4
                multiline: False
    
            ToggleButton:
                id: button_down
                on_state: app.down_vote(self, 'all')
                group: str(root._group)
                text: "DOWN"
                color: (1,1,1,1) if self.state=='normal' else (.8,0,0,1)
                font_size: "10dp"
                size_hint: 1, .3
                background_color: .2, .2, .2, 0
                canvas.before:
                    Color:
                        rgba: (.1,.1,.1,1)
                    RoundedRectangle:
                        pos: self.pos
                        size: self.size
                        radius: [6,]
                canvas:
                    Color:
                        rgba: (.2,.2,.2,1)
                    Line:
                        width: 1.4
                        rounded_rectangle:(self.x,self.y,self.width,self.height, 5)
        Label:
            id: label # For reference.
            text: root.text
            padding: "10dp", "12dp"
            size_hint: .9, None
            height: self.texture_size[1]
            font_size: "12dp"
            text_size: self.width, None
            color: 0,0,0,1
            multiline: True
            markup: True
            
    #        on_texture_size: app.update_message_size(root.message_id, self.texture_size)
    
    #        pos: self.pos
    
            canvas.before:
                Color:
                    rgba: (0.8, 0.8, 0.8, 1)
                RoundedRectangle:
                    size: self.texture_size
                    radius: [5, 5, 5, 5]
                    pos: self.x, self.y
            canvas:
                Color:
                    rgba:0.8,0,0,1
                Line:
                    width: 1.4
                    rounded_rectangle:(self.x,self.y,self.width,self.height, 5)