
Saudações a todos os fãs e especialistas da linguagem de programação Python!
Neste artigo, mostrarei como trabalhar com animações na estrutura Kivy de plataforma cruzada em conjunto com a biblioteca de componentes do Google Material Design - KivyMD . Veremos a estrutura de um projeto Kivy, usando componentes materiais para criar um aplicativo móvel de teste com algumas animações. O artigo não será pequeno, com muitas animações GIF, então sirva um pouco de café e pronto!
Para despertar o interesse dos leitores, quero mostrar imediatamente o resultado do que conseguimos no final:

Então, para trabalhar, precisamos da estrutura Kivy:
pip install kivy
E a biblioteca KivyMD, que fornece widgets de design de material para a estrutura Kivy:
pip install https://github.com/kivymd/KivyMD/archive/master.zip
Tudo pronto para começar! Vamos abrir o PyCharm e criar um novo projeto CallScreen com a seguinte estrutura de diretório:

A estrutura pode ser qualquer. Nem a estrutura Kivy nem a biblioteca KivyMD requerem quaisquer diretórios necessários além do requisito padrão - deve haver um arquivo denominado main.py na raiz do projeto . Este é o ponto de entrada para o aplicativo:

No diretório data / images , coloquei os recursos gráficos que o aplicativo requer:
![]() |
![]() |
No diretório uix / screens / baseclass , teremos um arquivo callscreen.py com a classe Python de mesmo nome, no qual implementaremos a lógica de operação da tela do aplicativo:

E no diretório uix / telas / kv , criaremos um arquivo callscreen.kv (deixe-o vazio por enquanto) - com uma descrição da IU na linguagem especial DSL Kivy :

Quando o projeto é criado, podemos abrir o arquivo callscreen.py e implementar a classe de tela do nosso aplicativo de teste.
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
A classe CallScreen é herdada do widget MDScreen da biblioteca KivyMD (quase todos os componentes desta biblioteca são prefixados com MD - Material Design). MDScreen é um análogo do widget Screen da estrutura Kivy do módulo kivy.uix.screenmanager , mas com propriedades adicionais. Além disso, o MDScreen permite que você coloque widgets e controladores um acima do outro da seguinte maneira:

Este é o posicionamento que usaremos ao colocar elementos flutuantes na tela.
No ponto de entrada do aplicativo - o arquivo main.py, crie a classe TestCallScreen , herdada da classe MDApp com um método de construção obrigatório , que deve retornar um widget ou layout para exibi-lo na tela. No nosso caso, esta será a classe de tela CallScreen criada anteriormente .
main.py:
from kivymd.app import MDApp
from uix.screens.baseclass.callscreen import CallScreen
class TestCallScreen(MDApp):
def build(self):
return CallScreen()
TestCallScreen().run()
Este é um aplicativo pronto que exibe uma tela em branco. Se executarmos o arquivo main.py , veremos:

Agora vamos começar a marcar a tela da IU no arquivo callscreen.kv . Para fazer isso, você precisa criar uma regra com o mesmo nome da classe base, na qual descreveremos os widgets e suas propriedades. Por exemplo, se temos uma classe Python chamada CallScreen , então a regra no arquivo KV deve ter exatamente o mesmo nome. Embora você possa criar todos os elementos de interface diretamente no código, isso não é, para dizer o mínimo, correto. Comparar:
MyRootWidget:
BoxLayout:
Button:
Button:
E um análogo do Python:
root = MyRootWidget()
box = BoxLayout()
box.add_widget(Button())
box.add_widget(Button())
root.add_widget(box)
É bastante óbvio que a árvore de widgets é muito mais legível na linguagem Kv do que no código Python. Além disso, quando os widgets têm argumentos, seu código Python se tornará apenas uma bagunça e depois de um dia você não será capaz de descobrir. Portanto, quem quer que diga alguma coisa, se o framework permite que você descreva os elementos da IU por meio de uma linguagem declarativa, isso é uma vantagem. Bem, no Kivy isso é uma vantagem dupla, porque na linguagem Kv você ainda pode executar instruções Python.
Então, vamos começar com a imagem do título:
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" #
O widget FitImage é automaticamente esticado para caber em todo o espaço alocado a ele, mantendo a proporção da imagem:

Podemos executar o arquivo main.py e ver o resultado:

Por enquanto, tudo é simples e é hora de começar a animar os widgets. Vamos adicionar um botão na tela pressionando o qual os métodos de animação da classe Python CallScreen : callscreen.kv serão chamados
:
#: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
Importações de módulos na linguagem Kv:
#:import get_color_from_hex kivy.utils.get_color_from_hex
#:import colors kivymd.color_definitions.colors
Será semelhante às seguintes importações no código 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

Depois de iniciar e clicar no botão verde, obtemos - AttributeError: o objeto 'CallScreen' não tem o atributo 'animation_title_image' . Portanto, vamos retornar ao arquivo CallScreen da classe base callscreen.py e criar nele um método animation_title_image , que irá animar a imagem do título.
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)
Como você já entendeu, a classe Animation , provavelmente, como em outros frameworks, simplesmente anima uma propriedade de widget. Em nosso caso, vamos animar a propriedade size_hint_y - a sugestão de altura, definindo o intervalo de execução da animação no parâmetro d - duração e o tipo de animação no parâmetro t - type. Podemos animar várias propriedades de um widget ao mesmo tempo, combinar animações usando os operadores + , + = ... A imagem abaixo mostra o resultado do nosso trabalho. Para efeito de comparação, para o GIF direita, eu usei as in_elastic e out_elastic tipos de animação :
![]() |
![]() |
Nosso próximo passo é adicionar um efeito de desfoque à imagem do título. Para esses fins, Kivy tem um EffectWidget . Precisamos definir as propriedades desejadas para o efeito e colocar o widget da imagem do título no 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(); \
[...]
Agora precisamos adicionar o atributo blur_value à classe base Python CallScreen e criar um método animation_blur_value que anime o valor do efeito de desfoque.
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)
Resultado:

Observe que os métodos de animação serão executados de forma assíncrona! Vamos animar o botão verde de chamada para que não incomode nossos olhos.
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); \
[...]

Vamos adicionar dois itens do tipo TwoLineAvatarListItem à tela principal.
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); \
[...]

Criamos dois itens ItemList e os colocamos em uma caixa vertical. Podemos criar um novo método animation_list_box na classe CallScreen para animar esta caixa.
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)

Vamos adicionar uma barra de ferramentas à tela.
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 e nome de usuário.
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); \
[...]

Animação típica das posições X e Y de um avatar e nome de usuário.
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)

Só precisamos criar uma caixa com botões:

No momento em que este livro foi escrito, descobri que o botão obrigatório não foi encontrado na biblioteca KivyMD . Eu tive que fazer rapidamente. Simplesmente adicionei instruções de tela à classe MDIconButton existente , definindo um círculo ao redor do botão e colocando-o junto com o rótulo em uma caixa vertical. 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
[...]

Em seguida, criamos uma caixa para abrigar os botões personalizados.
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"
[...]

Agora colocamos o CallBox criado na regra CallScreen e definimos sua posição ao longo do eixo Y além da borda inferior da tela.
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
Resta apenas animar a posição da caixa criada com botões.
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 final com um teste em um dispositivo móvel:

Só isso, espero que tenha sido útil!



