Python:如何在 RecyclerView 中添加垂直滚动

Posted

技术标签:

【中文标题】Python:如何在 RecyclerView 中添加垂直滚动【英文标题】:Python : How to add vertical scroll in RecycleView 【发布时间】:2018-10-17 13:25:41 【问题描述】:

我正在使用Python-2.7kivy。 我运行test.py 然后显示一个菜单Test。当我点击它然后显示list 的数据。 谁能告诉我如何在列表中添加垂直scrollbar

test.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 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()

        # 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):

        # 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, 'abc'), (1, 'abc'),(1, 'abc'),(1, 'abc'),(1, 'abc'),(1, 'abc'),(1, 'abc'),(1, 'abc'),(1, 'abc'),(1, 'abc'),(1, 'abc'),(1, 'abc'),(1, 'abc'),(1, 'abc')]

        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

<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"


    ScrollView:
        id: kr_scroll
        do_scroll_x: False
        height: 2
        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

【问题讨论】:

您需要在视图中使用行而不是列。 你想让两列一起滚动吗? @ jonyfries 谢谢。是的,我只想要两列滚动 1 个 【参考方案1】:

问题

如何仅在 customerId 列和其他列中设置文本右对齐 应该左对齐吗?

解决方案

片段

def apply_selection(self, rv, index, is_selected):
    ''' Respond to the selection of items in the view. '''
    self.selected = is_selected
    self.text_size = self.size
    if index == rv.data[index]['range'][0]:
        self.halign = 'right'
    else:
        self.halign = 'left'

输出

标签 - 文本垂直增长并以特定宽度换行

一个可以垂直增长但以一定宽度包裹文本的Label:

Label:
    size_hint_y: None
    text_size: self.width, None
    height: self.texture_size[1]

要在 X 轴/水平和 Y 轴/垂直滚动 GridLayout,请将 size_hint 属性设置为 (None, None)。

ScrollView - ScrollEffect, scroll_type, bar_width, etc.

默认情况下,ScrollView 允许沿 X 轴和 Y 轴滚动。您可以通过将 do_scroll_x 或 do_scroll_y 属性设置为 False 来显式禁用轴上的滚动。

要在 Y 轴/垂直方向上滚动 GridLayout,请设置孩子的 宽度为 ScrollView (size_hint_x=1),并设置 size_hint_y 属性为无:

当滚动超出 ScrollView 的边界时,它使用 ScrollEffect 来处理过度滚动。

滚动类型

设置用于滚动视图内容的滚动类型。 可用选项有:['content']、['bars']、['bars'、'content']

['bars'] 通过拖动或滑动滑动条来滚动内容。

Grid Layout

cols_minimum

每列的最小宽度字典。字典键是 列号,例如0、1、2……

cols_minimum 是一个 DictProperty,默认为 。

片段

<RV>:
    row_controller: row_controller

    bar_width: 10
    bar_color: 1, 0, 0, 1   # red
    bar_inactive_color: 0, 0, 1, 1   # blue
    effect_cls: "ScrollEffect"
    scroll_type: ['bars']

    data: root.rv_data
    viewclass: 'SelectableButton'

    SelectableRecycleGridLayout:
        id: row_controller
        key_selection: 'selectable'
        cols: root.total_col_headings
        cols_minimum: root.cols_minimum
        default_size: None, dp(26)
        default_size_hint: 1, None
        size_hint: None, None
        height: self.minimum_height
        width: self.minimum_width
        orientation: 'vertical'
        multiselect: True
        touch_multiselect: True

以下示例演示:

    在 X 轴上滚动表格标题。 支持X轴和Y轴滚动。 支持弹窗 Y 轴滚动。 适用于数据库表中的任何列。 使用 GridLayout 改变列宽 cols_minimum。 将两个 RecycleView 合二为一,因为不需要仅针对 ID 有一个 recycleview。 可选按钮。

SQLite 示例数据库

chinook SQLite sample database

示例

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
from kivy.uix.scrollview import ScrollView
from kivy.uix.label import Label
from kivy.uix.textinput import TextInput
from kivy.uix.recycleview import RecycleView
import sqlite3 as lite
import re

Window.size = (600, 325)


class PopupLabelCell(Label):
    pass


