Материальный Python. Кастомные карточки с OpenGL эффектами

3mhiwk9patkhsi4t52queehzu54.png


Приветствую, уважаемые любители и знатоки Python!

В этой статье я покажу вам, как применять эффекты OpenGL к своим кастомным карточкам, если вы используете в своих приложениях такие кроссплатформенные инструменты как фреймворк Kivy и библиотеку материального дизайна для этого фреймворка — KivyMD. Погнали!

В KivyMD есть стандартный компонент MDCard — базовый класс для создания различных пользовательских карточек (Material Design spec, Cards). Если не вдаваться в подробности, то под капотом MDCard находится обычный BoxLayout — контейнер, который позволяет размещать в себе другие виджеты в вертикальной или горизонтальной ориентации. То есть, если вам понадобилось сделать какую-нибудь карточку, например, информацию о пользователе, вы делаете это самостоятельно. MDCard реализует только поведения ripple_behavior, touch_behavior и отбрасываемую тень:

Пример программы которая выводит на экран пустую карту выглядит следующим образом:

from kivy.lang import Builder

from kivymd.app import MDApp

KV = '''
Screen:  # экран приложения

    MDCard:  # карта
        # подсказки размера и позиции
        size_hint: .6, .5
        pos_hint: {"center_x": .5, "center_y": .5}
'''


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

TestCard().run()


Результат:

3232e41db7bb670426b3e820c4bc918e.png


Выглядит довольно просто. Но что делать, если мы хотим красивую карточку с Blur эффектом при событии получения фокуса? Такую как, например, в приложении Flutter UI Designs:

1jtbdgbbxyxuffyc7qlet_8jjtq.gif


Придется сделать ее самому! Тем более, что ничего сложного в этом нет. Для начала создадим базовый класс будущей карты:

class RestaurantCard(MDCard):
    source = StringProperty()  # путь к главному изображению карточки
    shadow = StringProperty()  # путь к нижнему изображению-тени
    text = StringProperty()  # текст карточки


Главное изображение карточки:

fm6yf2-cqikzfbbd-hnx901y0vo.jpeg


Изображение-тень:

be96rybmkahlfsg_jhuopnntpjg.png


Теперь наполним карту компонентами, свойства которых мы определи при помощи специального DSL языка Kv-Language, предназначенного для удобного проектирования макетов интерфейса:


    elevation: 12

    RelativeLayout:

        # Компонент, который подгонят пропорции изображения под размеры макета.
        FitImage:  # главное изображение карточки
            source: root.source

        FitImage:  # изображение-тень
            source: root.shadow
            size_hint_y: None
            height: "120dp"

        MDLabel:  # текст карточки
            text: root.text
            markup: True
            size_hint_y: None
            height: self.texture_size[1]
            x: "10dp"
            y: "10dp"
            theme_text_color: "Custom"
            text_color: 1, 1, 1, 1

В карту мы поместили виджет RelativeLayout, который позволяет размешать в себе компоненты один над другим вот таким образом:

b-3gnzbjleed-50ugi1-vd6dqck.png

Мы же разместили сначала главное изображение, сверху положили тень и текст. Теперь если запустить наш код:

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

from kivymd.app import MDApp
from kivymd.uix.card import MDCard

KV = """

    elevation: 12

    RelativeLayout:

        FitImage:
            source: root.source

        FitImage:
            source: root.shadow
            size_hint_y: None
            height: "120dp"

        MDLabel:
            text: root.text
            markup: True
            size_hint_y: None
            height: self.texture_size[1]
            x: "10dp"
            y: "10dp"
            theme_text_color: "Custom"
            text_color: 1, 1, 1, 1


Screen:

    RestaurantCard:
        text: "[size=23][b]Restaurant[/b][/size]\\nTuborg Havnepark 15, Hellerup 2900 Denmark"
        shadow: "shadow-black.png"
        source: "restourant.jpg"
        pos_hint: {"center_x": .5, "center_y": .5}
        size_hint: .7, .5
"""


class RestaurantCard(MDCard):
    source = StringProperty()
    text = StringProperty()
    shadow = StringProperty()


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

BlurCard().run()

… получим результат:

ug4r8gmert32f2fyed80ptzyafk.png

