Groups keyboard shortcuts have been updated
Dismiss
See shortcuts

MDCardSwipe - Swiping stops when a touch moves onto other widgets

75 views
Skip to first unread message

大橋和季

unread,
Jun 29, 2024, 11:00:03 PM6/29/24
to Kivy users support
I've got into a practice of MDCardSwipe. My goal is creating an app that has swiping cards like Gmail for smart phones.

The problem I've got is when a touch to swipe a card moves onto other widgets, swiping stops.

To solve it, I tried this.

from kivy.lang import Builder
from kivy.properties import StringProperty

from kivymd.app import MDApp
from kivymd.uix.card import MDCardSwipe
from kivymd.uix.list import MDList

KV = '''
<SwipeToDeleteItem>:
    size_hint_y: None
    height: content.height
    type_swipe: "auto"
    # anchor: "right"
    max_swipe_x: .5
    on_swipe_complete: app.remove_item(self)

    MDCardSwipeLayerBox:
        # Content under the card.

    MDCardSwipeFrontBox:
        ripple_behavior: True

        # Content of card.
        OneLineListItem:
            id: content
            text: root.text
            _no_ripple_effect: True
            on_press: print(self.text)

<MyMDList>:
    spacing: '5dp'

MDScreen:

    MDBoxLayout:
        orientation: "vertical"
        spacing: "10dp"

        MDTopAppBar:
            elevation: 2
            title: "MDCardSwipe"

        ScrollView:
            scroll_timeout : 100

            MyMDList:
                id: my_md_list

                canvas.before:
                    Color:
                        rgba: .1, .2, .3, 1
                    Rectangle:
                        pos: self.pos
                        size: self.size

        MDBottomAppBar:
            elevation: 2
'''

class SwipeToDeleteItem(MDCardSwipe):
    '''Card with `swipe-to-delete` behavior.'''
    text = StringProperty()

    def on_touch_move(self, touch):
        self._distance += touch.dx
        expr = False

        if self.anchor == "left" and touch.dx >= 0:
            expr = abs(self._distance) < self.swipe_distance
        elif self.anchor == "right" and touch.dx < 0:
            expr = abs(self._distance) > self.swipe_distance

        if expr and not self._opens_process:
            self._opens_process = True
            self._to_closed = False
        if self._opens_process:
            self.open_progress = max(
                min(self.open_progress + touch.dx / self.width, 2.5), 0
            )
        return False

    def on_touch_up(self, touch):
        self._distance = 0
        if not self._to_closed:
            self._opens_process = False
            self.complete_swipe()
        return False

class MyMDList(MDList):
    is_dispatched = None
    touchable = True
   
    def on_touch_down(self, touch):
        self.touchable = True
        self.is_dispatched = [c.dispatch('on_touch_down', touch) for c in self.children]
        self.touchable = False
        return any(self.is_dispatched)

    def on_touch_move(self, touch):
        self.touchable = False
        try:
            return any([c.dispatch('on_touch_move', touch) if self.is_dispatched[i] else None for i, c in enumerate(self.children)])
        except:
            return False

    def on_touch_up(self, touch):
        self.touchable = True
        try:
            return any([c.dispatch('on_touch_up', touch) if self.is_dispatched[i] else None for i, c in enumerate(self.children)])
        except:
            return False

class TestCard(MDApp):

    def build(self):
        return Builder.load_string(KV)

    def on_start(self):
        '''Creates a list of cards.'''
        self.my_md_list = self.root.ids.my_md_list
        for i in range(10):
            self.my_md_list.add_widget(
                SwipeToDeleteItem(text=f"One-line item {i}")
            )

    def remove_item(self, instance):
        if instance.state == 'opened':
            self.my_md_list.remove_widget(instance)

TestCard().run()

It looks good, but a touch goes onto MDTopAppBar, MDBottomAppBar or out of the window, swiping stops.

Next I tried this.

from kivy.lang import Builder
from kivy.properties import StringProperty