class EditStatePopup(Popup):

    def __init__(self, obj, **kwargs):
        super(EditStatePopup, self).__init__(**kwargs)
        self.populate_content(obj)

    def populate_content(self, obj):
        for x in range(len(obj.table_header.col_headings)):
            self.container.add_widget(PopupLabelCell(text=obj.table_header.col_headings[x]))
            textinput = TextInput(text=str(obj.row_data[x]))
            if x == 0:
                textinput.readonly = True
            self.container.add_widget(textinput)


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

    selected_row = NumericProperty(0)
    obj = ObjectProperty(None)

    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.selected_row = 0
            self.select_row(nodes)
            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, obj):
        ''' Select next row '''
        self.obj = obj
        last, nodes = self.get_nodes()
        if not nodes:
            return

        if last == len(nodes) - 1:
            self.selected_row = nodes[0]
        else:
            self.selected_row = nodes[last + 1]

        self.selected_row += self.obj.total_col_headings
        self.select_row(nodes)

    def select_previous(self, obj):
        ''' Select previous row '''
        self.obj = obj
        last, nodes = self.get_nodes()
        if not nodes:
            return

        if not last:
            self.selected_row = nodes[-1]
        else:
            self.selected_row = nodes[last - 1]

        self.selected_row -= self.obj.total_col_headings
        self.select_row(nodes)

    def select_current(self, obj):
        ''' Select current row '''
        self.obj = obj
        last, nodes = self.get_nodes()
        if not nodes:
            return

        self.select_row(nodes)

    def select_row(self, nodes):
        col = self.obj.rv_data[self.selected_row]['range']
        for x in range(col[0], col[1] + 1):
            self.select_node(nodes[x])


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
        self.text_size = self.size
        if index == rv.data[index]['range'][0]:
            self.halign = 'right'
        else:
            self.halign = 'left'


class HeaderCell(Label):
    pass


class TableHeader(ScrollView):
    """Fixed table header that scrolls x with the data table"""
    header = ObjectProperty(None)
    col_headings = ListProperty([])
    cols_minimum = DictProperty()

    def __init__(self, **kwargs):
        super(TableHeader, self).__init__(**kwargs)
        self.db = lite.connect('chinook.db')
        self.db_cursor = self.db.cursor()
        self.get_table_column_headings()

    def get_table_column_headings(self):
        self.db_cursor.execute("PRAGMA table_info(customers)")
        col_headings = self.db_cursor.fetchall()

        for col_heading in col_headings:
            data_type = col_heading[2]
            if data_type == "INTEGER":
                self.cols_minimum[col_heading[0]] = 100
            else:
                self.cols_minimum[col_heading[0]] = int(re.findall(r'\d+', data_type).pop(0)) * 5
            self.col_headings.append(col_heading[1])
            self.header.add_widget(HeaderCell(text=col_heading[1], width=self.cols_minimum[col_heading[0]]))


