Move widgets added to a ScatterLayout in kivy

122 views
Skip to first unread message

Barbarur

unread,
Apr 16, 2022, 12:29:51 AM4/16/22
to Kivy users support

I'm working on a card game where I'm using ScatterLayout as Board because I am interested on the zoom in/out functionality, then I have some cards I want to move around. I know how to move the cards, but when using the ScatterLayout I end up moving the Scatterlayout rather than the cards. I can block the move of the ScatterLayout, because when zoomed in it's good I can move the layout around

I understand the issue, I need to make a distinction on when move the layout and when to move the card. Similar as what ScrollView does.

I've been reviewing the code the ScrollView and it makes this difference based on time and distance moved during the first touch. Which makes sense, But I haven't figurate out how to implement it on the ScatterLayout and the cards, as it seems I have to modify the on_touch_down and on_touch_move.

Is there any suggestion on how to implement this? i hope I'm not the first one facing this situation ;).

This is a sample code of what I'm working on:


from kivy.app import App
from kivy.lang import Builder
from kivy.uix.scatterlayout import ScatterLayout
from kivy.uix.widget import Widget

Builder.load_string("""
<Board>:
Card:
    pos: 100, 100
    size: 100, 100
Card:
    pos: 300, 100
    size: 50, 100

<Card>:
    size_hint: None, None
    canvas:
    Color:
        rgba: 0, 1, 0, 1
    Rectangle:
        pos: self.pos
        size: self.size
""")

class Card(Widget):

    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:
            self.center_x = touch.x
            self.center_y = touch.y
            return True
        return super().on_touch_move(touch)

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


class Board(ScatterLayout):
    do_collide_after_children=False


class MoveApp(App):
    def build(self):
        return Board()


MoveApp().run()



Elliot Garbus

unread,
Apr 16, 2022, 10:57:47 AM4/16/22
to kivy-...@googlegroups.com

Can you explain, in greater detail, how you want to user to interact with the UI?

 

You have a small number of cards on a board, you want to be able to zoom in on the cards as a group, you want to be able to move the cards individually?

 

If that is correct here are some ideas:

Control the card size:  If a zoom gesture is detected on the board (the enclosing layout) change the size of the cards.  This way you retain all of the attributes of the cards, and can move them independently.  Assuming the card is an Image, set keep_ratio to True.

 

Make each card a Scatter.  Use a scatter for each card, rather than having all of the cards in a scatter layout.  Each object remains independent.

 

Change the layout dynamically, use the Scatter Layout out for the zoom, but when it is time to turn zoom off and do other actions, replace the ScatterLayout with a FloatLayout.

--
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/b0e9bbfb-9625-44de-955f-2ee1457e62c8n%40googlegroups.com.

 

Barbarur

unread,
Apr 17, 2022, 12:21:42 AM4/17/22
to Kivy users support
Hello ElliotG,

Yes, what I'm trying to do is to have a grid of 4x4 grid of cards, with additional space around as cards might move outside of this grid. Then I intend to zoom in/out all the cards as a group so user can see details of the cards (image) as they will contain some text/icons and also the neighbor cards. Then each cards needs to be moved independently.

I thought on the ideas you mentioned before, but zooming on card is not enough as user needs to see the surrounding cards .

I managed to move the cards individually using 'do_collide_after_children = True' and Return true in the cards for On_Touch_Down if they collide with touch. But now the zoom in/out is difficult as children movement comes first.

I'm checking how to delay the action of the cards to return true if there is a touch with 1 finger and return false if the touch is with 2 fingers. I think that would help to make the distinction in the interaction between cards and board.

I didn't consider to change the layout dynamically. Could you elaborate a bit on it, or share some reading materials to understand this better?

Barbarur

unread,
Apr 17, 2022, 8:08:14 AM4/17/22
to Kivy users support

Here below is the current code and issues:

Current issues:

  1. After zoom in/out, Scatter move out of place. I don't think this is due the interaction with the ScrollView. I think this is just how the Scatter works and honestly, it is annoying. I haven't see anywhere on the Scatter code how to avoid this.

  2. Cards are getting on the way when I try to pinch the screen to zoom in/out. If first fingers touch a card (which is very likely), the touch grabs the card and I can't zoom anymore. I have avoided this issue happening with the second finger by counting the touches currently on the screen. But I'm unable to trigger an action with the 2nd touch to tell 1st touch to ungrab the card and grab the Scatter (or re-trigger the on_touch_down method, which would also solve the issue).

  3. I have tried using Buttons, as alternative to grab and move cards, but same issue. on_press gets on the way for zoom it.


Code:

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen

from kivy.uix.widget import Widget