from kivymd.app import MDApp
from kivymd.uix.screen import MDScreen
from kivymd.uix.card import MDCardSwipe

Builder.load_string('''
<SwipeToDeleteItem>:
    size_hint_y: None
    height: content.height
    type_swipe: "auto"
    # anchor: "right"
    max_swipe_x: .5
    on_swipe_complete: app.remove_item(self)

    MDCardSwipeLayerBox:
        # Content under the card.

    MDCardSwipeFrontBox:
        # on_press: print(root.text)
        ripple_behavior: True

        # Content of card.
        OneLineListItem:
            id: content
            text: root.text
            height: 50
            _no_ripple_effect: True
            on_press: print(self.text)

<MyMDScreen>:

    MDBoxLayout:
        orientation: "vertical"
        spacing: "10dp"

        MDTopAppBar:
            elevation: 2
            title: "MDCardSwipe"
 
        ScrollView:
            scroll_timeout : 100

            MDList:
                id: md_list

        MDBottomAppBar:
            elevation: 2
''')

class SwipeToDeleteItem(MDCardSwipe):
    '''Card with `swipe-to-delete` behavior.'''
    text = StringProperty()

    def on_touch_move(self, touch):
        self._distance += touch.dx
        expr = False

        if self.anchor == "left" and touch.dx >= 0:
            expr = abs(self._distance) < self.swipe_distance
        elif self.anchor == "right" and touch.dx < 0:
            expr = abs(self._distance) > self.swipe_distance

        if expr and not self._opens_process:
            self._opens_process = True
            self._to_closed = False
        if self._opens_process:
            self.open_progress = max(
                min(self.open_progress + touch.dx / self.width, 2.5), 0
            )
        return False

    def on_touch_up(self, touch):
        self._distance = 0
        if not self._to_closed:
            self._opens_process = False
            self.complete_swipe()
        return False

class MyMDScreen(MDScreen):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.is_dispatched = None
        self.touchable = True
        self.md_list = self.ids.md_list

    def on_touch_down(self, touch):
        self.touchable = True
        self.is_dispatched = [c.dispatch('on_touch_down', touch) for c in self.md_list.children]
        self.touchable = False
        return any(self.is_dispatched)

    def on_touch_move(self, touch):
        self.touchable = False
        return any([c.dispatch('on_touch_move', touch) if self.is_dispatched[i] else None for i, c in enumerate(self.md_list.children)])

    def on_touch_up(self, touch):
        self.touchable = True
        return any([c.dispatch('on_touch_up', touch) if self.is_dispatched[i] else None for i, c in enumerate(self.md_list.children)])

class TestCard(MDApp):
    def build(self):
        my_mdscreen = MyMDScreen()
        return my_mdscreen

    def on_start(self):
        '''Creates a list of cards.'''
        self.md_list = self.root.ids.md_list
        for i in range(12):
            self.md_list.add_widget(
                SwipeToDeleteItem(text=f"One-line item {i}")
            )

    def remove_item(self, instance):
        if instance.state == 'opened':
            self.md_list.remove_widget(instance)

TestCard().run()


In this case swiping doesn't stop, but it has a problem of coordinates of touch and the ScrollView doesn't work.

How can I solve it?

ELLIOT GARBUS

unread,
Jun 30, 2024, 11:51:17 AM6/30/24
to Kivy users support
I won't have  chance to run your code for a few days - but I can see an issue with the code.

In Kivy , touches are dispatched to all of the widgets.  The widget needs to check if it has been touched using the collide_point() method.

Read:
https://kivy.org/doc/stable/guide/inputs.html#touch-event-basics  also read the section on grabbing touch events

A description of how touches are sent to widgets is described here:

Let me know if you get things working.



From: kivy-...@googlegroups.com <kivy-...@googlegroups.com> on behalf of 大橋和季 <0cd3882...@gmail.com>
Sent: Saturday, June 29, 2024 8:00 PM
To: Kivy users support <kivy-...@googlegroups.com>
Subject: [kivy-users] MDCardSwipe - Swiping stops when a touch moves onto other widgets
 
