Приветствую вас, дорогие любители и знатоки 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
Базовый шаблон проекта KivyMD

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

Открываем проект, и удаляем, кроме имя правила, все содержимое файла blur_concept_screen.kv - это разметка нашего единственного экрана в приложении:

blur_concept_screen.kv
blur_concept_screen.kv

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

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

components для экрана BlurConceptScreenView
components для экрана BlurConceptScreenView

Создадим два пакета в директории 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

Теперь откроем в файл с классом представления экрана:

blur_concept_screen.py
blur_concept_screen.py

... и объявим необходимые константы и методы:

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

Комментарии (11)


  1. kAIST
    01.11.2021 23:37

    Спасибо за статью. А теперь вопрос:

    Можно ли сделать наоборот, чтобы блюра не было? ;)

    Сейчас мне нужно написать кое какие приложение, для простоты назовем его фотогалереей. Третий день мучаю kivy и kivymd. Так вот, основная проблема это windows с масштабом интерфейса, отличным от 100%. Приложения kivy выглядят размыто. Есть ли решение проблемы?


    1. KivyMD Автор
      02.11.2021 00:21

      Не совсем понял, зачем делать масштаб интерфейса, отличным от 100%


      1. kAIST
        02.11.2021 00:42

        У меня есть ноутбук 14 дюймов с разрешением FullHD, есть планшет на windows с разрешением 1920х1200. При 100% интерфейс будет очень мелкий. Даже сама windows "из коробки" ставит 150%.


        1. KivyMD Автор
          02.11.2021 01:23

          Нужно использовать метрики для шрифтов и размеров элементов интерфейса. Также можно попытаться изменить значение KIVY_DPI_DENSITY. Я с такой проблемой ни сталкивался во всяком случае.


          1. kAIST
            02.11.2021 13:12

            Поискал по issues, проблема оказывается много лет. Это камень не в ваш огород, а в огород kivy, потому что масштаб интерфейса отличный от 100%, это обычное дело в 2021 году и даже отсталый tkinter скейлится нормально (ручками правда нужно пару строк написать).


            1. KivyMD Автор
              02.11.2021 13:17

              Думаю, скоро поправят.


  1. Tuwogaka
    01.11.2021 23:43

    На десятый питон ставится? А то на сайте только 3.9.


    1. KivyMD Автор
      02.11.2021 00:19

      Пока поддержка Python 3.10 вроде как в разработке у Kivy еще.


      1. Tuwogaka
        02.11.2021 11:30

        Поскольку моя карма снижается даже если я ничего не делаю, то можно и лишний раз выразить своё мнение, пока ещё данный ресурс позволяет это проделать несмотря на явную заточку под промывку сознания.

        Десятый питон что, выпускался под покровом тайны и под охраной ЧВК, или Kivy сознательно культивирует культуру занятия интересным в ущерб неинтересному, сколь бы нужным таковое ни было? Судя по тому, как знакомство с Kivy начинается с настоятельнейших рекомендаций установить виртуальное окружение питона - второе.

        Поэтому речи про использование одной библиотеки поверх другой - весьма интересно, но при наличии малейшего шанса что своё творение придётся поддерживать сколь либо заметный срок - строго теоретически интересно. Хотя получить очередное приглашение в мир Открытого Софта со товарищи - весьма приятно, хотя бы как возможность полюбоваться трагической красотой смеси пока не отлаженного с уже устаревшим.

        Недавно Rust выпустил издание 2021 с документацией от издания 2018, а близкие к минимальным изменения между изданиями документировал отдельно. Объяснить, что если изменения минимальны, то документация должна была быть обновлена непременно, а если изменения значительны, то документация тем более должна была быть обновлена непременно, на хабре оказалось невозможно, культура такая уж сложилась вот.

        Вот примерно так видится со стороны. Кто первый переломит складывающуюся культуру - того будут и плюшки.


        1. KivyMD Автор
          02.11.2021 12:22

          Я не очень понял ваш посыл...


  1. stalker1984
    02.11.2021 08:49

    Это конечно наверно на вкус и цвет, но у вас верхние блоки по левому краю выравнены а нижние по центру. Ну и "фогот пассворд" наверное тоже стоило с засечками сделать. В общем стоит поработать со шрифтами.