Diseño de materiales. Creando animaciones en Kivy



¡Saludos a todos los fans y expertos del lenguaje de programación Python!



En este artículo, le mostraré cómo trabajar con animaciones en el marco de Kivy multiplataforma junto con la biblioteca de componentes de Google Material Design : KivyMD . Veremos la estructura de un proyecto de Kivy, utilizando componentes de material para crear una aplicación móvil de prueba con algunas animaciones. El artículo no será pequeño con muchas animaciones GIF, así que sírvete un poco de café y ¡vamos!



Para despertar el interés de los lectores, quiero mostrar de inmediato el resultado de lo que obtenemos al final:





Entonces, para el trabajo, necesitamos el marco Kivy:



pip install kivy


Y la biblioteca KivyMD, que proporciona widgets de Material Design para el marco Kivy:



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


¡Todo está listo! Vamos abierta PyCharm y crear un nuevo CallScreen proyecto con la siguiente estructura de directorios:





La estructura puede ser cualquiera. Ni el marco de Kivy ni la biblioteca de KivyMD requieren ningún directorio requerido que no sea el requisito estándar; debe haber un archivo llamado main.py en la raíz del proyecto . Este es el punto de entrada a la aplicación:





En el directorio de datos / imágenes , he colocado los recursos gráficos que requiere la aplicación:



En el directorio uix / pantallas / baseclass , tendremos un archivo callscreen.py con la clase Python del mismo nombre, en el que implementaremos la lógica del funcionamiento de la pantalla de la aplicación:





Y en el directorio uix / pantallas / kv , crearemos un archivo callscreen.kv (déjelo vacío por ahora), con una descripción de la interfaz de usuario en el lenguaje especial DSL Kivy :





Cuando se crea el proyecto, podemos abrir el archivo callscreen.py e implementar la clase de pantalla de nuestra aplicación de prueba.



callcreen.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


La clase CallScreen se hereda del widget MDScreen de la biblioteca KivyMD (casi todos los componentes de esta biblioteca tienen el prefijo MD - Material Design). MDScreen es un análogo del widget de pantalla del marco Kivy del módulo kivy.uix.screenmanager , pero con propiedades adicionales. Además, MDScreen le permite colocar widgets y controladores uno encima del otro de la siguiente manera:





Este es el posicionamiento que usaremos al colocar elementos flotantes en la pantalla.



En el punto de entrada a la aplicación, el archivo main.py, cree la clase TestCallScreen , heredada de la clase MDApp con un método de compilación obligatorio , que debe devolver un widget o diseño para mostrarlo en la pantalla. En nuestro caso, esta será la clase de pantalla CallScreen creada 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()


Esta es una aplicación lista para usar que muestra una pantalla en blanco. Si ejecutamos el archivo main.py , veremos:





Ahora comencemos a marcar la pantalla de la interfaz de usuario en el archivo callscreen.kv . Para hacer esto, necesita crear una regla con el mismo nombre con la clase base, en la que describiremos los widgets y sus propiedades. Por ejemplo, si tenemos una clase de Python llamada CallScreen , entonces la regla en el archivo KV debe tener exactamente el mismo nombre. Aunque puede crear todos los elementos de la interfaz directamente en el código, esto es, por decirlo suavemente, incorrecto. Comparar:



MyRootWidget:

    BoxLayout:

        Button:

        Button:


Y un análogo de Python:



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


Es bastante obvio que el árbol de widgets es mucho más legible en lenguaje Kv que en código Python. Además, cuando los widgets tienen argumentos, su código Python se convertirá en un desastre y después de un día no podrá entenderlo. Por lo tanto, quien diga algo, si el marco te permite describir elementos de la interfaz de usuario a través de un lenguaje declarativo, esto es una ventaja. Bueno, en Kivy esto es una ventaja doble, porque en Kv Language aún puedes ejecutar instrucciones Python.



Así que comencemos con la imagen del 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"  #   




El widget FitImage se estira automáticamente para adaptarse a todo el espacio que se le asigna mientras se mantiene la relación de aspecto de la imagen:





Podemos ejecutar el archivo main.py y ver el resultado:





Por ahora, todo es sencillo y es hora de empezar a animar los widgets. Agreguemos un botón a la pantalla presionando el cual se llamarán los métodos de animación de la clase 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


Importaciones de módulos en Kv Language:



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


Será similar a las siguientes importaciones en 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




Después de iniciar y hacer clic en el botón verde, obtenemos - AttributeError: El objeto 'CallScreen' no tiene el atributo 'animation_title_image' . Por lo tanto, volvamos a la clase base CallScreen archivo callscreen.py y crear en ella un método animation_title_image , que animará la imagen título.



callcreen.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 ya entendió, la clase Animation , probablemente, como en otros marcos, simplemente anima una propiedad de widget. En nuestro caso, animaremos la propiedad size_hint_y - la sugerencia de altura, estableciendo el intervalo de ejecución de la animación en el parámetro d - duración y el tipo de animación en el parámetro t - type. Podemos animar varias propiedades de un widget a la vez, combinar animaciones usando los operadores + , + = ... La siguiente imagen muestra el resultado de nuestro trabajo. A modo de comparación, para el GIF correcto, utilicé los tipos de animación in_elastic y out_elastic :



Nuestro siguiente paso es agregar un efecto de desenfoque a la imagen del título. Para estos fines, Kivy tiene un EffectWidget . Necesitamos establecer las propiedades deseadas para el efecto y colocar el widget de imagen de título en EffectWidget.



callcreen.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(); \
            [...]


Ahora necesitamos agregar el atributo blur_value a la clase base Python CallScreen y crear un método animation_blur_value que anime el valor del efecto de desenfoque.



callcreen.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:





Tenga en cuenta que los métodos de animación se ejecutarán de forma asincrónica. Animamos el botón de llamada verde para que no moleste a nuestros ojos.



callcreen.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)


callcreen.kv:



[...]

<CallScreen>

    EffectWidget:
        [...]

        FitImage:
            [...]

    MDFloatingActionButton:
        [...]
        on_release:
            #     .
            root.animation_call_button(self); \
            [...]




Agreguemos dos elementos de tipo TwoLineAvatarListItem a la pantalla principal.



callcreen.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); \
            [...]




Creamos dos elementos ItemList y los colocamos en un cuadro vertical. Podemos crear un nuevo método animation_list_box en la clase CallScreen para animar este cuadro.



callcreen.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)




Agreguemos una barra de herramientas a la pantalla.



callcreen.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 y nombre de usuario.



callcreen.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); \
        [...]




Animación típica de las posiciones X e Y de un avatar y un nombre de usuario.



callcreen.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)




Solo necesitamos crear un cuadro con botones:





En el momento de escribir este artículo, me encontré con el hecho de que el botón requerido no se encontraba en la biblioteca de KivyMD . Tuve que hacerlo yo mismo rápidamente. Simplemente agregué instrucciones de lienzo a la clase MDIconButton existente, definí un círculo alrededor del botón y lo coloqué junto con la etiqueta en un cuadro vertical. callcreen.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

[...]




A continuación, creamos un cuadro para albergar los botones personalizados.



callcreen.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"

[...]




Ahora colocamos el CallBox creado en la regla CallScreen y establecemos su posición a lo largo del eje Y más allá del borde inferior de la pantalla.



callcreen.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


Solo queda animar la posición del cuadro creado con botones.



callcreen.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 con una prueba en un dispositivo móvil:





Eso es todo, ¡espero que te haya sido útil!



All Articles