--
You received this message because you are subscribed to the Google Groups "Kivy users support" group.
To unsubscribe from this group and stop receiving emails from it, send an email to kivy-users+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/kivy-users/5f3d4b2d-a813-49f2-bee2-245bd3fae861n%40googlegroups.com.

ElliotG

unread,
Jul 3, 2024, 7:25:50 PM7/3/24
to Kivy users support
I think this is what you are looking for:

from kivy.lang import Builder
from kivy.properties import StringProperty

from kivymd.app import MDApp
from kivymd.uix.card import MDCardSwipe
from kivymd.uix.list import MDList

KV = '''
<SwipeToDeleteItem>:
    size_hint_y: None
    height: content.height
    type_swipe: "auto"
    # anchor: "right"
    max_swipe_x: .2
    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            touch.grab(self)
        return super().on_touch_down(touch)

    def on_touch_move(self, touch):
        if touch.grab_current is self and self.collide_point(*touch.pos):
            return super().on_touch_move(touch)
        else:

            return False

    def on_touch_up(self, touch):
        if touch.grab_current is self:
            touch.ungrab(self)
        return super().on_touch_up(touch)


class MyMDList(MDList):
    pass



class TestCard(MDApp):

    def build(self):
        return Builder.load_string(KV)

    def on_start(self):
        '''Creates a list of cards.'''
        self.my_md_list = self.root.ids.my_md_list
        for i in range(10):
            self.my_md_list.add_widget(
                SwipeToDeleteItem(text=f"One-line item {i}")
            )

    def remove_item(self, instance):
        print(f'{instance} {instance.state}')

        if instance.state == 'opened':
            self.my_md_list.remove_widget(instance)


TestCard().run()

大橋和季

unread,
Jul 3, 2024, 9:22:45 PM7/3/24
to Kivy users support
I'm sorry for not replying to your message. I also didn't have time enough.

I tried your code and found a point I want to fix. When the mouse cursor go out of a card while swiping, it's stuck. This is why I tried to override the on_touch_move and on_touch_down method without collide_point in my previous message. I need cards that can be swiped wherever the cursor is, even out of the window.

Do you have any ideas?

Thank you for your cooperations!  
2024年7月4日木曜日 8:25:50 UTC+9 ElliotG:

ELLIOT GARBUS

unread,
Jul 4, 2024, 11:32:22 AM7/4/24
to Kivy users support
You will need to modify the code in the card.py.   The issue is that the on_touch methods in card.py do not use the touch.grab() method.  

Go into your venv (or copy the files from github), and copy the card directory into your local project directory.  Then modify the on_touch methods.  (https://github.com/kivymd/KivyMD/tree/master/kivymd/uix/card)


I see the current behavior as a bug.  I'd recommend report the issue on github and making a PR on the master branch.


Sent: Wednesday, July 3, 2024 6:22 PM

To: Kivy users support <kivy-...@googlegroups.com>
Subject: Re: [kivy-users] MDCardSwipe - Swiping stops when a touch moves onto other widgets
 

大橋和季

unread,
Jul 9, 2024, 2:08:49 AM7/9/24
to Kivy users support
I haven't reported the issue but succeeded to achieve my goal so let me tell you about it. This is it:

from kivy.lang import Builder
from kivy.properties import StringProperty, NumericProperty
from kivy.core.window import Window
from kivy.uix.scrollview import ScrollView

from kivymd.app import MDApp
from kivymd.uix.screen import MDScreen
from kivymd.uix.card import MDCardSwipe

import asynckivy as ak

KV = '''
<SwipeToDeleteItem>:
    size_hint_y: None
    height: content.height
    type_swipe: "auto"
    # anchor: "right"
    max_swipe_x: .5
    on_swipe_complete: app.remove_item(self)

    MDCardSwipeLayerBox:
        # Content under the card.

    MDCardSwipeFrontBox:
        # on_press: print(root.text)
        ripple_behavior: True

        # Content of card.
        OneLineListItem:
            id: content
            text: root.text
            height: 50
            _no_ripple_effect: True
            on_press: print(self.text)