class RV(RecycleView):
    row_data = ()
    rv_data = ListProperty([])
    row_controller = ObjectProperty(None)
    total_col_headings = NumericProperty(0)
    cols_minimum = DictProperty()
    table_header = ObjectProperty(None)

    def __init__(self, table_header, **kwargs):
        super(RV, self).__init__(**kwargs)
        self.table_header = table_header
        self.total_col_headings = len(table_header.col_headings)
        self.cols_minimum = table_header.cols_minimum
        self.database_connection()
        self.get_states()
        Clock.schedule_once(self.set_default_first_row, .0005)
        self._request_keyboard()

    def database_connection(self):
        self.db = lite.connect('chinook.db')
        self.db_cursor = self.db.cursor()

    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.row_controller.select_next(self)

        elif keycode[1] == 'up':    # keycode[273, 'up] pressed
            # Respond to keyboard up arrow pressed
            self.display_keystrokes(keyboard, keycode, text, modifiers)
            self.row_controller.select_previous(self)

        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)
            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.setup_row_data(self.rv_data[self.row_controller.selected_row]['Index'])

        # call Popup
        self.popup_callback()

    def on_mouse_select(self, instance):
        ''' Respond to mouse event to call Popup '''

        if self.row_controller.selected_row != instance.index:
            # Mouse clicked on row is not equal to current selected row
            self.row_controller.selected_row = instance.index

            # Hightlight mouse clicked/selected row
            self.row_controller.select_current(self)

        # setup row data for Popup
        self.setup_row_data(self.rv_data[instance.index]['Index'])

        # call Popup
        self.popup_callback()

        # enable keyboard request
        self._request_keyboard()

    def setup_row_data(self, value):
        self.db_cursor.execute("SELECT * FROM customers WHERE CustomerId=?", value)
        self.row_data = self.db_cursor.fetchone()

    def popup_callback(self):
        ''' Instantiate and Open Popup '''
        popup = EditStatePopup(self)
        popup.open()

    def set_default_first_row(self, dt):
        ''' Set default first row as selected '''
        self.row_controller.select_next(self)

    def get_states(self):
        self.db_cursor.execute("SELECT * FROM customers ORDER BY CustomerId ASC")
        rows = self.db_cursor.fetchall()

        data = []
        low = 0
        high = self.total_col_headings - 1
        for row in rows:
            for i in range(len(row)):
                data.append([row[i], row[0], [low, high]])
            low += self.total_col_headings
            high += self.total_col_headings

        self.rv_data = ['text': str(x[0]), 'Index': str(x[1]), 'range': x[2], 'selectable': True for x in data]


class Table(BoxLayout):
    rv = ObjectProperty(None)

    def __init__(self, **kwargs):
        super(Table, self).__init__(**kwargs)
        self.orientation = "vertical"
        self.header = TableHeader()
        self.rv = RV(self.header)

        self.rv.fbind('scroll_x', self.scroll_with_header)

        self.add_widget(self.header)
        self.add_widget(self.rv)

    def scroll_with_header(self, obj, value):
        self.header.scroll_x = value


class MainMenu(BoxLayout):
    states_cities_or_areas = ObjectProperty(None)
    table = ObjectProperty(None)

    def display_states(self):
        self.remove_widgets()
        self.table = Table()
        self.states_cities_or_areas.add_widget(self.table)

    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

<PopupLabelCell>
    size_hint: (None, None)
    height: 30
    text_size: self.size
    halign: "left"
    valign: "middle"

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

    BoxLayout:
        orientation: "vertical"
        ScrollView:
            bar_width: 10
            bar_color: 1, 0, 0, 1   # red
            bar_inactive_color: 0, 0, 1, 1   # blue
            effect_cls: "ScrollEffect"
            scroll_type: ['bars']
            size_hint: (1, None)

            GridLayout:
                id: container
                cols: 2
                row_default_height: 30
                cols_minimum: 0: 100, 1: 300
                # spacing: 10, 10
                # padding: 20, 20
                size_hint: (None, None)
                height: self.minimum_height

        BoxLayout:
            Button:
                size_hint: 1, 0.2
                text: "Save Changes"
                on_release:
                    root.dismiss()
            Button:
                size_hint: 1, 0.2
                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.table.rv.on_mouse_select(self)

<HeaderCell>
    size_hint: (None, None)
    height: 25
    text_size: self.size
    halign: "left"
    valign: "middle"
    background_disabled_normal: ''
    disabled_color: (1, 1, 1, 1)

    canvas.before:
        Color:
            rgba: 1, 0.502, 0, 1
        Rectangle:
            pos: self.pos
            size: self.size

<TableHeader>:
    header: header
    bar_width: 0
    do_scroll: False
    size_hint: (1, None)
    height: 25
    effect_cls: "ScrollEffect"

    GridLayout:
        id: header
        rows: 1
        cols_minimum: root.cols_minimum
        size_hint: (None, None)
        width: self.minimum_width
        height: self.minimum_height

<RV>:
    row_controller: row_controller

    bar_width: 10
    bar_color: 1, 0, 0, 1   # red
    bar_inactive_color: 0, 0, 1, 1   # blue
    effect_cls: "ScrollEffect"
    scroll_type: ['bars']

    data: root.rv_data
    viewclass: 'SelectableButton'

    SelectableRecycleGridLayout:
        id: row_controller
        key_selection: 'selectable'
        cols: root.total_col_headings
        cols_minimum: root.cols_minimum
        default_size: None, dp(26)
        default_size_hint: 1, None
        size_hint: None, None
        height: self.minimum_height
        width: self.minimum_width
        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'

        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