И результат, конечно, далек от ожидаемого, потому что ни blur эффекта, ни заругленных краев у карточки мы не увидим. Начнем с blur эффекта. В Kivy есть стандартный виджет EffectWidget, который способен применять различные графические эффекты для своих детей. Он работает путем рендеринга Fbo экземпляров с помощью пользовательских шейдеров OpenGL. Нам нужно применить эффект размытия к главному изображению и изображению-тени на карточке. Поэтому мы должны поместить их компоненты в виджет EffectWidget:

#:import effect kivy.uix.effectwidget.EffectWidget
#:import HorizontalBlurEffect kivy.uix.effectwidget.HorizontalBlurEffect



    ...

    RelativeLayout:
        
        # Здесь находятся компоненты, к которым будет применён выбранный эффект.
        EffectWidget:
            # Тип эффекта.
            effects: (HorizontalBlurEffect(size=root.blur),)

            FitImage:
                source: root.source

            FitImage:
                source: root.shadow
                size_hint_y: None
                height: "120dp"

    ...    

Добавим поле для значения степени эффекта размытия:

class RestaurantCard(MDCard):
    ...
    blur = NumericProperty(8)

Запускаем и видим:

ymk18_tljk7wivsmvuybguilchi.png

При наведении курсора (если это десктоп) или при тапе (если это mobile) ничего не происходит. Чтобы карточка реагировала на событие on_focus, мы должны включить чтение этого события в свойствах правила RestaurantCard и назначить методы, которые будут выполняться при регистрации этого собития:

#:import Animation kivy.animation.Animation



    focus_behavior: True  # включаем чтение события on_focus
    # Методы, которые вызываются при захвате и потере фокуса.
    # Используя класс Animation, меняем значение степени размытия.
    on_enter: Animation(blur=0, d=0.3).start(self)
    on_leave: Animation(blur=8, d=0.3).start(self)

Уже лучше:

vkcsbi5ntclwz5zukkcugcve9ag.gif

Для обрезания углов у карточки я решил применить Stencil (трафарет) к виджету EffectWidget:

#:import Stencil kivymd.uix.graphics.Stencil


# Создаем новое правило, унаследованное от EffectWidget и Stencil.

    radius: [20,]



    ...

    RelativeLayout:

        Effect:
            ...

И вот теперь все работает так, как мы планировали:

nxt8f1aih9gnb_bgtiy3rlbrgxs.gif
Полный код примера
from kivy.lang import Builder
from kivy.properties import StringProperty, NumericProperty

from kivymd.app import MDApp
from kivymd.uix.card import MDCard

KV = """
#:import Stencil kivymd.uix.graphics.Stencil
#:import Animation kivy.animation.Animation
#:import effect kivy.uix.effectwidget.EffectWidget
#:import HorizontalBlurEffect kivy.uix.effectwidget.HorizontalBlurEffect



    radius: [20,]



    md_bg_color: 0, 0, 0, 0
    elevation: 12
    focus_behavior: True
    on_enter: Animation(blur=0, d=0.3).start(self)
    on_leave: Animation(blur=8, d=0.3).start(self)
    radius: [20,]

    RelativeLayout:

        Effect:
            effects: (HorizontalBlurEffect(size=root.blur),)

            FitImage:
                source: root.source

            FitImage:
                source: root.shadow
                size_hint_y: None
                height: "120dp"

        MDLabel:
            text: root.text
            markup: True
            size_hint_y: None
            height: self.texture_size[1]
            x: "10dp"
            y: "10dp"
            theme_text_color: "Custom"
            text_color: 1, 1, 1, 1


FloatLayout:

    RestaurantCard:
        text: "[size=23][b]Restaurant[/b][/size]\\nTuborg Havnepark 15, Hellerup 2900 Denmark"
        shadow: "shadow-black.png"
        source: "restourant.jpg"
        pos_hint: {"center_x": .5, "center_y": .5}
        size_hint: .7, .5
"""


class RestaurantCard(MDCard):
    source = StringProperty()
    text = StringProperty()
    shadow = StringProperty()
    blur = NumericProperty(8)


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


BlurCard().run()

Ну, и напоследок хочу показать видео, в котором работают две программы: Одна, написанная с использованием фреймворка Flutter, а вторая — c использованием Kivy и KivyMD. В конце статьи оставляю опрос, в котором вам нужно угадать, какая технология и где используется.

© Habrahabr.ru