Vishak Raj
Vishak Raj

Reputation: 171

Can I implement this KivyMD example without using KV language?

I am trying to implement this example of the kivy Hero component, but without using the KV language.

from kivy.animation import Animation
from kivy.clock import Clock
from kivy.properties import StringProperty, ObjectProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.scrollview import ScrollView
from kivymd.app import MDApp
from kivymd.uix.hero import MDHeroFrom, MDHeroTo
from kivymd.uix.imagelist.imagelist import MDSmartTile, MDSmartTileImage, MDSmartTileOverlayContainer
from kivy.metrics import dp
from kivymd.uix.button import MDButton, MDButtonIcon, MDButtonText
from kivymd.uix.screen import MDScreen
from kivymd.uix.screenmanager import MDScreenManager
from kivymd.uix.label.label import MDLabel
from kivymd.uix.scrollview import StretchOverScrollStencil, MDScrollView
from kivymd.uix.gridlayout import MDGridLayout
from kivy.core.window import Window

class HeroItem(MDHeroFrom):
    text = StringProperty()
    tag = StringProperty()
    manager = ObjectProperty()

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.size_hint_y = None
        self.height = "200dp"
        self.radius = "24dp"
        window_width, window_height = Window.size

    def create_tile(self, img_path):
        # Create SmartTile
        print('type', type(Window.size), type(self.manager), self.manager)
        print('parent_size', (self.width, self.height), Window.size, self.manager.size)
        print('self.manager', self.manager, self.size_hint)
        self.tile = MDSmartTile(id='tile', size_hint=(None, None))#pos_hint={"x": 0, "y": 0.1}, - not worked
        self.tile.size = self.manager.size
        #size_hint=(None, None) should be None, then only the smooth opening and closing is happening on each tile
        #size=(380, 200) is fine but rearrening the window size it is not dynamic
        self.tile.bind(on_release=self.on_release)

        # Create SmartTileImage
        self.image = MDSmartTileImage(id='image', source=img_path, radius="24dp",)
        #self.image.size = self.tile.size
        self.image.ripple_duration_in_fast = 0.05
        self.tile.add_widget(self.image)

        # Create Overlay Container
        self.overlay = MDSmartTileOverlayContainer(id='overlay',
                                                md_bg_color=(0, 0, 0, .5),
                                                adaptive_height=True,
                                                radius=[0, 0, dp(24), dp(24)],
                                                    padding="8dp",
                                                    spacing="8dp",
                                                )

        #self.overlay.size = self.tile.size

        # Create Label
        #self.label = Label(text=self.tag, color=(1, 1, 1, 1), size_hint_y=None, height=dp(40))
        self.label = MDLabel(text=self.tag+str(Window.size[0]), theme_text_color='Custom', text_color="white", adaptive_height=True)
        self.overlay.add_widget(self.label)
        self.tile.add_widget(self.overlay)

        self.add_widget(self.tile)

    def on_transform_in(self, instance_hero_widget, duration):
        print('in', duration)
        for instance in [
            instance_hero_widget,
            instance_hero_widget._overlay_container,
            instance_hero_widget._image,
        ]:
            Animation(radius=[0, 0, 0, 0], duration=duration).start(instance)

    def on_transform_out(self, instance_hero_widget, duration):
        print('on', duration)
        for instance, radius in {
            instance_hero_widget: [dp(24), dp(24), dp(24), dp(24)],
            instance_hero_widget._overlay_container: [0, 0, dp(24), dp(24)],
            instance_hero_widget._image: [dp(24), dp(24), dp(24), dp(24)],
        }.items():
            print('radius', radius)
            Animation(
                radius=radius,
                duration=duration,
            ).start(instance)

    def on_release(self, *args):
        print('a')
        def switch_screen(*args):
            #print('self.manager.current_heroes', self.manager.current_heroes)
            self.manager.current_heroes = [self.tag]
            #print('self.manager.current_heroes', self.manager.current_heroes)

            print('?', dir(self.manager.ids))
            print('tag', self.tag)
            screen_b = self.manager.get_screen("screen B")
            screen_b.hero_to.tag = self.tag
            self.manager.current = "screen B"

        Clock.schedule_once(switch_screen, 0.2)


