Материальный Python. Кастомные карточки с OpenGL эффектами
Приветствую, уважаемые любители и знатоки 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()
Результат:
Выглядит довольно просто. Но что делать, если мы хотим красивую карточку с Blur эффектом при событии получения фокуса? Такую как, например, в приложении Flutter UI Designs:
Придется сделать ее самому! Тем более, что ничего сложного в этом нет. Для начала создадим базовый класс будущей карты:
class RestaurantCard(MDCard):
source = StringProperty() # путь к главному изображению карточки
shadow = StringProperty() # путь к нижнему изображению-тени
text = StringProperty() # текст карточки
Главное изображение карточки:
Изображение-тень:
Теперь наполним карту компонентами, свойства которых мы определи при помощи специального 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, который позволяет размешать в себе компоненты один над другим вот таким образом:
Мы же разместили сначала главное изображение, сверху положили тень и текст. Теперь если запустить наш код:
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()
… получим результат:
И результат, конечно, далек от ожидаемого, потому что ни 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)
Запускаем и видим:
При наведении курсора (если это десктоп) или при тапе (если это 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)
Уже лучше:
Для обрезания углов у карточки я решил применить Stencil (трафарет) к виджету EffectWidget:
#:import Stencil kivymd.uix.graphics.Stencil
# Создаем новое правило, унаследованное от EffectWidget и Stencil.
radius: [20,]
...
RelativeLayout:
Effect:
...
И вот теперь все работает так, как мы планировали:
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. В конце статьи оставляю опрос, в котором вам нужно угадать, какая технология и где используется.