You need to enable JavaScript to run this app. Kivy中RecycleView添加DownloadItem实例出现重复、消失异常问题
优惠活动
大模型
产品
解决方案
定价
更多

Kivy中RecycleView添加DownloadItem实例出现重复、消失异常问题

问题根因

你遇到的条目重复异常是Kivy RecycleView的视图复用机制导致的:

  • RecycleView不会为每一条数据创建独立的DownloadItem实例,只会创建刚好铺满显示区域的少量实例,滚动时通过将新数据绑定到旧视图实例的方式复用控件,降低内存占用
  • 你当前代码里的paused、fill_animation是DownloadItem的普通实例属性,且对按钮图标、进度条状态的修改没有同步到RecycleView的data数据源中,视图复用时旧视图的残留状态就会被带到新数据的展示上,看起来就像条目被复制了。

修复方案

需要把所有动态状态都和数据源绑定,所有视图状态的变化都通过修改数据源触发,修改后的可运行代码如下:

from kivy.lang import Builder
from kivy.properties import StringProperty, ColorProperty, BooleanProperty, NumericProperty
from kivy.animation import Animation
from kivymd.uix.card import MDCard
from kivymd.app import MDApp


class DownloadItem(MDCard):
    path = StringProperty()
    url_type = StringProperty("file")
    paused = BooleanProperty(False)
    progress_value = NumericProperty(50)
    progress_color = ColorProperty()
    # 记录当前项在数据源中的索引
    item_index = NumericProperty(-1)

    def __init__(self, **kwargs):
        self.fill_animation = None
        super(DownloadItem, self).__init__(**kwargs)

    def pause_resume_download(self):
        # 先获取当前RecycleView的数据源
        rv = self.parent.parent
        if self.item_index < 0 or self.item_index >= len(rv.data):
            return
        # 修改数据源属性,自动触发视图更新
        item_data = rv.data[self.item_index]
        if not self.paused:
            item_data["paused"] = True
            if self.fill_animation is not None:
                self.fill_animation.cancel(self.ids.progress_bar)
            item_data["progress_color"] = app.pause_color
        else:
            item_data["paused"] = False
            self.fill_animation = Animation(value=100, duration=4)
            def on_anim_complete(*args):
                if self.item_index < len(rv.data):
                    rv.data[self.item_index]["progress_color"] = self.theme_cls.accent_color
            self.fill_animation.bind(on_complete=on_anim_complete)
            self.fill_animation.start(self.ids.progress_bar)
            item_data["progress_color"] = self.theme_cls.primary_color
        # 刷新数据源触发重绘
        rv.data[self.item_index] = item_data

    def cancel_download(self):
        rv = self.parent.parent
        if self.item_index < 0 or self.item_index >= len(rv.data):
            return
        if self.fill_animation is not None:
            self.fill_animation.cancel(self.ids.progress_bar)
        # 修改数据源
        item_data = rv.data[self.item_index]
        item_data["progress_color"] = app.fail_color
        rv.data[self.item_index] = item_data

class Example(MDApp):
    fail_color = ColorProperty([255 / 255, 99 / 255, 71 / 255, 1.0])
    pause_color = ColorProperty([240 / 255, 163 / 255, 10 / 255, 1.0])
    success_color = ColorProperty(None)
    color_duration = .15


    def __init__(self, **kwargs):
        global app
        super(Example, self).__init__(**kwargs)
        self.kv = Builder.load_string('''
#:kivy 2.0.0
<TooltipMDLabel@MDLabel+MDTooltip>
<DownloadItem>:
    orientation: "vertical"
    padding: 10
    spacing: 10
    size_hint_y: None
    height: 100
    elevation: 20
    border_radius: 5
    radius: [5]
    MDBoxLayout:
        adaptive_height: True
        spacing: 5
        MDIcon:
            icon: root.url_type
            size_hint_x: None
            width: self.texture_size[0]
        TooltipMDLabel:
            text: root.path if len(root.path) <= 30 else root.path[:31] + " ..."
            tooltip_text: f"Path: {root.path} | Type: {root.url_type}"
            tooltip_bg_color: app.theme_cls.bg_darkest
            tooltip_text_color: app.theme_cls.opposite_bg_darkest
            size_hint_y: None
            height: self.texture_size[1]
    MDSeparator:
    MDBoxLayout:
        spacing: 10
        MDProgressBar:
            id: progress_bar
            min: 0
            max: 100
            value: root.progress_value
            color: root.progress_color
        MDIconButton:
            id: pause_resume_button
            icon: "play" if root.paused else "pause"
            pos_hint: {"center_x": .5, "center_y": .5}
            on_release: root.pause_resume_download()
        MDIconButton:
            icon: "close"
            pos_hint: {"center_x": .5, "center_y": .5}
            on_release: root.cancel_download()
BoxLayout:
    orientation: "vertical"
    spacing: 10
    RecycleView:
        id: downloads_list
        viewclass: "DownloadItem"
        RecycleBoxLayout:
            default_size: None, 100
            default_size_hint: 1, None
            size_hint_y: None
            height: self.minimum_height
            orientation: "vertical"
            padding: 15
            spacing: 15
    MDRaisedButton:
        text: "Add"
        size_hint_x: 1
        on_release: app.add_item()''')
        self.path = "C:/Users/Family/Downloads/"
        app = self

    def build(self):
        self.theme_cls.theme_style = "Dark"
        return self.kv

    def add_item(self):
        # 新增条目时把所有动态属性都加入数据源
        self.kv.ids.downloads_list.data.append({
            "path": self.path + '/' if self.path[-1] != '/' else self.path,
            "url_type": "file",
            "paused": False,
            "progress_value": 50,
            "progress_color": self.theme_cls.primary_color,
            "item_index": len(self.kv.ids.downloads_list.data)
        })


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

内容的提问来源于stack exchange,提问作者Omid Ketabollahi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.16 23:45:08