Greetings to all fans and experts of the Python programming language!
In this article, I'll show you how to work with animations in the cross-platform Kivy framework in conjunction with the Google Material Design component library - KivyMD . We will look at the structure of a Kivy project, using material components to create a test mobile application with some animations. The article will not be small with a lot of GIF animations, so pour some coffee and let's go!
To pique the interest of readers, I want to immediately show the result of what we get in the end:
So, for work we need the Kivy framework:
pip install kivy
And the KivyMD library, which provides Material Design widgets for the Kivy framework:
pip install https://github.com/kivymd/KivyMD/archive/master.zip
Everything is ready to go! Let's open PyCharm and create a new CallScreen project with the following directory structure:
The structure can be any. Neither the Kivy framework nor the KivyMD library require any required directories other than the standard requirement - there must be a file named main.py in the root of the project . This is the entry point to the application:
In the data / images directory, I have placed the graphic resources that the application requires:
In the uix / screens / baseclass directory , we will have a callscreen.py file with the Python class of the same name, in which we will implement the logic of the application screen operation:
And in the uix / screens / kv directory, we will create a callscreen.kv file (leave it blank for now) - with a description of the UI in a special DSL language Kivy Language :
When the project is created, we can open the callscreen.py file and implement the screen class of our test application.
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
The CallScreen class is inherited from the MDScreen widget of the KivyMD library (almost all components of this library have the MD - Material Design prefix ). MDScreen is an analogue of the Screen widget of the Kivy framework from the kivy.uix.screenmanager module , but with additional properties. Also MDScreen allows you to place widgets and controllers one above the other in itself as follows:
This is the positioning we will use when placing floating elements on the screen.
At the entry point to the application - the main.py file , create the TestCallScreen class , inherited from the MDApp class with a mandatory build method , which must return a widget or layout to display it on the screen. In our case, this will be the CallScreen screen class created earlier .
main.py:
from kivymd.app import MDApp
from uix.screens.baseclass.callscreen import CallScreen
class TestCallScreen(MDApp):
def build(self):
return CallScreen()
TestCallScreen().run()
This is a ready-made application that displays a blank screen. If we run the main.py file , we will see:
Now let's start marking up the UI screen in the callscreen.kv file . To do this, you need to create a rule of the same name with the base class, in which we will describe widgets and their properties. For example, if we have a Python class called CallScreen , then the rule in the KV file must have exactly the same name. Although you can create all interface elements right in the code, this is, to put it mildly, not correct. Compare:
MyRootWidget:
BoxLayout:
Button:
Button:
And a Python analog:
root = MyRootWidget()
box = BoxLayout()
box.add_widget(Button())
box.add_widget(Button())
root.add_widget(box)
It is quite obvious that the widget tree is much more readable in Kv Language than in Python code. In addition, when the arguments for the widgets appear, your Python code will become just a mess and after a day you will not be able to figure it out. Therefore, no matter what they say, but if the framework allows you to describe UI elements through a declarative language, this is a plus. Well, in Kivy this is a double plus, because in Kv Language you can still execute Python instructions.
So let's start with the title image:
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" #
The FitImage widget is automatically stretched to fit the entire space allocated to it while maintaining the aspect ratio of the image:
We can run the main.py file and see the result:
For now, everything is simple and it's time to start animating the widgets. Let's add a button to the screen by pressing which the animation methods from the Python CallScreen class : callscreen.kv will be called
:
#: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
Module imports in Kv Language:
#:import get_color_from_hex kivy.utils.get_color_from_hex
#:import colors kivymd.color_definitions.colors
Will be similar to the following imports in Python code:
# 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
After launching and clicking on the green button, we get - AttributeError: 'CallScreen' object has no attribute 'animation_title_image' . Therefore, let us return to the base class CallScreen file callscreen.py and create in it a method animation_title_image , which will animate the 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)
As you already understood, the Animation class , probably, like in other frameworks, simply animates a widget property. In our case, we will animate the size_hint_y property - the height hint, setting the animation execution interval in the d parameter - duration and the animation type in the t - type parameter . We can animate several properties of one widget at once, combine animations using operators + , + = ... The image below shows the result of our work. For comparison, for the right GIF, I used the in_elastic and out_elastic animation types :
Our next step is to add a blur effect to the title image. For these purposes, Kivy has an EffectWidget . We need to set the desired properties for the effect and place the title image widget in the EffectWidget.
callscreen.kv:
#:import effect kivy.uix.effectwidget.EffectWidget
#:import HorizontalBlurEffect kivy.uix.effectwidget.HorizontalBlurEffect
#:import VerticalBlurEffect kivy.uix.effectwidget.VerticalBlurEffect
<CallScreen>
EffectWidget:
effects:
# blur_value .
(\
HorizontalBlurEffect(size=root.blur_value), \
VerticalBlurEffect(size=root.blur_value), \
)
FitImage:
[...]
MDFloatingActionButton:
[...]
on_release:
# blur .
root.animation_blur_value(); \
[...]
Now we need to add the blur_value attribute to the Python CallScreen base class and create an animation_blur_value method that animates the value of the blur effect.
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)
Result:
Note that animation methods will execute asynchronously! Let's animate the green call button so that it doesn't bother our eyes.
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); \
[...]
Let's add two items of type TwoLineAvatarListItem to the main screen.
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); \
[...]
We've created two ItemList items and placed them in a vertical box. We can create a new method animation_list_box in the CallScreen class to animate this box.
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)
Let's add a toolbar to the screen.
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:
[...]
Avatar and username.
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); \
[...]
Typical animation of the X and Y positions of an avatar and username.
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)
We just need to create a box with buttons:
At the time of this writing, I came across the fact that the required button was not found in the KivyMD library . I had to quickly make it myself. I simply added canvas instructions to the existing MDIconButton class , defining a circle around the button, and placing it in a vertical box along with the label. 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
[...]
Next, we create a box for the custom buttons.
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"
[...]
Now we place the created CallBox in the CallScreen rule and set its position along the Y axis beyond the bottom border of the screen.
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
It remains only to animate the position of the created box with buttons.
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)
Final GIF with a test on a mobile device:
That's all, I hope it was useful!