MDScreen:

    MDBoxLayout:
        orientation: "vertical"
        spacing: "10dp"

        MDTopAppBar:
            elevation: 2
            title: "MDCardSwipe"
 
        ScrollView:
            scroll_timeout : 100

            MDList:
                id: md_list
                padding: 20

        MDBottomAppBar:
            elevation: 2
'''

class SwipeToDeleteItem(MDCardSwipe):
    '''Card with `swipe-to-delete` behavior.'''
    text = StringProperty()
    selected_card = None
    is_being_dragged = False
    drag_distance = NumericProperty(ScrollView.scroll_distance.defaultvalue)
    drag_timeout = NumericProperty(ScrollView.scroll_timeout.defaultvalue)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        Window.fbind('on_touch_down', self.on_touch_down_window)
        Window.fbind('on_touch_move', self.on_touch_move_window)
        Window.fbind('on_touch_up', self.on_touch_up_window)

    def on_touch_down_window(self, window, touch):
        try:
            if not self.is_being_dragged:
                scroll_view = self.parent.parent
                scroll_view.dispatch('on_touch_down', touch)
                scroll_view_pos_on_window = scroll_view.to_window(*scroll_view.pos)

                if scroll_view_pos_on_window[0] < touch.x < scroll_view_pos_on_window[0] + scroll_view.width \
                and scroll_view_pos_on_window[1] < touch.y < scroll_view_pos_on_window[1] + scroll_view.height:
                    self.is_being_dragged = True
                    md_list = self.parent
                    md_list.dispatch('on_touch_down', touch)
                    cards = [child for child in md_list.children]
                    card_pos_on_window = [self.to_window(*card.pos) for card in cards]

                    self.num_of_cards = len(md_list.children)
                    for i in range(self.num_of_cards):
                        if card_pos_on_window[i][0] < touch.x < card_pos_on_window[i][0] + self.width \
                        and card_pos_on_window[i][1] < touch.y < card_pos_on_window[i][1] + self.height:
                            self.selected_card = md_list.children[i]
                            one_line_list_item = self.selected_card.ids.content
                            if self is self.selected_card and not touch.is_mouse_scrolling:
                                ak.start(self._see_if_a_touch_actually_is_a_dragging_gesture(self.selected_card, touch))

        except AttributeError:
            pass

        return False

    def on_touch_move_window(self, window, touch):
        if self is self.selected_card:
            self._distance += touch.dx
            expr = False

            if self.anchor == "left" and touch.dx >= 0:
                expr = abs(self._distance) < self.swipe_distance
            elif self.anchor == "right" and touch.dx < 0:
                expr = abs(self._distance) > self.swipe_distance

            if expr and not self._opens_process:
                self._opens_process = True
                self._to_closed = False
            if self._opens_process:
                self.open_progress = max(
                    min(self.open_progress + touch.dx / self.width, 2.5), 0
                )
        return False

    def on_touch_up_window(self, window, touch):
        if self is self.selected_card:
            self._distance = 0
            if not self._to_closed:
                self._opens_process = False
                self.complete_swipe()
        self.selected_card = None
        self.is_being_dragged = False
        return False

    def on_touch_down(self, touch):
        pass
   
    def on_touch_move(self, touch):
        pass

    def on_touch_up(self, touch):
        pass

    async def _see_if_a_touch_actually_is_a_dragging_gesture(self, instance, touch):
        print('1')
        async with ak.wait_any_cm(ak.sleep(self.drag_timeout / 1000.)):  # <- 'trio.move_on_after()' equivalent
            # LOAD_FAST
            abs_ = abs
            drag_distance = self.drag_distance
            ox, oy = touch.opos
            do_touch_up = True
            async with ak.watch_touch(instance, touch) as in_progress:
                while await in_progress():
                    dx = abs_(touch.x - ox)
                    dy = abs_(touch.y - oy)
                    if dy > drag_distance or dx > drag_distance:
                        do_touch_up = False
                        break

            # Reaching here means the given touch is not a dragging gesture.
            print('2')
            return

        # Reaching here means the given touch is not a dragging gesture.
        print('3')
        one_line_list_item = instance.ids.content
        one_line_list_item.dispatch('on_press')

class TestCard(MDApp):
    def build(self):
        return Builder.load_string(KV)

    def on_start(self):
        '''Creates a list of cards.'''
        self.md_list = self.root.ids.md_list
        for i in range(15):
            self.md_list.add_widget(
                SwipeToDeleteItem(text=f"One-line item {i}")
            )

    def remove_item(self, instance):
        if instance.state == 'opened':
            self.md_list.remove_widget(instance)

TestCard().run()


Touch methods are binded to window, that makes the cards not to be stuck wherever touch events occur.

For more convenience I added an async function from gottadiveintopython's kivy_garden.draggable. It distinguishes a click and a dragging.

Now I'd like to ask you about my code:
1) I couldn't use collide_point
2) touch.grab(), grab_current either
I think they would make it smarter.

And  3) MDCardSwipeFrontBox's 'ripple_behavior: True' doesn't work.

Do you know how to solve it?
2024年7月5日金曜日 0:32:22 UTC+9 ElliotG:

大橋和季

unread,
Jul 9, 2024, 2:19:22 AM7/9/24
to Kivy users support
PS. In the async function there are two 'Reaching here means the given touch is not a dragging gesture.' comment but I need to correct the upper one to 'Reaching here means the given touch is a dragging gesture.'

2024年7月9日火曜日 15:08:49 UTC+9 大橋和季:

ElliotG

unread,
Jul 10, 2024, 12:56:51 AM7/10/24
to Kivy users support
I was thinking about your problem and have a tricky - slightly hacky fix that I believe does just what you want.
I renamed MyMDList to SwipeContainer.  I use the MD List to capture the touches.
As you look at the code, I am testing if a child in the MDList was touched.  It it was I save the touched widget.
It on_touch_move() I modify the touch so touch.y is always touching the original touched widget.

Let me know it this does what you wanted.  I would still encourage you to report an issue on the KivyMD site.  I believe you have found a bug.

from kivy.lang import Builder
from kivy.properties import StringProperty, ObjectProperty


from kivymd.app import MDApp
from kivymd.uix.card import MDCardSwipe
from kivymd.uix.list import MDList

KV = '''
<SwipeToDeleteItem>:
    size_hint_y: None
    height: content.height
    type_swipe: "auto"
    # anchor: "right"
    max_swipe_x: .2
    on_swipe_complete: app.remove_item(self)

    MDCardSwipeLayerBox:
        # Content under the card.

    MDCardSwipeFrontBox:
        ripple_behavior: True

        # Content of card.
        OneLineListItem:
            id: content
            text: root.text
            _no_ripple_effect: True
            on_press: print(self.text)