#class ScreenA(MDScreen):
#    pass


class ScreenB(MDScreen):
    hero_to = ObjectProperty()  # Use ObjectProperty

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.hero_to = MDHeroTo(size_hint=(1, None), height="220dp", pos_hint={"top": 1})
        self.add_widget(self.hero_to)

        self.button = MDButton(MDButtonText(text="Move Hero To Screen A"), 
                            pos_hint={"center_x": .5}, y = "36dp")#size_hint_y=None, height=dp(50)
        self.button.bind(on_release=self.switch_to_screen_a)
        self.add_widget(self.button)

    def switch_to_screen_a(self, *args):
        print('b', self.hero_to.tag)
        self.manager.current = "screen A"
        self.manager.current_heroes = [self.hero_to.tag]

class Example(MDApp):
    def build(self):
        self.theme_cls.theme_style_switch_animation = True
        self.theme_cls.primary_palette = "Orange"
        self.theme_cls.theme_style = "Dark"  # "Light"
        print('self.theme_cls.backgroundColor', self.theme_cls.backgroundColor)
        
        self.manager = MDScreenManager()
        self.manager.primary_palette = "Orange"
        self.manager.theme_style = "Dark"  # "Light"
        self.manager.md_bg_color = (1, 1, 1, 1) #self.theme_cls.backgroundColor
        
        screen_a = MDScreen(name="screen A")
        screen_b = ScreenB(name="screen B")

        # ScrollView
        scroll_view = MDScrollView()
        grid = MDGridLayout(cols=2, spacing="12dp", padding="12dp", adaptive_height = True,)
        #print('grid', dir(grid))
        #print('??', grid.width, grid.height, grid.x, grid.y, grid.adaptive_height, grid.adaptive_width, grid.adaptive_size,
        #      grid.line_width, grid.minimum_height, grid.minimum_width, grid.minimum_size,
        #      grid.size_hint)
        #grid.width - 100 
        #0 0 0 True False False 1 0 0 [0, 0] 
        #[1, None]
        #grid.bind(minimum_height=grid.setter('height'))
        scroll_view.add_widget(grid)

        screen_a.add_widget(scroll_view)
        self.manager.add_widget(screen_a)
        self.manager.add_widget(screen_b)

        # Add HeroItems to Grid
        for i in range(12):
            hero_item = HeroItem(text=f"Item {i + 1}", tag=f"Tag {i}", manager=self.manager)
            hero_item.create_tile('3.png')
            if not i % 2:
                hero_item.md_bg_color = "lightgrey"
            grid.add_widget(hero_item)

        return self.manager


Example().run()

However, the images are not showing in proper order. Also, when I resize the window, the image is not resizing automatically with respect to the window size: see output video 1.

I modified the size_hint parameter of the MDSmartTile from (None, None) to (0.5, 1), then the images are showing properly and also when resizing the window size, the image also resizing automatically with respect to the window size, but the issue in modifying this size_hint - there is a distortion in the animation when opening the images: see output video 2.

Could this be a parameter issue?

Upvotes: 0

Views: 57

Answers (1)

mz3r0
mz3r0

Reputation: 11

I found a partial solution. Before that, some observations.

When you resize the window, the layout gets resized which is also responsible for resizing its children depending on their size_hint. The MDGridLayout should have children with size_hint_x of 1, or just size_hint of (1, None).

Therefore, you should give every Hero widget a size hint of (1, None) for it to extend horizontally

The image order looks alright. I see that the hero widgets with the largest tag id show at the bottom.

The size hint on the MDSmartTile should be 1,1 because you want it to take as much space as its hero parent allows. You should rely on kivy language; it's there to make your life easier.

In the demo example you based your code on, the size of the Tile is bound to that of the Hero widget. Whenever the Hero widget size changes, it automatically updates the Tile widget's size. I tried doing this manually with your code and it didn't work. I'm unsure why, but using kivy language is surely better in this case.

The following animation-related steps also help:

  1. Change the Tile's size hint to (1,1)
  2. Change it to (None, None) before the transition animation
  3. Change it back to (1,1) after the transition animation

These steps combined with the addition of an MDSharedAxisTransition to the demo code make the animation less unpleasant. So, it's not perfect in my opinion.

