Python / Kivy:使用`Up`和`Down`键在RecycleView中选择行

时间:2018-04-09 11:41:25

标签: python-2.7 kivy kivy-language

我正在使用python-2.7和kivy。当我运行test.py并点击Test菜单时,屏幕会显示为附加图片 1.如何在屏幕加载时默认highlight第一行?按键盘的updown键后,应根据updown键选择行。
2.当我点击任意行时,它会在modify案例中打开。如何使用modify而不是点击来打开ctrl+e案例中的选定行?

test.py

import kivy

kivy.require('1.9.0')
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import BooleanProperty, ListProperty, ObjectProperty,NumericProperty
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView

from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.button import Button
from kivy.uix.recyclegridlayout import RecycleGridLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.uix.popup import Popup
from kivy.core.window import Window
Window.size = (600, 325)
from kivy.clock import Clock


class EditStatePopup(Popup):
    col_data = ListProperty(["?", "?"])
    index = NumericProperty(0)

    def __init__(self, obj, **kwargs):
        super(EditStatePopup, self).__init__(**kwargs)
        self.index = obj.index
        self.col_data[0] = obj.rv_data[self.index]["Id"]
        self.col_data[1] = obj.rv_data[self.index]["Name"]



class SelectableRecycleGridLayout(FocusBehavior, LayoutSelectionBehavior,
                                  RecycleGridLayout):
    ''' Adds selection and focus behaviour to the view. '''


class SelectableButton(RecycleDataViewBehavior, Button):
    ''' Add selection support to the Button '''
    index = None
    selected = BooleanProperty(False)
    selectable = BooleanProperty(True)
    rv_data = ObjectProperty(None)
    start_point = NumericProperty(0)

    def __init__(self, **kwargs):
        super(SelectableButton, self).__init__(**kwargs)
        Clock.schedule_interval(self.update, .0005)



    def update(self, *args):
        self.text = self.rv_data[self.index][self.key]

    def refresh_view_attrs(self, rv, index, data):
        ''' Catch and handle the view changes '''
        self.index = index
        return super(SelectableButton, self).refresh_view_attrs(rv, index, data)

    def on_touch_down(self, touch):
        ''' Add selection on touch down '''
        if super(SelectableButton, 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):
        self.selected = is_selected
        self.rv_data = rv.data



    def on_press(self):
        popup = EditStatePopup(self)
        popup.open()

class MyRV(RecycleView):
    def __init__(self, **kwargs):
        super(MyRV, self).__init__(**kwargs)
        self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
        self._keyboard.bind(on_key_down=self._on_keyboard_down)
        self.selectedItem = -1

    def _keyboard_closed(self):
        pass

    def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
        if keycode[1] == 'down':
            self.clearAll()
            self.nextItem()
            print('down')
        elif keycode[1] == 'up':
            self.clearAll()
            self.prevItem()
            print("up")
        elif keycode[1] == 'e' and len(modifiers) > 0 and modifiers[0] == 'ctrl':
            self.view_adapter.views[self.selectedItem].on_press()

    def clearAll(self):
        if (self.selectedItem > -1):
            for i in range(len(self.view_adapter.views) - 1):
                self.view_adapter.views[self.selectedItem].selected = 0


    def nextItem(self):
        if self.selectedItem < len(self.view_adapter.views) - 1:
            self.selectedItem += 1
        else:
            self.selectedItem = 0
        self.view_adapter.views[self.selectedItem].selected = 1
        print(self.selectedItem)


    def prevItem(self):
        if self.selectedItem > 0:
            self.selectedItem -= 1
        else:
            self.selectedItem = len(self.view_adapter.views) - 1
        self.view_adapter.views[self.selectedItem].selected = 1
        print(self.selectedItem)



class RV(RecycleView):
    data_items = ListProperty([])
    col1 = ListProperty()
    col2 = ListProperty()

    def __init__(self, **kwargs):
        super(RV, self).__init__(**kwargs)
        self.get_states()

    def update(self):
        self.col1 = [{'Id': str(x[0]), 'Name': x[1], 'key': 'Id', 'text': str(x[2])} for x in self.data_items]
        self.col2 = [{'Id': str(x[0]), 'Name': x[1], 'key': 'Name', 'text': str(x[2])} for x in self.data_items]


    def get_states(self):
        rows = [(1, 'Test1'), (2, 'Test2'), (3, 'Test3')]

        i = 0
        for row in rows:
            self.data_items.append([row[0], row[1], i])
            i += 1
        print(self.data_items)
        self.update()



