Conception matérielle. Créer des animations dans Kivy



Salutations à tous les fans et experts du langage de programmation Python!



Dans cet article, je vais vous montrer comment travailler avec des animations dans le framework Kivy multiplateforme en conjonction avec la bibliothèque de composants Google Material Design - KivyMD . Nous examinerons la structure d'un projet Kivy, en utilisant des composants matériels pour créer une application mobile de test avec quelques animations. L'article ne sera pas petit avec beaucoup d'animations GIF, alors versez un peu de café et c'est parti!



Pour piquer l'intérêt des lecteurs, je veux montrer immédiatement le résultat de ce que nous obtenons à la fin:





Donc, pour travailler, nous avons besoin du framework Kivy:



pip install kivy


Et la bibliothèque KivyMD, qui fournit des widgets Material Design pour le framework Kivy:



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


Tout est prêt à partir! Let open PyCharm et créer un nouveau CallScreen projet avec la structure de répertoire suivant:





La structure peut être n'importe laquelle. Ni le framework Kivy ni la bibliothèque KivyMD ne nécessitent de répertoires requis autres que l'exigence standard - il doit y avoir un fichier nommé main.py à la racine du projet . C'est le point d'entrée de l'application:





Dans le répertoire data / images , j'ai placé les ressources graphiques nécessaires à l'application:



Dans le répertoire uix / screens / baseclass , nous aurons un fichier callscreen.py avec la classe Python du même nom, dans lequel nous implémenterons la logique de l'opération de l'écran de l'application:





Et dans le répertoire uix / screens / kv , nous allons créer un fichier callscreen.kv (pour l'instant, laissez-le vide) - avec une description de l'interface utilisateur dans un langage DSL Kivy spécial :





Lorsque le projet est créé, nous pouvons ouvrir le fichier callscreen.py et implémenter la classe screen de notre application de test.



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


La classe CallScreen est héritée du widget MDScreen de la bibliothèque KivyMD (presque tous les composants de cette bibliothèque ont le préfixe MD - Material Design). MDScreen est un analogue du widget Screen du framework Kivy du module kivy.uix.screenmanager , mais avec des propriétés supplémentaires. MDScreen vous permet également de placer les widgets et les contrôleurs les uns au-dessus des autres comme suit:





C'est le positionnement que nous utiliserons pour placer des éléments flottants sur l'écran.



Au point d'entrée dans l'application - le fichier main.py, créez la classe TestCallScreen , héritée de la classe MDApp avec une méthode de construction obligatoire qui doit renvoyer un widget ou une mise en page pour l'afficher à l'écran. Dans notre cas, ce sera la classe d'écran CallScreen créée précédemment .



main.py:



from kivymd.app import MDApp

from uix.screens.baseclass.callscreen import CallScreen


class TestCallScreen(MDApp):
    def build(self):
        return CallScreen()


TestCallScreen().run()


Il s'agit d'une application prête à l'emploi qui affiche un écran vide. Si nous exécutons le fichier main.py , nous verrons:





Commençons maintenant à marquer l'écran de l'interface utilisateur dans le fichier callscreen.kv . Pour ce faire, vous devez créer une règle du même nom avec la classe de base, dans laquelle nous décrirons les widgets et leurs propriétés. Par exemple, si nous avons une classe Python appelée CallScreen , la règle du fichier KV doit avoir exactement le même nom. Bien que vous puissiez créer tous les éléments d'interface directement dans le code, ce n'est pas correct. Comparer:



MyRootWidget:

    BoxLayout:

        Button:

        Button:


Et un analogue de Python:



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


Il est bien évident que l'arborescence des widgets est beaucoup plus lisible en langage Kv qu'en code Python. De plus, lorsque les arguments pour les widgets apparaissent, votre code Python deviendra juste un gâchis et après une journée, vous ne pourrez pas le comprendre. Par conséquent, peu importe ce qu'ils disent, mais si le framework vous permet de décrire les éléments de l'interface utilisateur via un langage déclaratif, c'est un plus. Eh bien, dans Kivy, c'est un double plus, car en langage Kv, vous pouvez toujours exécuter des instructions Python.



Commençons donc par l'image de titre:



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"  #   




Le widget FitImage est automatiquement étiré pour s'adapter à tout l'espace qui lui est alloué tout en conservant le rapport hauteur / largeur de l'image:





Nous pouvons exécuter le fichier main.py et voir le résultat:





Pour l'instant, tout est simple et il est temps de commencer à animer les widgets. Ajoutons un bouton à l'écran en appuyant sur lequel les méthodes d'animation de la classe Python CallScreen : callscreen.kv seront appelées



:



#: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


Importation de modules en langage Kv:



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


Sera similaire aux importations suivantes en code 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




Après avoir lancé et cliqué sur le bouton vert, nous obtenons - AttributeError: l'objet 'CallScreen' n'a pas d'attribut 'animation_title_image' . Revenons donc au fichier CallScreen de la classe de base callscreen.py et créons-y une méthode animation_title_image , qui animera l'image du titre.



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)


Comme vous l'avez déjà compris, la classe Animation , probablement, comme dans d'autres frameworks, anime simplement une propriété de widget. Dans notre cas, nous animerons la propriété size_hint_y - l'indice de hauteur, en définissant l'intervalle d'exécution de l'animation dans le paramètre d - durée et le type d'animation dans le paramètre t - type. Nous pouvons animer plusieurs propriétés d'un widget à la fois, combiner des animations à l'aide des opérateurs + , + = ... L'image ci-dessous montre le résultat de notre travail. A titre de comparaison, pour le bon GIF, j'ai utilisé les types d'animation in_elastic et out_elastic :



Notre prochaine étape consiste à ajouter un effet de flou à l'image de titre. À ces fins, Kivy dispose d'un EffectWidget . Nous devons définir les propriétés souhaitées pour l'effet et placer le widget d'image de titre dans 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(); \
            [...]


Nous devons maintenant ajouter l'attribut blur_value à la classe de base Python CallScreen et créer une méthode animation_blur_value qui anime la valeur de l'effet de flou.



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)


Résultat:





Notez que les méthodes d'animation s'exécuteront de manière asynchrone! Animons le bouton d'appel vert pour qu'il ne dérange pas nos yeux.



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




Ajoutons deux éléments de type TwoLineAvatarListItem à l'écran 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); \
            [...]




Nous avons créé deux éléments ItemList et les avons placés dans une boîte verticale. Nous pouvons créer une nouvelle méthode animation_list_box dans la classe CallScreen pour animer cette boîte.



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)




Ajoutons une barre d'outils à l'écran.



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 et nom d'utilisateur.



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




Animation typique des positions X et Y d'un avatar et d'un nom d'utilisateur.



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)




Il suffit de créer une boîte avec des boutons:





Au moment d'écrire ces lignes, je suis tombé sur le fait que le bouton requis n'a pas été trouvé dans la bibliothèque KivyMD . J'ai dû le faire rapidement moi-même. J'ai simplement ajouté des instructions de canevas à la classe MDIconButton existante , en définissant un cercle autour du bouton et en le plaçant dans une boîte verticale avec l'étiquette. 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

[...]




Ensuite, nous créons une boîte pour héberger les boutons personnalisés.



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"

[...]




Nous plaçons maintenant la CallBox créée dans la règle CallScreen et définissons sa position le long de l'axe Y au-delà de la bordure inférieure de l'écran.



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


Il ne reste plus qu'à animer la position de la boîte créée avec des boutons.



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 avec un test sur un appareil mobile:





C'est tout, j'espère que cela a été utile!



All Articles