Материальный дизайн. Создание анимаций в Kivy

от автора

Приветствую всех любителей и знатоков языка программирования Python!
В этой статье я покажу, как работать с анимациями в кроссплатформенном фреймворке Kivy в связке с библиотекой компонентов Google Material DesignKivyMD. Мы рассмотрим структуру Kivy проекта, использование material компонентов для создания тестового мобильного приложения с одним экраном и большим количеством анимаций. Статья будет большая с большим количеством GIF анимаций поэтому наливайте кофе и погнали!

Чтобы подогреть интерес читателей, я хочу сразу показать результат того, что у нас получится в итоге:

Итак, для работы нам понадобится фреймворк Kivy:

pip install kivy 

И библиотека KivyMD, которая предоставляет виджеты в стиле Material Design для фреймворка Kivy:

pip install https://github.com/kivymd/KivyMD/archive/master.zip 

Все готово к работе! Откроем PyCharm и создадим новый проект CallScreen со следующей структурой катологов:

Структура может любая. Ни фреймворк Kivy, ни библиотека KivyMD не требует никаких обязательных директорий, кроме стандартного требования — в корне проекта должен быть файл с именем main.py. Это точка входа в приложение:

В каталоге data/images я разместил графические ресурсы, которые требуются приложению:

В директории uix/screens/baseclass у нас будет размещаться файл callscreen.py с одноименным Python классом, в котором мы будем реализовывать логику работы экрана приложения:

А в директории uix/screens/kv мы создадим файл callscreen.kv (пока оставим пустым) — с описанием UI на специальном DSL языке Kivy Language:

Когда проект создан, мы можем открыть файл callscreen.py и реализовать класс экрана нашего тестового приложения.

callscreen.py:

import os  from kivy.lang import Builder  from kivymd.uix.screen import MDScreen  # Читаем и загружаем KV файл with open(os.path.join(os.getcwd(), "uix", "screens", "kv", "callscreen.kv"), encoding="utf-8") as KV:     Builder.load_string(KV.read())   class CallScreen(MDScreen):     pass 

Класс CallScreen унаследован от виджета MDScreen библиотеки KivyMD (почти все компоненты этой библиотеки имеют префикс MD — Material Design). MDScreen — это аналог виджета Screen фреймворка Kivy из модуля kivy.uix.screenmanager, но с дополнительными свойствами. Также MDScreen позволяет размещать в себе виджеты и контроллы один над другим следующим образом:

Именно это позиционирование мы будем использовать, размещая «плавающие» элементы на экране.

В точке входа в приложение — файл main.py создадим класс TestCallScreen, унаследованный от класса MDApp с обязательным методом build, который должен возвращать виджет или лайоут, для отображения его на экране. В нашем случае это будет созданный ранее класс экрана CallScreen.

main.py:

from kivymd.app import MDApp  from uix.screens.baseclass.callscreen import CallScreen   class TestCallScreen(MDApp):     def build(self):         return CallScreen()   TestCallScreen().run() 

Это уже готовое приложение, которое отображает пустой экран. Если запустить файл main.py, увидим:

Теперь приступим к разметке UI экрана в файле callscreen.kv. Для этого нужно создать одноименное с базовым классом правило, в котором мы будем описывать виджеты и их свойства. Например, если у нас есть Python класс c именем CallScreen, то и правило в KV файле должно иметь точно такое же имя. Хотя вы можете создавать все элементы интерфейса прямо в коде, но это, мягко говоря, не правильно. Сравните:

MyRootWidget:      BoxLayout:          Button:          Button: 

И аналог на Python:

root = MyRootWidget() box = BoxLayout() box.add_widget(Button()) box.add_widget(Button()) root.add_widget(box) 

Совершенно очевидно, что дерево виджетов намного читабельнее в Kv Language, чем в Python коде. К тому же, когда появятся аргументы у виджетов, ваш Python код станет просто сплошной кашей и уже через день вы не сможете разобраться в нем. Поэтому кто бы что ни говорил, но если фреймворк позволяет описывать элементы UI посредством декларативного языка, это плюс. Ну, а в Kivy это двойной плюс, потому что в Kv Language еще можно выполнять инструкции Python.

Итак, начнем, пожалуй, с титульного изображения:

callscreen.kv:

<CallScreen>      FitImage:         id: title_image  # id для обращения к данному виджету         size_hint_y: .45  # высота изображения (45% от высоты экрана)         # Идентификатор root всегда ссылается на базовый класс.         # В нашем случае это <class 'uix.screens.baseclass.callscreen.CallScreen'>,         # а self - объект самого виджета - <kivymd.utils.fitimage.FitImage object>.         y: root.height - self.height  # положение по оси Y         source: "data/images/avatar.jpg"  # путь к изображению 