MDScreen:
    MDBoxLayout:
        orientation: "vertical"
        spacing: "10dp"

        MDTopAppBar:
            elevation: 2
            title: "MDCardSwipe"

        ScrollView:
            scroll_timeout: 100

            SwipeContainer:
                id: swipe_container
                spacing: '5dp'

                canvas.before:
                    Color:
                        rgba: .1, .2, .3, 1
                    Rectangle:
                        pos: self.pos
                        size: self.size

        MDBottomAppBar:
            elevation: 2
'''


class SwipeToDeleteItem(MDCardSwipe):
    '''Card with `swipe-to-delete` behavior.'''
    text = StringProperty()


class SwipeContainer(MDList):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.touched = None  # hold the child widget that was touched

    def on_touch_down(self, touch):
        # check each of the children to see if it was touched...
        # "grab" the touched widget...
        for child in self.children:
            if child.collide_point(*touch.pos):
                print(f'touch grabbed {child=}')
                self.touched = child
                break

        return super().on_touch_down(touch)

    def on_touch_move(self, touch):
        if self.touched in self.children:
            # if a child was touched, force touch.y to be on the child
            # this "fools" the child
            touch.y = self.touched.center_y
        return self.touched.on_touch_move(touch)

    def on_touch_up(self, touch):
        if self.touched in self.children:
            # if the child is touched, force the touch.y to be on the child
            touch.y = self.touched.center_y
            # clear self.touched
            self.touched = None
        return super().on_touch_up(touch)



class TestCard(MDApp):

    def build(self):
        return Builder.load_string(KV)

    def on_start(self):
        '''Creates a list of cards.'''
        for i in range(10):
            self.root.ids.swipe_container.add_widget(

                SwipeToDeleteItem(text=f"One-line item {i}")
            )

    def remove_item(self, instance):
        print(f'{instance} {instance.state}')
        if instance.state == 'opened':
            self.root.ids.swipe_container.remove_widget(instance)


TestCard().run()

card_swipe_container.py

大橋和季

unread,
Jul 15, 2024, 1:36:16 AM7/15/24
to Kivy users support
Thank you for your modified code. It's quite good!

But if I may add a thing, I'd like to make the touch_move() and the touch_up() work even if a touch is on the MDTopAppBar or the MDBottomAppBar, so the cards never get stuck in the middle of the layout behind. 

And I've reported an issue about this in https://github.com/kivymd/KivyMD/issues/1720. I'm not sure if I wrote correctly since this was the first time to participate in a github issue...
2024年7月10日水曜日 13:56:51 UTC+9 ElliotG:

ElliotG

unread,
Jul 15, 2024, 8:14:32 PM7/15/24
to Kivy users support
For the bug report on github, I suggest you add a minimal runnable example that highlights the problem, a description of the problem, and what you expected as the output.

ElliotG

unread,
Jul 16, 2024, 5:17:40 PM7/16/24
to Kivy users support
I tried a few experiments - the cleanest solution is to fix the source code.

Follow these steps:
1) go into your .venv and find directory:  .venv/Lib/site-packages/kivymd/uix/card
Copy the card directory into your project directory.
Make the edits highlighted below to card.py, adding the grab controls.

change the import in your program from:
from kivymd.uix.card import MDCardSwipe
to:
from card import MDCardSwipe


    def on_touch_move(self, touch):
        # if self.collide_point(touch.x, touch.y):
        if touch.grab_current is self:

            self._distance += touch.dx
            expr = False

            if self.anchor == "left" and touch.dx >= 0:
                expr = abs(self._distance) < self.swipe_distance
            elif self.anchor == "right" and touch.dx < 0:
                expr = abs(self._distance) > self.swipe_distance

            if expr and not self._opens_process:
                self._opens_process = True
                self._to_closed = False
            if self._opens_process:
                self.open_progress = max(
                    min(self.open_progress + touch.dx / self.width, 2.5), 0
                )
        return super().on_touch_move(touch)


    def on_touch_up(self, touch):
        self._distance = 0
        # if self.collide_point(touch.x, touch.y):
        if touch.grab_current is self:

            touch.ungrab(self)

            if not self._to_closed:
                self._opens_process = False
                self.complete_swipe()
        return super().on_touch_up(touch)

    def on_touch_down(self, touch):
        if self.collide_point(touch.x, touch.y):
            touch.grab(self)
            if self.state == "opened":
                self._to_closed = True
                Clock.schedule_once(self.close_card, self.closing_interval)
        return super().on_touch_down(touch)



I have attached the modified version of card.py, and a modified version of  the test code.
card.py
card_swipe_container.py
Reply all
Reply to author
Forward
0 new messages