class MainMenu(BoxLayout):
    states_cities_or_areas = ObjectProperty()

    def display_states(self):
        self.remove_widgets()
        self.rv = RV()
        self.states_cities_or_areas.add_widget(self.rv)

    def remove_widgets(self):
        self.states_cities_or_areas.clear_widgets()

class TestApp(App):
    title = "test"

    def build(self):
        return MainMenu()



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

test.kv

<SelectableButton>:
    canvas.before:
        Color:
            rgba: (10, 10, 10, 10) if self.selected else (0, 0.517, 0.705, 1)
        Rectangle:
            pos: self.pos
            size: self.size


<MyRV@RecycleView>:
    viewclass: 'SelectableButton'
    SelectableRecycleGridLayout:
        cols: 1
        default_size: None, dp(26)
        default_size_hint: 1, None
        size_hint_y: None
        height: self.minimum_height
        orientation: 'vertical'
        multiselect: True
        touch_multiselect: True

<RV>:
    BoxLayout:
        orientation: "vertical"
        viewclass: 'SelectableButton'
        GridLayout:
            size_hint: 1, None
            size_hint_y: None
            height: 25
            cols: 3

            Label:
                size_hint_x: .1
                text: "Id"
            Label:
                size_hint_x: .5
                text: "Name"

        BoxLayout:
            MyRV:
                size_hint_x: .1
                data: root.col1
            MyRV:
                size_hint_x: .5
                data: root.col2



<EditStatePopup>:
    size_hint: None, None
    title_size: 20
    title_font: "Verdana"
    size: 400, 275
    auto_dismiss: False

    BoxLayout:
        orientation: "vertical"
        GridLayout:
            cols: 2
            #backgroun_color: 0, 0.517, 0.705, 1
            spacing: 10, 10
            padding: 20, 20
            Label:
                text: "Id"
                text_size: self.size
            Label:
                text: root.col_data[0]
                text_size: self.size
            Label:
                text: "Name"
                text_size: self.size
                valign: 'middle'
            TextInput:
                focus : True
                text: root.col_data[1]
                text_size: self.size

        GridLayout:
            cols: 2
            padding: 10, 0, 10, 10
            spacing: 10, 10
            row_default_height: '20dp'
            size_hint: .5, .2
            pos_hint: {'x': .25, 'y':.65}

            Button:
                text: 'Ok'


            Button:
                text: 'Cancel'
                size_hint_x: .5
                on_release: root.dismiss()




<MenuButton@Button>:
    text_size: self.size
    valign: "middle"
    padding_x: 5
    size : (80,30)
    size_hint : (None, None)
    background_color: 90 , 90, 90, 90
    background_normal: ''
    color: 0, 0.517, 0.705, 1
    border: (0, 10, 0, 0)


<MainMenu>:
    states_cities_or_areas: states_cities_or_areas


    BoxLayout:
        orientation: 'vertical'
        #spacing : 10

        BoxLayout:
            canvas.before:
                Rectangle:
                    pos: self.pos
                    size: self.size

            size_hint_y: 1

            MenuButton:
                id: btn
                text: 'Test'
                size : (60,30)
                on_release: root.display_states()


        BoxLayout:
            canvas.before:
                Rectangle:
                    pos: self.pos
                    size: self.size

                Color:
                    rgb: (1,1,1)

            Label:
                size_hint_x: 45

        BoxLayout:
            id: states_cities_or_areas
            size_hint_y: 10

        Label:
            size_hint_y: 1

2 个答案:

答案 0 :(得分:2)

  1. 在SelectableButton上使用2种不同的颜色
  2. 检查按键并触发on_press
  3. text.kv

    <SelectableButton>:
        canvas.before:
            Color:
                rgba: (0.5, 0.5, 0.5, 1) if self.selected else (0, 0.517, 0.705, 1)
            Rectangle:
                pos: self.pos
                size: self.size
    
    
    <MyRV@RecycleView>:
        viewclass: 'SelectableButton'
        SelectableRecycleGridLayout:
            cols: 1
            default_size: None, dp(26)
            default_size_hint: 1, None
            size_hint_y: None
            height: self.minimum_height
            orientation: 'vertical'
            multiselect: True
            touch_multiselect: True
    
    <RV>:
        BoxLayout:
            orientation: "vertical"
            viewclass: 'SelectableButton'
            GridLayout:
                size_hint: 1, None
                size_hint_y: None
                height: 25
                cols: 3
    
                Label:
                    size_hint_x: .1
                    text: "Id"
                Label:
                    size_hint_x: .5
                    text: "Name"
    
            BoxLayout:
                MyRV:
                    size_hint_x: .1
                    data: root.col1
                MyRV:
                    size_hint_x: .5
                    data: root.col2
    
    
    <EditStatePopup>:
        size_hint: None, None
        title_size: 20
        title_font: "Verdana"
        size: 400, 275
        auto_dismiss: False
    
        BoxLayout:
            orientation: "vertical"
            GridLayout:
                cols: 2
                #backgroun_color: 0, 0.517, 0.705, 1
                spacing: 10, 10
                padding: 20, 20
                Label:
                    text: "Id"
                    text_size: self.size
                Label:
                    text: root.col_data[0]
                    text_size: self.size
                Label:
                    text: "Name"
                    text_size: self.size
                    valign: 'middle'
                TextInput:
                    text: root.col_data[1]
                    text_size: self.size
    
    
    
    <MenuButton@Button>:
        text_size: self.size
        valign: "middle"
        padding_x: 5
        size : (80,30)
        size_hint : (None, None)
        background_color: 90 , 90, 90, 90
        background_normal: ''
        color: 0, 0.517, 0.705, 1
        border: (0, 10, 0, 0)
    
    
    <MainMenu>:
        states_cities_or_areas: states_cities_or_areas
    
    
        BoxLayout:
            orientation: 'vertical'
            #spacing : 10
    
            BoxLayout:
                canvas.before:
                    Rectangle:
                        pos: self.pos
                        size: self.size
    
                size_hint_y: 1
    
                MenuButton:
                    id: btn
                    text: 'Test'
                    size : (60,30)
                    on_release: root.display_states()
    
    
            BoxLayout:
                canvas.before:
                    Rectangle:
                        pos: self.pos
                        size: self.size
    
                    Color:
                        rgb: (1,1,1)
    
                Label:
                    size_hint_x: 45
    
            BoxLayout:
                id: states_cities_or_areas
                size_hint_y: 10
    
            Label:
                size_hint_y: 1
    

    TestApp.py

    import kivy
    
    kivy.require('1.9.0')
    from kivy.app import App
    from kivy.uix.boxlayout import BoxLayout
    from kivy.properties import BooleanProperty, ListProperty, ObjectProperty,NumericProperty
    from kivy.lang import Builder
    from kivy.uix.recycleview import RecycleView
    
    from kivy.uix.recycleview.views import RecycleDataViewBehavior
    from kivy.uix.button import Button
    from kivy.uix.recyclegridlayout import RecycleGridLayout
    from kivy.uix.behaviors import FocusBehavior
    from kivy.uix.recycleview.layout import LayoutSelectionBehavior
    from kivy.uix.popup import Popup
    from kivy.core.window import Window
    Window.size = (600, 325)
    from kivy.clock import Clock
    
    
    class EditStatePopup(Popup):
        col_data = ListProperty(["?", "?"])
        index = NumericProperty(0)
    
        def __init__(self, obj, **kwargs):
            super(EditStatePopup, self).__init__(**kwargs)
            self.index = obj.index
            self.col_data[0] = obj.rv_data[self.index]["Id"]
            self.col_data[1] = obj.rv_data[self.index]["Name"]
    
    
    
    class SelectableRecycleGridLayout(FocusBehavior, LayoutSelectionBehavior,
                                      RecycleGridLayout):
        ''' Adds selection and focus behaviour to the view. '''
    
    
    class SelectableButton(RecycleDataViewBehavior, Button):
        ''' Add selection support to the Button '''
        index = None
        selected = BooleanProperty(False)
        selectable = BooleanProperty(True)
        rv_data = ObjectProperty(None)
        start_point = NumericProperty(0)
    
        def __init__(self, **kwargs):
            super(SelectableButton, self).__init__(**kwargs)
            Clock.schedule_interval(self.update, .0005)
    
    
    
        def update(self, *args):
            self.text = self.rv_data[self.index][self.key]
    
        def refresh_view_attrs(self, rv, index, data):
            ''' Catch and handle the view changes '''
            self.index = index
            return super(SelectableButton, self).refresh_view_attrs(rv, index, data)
    
        def on_touch_down(self, touch):
            ''' Add selection on touch down '''
            if super(SelectableButton, 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):
            self.selected = is_selected
            self.rv_data = rv.data
    
    
    
        def on_press(self):
            popup = EditStatePopup(self)
            popup.open()
    
    class MyRV(RecycleView):
        def __init__(self, **kwargs):
            super(MyRV, self).__init__(**kwargs)
            self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
            self._keyboard.bind(on_key_down=self._on_keyboard_down)
            self.selectedItem = -1
    
        def _keyboard_closed(self):
            pass
    
        def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
            if keycode[1] == 'down':
                self.clearAll()
                self.nextItem()
                print('down')
            elif keycode[1] == 'up':
                self.clearAll()
                self.prevItem()
                print("up")
            elif keycode[1] == 'e' and len(modifiers) > 0 and modifiers[0] == 'ctrl':
                self.view_adapter.views[self.selectedItem].on_press()
    
        def clearAll(self):
            if (self.selectedItem > -1):
                for i in range(len(self.view_adapter.views) - 1):
                    self.view_adapter.views[self.selectedItem].selected = 0
    
    
        def nextItem(self):
            if self.selectedItem < len(self.view_adapter.views) - 1:
                self.selectedItem += 1
            else:
                self.selectedItem = 0
            self.view_adapter.views[self.selectedItem].selected = 1
            print(self.selectedItem)
    
    
        def prevItem(self):
            if self.selectedItem > 0:
                self.selectedItem -= 1
            else:
                self.selectedItem = len(self.view_adapter.views) - 1
            self.view_adapter.views[self.selectedItem].selected = 1
            print(self.selectedItem)
    
    
    
    class RV(RecycleView):
        data_items = ListProperty([])
        col1 = ListProperty()
        col2 = ListProperty()
    
        def __init__(self, **kwargs):
            super(RV, self).__init__(**kwargs)
            self.get_states()
    
        def update(self):
            self.col1 = [{'Id': str(x[0]), 'Name': x[1], 'key': 'Id', 'text': str(x[2])} for x in self.data_items]
            self.col2 = [{'Id': str(x[0]), 'Name': x[1], 'key': 'Name', 'text': str(x[2])} for x in self.data_items]
    
    
        def get_states(self):
            rows = [(1, 'Test1'), (2, 'Test2'), (3, 'Test3')]
    
            i = 0
            for row in rows:
                self.data_items.append([row[0], row[1], i])
                i += 1
            print(self.data_items)
            self.update()
    
    
    
    class MainMenu(BoxLayout):
        states_cities_or_areas = ObjectProperty()
    
        def display_states(self):
            self.remove_widgets()
            self.rv = RV()
            self.states_cities_or_areas.add_widget(self.rv)
    
        def remove_widgets(self):
            self.states_cities_or_areas.clear_widgets()
    
    class TestApp(App):
        title = "test"
    
        def build(self):
            return MainMenu()
    
    
    
    if __name__ == '__main__':
        TestApp().run()
    

