Приветствую вас, дорогие любители и знатоки 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 изображения, которые пошагово демонстрируют добавление всех элементов, потому что новый редактор Хабра просто наотрез отказался грузить какие-либо изображения после последней гифки в статье. Обидно, но ладно. Всем спасибо за внимание и до новых встреч!
