Search code examples
pythonpython-3.xkivykivy-language

Kivy Multiple Column RecyclerView


i was just playing with Python and kivy , I've loaded my String data into a RecyclerView as per the kivy official documentation. but I've faced trouble on loading an object to multiple columns inside the list like a form data. for example i wanted to have name,family name and age to three columns with title headers row by row , I've also tried RecyclerGridLayout with 3 columns , but it can load just name into grids regardless of row by row requirement

<RV>:
    viewclass: 'Label'
    RecycleBoxLayout:
        default_size: None, dp(56)
        default_size_hint: 1, None
        size_hint_y: None
        height: self.minimum_height
        orientation: 'vertical'

Will appreciate any hint or sample code to learn how RecyclerView works on kivy


Solution

  • I was also looking for this and I could not find a specific example, so I have provided my solution. As el3ien has said, you will need to create a custom class which will represent each row of your selectable label.

    <SelectableLabel>:
    # Draw a background to indicate selection
    canvas.before:
        Color:
            rgba: (.0, 0.9, .1, .3) if self.selected else (0, 0, 0, 1)
        Rectangle:
            pos: self.pos
            size: self.size
    label1_text: 'label 1 text'  # I have included two methods of accessing the labels
    label2_text: 'label 2 text'  # This is method 1
    label3_text: 'label 3 text'
    pos: self.pos
    size: self.size
    Label:
        id: id_label1  # method 2 uses the label id
        text: root.label1_text
    Label:
        id: id_label2
        text: root.label2_text
    Label:
        id: id_label3
        text: root.label3_text
    

    In applying your data into the RV, you will need to restructure the dictionary to reflect the label layout

    class RV(RecycleView):
    def __init__(self, **kwargs):
        super(RV, self).__init__(**kwargs)
        paired_iter = zip(items_1, items_2)  # items_1 and items_2 are defined elsewhere
        self.data = []
        for i1, i2 in paired_iter:
            d = {'label2': {'text': i1}, 'label3': {'text': i2}}
            self.data.append(d)
    

    Finally in the refresh_view_attrs, you will specify .label_text which is bound to each label, or you can use label id's.

    def refresh_view_attrs(self, rv, index, data):
        ''' Catch and handle the view changes '''
        self.index = index
        self.label1_text = str(index)
        self.label2_text = data['label2']['text']
        self.ids['id_label3'].text = data['label3']['text']  # As an alternate method of assignment
        return super(SelectableLabel, self).refresh_view_attrs(
            rv, index, data)
    

    The entire code is below:

    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.label import Label
    from kivy.uix.gridlayout import GridLayout
    from kivy.properties import BooleanProperty
    from kivy.uix.recycleboxlayout import RecycleBoxLayout
    from kivy.uix.behaviors import FocusBehavior
    from kivy.uix.recycleview.layout import LayoutSelectionBehavior
    
    Builder.load_string('''
    <SelectableLabel>:
        # Draw a background to indicate selection
        canvas.before:
            Color:
                rgba: (.0, 0.9, .1, .3) if self.selected else (0, 0, 0, 1)
            Rectangle:
                pos: self.pos
                size: self.size
        label1_text: 'label 1 text'
        label2_text: 'label 2 text'
        label3_text: 'label 3 text'
        pos: self.pos
        size: self.size
        Label:
            id: id_label1
            text: root.label1_text
        Label:
            id: id_label2
            text: root.label2_text
        Label:
            id: id_label3
            text: root.label3_text
    
    <RV>:
        viewclass: 'SelectableLabel'
        SelectableRecycleBoxLayout:
            default_size: None, dp(56)
            default_size_hint: 1, None
            size_hint_y: None
            height: self.minimum_height
            orientation: 'vertical'
            multiselect: True
            touch_multiselect: True
    ''')
    
    
    items_1 = {'apple', 'banana', 'pear', 'pineapple'}
    items_2 = {'dog', 'cat', 'rat', 'bat'}
    
    
    class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
                                     RecycleBoxLayout):
        ''' Adds selection and focus behaviour to the view. '''
    
    
    class SelectableLabel(RecycleDataViewBehavior, GridLayout):
        ''' Add selection support to the Label '''
        index = None
        selected = BooleanProperty(False)
        selectable = BooleanProperty(True)
        cols = 3
    
        def refresh_view_attrs(self, rv, index, data):
            ''' Catch and handle the view changes '''
            self.index = index
            self.label1_text = str(index)
            self.label2_text = data['label2']['text']
            self.ids['id_label3'].text = data['label3']['text']  # As an alternate method of assignment
            return super(SelectableLabel, self).refresh_view_attrs(
                rv, index, data)
    
        def on_touch_down(self, touch):
            ''' Add selection on touch down '''
            if super(SelectableLabel, 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:
                print("selection changed to {0}".format(rv.data[index]))
            else:
                print("selection removed for {0}".format(rv.data[index]))
    
    
    class RV(RecycleView):
        def __init__(self, **kwargs):
            super(RV, self).__init__(**kwargs)
            paired_iter = zip(items_1, items_2)
            self.data = []
            for i1, i2 in paired_iter:
                d = {'label2': {'text': i1}, 'label3': {'text': i2}}
                self.data.append(d)
            # can also be performed in a complicated one liner for those who like it tricky
            # self.data = [{'label2': {'text': i1}, 'label3': {'text': i2}} for i1, i2 in zip(items_1, items_2)]
    
    
    class TestApp(App):
        def build(self):
            return RV()
    
    if __name__ == '__main__':
        TestApp().run()