Виджет FitImage автоматически растягивается на все выделенное ему пространство с сохранением пропорций изображения:

Можем запустить файл main.py и посмотреть результат:

Пока все просто и самое время приступить к анимированию виджетов. Добавим кнопку в экран по нажатию которой будут вызываться методы анимации из Python класса CallScreen:

callscreen.kv:

#:import get_color_from_hex kivy.utils.get_color_from_hex #:import colors kivymd.color_definitions.colors   <CallScreen>      FitImage:         [...]      MDFloatingActionButton:         icon: "phone"         x: root.width - self.width - dp(20)         y: app.root.height * 45 / 100 + self.height / 2         md_bg_color: get_color_from_hex(colors["Green"]["A700"])         on_release:             # Вызов метода анимации титульного изображения.             root.animation_title_image(title_image); \             root.open_call_box = True if not root.open_call_box else False 

Импорты модулей в Kv Language:

#:import get_color_from_hex kivy.utils.get_color_from_hex #:import colors kivymd.color_definitions.colors 

Будут аналогичны следующим импортам в Python коде:

# Метод get_color_from_hex нужен дляпреобразования цвета # из шестнадцатеричной строки в формат rgba. from kivy.utils import get_color_from_hex # Словарь оттенков цветов различных цветовых схем: # # colors = { #     "Red": { #         "50": "FFEBEE", #         "100": "FFCDD2", #         ..., #     }, #     "Pink": { #         "50": "FCE4EC", #         "100": "F8BBD0", #         ..., #     }, #     ... # } # # https://kivymd.readthedocs.io/en/latest/themes/color-definitions/ from kivymd.color_definitions import colors 

После запуска и нажатия на зеленую кнопку получим — AttributeError: ‘CallScreen’ object has no attribute ‘animation_title_image’. Поэтому вернемся к базовому классу CallScreen в файле callscreen.py и создадим в нем метод animation_title_image, в котором будем анимировать титульное изображение.

callscreen.py:

# Класс для анимирования свойств виджетов. from kivy.animation import Animation  [...]  class CallScreen(MDScreen):     # Флаг для анимации возврата экрана к исходному состоянию.     open_call_box = False      def animation_title_image(self, title_image):         """         :type title_image: <kivymd.utils.fitimage.FitImage object>         """          if not self.open_call_box:             # Анимация развертывания титульного изображения на весь экран.             Animation(size_hint_y=1, d=0.6, t="in_out_quad").start(title_image)         else:             # Анимация возврата титульного изображения к исходному состоянию.             Animation(size_hint_y=0.45, d=0.6, t="in_out_quad").start(title_image) 

Как вы уже поняли, класс Animation, наверное, как и в других фреймворках, просто анимирует свойство виджета. В нашем случае мы анимируем свойство size_hint_y — подсказка высоты, задавая интервал выполнения анимации в параметре d — duration и тип анимации в параметре t — type. Мы можем анимировать сразу несколько свойств одного виджета, комбинировать анимации с помощью операторов +, +=… На изображении ниже показан результат нашей работы. Для сравнения для правой гифки я использовал типы анимаций in_elastic и out_elastic:

Следующий наш шаг — добавить blur эффект к титульному изображению. Для этих целей в Kivy существует EffectWidget. Нам нужно установить нужные свойства для эффекта и поместить виджет титульного изображения в EffectWidget.

callscreen.kv:

#:import effect kivy.uix.effectwidget.EffectWidget #:import HorizontalBlurEffect kivy.uix.effectwidget.HorizontalBlurEffect #:import VerticalBlurEffect kivy.uix.effectwidget.VerticalBlurEffect #:import get_color_from_hex kivy.utils.get_color_from_hex  #:import colors kivymd.color_definitions.colors   <CallScreen>      EffectWidget:         effects:             # blur_value значение степени размытия.             (\             HorizontalBlurEffect(size=root.blur_value), \             VerticalBlurEffect(size=root.blur_value), \             )          FitImage:             [...]      MDFloatingActionButton:         [...]         on_release:             # Вызов метода анимации blur эффекта.             root.animation_blur_value(); \             [...] 

Теперь нужно добавить атрибут blur_value в базовый класс Python CallScreen и создать метод animation_blur_value, который будет анимировать значение эффекта размытия.

callscreen.py:

from kivy.properties import NumericProperty [...]   class CallScreen(MDScreen):     # Значение степени размытия для EffectWidget.     blur_value = NumericProperty(0)      [...]      def animation_blur_value(self):         if not self.open_call_box:             Animation(blur_value=15, d=0.6, t="in_out_quad").start(self)         else:             Animation(blur_value=0, d=0.6, t="in_out_quad").start(self) 

Результат:

Обратите внимание, что методы анимирования будут выполнятся асинхронно! Давайте анимируем зеленую кнопку вызова, чтобы она не мозолила нам глаза.

callscreen.py:

from kivy.utils import get_color_from_hex from kivy.core.window import Window  from kivymd.color_definitions import colors  [...]   class CallScreen(MDScreen):     [...]      def animation_call_button(self, call_button):         if not self.open_call_box:             Animation(                 x=self.center_x - call_button.width / 2,                 y=dp(40),                 md_bg_color=get_color_from_hex(colors["Red"]["A700"]),                 d=0.6,                 t="in_out_quad",             ).start(call_button)         else:             Animation(                 y=Window.height * 45 / 100 + call_button.height / 2,                 x=self.width - call_button.width - dp(20),                 md_bg_color=get_color_from_hex(colors["Green"]["A700"]),                 d=0.6,                 t="in_out_quad",             ).start(call_button) 

callscreen.kv:

[...]  <CallScreen>      EffectWidget:         [...]          FitImage:             [...]      MDFloatingActionButton:         [...]         on_release:             # Вызов метода анимации кнопки вызова.             root.animation_call_button(self); \             [...] 

Добавим два пункиа типа TwoLineAvatarListItem на главный экран.

callscreen.kv:

#:import STANDARD_INCREMENT kivymd.material_resources.STANDARD_INCREMENT #:import IconLeftWidget kivymd.uix.list.IconLeftWidget  [...]   <ItemList@TwoLineAvatarListItem>     icon: ""     font_style: "Caption"     secondary_font_style: "Caption"     height: STANDARD_INCREMENT      IconLeftWidget:         icon: root.icon   <CallScreen>      EffectWidget:         [...]          FitImage:             [...]      MDBoxLayout:         id: list_box         orientation: "vertical"         adaptive_height: True         y: root.height * 45 / 100 - self.height / 2          ItemList:             icon: "phone"             text: "Phone"             secondary_text: "123 456 789"          ItemList:             icon: "mail"             text: "Email"             secondary_text: "kivydevelopment@gmail.com"      MDFloatingActionButton:         [...]         on_release:             root.animation_list_box(list_box); \             [...] 

Мы создали два пункта ItemList и разместили их в вертикальном боксе. Можем создать новый метод animation_list_box в классе CallScreen для анимации этого бокса.

callscreen.py:

[...]   class CallScreen(MDScreen):     [...]      def animation_list_box(self, list_box):         if not self.open_call_box:             Animation(                 y=-list_box.y,                 opacity=0,                 d=0.6,                 t="in_out_quad"б             ).start(list_box)         else:             Animation(                 y=self.height * 45 / 100 - list_box.height / 2,                 opacity=1,                 d=0.6,                 t="in_out_quad",             ).start(list_box) 

Добавим панель инструментов в экран.

callscreen.kv:

[...]  <CallScreen>      EffectWidget:         [...]          FitImage:             [...]      MDToolbar:         y: root.height - self.height - dp(20)         md_bg_color: 0, 0, 0, 0         opposite_colors: True         title: "Profile"         left_action_items:  [["menu", lambda x: x]]         right_action_items: [["dots-vertical", lambda x: x]]      MDBoxLayout:         [...]          ItemList:             [...]          ItemList:             [...]      MDFloatingActionButton:         [...] 

Аватар и имя пользователя.

callscreen.kv:

[...]  <CallScreen>      EffectWidget:         [...]          FitImage:             [...]      MDToolbar:         [...]      MDFloatLayout:         id: round_avatar         size_hint: None, None         size: "105dp", "105dp"         md_bg_color: 1, 1, 1, 1         radius: [self.height / 2,]         y: root.height * 45 / 100 + self.height         x: root.center_x - (self.width + user_name.width + dp(20)) / 2          FitImage:             size_hint: None, None             size: "100dp", "100dp"             mipmap: True             source: "data/images/round-avatar.jpg"             radius: [self.height / 2,]             pos_hint: {"center_x": .5, "center_y": .5}             mipmap: True      MDLabel:         id: user_name         text: "Irene"         font_style: "H3"         bold: True         size_hint: None, None         -text_size: None, None         size: self.texture_size         theme_text_color: "Custom"         text_color: 1, 1, 1, 1         y: round_avatar.y + self.height / 2         x: round_avatar.x + round_avatar.width + dp(20)      MDBoxLayout:         [...]          ItemList:             [...]          ItemList:             [...]      MDFloatingActionButton:         root.animation_round_avatar(round_avatar, user_name); \         root.animation_user_name(round_avatar, user_name); \         [...] 