Also keep in mind:

  • The main changes involved are inside on_transform_in, and on_transform_out methods.
  • There is a bug in the animation. It occurs when you use a Hero widget to go to screen B after the 1st time. The first time you enter screen B and the animation plays as it should, then you go back, and as you enter again using the same hero, it will play a different animation that looks off. Hours of debugging led me nowhere.
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import StringProperty, ObjectProperty

from kivymd.app import MDApp
from kivymd.uix.hero import MDHeroFrom
from kivymd.uix.transition import MDSharedAxisTransition


KV = '''
<HeroItem>
    size_hint_x: 1
    size_hint_y: None
    height: "200dp"
    radius: "24dp"

    MDSmartTile:
        id: tile
        size_hint: None, None
        size: root.size
        on_release: root.on_release()

        MDSmartTileImage:
            id: image
            source: 'https://github.com/kivymd/internal/raw/main/logo/kivymd_logo_blue.png'
            radius: dp(24)

        MDSmartTileOverlayContainer:
            id: overlay
            md_bg_color: 0, 0, 0, .5
            adaptive_height: True
            padding: "8dp"
            spacing: "8dp"
            radius: [0, 0, dp(24), dp(24)]

            MDLabel:
                text: root.tag
                theme_text_color: "Custom"
                text_color: "white"
                adaptive_height: True


MDScreenManager:
    md_bg_color: self.theme_cls.backgroundColor
    transition: app.transition

    MDScreen:
        name: "screen A"

        ScrollView:

            MDGridLayout:
                id: box
                cols: 2
                spacing: "12dp"
                padding: "12dp"
                adaptive_height: True

    MDScreen:
        name: "screen B"
        heroes_to: [hero_to]

        MDBoxLayout:
            orientaion: 'horizontal'
            
            MDHeroTo:
                id: hero_to
                size_hint: 1, None
                height: "220dp"
                pos_hint: {"top": 1}
    
        MDButton:
            pos_hint: {"center_x": .5}
            y: "36dp"
            on_release:
                root.current_heroes = [hero_to.tag]
                root.current = "screen A"

            MDButtonText:
                text: "Move Hero To Screen A"
'''

def set_size_hint_one(widget):
    widget.ids.tile.size_hint = (1, 1)

def set_size_hint_none(widget):
    widget.ids.tile.size_hint = (None, None)

class HeroItem(MDHeroFrom):
    text = StringProperty()
    manager = ObjectProperty()

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.ids.image.ripple_duration_in_fast = 0.05

    def on_transform_in(self, instance_hero_widget, duration):
        set_size_hint_none(self)

        for instance in [
            instance_hero_widget,
            instance_hero_widget._overlay_container,
            instance_hero_widget._image,
        ]:
            Animation(radius=[0, 0, 0, 0], duration=duration).start(instance)

        Clock.schedule_once(lambda x: set_size_hint_one(self), duration+0.1)

    def on_transform_out(self, instance_hero_widget, duration):
        set_size_hint_none(self)

        for instance, radius in {
            instance_hero_widget: [dp(24), dp(24), dp(24), dp(24)],
            instance_hero_widget._overlay_container: [0, 0, dp(24), dp(24)],
            instance_hero_widget._image: [dp(24), dp(24), dp(24), dp(24)],
        }.items():
            Animation(
                radius=radius,
                duration=duration,
            ).start(instance)

        Clock.schedule_once(lambda x: set_size_hint_one(self), duration+0.1)


    def on_release(self):
        def switch_screen(*args):
            self.manager.current_heroes = [self.tag]
            self.manager.ids.hero_to.tag = self.tag
            self.manager.current = "screen B"

        Clock.schedule_once(switch_screen, 0.2)


class Example(MDApp):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.transition = MDSharedAxisTransition()
        self.transition.transition_axis = "z"
        self.transition.duration = 0.2

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

    def on_start(self):
        for i in range(12):
            hero_item = HeroItem(
                text=f"Item {i + 1}", tag=f"Tag {i}", manager=self.root
            )
            if not i % 2:
                hero_item.md_bg_color = "lightgrey"
            self.root.ids.box.add_widget(hero_item)


Example().run()

Upvotes: 1

Related Questions