Builder.load_string("""
<CardG>:

    size_hint: None, None
    canvas:
        Color:
            rgba: 0, 1, 0, 1
        Rectangle:
            pos: self.pos
            size: self.size
           
<MenuScreen>:
    ScrollView:
        bar_width: 2
        scroll_x: .5
        scroll_y: .5
        AnchorLayout:
            size_hint: None, None
            size: 1000, 1000
            anchor_x: "center"
            anchor_y: "center"
            Scatter:
                size_hint: None, None
                size: 1000, 1000
                do_translation_x: False
                do_translation_y: False
                do_rotation: False
                scale_min: 1
                scale_max: 3
                do_collide_after_children: True
                RelativeLayout:
                    CardG:
                        pos: 0, 0
                        size: 100, 100
                    Button:
                        text: 'click'
                        pos: 450, 450
                        size_hint: None, None
                        size: 100, 100
""")


class CardG(Widget):
    def __init__(self, **kwargs):
        super(CardG, self).__init__(**kwargs)
        self._touch = None

    def on_touch_down(self, touch):
        app = App.get_running_app()
        n = app.sm.get_screen('menu')._touches
        print(f'CardG: {len(n)}')
        if len(n) == 1 and self.collide_point(*touch.pos):
            touch.grab(self)
            self._touch = touch
            return True
        elif len(n) > 1 and self._touch != None:
            # Here needs to check previous touch and ungrab widget
            super().on_touch_down(touch)
        else:

            super().on_touch_down(touch)

    def on_touch_move(self, touch):
        if touch.grab_current is self:
            self.center_x = touch.x
            self.center_y = touch.y
            return True

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

class MenuScreen(Screen):
    def __init__(self, **kwargs):
        self._touches = []
        super(MenuScreen, self).__init__(**kwargs)

    def on_touch_down(self, touch):
        self._touches.append(touch)
        print(f'Down: {len(self._touches)}')
        touch.push()
        super(MenuScreen, self).on_touch_down(touch)
        touch.pop()

    def on_touch_up(self, touch):
        self._touches.remove(touch)
        print(f'Up: {len(self._touches)}')
        return False



class MoveApp(App):
    def build(self):
        self.sm = ScreenManager()
        self.sm.add_widget(MenuScreen(name="menu"))
        self.sm.current = "menu"
        return self.sm


MoveApp().run()

Elliot Garbus

unread,
Apr 17, 2022, 9:49:52 AM4/17/22
to kivy-...@googlegroups.com

Here are a few more ideas:

 

Consider there are 2 modes Zoom and Play.  You could try this with a toggle button on the bottom of the screen.

In zoom mode the cards are on a screen in a ScatterLayout out, when pressing the toggle button again, the screen changes to the cards on a different screen in a Grid Layout.

 

Perhaps you could smooth the transition by animating the ScatterLayout parmaters to get the ScatterLayout back to the original view prior to switching screens.

Elliot Garbus

unread,
Apr 17, 2022, 10:24:35 AM4/17/22
to kivy-...@googlegroups.com

Here is an example of using  a ScreenManager to change the layouts switching between a play and zoom mode.

 

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import
Screen

kv =
"""
#:import NoTransition kivy.uix.screenmanager.NoTransition
<Box@Widget>:
    canvas:
        Color:
            rgb: .5, .5, .5
        Rectangle:
            size: self.size
            pos: self.pos


<GridScreen@Screen>:
    GridLayout:
        padding: 10
        spacing: 10
        cols: 2
        rows: 2
        Box:
        Box:
        Box:
        Box:

<ScatterScreen>:
    ScatterLayout:
        id: sl
        GridLayout:
            padding: 10
            spacing: 10
            cols: 2
            rows: 2
            Box:
            Box:
            Box:
            Box:

BoxLayout:
    orientation: 'vertical'
    ScreenManager:
        id: sm
        transition: NoTransition()
        GridScreen:
            name: 'grid'
        ScatterScreen:
            name: 'scatter'
    ToggleButton:
        size_hint_y: None
        height: 48
        text: {'normal': 'Play', 'down': 'Zoom'}[self.state]
        on_state:
            sm.current = {'normal': 'grid', 'down': 'scatter'}[self.state]
            # root.ids.sm.get_screen('scatter').reset()
"""


class ScatterScreen(Screen):
   
def reset(self):
       
self.ids.sl.scale = 1
        
self.ids.sl.rotation = 0
       
self.ids.sl.pos = 0,0


class MoveApp(App):
   
def build(self):
       
return Builder.load_string(kv)


MoveApp().run()
Reply all
Reply to author
Forward
0 new messages