Приветствую всех любителей и знатоков языка программирования Python!
В этой статье я покажу, как работать с анимациями в кроссплатформенном фреймворке Kivy в связке с библиотекой компонентов Google Material Design KivyMD. Мы рассмотрим структуру 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 osfrom kivy.lang import Builderfrom 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 MDAppfrom uix.screens.baseclass.callscreen import CallScreenclass 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_hexfrom kivy.core.window import Windowfrom 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-ка с тестом на мобильном устройстве:
На этом все, надеюсь, был полезен!