答案 1 :(得分:1)

我在类 SelectableRecycleGridLayout 中实现了一些方法,以使用类 LayoutSelectionBehavior 中的方法。有关详细信息,请参阅测试计划,问题,示例和输出。

的关注

原始程序有两个问题会影响性能和内存使用。

第1期

调用方法更新每隔0.0005秒设置小部件的文本。这非常昂贵。请参阅Programming Guide » Events and Properties » Scheduling a repetitive event & Trigger events

class SelectableButton(RecycleDataViewBehavior, Button):
    ...
    def __init__(self, **kwargs):
        super(SelectableButton, self).__init__(**kwargs)
        Clock.schedule_interval(self.update, .0005)

    def update(self, *args):
        self.text = self.rv_data[self.index][self.key]

解决方案 - 问题1

RV 类的方法 update 中添加 text

class RV(BoxLayout):
    ...
    def update(self):
        self.col1_data = [{'text': str(x[0]), 'Id': str(x[0]), 'Name': x[1], 'key': 'Id', 'selectable': True}
                          for x in self.data_items]

        self.col2_data = [{'text': x[1], 'Id': str(x[0]), 'Name': x[1], 'key': 'Name', 'selectable': True}
                          for x in self.data_items]

第2期

col1_data col2_data 除了 key Id 名称的文本和dictonary)之外是相似的)。当 数据变大 时,应用可能无法运行,因为内存不足,如果确实运行,则效果不佳。

解决方案 - 第2期

没有提供解决方案,因为它不属于本主题,而是留给您练习。

测试计划

  1. 单击测试,显示RecycleView并突出显示第一行。
  2. 按向下箭头键,突出显示下一行。
  3. 在最后一行,按下箭头键,第一行高亮显示。
  4. 按向上箭头键,上一行高亮显示。
  5. 在第一行,按向上箭头键,最后一行突出显示。
  6. 按Ctrl + e,弹出窗口显示突出显示的行的信息。
  7. 鼠标单击未突出显示的任何行,选中鼠标单击的行,然后打开弹出窗口。
  8. 鼠标点击任意一行,弹出窗口解除后,键盘事件仍然有效!
  9. 实施例

    main.py

    from kivy.app import App
    from kivy.uix.boxlayout import BoxLayout
    from kivy.properties import BooleanProperty, ListProperty, ObjectProperty, NumericProperty, DictProperty
    
    from kivy.uix.recycleview.views import RecycleDataViewBehavior
    from kivy.uix.button import Button
    from kivy.uix.recyclegridlayout import RecycleGridLayout
    from kivy.uix.behaviors import FocusBehavior
    from kivy.uix.recycleview.layout import LayoutSelectionBehavior
    from kivy.uix.popup import Popup
    from kivy.core.window import Window
    from kivy.clock import Clock
    
    Window.size = (600, 325)
    
    
    class EditStatePopup(Popup):
        col_data = ListProperty(["?", "?"])
    
        def __init__(self, obj, **kwargs):
            super(EditStatePopup, self).__init__(**kwargs)
            self.col_data[0] = obj["Id"]
            self.col_data[1] = obj["Name"]
    
    
    class SelectableRecycleGridLayout(FocusBehavior, LayoutSelectionBehavior,
                                      RecycleGridLayout):
        ''' Adds selection and focus behaviour to the view. '''
    
        selected_row = NumericProperty(0)
    
        def get_nodes(self):
            nodes = self.get_selectable_nodes()
            if self.nodes_order_reversed:
                nodes = nodes[::-1]
            if not nodes:
                return None, None
    
            selected = self.selected_nodes
            if not selected:    # nothing selected, select the first
                self.select_node(nodes[0])
                self.selected_row = 0
                return None, None
    
            if len(nodes) == 1:     # the only selectable node is selected already
                return None, None
    
            last = nodes.index(selected[-1])
            self.clear_selection()
            return last, nodes
    
        def select_next(self):
            ''' Select next row '''
            last, nodes = self.get_nodes()
            if not nodes:
                return
    
            if last == len(nodes) - 1:
                self.select_node(nodes[0])
                self.selected_row = nodes[0]
            else:
                self.select_node(nodes[last + 1])
                self.selected_row = nodes[last + 1]
    
        def select_previous(self):
            ''' Select previous row '''
            last, nodes = self.get_nodes()
            if not nodes:
                return
    
            if not last:
                self.select_node(nodes[-1])
                self.selected_row = nodes[-1]
            else:
                self.select_node(nodes[last - 1])
                self.selected_row = nodes[last - 1]
    
        def select_current(self):
            ''' Select current row '''
            last, nodes = self.get_nodes()
            if not nodes:
                return
    
            self.select_node(nodes[self.selected_row])
    
    
    class SelectableButton(RecycleDataViewBehavior, Button):
        ''' Add selection support to the Button '''
        index = None
        selected = BooleanProperty(False)
        selectable = BooleanProperty(True)
    
        def refresh_view_attrs(self, rv, index, data):
            ''' Catch and handle the view changes '''
    
            self.index = index
            return super(SelectableButton, self).refresh_view_attrs(rv, index, data)
    
        def on_touch_down(self, touch):
            ''' Add selection on touch down '''
            if super(SelectableButton, self).on_touch_down(touch):
                return True
            if self.collide_point(*touch.pos) and self.selectable:
                print("on_touch_down: self=", self)
                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
    
    
    class RV(BoxLayout):
        data_items = ListProperty([])
        row_data = DictProperty({})
        col1_data = ListProperty([])
        col2_data = ListProperty([])
        col1_row_controller = ObjectProperty(None)
        col2_row_controller = ObjectProperty(None)
    
        def __init__(self, **kwargs):
            super(RV, self).__init__(**kwargs)
            self.get_states()
            Clock.schedule_once(self.set_default_first_row, .0005)
            self._request_keyboard()
    
        def _request_keyboard(self):
            self._keyboard = Window.request_keyboard(
                self._keyboard_closed, self, 'text'
            )
            if self._keyboard.widget:
                # If it exists, this widget is a VKeyboard object which you can use
                # to change the keyboard layout.
                pass
            self._keyboard.bind(on_key_down=self._on_keyboard_down)
    
        def _keyboard_closed(self):
            self._keyboard.unbind(on_key_down=self._on_keyboard_down)
            self._keyboard = None
    
        def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
            if keycode[1] == 'down':    # keycode[274, 'down'] pressed
                # Respond to keyboard down arrow pressed
                self.display_keystrokes(keyboard, keycode, text, modifiers)
                self.col1_row_controller.select_next()
                self.col2_row_controller.select_next()
    
            elif keycode[1] == 'up':    # keycode[273, 'up] pressed
                # Respond to keyboard up arrow pressed
                self.display_keystrokes(keyboard, keycode, text, modifiers)
                self.col1_row_controller.select_previous()
                self.col2_row_controller.select_previous()
    
            elif len(modifiers) > 0 and modifiers[0] == 'ctrl' and text == 'e':     # ctrl + e pressed
                # Respond to keyboard ctrl + e pressed, and call Popup
                self.display_keystrokes(keyboard, keycode, text, modifiers)
                keyboard.release()
                self.on_keyboard_select()
    
            # Keycode is composed of an integer + a string
            # If we hit escape, release the keyboard
            if keycode[1] == 'escape':
                keyboard.release()
    
            # Return True to accept the key. Otherwise, it will be used by
            # the system.
            return True
    
        def display_keystrokes(self, keyboard, keycode, text, modifiers):
            print("\nThe key", keycode, "have been pressed")
            print(" - text is %r" % text)
            print(" - modifiers are %r" % modifiers)
    
        def on_keyboard_select(self):
            ''' Respond to keyboard event to call Popup '''
    
            # setup row data for Popup
            self.row_data = self.col1_data[self.col1_row_controller.selected_row]
    
            # call Popup
            self.popup_callback()
    
        def on_mouse_select(self, instance):
            ''' Respond to mouse event to call Popup '''
    
            if (self.col1_row_controller.selected_row != instance.index
                    or self.col2_row_controller.selected_row != instance.index):
                # Mouse clicked on row is not equal to current selected row
                self.col1_row_controller.selected_row = instance.index
                self.col2_row_controller.selected_row = instance.index
    
                # Hightlight mouse clicked/selected row
                self.col1_row_controller.select_current()
                self.col2_row_controller.select_current()
    
            # setup row data for Popup
            # we can use either col1_data or col2_data because they are duplicate and each stores the same info
            self.row_data = self.col1_data[instance.index]
    
            # call Popup
            self.popup_callback()
    
        def popup_callback(self):
            ''' Instantiate and Open Popup '''
            popup = EditStatePopup(self.row_data)
            popup.open()
    
            # enable keyboard request
            self._request_keyboard()
    
        def set_default_first_row(self, dt):
            ''' Set default first row as selected '''
            self.col1_row_controller.select_next()
            self.col2_row_controller.select_next()
    
        def update(self):
            self.col1_data = [{'text': str(x[0]), 'Id': str(x[0]), 'Name': x[1], 'key': 'Id', 'selectable': True}
                              for x in self.data_items]
    
            self.col2_data = [{'text': x[1], 'Id': str(x[0]), 'Name': x[1], 'key': 'Name', 'selectable': True}
                              for x in self.data_items]
    
        def get_states(self):
            rows = [(1, 'Test1'), (2, 'Test2'), (3, 'Test3')]
    
            i = 0
            for row in rows:
                self.data_items.append([row[0], row[1], i])
                i += 1
            print(self.data_items)
            self.update()
    
    
    class MainMenu(BoxLayout):
        states_cities_or_areas = ObjectProperty(None)
        rv = ObjectProperty(None)
    
        def display_states(self):
            self.remove_widgets()
            self.rv = RV()
            self.states_cities_or_areas.add_widget(self.rv)
    
        def remove_widgets(self):
            self.states_cities_or_areas.clear_widgets()
    
    
    class TestApp(App):
        title = "test"
    
        def build(self):
            return MainMenu()
    
    
    if __name__ == '__main__':
        TestApp().run()
    

    test.kv

    #:kivy 1.10.0
    
    <EditStatePopup>:
        size_hint: None, None
        title_size: 20
        title_font: "Verdana"
        size: 400, 275
        auto_dismiss: False
    
        BoxLayout:
            orientation: "vertical"
            GridLayout:
                cols: 2
                #background_color: 0, 0.517, 0.705, 1
                spacing: 10, 10
                padding: 20, 20
                Label:
                    text: "Id"
                    text_size: self.size
                Label:
                    text: root.col_data[0]
                    text_size: self.size
                Label:
                    text: "Name"
                    text_size: self.size
                    valign: 'middle'
                TextInput:
                    text: root.col_data[1]
                    text_size: self.size
                    focus: True
            BoxLayout:
                Button:
                    size_hint: 1, 0.4
                    text: "Save Changes"
                    on_release:
                        root.dismiss()
                Button:
                    size_hint: 1, 0.4
                    text: "Cancel Changes"
                    on_release: root.dismiss()
    
    
    <SelectableButton>:
        canvas.before:
            Color:
                rgba: (0, 0.517, 0.705, 1) if self.selected else (0, 0.517, 0.705, 1)
            Rectangle:
                pos: self.pos
                size: self.size
        background_color: [1, 0, 0, 1]  if self.selected else [1, 1, 1, 1]  # dark red else dark grey
        on_press: app.root.rv.on_mouse_select(self)
    
    
    <RV>:
        col1_row_controller: col1_row_controller
        col2_row_controller: col2_row_controller
    
        orientation: "vertical"
    
        GridLayout:
            size_hint: 1, None
            size_hint_y: None
            height: 25
            cols: 3
    
            Label:
                size_hint_x: .1
                text: "Id"
            Label:
                size_hint_x: .5
                text: "Name"
    
        BoxLayout:
            RecycleView:
                size_hint_x: .1
                data: root.col1_data
                viewclass: 'SelectableButton'
                SelectableRecycleGridLayout:
                    id: col1_row_controller
                    key_selection: 'selectable'
                    cols: 1
                    default_size: None, dp(26)
                    default_size_hint: 1, None
                    size_hint_y: None
                    height: self.minimum_height
                    orientation: 'vertical'
                    multiselect: True
                    touch_multiselect: True
    
            RecycleView:
                size_hint_x: .5
                data: root.col2_data
                viewclass: 'SelectableButton'
                SelectableRecycleGridLayout:
                    id: col2_row_controller
                    key_selection: 'selectable'
                    cols: 1
                    default_size: None, dp(26)
                    default_size_hint: 1, None
                    size_hint_y: None
                    height: self.minimum_height
                    orientation: 'vertical'
                    multiselect: True
                    touch_multiselect: True
    
    
    <MenuButton@Button>:
        text_size: self.size
        valign: "middle"
        padding_x: 5
        size : (80,30)
        size_hint : (None, None)
        background_color: 90 , 90, 90, 90
        background_normal: ''
        color: 0, 0.517, 0.705, 1
        border: (0, 10, 0, 0)
    
    
    <MainMenu>:
        states_cities_or_areas: states_cities_or_areas
    
        BoxLayout:
            orientation: 'vertical'
            #spacing : 10
    
            BoxLayout:
                canvas.before:
                    Rectangle:
                        pos: self.pos
                        size: self.size
    
                size_hint_y: 1
    
                MenuButton:
                    id: btn
                    text: 'Test'
                    size : (60,30)
                    on_release: root.display_states()
    
    
            BoxLayout:
                canvas.before:
                    Rectangle:
                        pos: self.pos
                        size: self.size
    
                    Color:
                        rgb: (1,1,1)
    
                Label:
                    size_hint_x: 45
    
            BoxLayout:
                id: states_cities_or_areas
                size_hint_y: 10
    
            Label:
                size_hint_y: 1
    

    输出

    Img01 - App Startup Img02 - RV displayed after clicked Test Img03 - Ctrl+e at first row Img04 - Down Arrow key pressed Img05 - Up Arrow key pressed Img06 - Ctrl+e pressed Img07 - Mouse click on first row