输出

【讨论】:

如何在列表中自动换行并相应地设置行高 @Martin:我已经更新了我的帖子以换行并相应地设置高度。 请您告诉我如何在仅customerId 列中设置文本right align,其他列应为left align。非常感谢您 有关文本对齐的解决方案,请参阅更新后的帖子。【参考方案2】:

诀窍是更改viewclass 以满足您的需求。你曾经使用过Button,但在我看来这只是为了尝试同步你的两个RecycleViews,所以我使用了标签。但是,由于我创建的 TwoLabelListItem 只是一个 BoxLayout,因此您可以轻松地对其进行操作以完全满足您的需求。

viewclass 创建适当的布局后,您只需确保正确应用传递给它的数据。在这里,我将数据应用于两个不同的标签。我还抓取了数据,因为您的笔记表明您希望能够为不同的订单项弹出一些内容。

def refresh_view_attrs(self, rv, index, data):
    ''' Catch and handle the view changes '''
    self.index = index
    self.ids.label1.text = data['text1']
    self.ids.label2.text = data['text2']
    self.data = data

所以保留数据可以让您在选择元素时引用它。

    def apply_selection(self, rv, index, is_selected):
        ''' Respond to the selection of items in the view. '''
        self.selected = is_selected
        if self.selected:
            print(self.data)

向 RecycleView 添加滚动条只需在 kv 中告诉它您要使用它。如果您想让用户能够移动内容或栏,只需将内容添加到列表中。 ['bars', 'content']

RecycleView:        
    scroll_type: ['bars']
    bar_width: 25
    ...

testApp.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 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 TwoLabelListItem(RecycleDataViewBehavior, BoxLayout):
    ''' Add selection support to the Label '''
    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
        self.ids.label1.text = data['text1']
        self.ids.label2.text = data['text2']
        self.data = data

    def on_touch_down(self, touch):
        ''' Add selection on touch down '''
        if super(TwoLabelListItem, 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 self.selected:
            print(self.data)


class RV(BoxLayout):
    data_items = ListProperty([])
    row_data = DictProperty()
    data = ListProperty([])
    col_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.col_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.col_row_controller.select_previous()

        # 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.data[self.col_row_controller.selected_row]

        # call Popup
        self.popup_callback()

    def on_mouse_select(self, instance):
        ''' Respond to mouse event to call Popup '''

        if (self.col_row_controller.selected_row != instance.index):
            # Mouse clicked on row is not equal to current selected row
            self.col_row_controller.selected_row = instance.index

            # Hightlight mouse clicked/selected row
            self.col_row_controller.select_current()

        # setup row data for Popup
        self.row_data = self.data[instance.index]

        # call Popup
        self.popup_callback()

    def popup_callback(self):

        # enable keyboard request
        self._request_keyboard()

    def set_default_first_row(self, dt):
        ''' Set default first row as selected '''
        self.col_row_controller.select_next()

    def update(self):
        self.data = ['text1': str(x[0]), 'text2': x[1], 'Id': str(x[0]), 'Name': x[1], 'key': 'Id', 'selectable': True
                          for x in self.data_items]

    def get_states(self):
        rows = [(x, 'abc') for x in range(25)]

        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

<TwoLabelListItem>:
    # 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
    Label:
        id: label1
    Label:
        id: label2


<RV>:
    col_row_controller: col_row_controller
    RecycleView:
        scroll_type: ['bars']
        bar_width: 25
        size_hint_x: .1
        data: root.data
        viewclass: 'TwoLabelListItem'
        SelectableRecycleGridLayout:
            padding: 0,0,25,0
            id: col_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: False
            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

【讨论】:

以上是关于Python:如何在 RecyclerView 中添加垂直滚动的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Recyclerview 中显示图像 - Recyclerview 图像显示错误

如何在 RecyclerView 中使用 ItemAnimator?

如何在 RecyclerView 中选择位置?

如何在 RecyclerView 中使用 offsetChildrenHorizo​​ntal()

如何在 RecyclerView 和 AsyncTask 中禁用按钮

如何从RecyclerView项中获取Value