Типичное анимирование позиций X и Y аватара и имени пользователя.

callscreen.py:

[...]   class CallScreen(MDScreen):     [...]      def animation_round_avatar(self, round_avatar, user_name):         if not self.open_call_box:             Animation(                 x=self.center_x - round_avatar.width / 2,                 y=round_avatar.y + dp(50),                 d=0.6,                 t="in_out_quad",             ).start(round_avatar)         else:             Animation(                 x=self.center_x - (round_avatar.width + user_name.width + dp(20)) / 2,                 y=self.height * 45 / 100 + round_avatar.height,                 d=0.6,                 t="in_out_quad",             ).start(round_avatar)      def animation_user_name(self, round_avatar, user_name):         if not self.open_call_box:             Animation(                 x=self.center_x - user_name.width / 2,                 y=user_name.y - STANDARD_INCREMENT,                 d=0.6,                 t="in_out_quad",             ).start(self.ids.user_name)         else:             Animation(                 x=round_avatar.x + STANDARD_INCREMENT,                 y=round_avatar.center_y - user_name.height - dp(20),                 d=0.6,                 t="in_out_quad",             ).start(user_name) 

Нам осталось создать бокс с кнопками:

На момент написания статьи я столкнулся с тем, что в библиотеке KivyMD не обнаружилось нужной кнопки. Пришлось по-быстрому смастерить её самому. Я просто добавил в существующий класс MDIconButton инструкции canvas, в которых определил окружность вокруг кнопки, и поместил ее вместе с меткой в вертикальный бокс.

callscreen.kv:

<CallBoxButton@MDBoxLayout>     orientation: "vertical"     adaptive_size: True     spacing: "8dp"     icon: ""     text: ""      MDIconButton:         icon: root.icon         theme_text_color: "Custom"         text_color: 1, 1, 1, 1          canvas:             Color:                 rgba: 1, 1, 1, 1             Line:                 width: 1                 circle:                     (\                     self.center_x, \                     self.center_y, \                     min(self.width, self.height) / 2, \                     0, \                     360, \                     )      MDLabel:         text: root.text         size_hint_y: None         height: self.texture_size[1]         font_style: "Caption"         halign: "center"         theme_text_color: "Custom"         text_color: 1, 1, 1, 1  [...] 

Далее мы создаем бокс для размещения кастомных кнопок.

callscreen.kv:

<CallBox@MDGridLayout>     cols: 3     rows: 2     adaptive_size: True     spacing: "24dp"      CallBoxButton:         icon: "microphone-off"         text: "Mute"     CallBoxButton:         icon: "volume-high"         text: "Speaker"     CallBoxButton:         icon: "dialpad"         text: "Keypad"      CallBoxButton:         icon: "plus-circle"         text: "Add call"     CallBoxButton:         icon: "call-missed"         text: "Transfer"     CallBoxButton:         icon: "account"         text: "Contact"  [...] 

Теперь созданный CallBox размещаем в правиле CallScreen и устанавливаем его положение по оси Y за нижней границей экрана.

callscreen.kv:

[...]  <CallScreen>      EffectWidget:         [...]          FitImage:             [...]      MDToolbar:         [...]      MDFloatLayout:         [...]          FitImage:             [...]      MDLabel:         [...]      MDBoxLayout:         [...]          ItemList:             [...]          ItemList:             [...]      MDFloatingActionButton:         root.animation_call_box(call_box, user_name); \         [...]      CallBox:         id: call_box         pos_hint: {"center_x": .5}         y: -self.height         opacity: 0 

Остается только анимировать положение созданного бокса с кнопками.

callscreen.py:

from kivy.metrics import dp [...]   class CallScreen(MDScreen):     [...]      def animation_call_box(self, call_box, user_name):         if not self.open_call_box:             Animation(                 y=user_name.y - call_box.height - dp(100),                 opacity=1,                 d=0.6,                 t="in_out_quad",             ).start(call_box)         else:             Animation(                 y=-call_box.height,                 opacity=0,                 d=0.6,                 t="in_out_quad",             ).start(call_box) 

Финальная GIF-ка с тестом на мобильном устройстве:

На этом все, надеюсь, был полезен!

ссылка на оригинал статьи https://habr.com/ru/post/528154/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *