Приветствую вас, дорогие любители и знатоки Python! Сегодня как всегда будем делать обзор фреймворка для кроссплатформенной разработки Kivy и библиотеки виджетов в стиле Google Material Design для фреймворка Kivy - KivyMD. В этой статье мы рассмотрим как сделать blur эффект отдельных компонентов интерфейса пользователя. Я уже делал похожий обзор в статье Материальный Python. Кастомные карточки с OpenGL-эффектами, но нижеследующий материал несколько сложнее и в прямом смысле динамичнее. Что ж, начнем...
А для тех, кто незнаком ни с Kivy ни с библиотекой KivyMD, вкратце напомню:

Kivy - кроссплатформенный фреймворк с открытым исходным кодом, написанный с использованием Python/Cython. Поддерживает устройства ввода: WM_Touch, WM_Pen, Mac OS X Trackpad Magic Mouse, Mtdev, Linux Kernel HID, TUIO...

KivyMD - сторонняя open source библиотека, написанная на чистом Python, реализующая набор виджетов в стиле Google Material Design, для использования с фреймворком Kivy. Вне экосистемы Kivy библиотека не используется. Текущее состояние - бета. Мастер версия на GitHub - 1.0.0
Два этих проекта развиваются отдельно друг от друга, но поскольку из коробки Kivy имеет стандартный "убогий" дизайн виджетов, а современные приложения не приемлют серости и всем хочется юзать красивые красивости, люди часто путают Kivy и KivyMD, задавая, например, вопросы типа "Как мне скомпилировать для моего приложения KivyMD вместо Kivy?", что, конечно же, некорректно, поскольку KivyMD это всего лишь дополнительные виджеты для фреймворка Kivy, реализованные с помощью него же. Эти виджеты никогда не будут включены в стандартную библиотеку Kivy и не заменят собой страшные дефолтные виджеты Kivy, потому что Kivy хочет быть кроссплатформенным на 100% и не привязывается ни к материальному дизайну ни к какому-либо еще, оставляя все на стороне пользователя: хочется дизайн как у Mac OS - делайте как у Mac OS, хочется дизайн как в Windows - делайте как в Windows, благо все виджеты Kivy кастомизируются легко и быстро.
Ну а наша сегодняшняя задача - сделать нижеследующий концепт экрана:

Основная цель - blur эффект для двух текстовых полей. Анимированный фон я сделал для того, чтобы сделать акцент на динамике. Итак, для работы нам понадобится Python 3.6 - 3.9 (3.10 пока в разработке у Kivy), фреймворк Kivy и библиотека KivyMD. Установим все это дело:
pip install "kivy[base] @ https://github.com/kivy/kivy/archive/master.zip"
pip install https://github.com/kivymd/KivyMD/archive/master.zip
Первая команда установит фреймворк Kivy мастер версии, а вторая - библиотеку KivyMD также мастер версии. Все. Можем работать. Откроем консоль и создадим шаблон проекта с помощью инструментов KivyMD:
python3.9 -m kivymd.tools.patterns.create_project \
MVC \
/Users/macbookair \
BlurConcept \
python3.9 \
master \
--name_screen BlurConceptScreen
Будет создан проект с шаблоном MVC в директории /Users/macbookair с именем BlurConcept, виртуальным окружением и всеми необходимыми зависимостями:

Детально об архитектуре проекта, которую предлагает использовать KivyMD вы можете прочитать в статье Python GUI. Библиотека KivyMD. Шаблон MVC, parallax эффект и анимация контента слайдов
Открываем проект, и удаляем, кроме имя правила, все содержимое файла blur_concept_screen.kv - это разметка нашего единственного экрана в приложении:

Посмотрим на готовый экран приложения и прикинем какие элементы UI мы можем переиспользовать:

Я выделил метки и два текстовых поля. В проекте в каждой директории представлений для каждого экрана (View) есть пакет components:

Создадим два пакета в директории components для экрана BlurConceptScreenView - commonlabel и fillfield - метку и текстовое поле с общими параметрами:

commonlabel.py:
from kivymd.uix.label import MDLabel
class CommonLabel(MDLabel):
"""It is just a base class for a label with common parameters."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.color = (1, 1, 1, 1)
self.font_style = "H4"
self.font_name = "assets/fonts/PlayfairDisplay-Black.otf"
self.adaptive_height = True
self.markup = True
fillfield.py:
from kivymd.uix.textfield import MDTextField
class FillField(MDTextField):
"""It is just a base class for a text field with common parameters."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.size_hint_x = None
self.width = "320dp"
self.mode = "fill"
self.fill_color_normal = (1, 1, 1, 0.1)
self.fill_color_focus = (1, 1, 1, 0.3)
В файле __init__.py сделаем импорт этих классов, чтобы в дальше их можно было импортировать непосредственно из пакета components:
components/__init__.py:
from .fillfield.fill_field import FillField
from .commonlabel.common_label import CommonLabel
Теперь откроем в файл с классом представления экрана:

... и объявим необходимые константы и методы:
from kivy.animation import Animation
from kivy.metrics import dp
from kivy.properties import NumericProperty
from View.base_screen import BaseScreenView
from View.BlurConceptScreen.components import FillField, CommonLabel
class BlurConceptScreenView(BaseScreenView):
"""Implements the login start screen in the user application."""
OPACITY = NumericProperty(0)
SHIFT_Y = NumericProperty(dp(0))
FIELD_WIDTH = NumericProperty(dp(320))
FIELD_HEIGHT = NumericProperty(dp(52))
PADDING = NumericProperty(dp(24))
def on_enter(self, *args):
"""
Event called when the screen is displayed: the entering animation is
complete.
"""
animation = Animation(SHIFT_Y=dp(140), d=2, t="in_out_quart")
animation.bind(on_complete=self.animation_bg_zoom)
animation.start(self)
Animation(OPACITY=1, d=3).start(self)
def animation_bg_zoom(self, *args):
Animation(height=self.ids.bg.height + self.SHIFT_Y, d=2, t="in_out_quart").start(
self.ids.bg
)
Алгоритм прост: в момент появления экрана в методе on_enter с помощью класса Animation мы будет анимировать два поля класса BlurConceptScreen: OPACITY и SHIFT_Y - прозрачность меток экрана и сдвиг меток по оси Y. Ну, а изменения свойств базового класса в одноименном правиле разметки в KV файле применяются автоматически:

blur_concept_screen.kv:
#:import STANDARD_INCREMENT kivymd.material_resources.STANDARD_INCREMENT
<BlurConceptScreenView>
FitImage:
id: bg
size_hint_y: None
source: "assets/images/bg.jpg"
height: root.height + root.SHIFT_Y
CommonLabel:
x: field_login.x
y: root.height - root.SHIFT_Y
opacity: root.OPACITY
text: "You are\nUp Above the Sky"
CommonLabel:
x: field_login.x
opacity: root.OPACITY
font_name: "assets/fonts/PlayfairDisplay-Regular.otf"
text: "Get Started Here"
y: (field_login.y + root.SHIFT_Y) - (root.PADDING + self.height)
FillField:
id: field_login
opacity: root.OPACITY
hint_text: "Login"
icon_left: "account"
pos:
root.center[0] - root.FIELD_WIDTH / 2, \
root.center[1] + root.SHIFT_Y / 4
FillField:
id: field_password
opacity: root.OPACITY
hint_text: "Password"
icon_left: "lock"
pos:
root.center[0] - root.FIELD_WIDTH / 2, \
root.center[1] - root.SHIFT_Y / 4
CommonLabel:
id: lbl_forgot_password
opacity: root.OPACITY
font_style: "Caption"
halign: "center"
y: field_password.y - root.PADDING * 2
text: "Forgot Password?"
MDFillRoundFlatButton:
id: sign_in_button
text: "Sign In"
opacity: root.OPACITY
size_hint_x: None
-width: field_password.width
pos_hint: {"center_x": .5}
font_size: "24sp"
font_name: "assets/fonts/PlayfairDisplay-Black.otf"
md_bg_color: .8, .8, .8, 1
text_color: app.theme_cls.primary_color
ripple_color: app.theme_cls.primary_color[:-1] + [.4]
y: root.SHIFT_Y
CommonLabel:
opacity: root.OPACITY
font_style: "Caption"
font_name: "assets/fonts/PlayfairDisplay-Regular.otf"
halign: "center"
y: dp(160) - root.SHIFT_Y
text:
"Don`t have an Account? " \
"[font=assets/fonts/PlayfairDisplay-Black.otf]Sign Up[/font]"
Уже можем запустить наш проект и увидеть следующую картину:

Как видим, blur эффекта нет. Для добавления эффекта воспользуемся виджетом EffectWidget:
#:import STANDARD_INCREMENT kivymd.material_resources.STANDARD_INCREMENT
#:import HorizontalBlurEffect kivy.uix.effectwidget.HorizontalBlurEffect
#:import VerticalBlurEffect kivy.uix.effectwidget.VerticalBlurEffect
#:import effect kivy.uix.effectwidget.EffectWidget
<BlurConceptScreenView>
FitImage:
CommonLabel:
CommonLabel:
EffectWidget:
opacity: root.OPACITY
effects:
( \
HorizontalBlurEffect(size=12), \
VerticalBlurEffect(size=12), \
)
canvas.before:
StencilPush
RoundedRectangle:
radius: [10, 10, 0, 0]
size: root.FIELD_WIDTH, root.FIELD_HEIGHT
pos:
root.center[0] - root.FIELD_WIDTH / 2, \
root.center[1] + root.SHIFT_Y / 4
RoundedRectangle:
size: root.FIELD_WIDTH, root.FIELD_HEIGHT
radius: [10, 10, 0, 0]
pos:
root.center[0] - root.FIELD_WIDTH / 2, \
root.center[1] - root.SHIFT_Y / 4
StencilUse
canvas.after:
StencilUnUse
RoundedRectangle:
size: root.FIELD_WIDTH, root.FIELD_HEIGHT
radius: [10, 10, 0, 0]
pos:
root.center[0] - root.FIELD_WIDTH / 2, \
root.center[1] + root.SHIFT_Y / 4
RoundedRectangle:
size: root.FIELD_WIDTH, root.FIELD_HEIGHT
radius: [10, 10, 0, 0]
pos:
root.center[0] - root.FIELD_WIDTH / 2, \
root.center[1] - root.SHIFT_Y / 4
StencilPop
FitImage:
id: bg_blur
size_hint_y: None
source: "assets/images/bg.jpg"
height: root.height + root.SHIFT_Y
FillField:
FillField:
CommonLabel:
MDFillRoundFlatButton:
CommonLabel:
EffectWidget способен применять различные графические эффекты к своим потомкам с помощью пользовательских шейдеров и OpenGL. Что мы сделали в нашем конкретном случае, чтобы получить blur эффект? Мы воспользовались инструкциями холста (canvas), определили для них размер и позицию равные размеру и позиции двум текстовым полям, с помощью трафаретов (StencilPush/StencilUse) выделили эти области на холсте и применили blur эффект в свойствах EffectWidget к фоновому изображению:
EffectWidget:
effects:
( \
HorizontalBlurEffect(size=12), \
VerticalBlurEffect(size=12), \
)
canvas.before:
...
canvas.after:
...
FitImage:
id: bg_blur
size_hint_y: None
source: "assets/images/bg.jpg"
height: root.height + root.SHIFT_Y
Теперь нам нужно синхронизировать анимации фоновых изображений, добавив в метод animation_bg_zoom в классе представления анимацию для изображения bg_blur:
class BlurConceptScreenView(BaseScreenView):
def animation_bg_zoom(self, *args):
Animation(height=self.ids.bg_blur.height + self.SHIFT_Y, d=2, t="in_out_quart").start(
self.ids.bg_blur
)
Если мы в KV разметке в файле blur_concept_screen.kv оставим только EffectWidget с инструкциями холста:
#:import HorizontalBlurEffect kivy.uix.effectwidget.HorizontalBlurEffect
#:import VerticalBlurEffect kivy.uix.effectwidget.VerticalBlurEffect
#:import effect kivy.uix.effectwidget.EffectWidget
<BlurConceptScreenView>
EffectWidget:
opacity: root.OPACITY
effects:
( \
HorizontalBlurEffect(size=12), \
VerticalBlurEffect(size=12), \
)
canvas.before:
StencilPush
RoundedRectangle:
radius: [10, 10, 0, 0]
size: root.FIELD_WIDTH, root.FIELD_HEIGHT
pos:
root.center[0] - root.FIELD_WIDTH / 2, \
root.center[1] + root.SHIFT_Y / 4
RoundedRectangle:
size: root.FIELD_WIDTH, root.FIELD_HEIGHT
radius: [10, 10, 0, 0]
pos:
root.center[0] - root.FIELD_WIDTH / 2, \
root.center[1] - root.SHIFT_Y / 4
StencilUse
canvas.after:
StencilUnUse
RoundedRectangle:
size: root.FIELD_WIDTH, root.FIELD_HEIGHT
radius: [10, 10, 0, 0]
pos:
root.center[0] - root.FIELD_WIDTH / 2, \
root.center[1] + root.SHIFT_Y / 4
RoundedRectangle:
size: root.FIELD_WIDTH, root.FIELD_HEIGHT
radius: [10, 10, 0, 0]
pos:
root.center[0] - root.FIELD_WIDTH / 2, \
root.center[1] - root.SHIFT_Y / 4
StencilPop
FitImage:
id: bg_blur
size_hint_y: None
source: "assets/images/bg.jpg"
height: root.height + root.SHIFT_Y
... и запустим проект, то сможем наглядно увидеть как это работает:

Далее мы просто добавляем все остальные виджеты на экран. К сожалению, я не смог добавить gif изображения, которые пошагово демонстрируют добавление всех элементов, потому что новый редактор Хабра просто наотрез отказался грузить какие-либо изображения после последней гифки в статье. Обидно, но ладно. Всем спасибо за внимание и до новых встреч!