Русский
Русский
English
Статистика
Реклама

Kivymd

Материальный дизайн. Создание анимаций в Kivy

15.11.2020 22:16:27 | Автор: admin


Приветствую всех любителей и знатоков языка программирования 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-ка с тестом на мобильном устройстве:



На этом все, надеюсь, был полезен!
Подробнее..

Разработка мобильных приложений на Python. Создание анимаций в Kivy. Part 2

23.11.2020 08:20:36 | Автор: admin

Приветствую всех любителей и знатоков языка программирования Python!

Сегодня продолжим разбираться с темой анимаций в кроссплатформенном фреймворке для с поддержкой мультитач Kivy в связке с библиотекой компонентов Google Material Design KivyMD. В прошлой статье мы уже разбирали пример тестового приложения на Python/Kivy/KivyMD, в этой пройдемся по теме анимаций более подробно. В конце статьи я приведу ссылку на репозиторий проекта, в котором вы сможете скачать и сами пощупать, демонстрационное Kivy/KivyMD приложение. Как и предыдущая, эта статья будет содержать не маленькое количество GIF анимаций и видео, а поэтому наливайте кофе и погнали!

Kivy работает на Linux, Windows, OS X, Android, iOS и Raspberry Pi. Вы можете запустить один и тот же код на всех поддерживаемых платформах без внесения дополнительных изменений в кодовую базу. Kivy поддерживает большое количество устройств ввода, включая WM_Touch, WM_Pen, Mac OS X Trackpad и Magic Mouse, Mtdev, Linux Kernel HID, TUIO и так же как и Flutter, не задействует нативные элементы управления. Все его виджеты настраиваются. Это значит, что приложения Kivy будут выглядеть одинаково на всех платформах. Но благодаря тому, что виджеты Kivy могут быть кастомизированы как угодно, вы можете создавать свои собственные виджеты. Например, так появилась библиотека KivyMD. Прежде чем продолжить, давайте посмотрим небольшой обзор возможностей Kivy:

Демонстрационные ролики Kivy приложений






В этих роликах наглядно продемонстрировано взаимодействие Kivy приложения с пользователем с помощью жестов и анимаций. Давайте и мы создадим простейшее приложение с анимацией двух лейблов:

from kivy.animation import Animationfrom kivy.lang import Builderfrom kivymd.app import MDAppKV = """<CommonLabel@MDLabel>    opacity: 0    adaptive_height: True    halign: "center"    y: -self.heightMDScreen:    on_touch_down: app.start_animation()    CommonLabel:        id: lbl_1        font_size: "32sp"        text: "M A R S"    CommonLabel:        id: lbl_2        font_size: "12sp"        text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit""""class TestAnimation(MDApp):    def build(self):        return Builder.load_string(KV)    def start_animation(self):        lbl_1 = self.root.ids.lbl_1        lbl_2 = self.root.ids.lbl_2        Animation(            opacity=1, y=lbl_1.height * 2, d=0.9, t="in_out_back"        ).start(lbl_1)        Animation(            opacity=1, y=lbl_2.height + ids.lbl_1.height, d=1, t="in_out_back"        ).start(lbl_2)TestAnimation().run()

Это уже готовое приложение. Мы будем его лишь слегка редактировать. Правило CommonLabel в KV строке аналогично созданию класса в Python коде. Сравните:


Код в Kivy Language всегда короче и читабельнее. Поэтому в Python коде у нас будет только логика. Мы создали две метки с общими свойствами, описанными в правиле CommonLabel: прозрачность (opacity), размер текстуры метки (adaptive_height), горизонтальное выравнивание (halign), положение по оси Y (y ) и дали этим меткам id-шники (lbl_1, lbl_2), чтобы иметь возможность обращаться к свойствам объектов меток и манипулировать ими из Python кода. Далее мы привязали к событию on_touch_down (сработает при прикосновении к экрану в любом месте) вызов метода start_animation, в котором будем анимировать наши две метки.

Animation


Для анимарования объектов в Kivy используется класс Animation. Использовать его очень просто: при инициализации класса Animation вы должны передать в качестве аргументов имена свойств с целевыми значениями, которые будут достигнуты в конце анимации. Например:

    def start_animation(self):        # Получаем объекты меток из KV разметки        lbl_1 = self.root.ids.lbl_1        lbl_2 = self.root.ids.lbl_2        # Анимация первой метки        Animation(            opacity=1,  # анимация прозрачности до значения 1            y=lbl_1.height * 2,  # анимация положения виджета по оси Y            d=0.9,  # время выполнения анимация            t="in_out_back"  # тип анимации        ).start(lbl_1)  # в метод start передаем объект, который нужно анимаровать        # Анимация второй метки        Animation(            opacity=1, y=lbl_2.height + lbl_1.height, d=1, t="in_out_back"        ).start(lbl_2)

На нижеследующей анимации я продемонстрировал результат простейшей анимации, которую мы создали, с разными типами анимирования:

  1. in_out_back
  2. out_elastic
  3. linear


Давайте немного усложним задачу и попробуем анимировать вращение меток на плоскости. Для этого будем использовать матричные манипуляции (PushMatrix, PopMatrix, Rotate, Translate, Scale). Добавим к общей метке инструкции canvas:

<CommonLabel@MDLabel>    angle: 180  # значение вращения    [...]    canvas.before:        PushMatrix        Rotate:            angle: self.angle            origin: self.center    canvas.after:        PopMatrix

А в Python коде в класс Animation передадим новое свойство angle для анимации:

    def start_animation(self):        lbl_1 = self.root.ids.lbl_1        lbl_2 = self.root.ids.lbl_2        Animation(angle=0, [...]).start(lbl_1)        Animation(angle=0, [...]).start(lbl_2)

Результат:

Добавим анимирование масштаба меток:

<CommonLabel@MDLabel>    scale: 5  # значение масшбирования    [...]    canvas.before:        PushMatrix        [...]        Scale:            # масштабирование по трем осям            x: self.scale            y: self.scale            z: self.scale            origin: self.center    canvas.after:        PopMatrix

В Python коде в класс Animation передадим новое свойство scale для анимации:

    def start_animation(self):        lbl_1 = self.root.ids.lbl_1        lbl_2 = self.root.ids.lbl_2        Animation(scale=1, [...]).start(lbl_1)        Animation(scale=1, [...]).start(lbl_2)

Результат:

Класс Animation имеет ряд событий для отслеживания процесса анимации: on_start, on_progress, on_complete. Рассмотрим последний. on_complete вызывается в момент завершения процесса анимации. Привяжем это событие к методу complete_animation, который мы сейчас создадим:

[...]class TestAnimation(MDApp):    [...]    def complete_animation(self, animation, animated_instance):        """        :type animation: <kivy.animation.Animation object>        :type animated_instance: <WeakProxy to <kivy.factory.CommonLabel object>>        """        # Анимируем масштаб и цвет первой метки.        Animation(scale=1.4, d=1, t="in_out_back").start(animated_instance)        Animation(color=(1, 0, 1, 1), d=1).start(animated_instance)

Привязываем событие:

    def start_animation(self):        [...]        animation = Animation(            angle=0, scale=1, opacity=1, y=lbl_1.height * 2, d=0.9, t="in_out_back"        )        animation.bind(on_complete=self.complete_animation)        animation.start(lbl_1)        [....]

Результат:

На этом пока все. Просили:

Ниже прикрепляю превью Kivy/KivyMD проекта и ссылку на репозиторий, где можно скачать APK и пощупать:

Репозиторий github.com/HeaTTheatR/Articles
APK можно найти в директории репозитория StarTest/bin
Подробнее..

Трепещущий Kivy. Обзор возможностей фреймворка Kivy и библиотеки KivyMD

12.03.2021 12:15:29 | Автор: admin

Kivy и Flutter два фреймворка с открытым исходным кодом для кроссплатформенной разработки.

Flutter:


  • создан компанией Google и выпущенный в 2017 году;

  • в качестве языка программирования использует Dart;

  • не использует нативные компоненты, рисуя весь интерфейс внутри собственного графического движка;

Kivy:


  • создан сообществом Kivy в 2010 году;

  • в качестве языка программирования использует Python и собственный декларативный язык для разметки UI элементов KV Language;

  • не использует нативные компоненты, рисуя весь интерфейс с помощью OpenGL ES 2.0 и SDL2;


Недавно на просторах Ютуба наткнулся на видео демонстрацию Flutter приложения Facebook Desktop Redesign built with Flutter Desktop. Отличное демонстрационное приложение в стиле material design! И поскольку я один из разработчиков библиотеки KivyMD (набор material компонентов для фреймворка Kivy) мне стало интересно, насколько просто будет сделать такой же красивый интерфейс. К счастью автор оставил ссылку на репозиторий проекта.






Как вы думаете, какое приложение на вышеприведенных скриншотах написано с использованием Flutter и какое с помощью Kivy? Ответить сходу трудно, поскольку ярко выраженных отличий нет. Единственное, что сразу бросается в глаза (нижний скриншот) в Kivy все еще нет нормального сглаживания. И это грустно, но не критично. Сравнивать мы будем отдельные элементы приложения и их исходный код на Dart (Flutter) и Python/KV language (Kivy).

Посмотрим теперь как выглядят компоненты изнутри

StoryCard


Kivy


Разметка карточки на языке KV-Language:


Базовый Python класс:

from kivy.properties import StringPropertyfrom kivymd.uix.relativelayout import MDRelativeLayoutclass StoryCard(MDRelativeLayout):    avatar = StringProperty()    story = StringProperty()    name = StringProperty()    def on_parent(self, *args):        if not self.avatar:            self.remove_widget(self.ids.avatar)

Flutter:


import 'package:flutter/material.dart';import 'package:flutter/widgets.dart';class Story extends StatefulWidget {  final String name;  final String avatar;  final String story;  const Story({    Key key,    this.name,    this.avatar,    this.story,  }) : super(key: key);  @override  _StoryState createState() => _StoryState();}class _StoryState extends State<Story> {  @override  Widget build(BuildContext context) {    return Container(      width: 150,      margin: const EdgeInsets.only(top: 30),      decoration: BoxDecoration(        borderRadius: BorderRadius.circular(30),        boxShadow: [          BoxShadow(            color: Colors.black.withOpacity(0.3),            blurRadius: 20,            offset: Offset(0, 10),          ),        ],      ),      child: Stack(        overflow: Overflow.visible,        fit: StackFit.expand,        children: [          ClipRRect(            borderRadius: BorderRadius.circular(30),            child: Image.network(              widget.story,              fit: BoxFit.cover,            ),          ),          if (widget.avatar != null)            Positioned.fill(              top: -30,              child: Align(                alignment: Alignment.topCenter,                child: Container(                  decoration: BoxDecoration(                    borderRadius: BorderRadius.circular(30),                    boxShadow: [                      BoxShadow(                        color: Colors.black.withOpacity(0.4),                        blurRadius: 5,                        offset: Offset(0, 3),                      ),                    ],                  ),                  child: ClipRRect(                    borderRadius: BorderRadius.circular(30),                    child: Image.network(                      widget.avatar,                      fit: BoxFit.cover,                      width: 60,                      height: 60,                    ),                  ),                ),              ),            ),          if (widget.avatar != null)            Positioned.fill(              child: Align(                alignment: Alignment.bottomCenter,                child: Row(                  children: [                    Expanded(                      child: Container(                        padding: const EdgeInsets.all(15),                        decoration: BoxDecoration(                          borderRadius: BorderRadius.circular(30),                          gradient: LinearGradient(                            begin: Alignment.topCenter,                            end: Alignment.bottomCenter,                            colors: [                              Colors.transparent,                              Colors.black,                            ],                          ),                        ),                        child: widget.name != null ? Text(                          widget.name,                          textAlign: TextAlign.center,                          maxLines: 1,                          overflow: TextOverflow.ellipsis,                          style: TextStyle(                            color: Colors.white,                            fontWeight: FontWeight.bold,                          ),                        ) : SizedBox(),                      ),                    ),                  ],                ),              ),            ),        ],      ),    );  }}

Как видим, код на Python и KV-Language получается вдвое короче. Исходный код проекта на Python/Kivy, который рассматривается в этой статье, имеет общий размер 31 килобайт. 3 килобайта из этого объема приходится на Python код, остальное KV-Language. Исходный код на Flutter 54 килобайт. Впрочем, здесь удивляться, кажется, нечему Python один их самый лаконичных языков программирования в мире.

Мы не будем спорить о том, что лучше: описывать UI при помощи DSL языков или прямо в коде. В Kivy, кстати, также можно строить виджеты Python кодом, но это не очень хорошее решение.

TopBar


Flutter:

Kivy:

Реализация этого бара, включая анимацию, на Python/Kivy заняла всего 88 строчек кода. На Dart/Flutter 325 строк и 9 килобайт на диске. Посмотрим, что представляет из себя этот виджет:


Лого, три таба, аватар, три таба и один таб кнопка настроек. Реализация таба с анимированным индикатором:



Анимация индикатора и смена типа курсора мыши реализована в Python файле в одноименном с правилом разметки классе:

from kivy.animation import Animationfrom kivy.properties import StringProperty, BooleanPropertyfrom kivy.core.window import Windowfrom kivymd.uix.boxlayout import MDBoxLayoutfrom kivymd.uix.behaviors import FocusBehaviorclass Tab(FocusBehavior, MDBoxLayout):    icon = StringProperty()    active = BooleanProperty(False)    def on_enter(self):        Window.set_system_cursor("hand")    def on_leave(self):        Window.set_system_cursor("arrow")    def on_active(self, instance, value):        Animation(            opacity=value,            width=self.width if value else 0,            d=0.25,            t="in_sine" if value else "out_sine",        ).start(self.ids.separator)

Мы просто анимируем ширину и opacity индикатора в зависимости от состояния кнопки (active). Состояние кнопки устанавливается в главном классе экрана приложения:

class FacebookDesktop(ThemableBehavior, MDScreen):    def set_active_tab(self, instance_tab):        for widget in self.ids.tab_box.children:            if issubclass(widget.__class__, MDBoxLayout):                if widget == instance_tab:                    widget.active = True                else:                    widget.active = False

Подробнее об анимации а Kivy:

Материальный дизайн. Создание анимаций в Kivy
Разработка мобильных приложений на Python. Создание анимаций в Kivy. Part 2

Реализация на Dart/Flutter.

Поскольку кода очень много, я спрятал все под спойлеры:

app_logo.dart
import 'package:flutter/material.dart';import 'package:flutter/widgets.dart';class AppLogo extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Container(      decoration: BoxDecoration(        borderRadius: BorderRadius.circular(10),        boxShadow: [          BoxShadow(            color: Colors.blue.withOpacity(.6),            blurRadius: 5,            spreadRadius: 1,          ),        ],      ),      child: ClipRRect(        borderRadius: BorderRadius.circular(10),        child: Image.asset(          'assets/images/facebook_logo.jpg',          width: 30,          height: 30,        ),      ),    );  }}


avatar.dart
import 'package:flutter/material.dart';import 'package:flutter/rendering.dart';import 'package:flutter/widgets.dart';class TopBarAvatar extends StatefulWidget {  @override  _TopBarAvatarState createState() => _TopBarAvatarState();}class _TopBarAvatarState extends State<TopBarAvatar>    with SingleTickerProviderStateMixin {  Animation<Color> _animation;  AnimationController _animationController;  @override  void initState() {    _animationController = AnimationController(      vsync: this,      duration: Duration(milliseconds: 150),    );    _animation = ColorTween(      begin: Colors.grey.withOpacity(.4),      end: Colors.blue.withOpacity(.6),    ).animate(_animationController);    _animation.addListener(() {      setState(() {});    });    super.initState();  }  @override  Widget build(BuildContext context) {    return MouseRegion(      onHover: (event) {        setState(() {          _animationController.forward();        });      },      onExit: (event) {        setState(() {          _animationController.reverse();        });      },      cursor: SystemMouseCursors.click,      child: Padding(        padding: const EdgeInsets.symmetric(horizontal: 15),        child: Container(          decoration: BoxDecoration(            borderRadius: BorderRadius.circular(15),            boxShadow: [              BoxShadow(                color: _animation.value,                blurRadius: 10,                spreadRadius: 0,              ),            ],          ),          child: ClipRRect(            borderRadius: BorderRadius.circular(15),            child: Image.asset(              'assets/images/avatar.jpg',              width: 50,              height: 50,            ),          ),        ),      ),    );  }}


button.dart
import 'package:flutter/material.dart';import 'package:flutter/rendering.dart';import 'package:flutter/widgets.dart';class TopBarButton extends StatefulWidget {  final IconData icon;  final bool isActive;  final Function onTap;  const TopBarButton({    Key key,    this.icon,    this.isActive = false,    this.onTap,  }) : super(key: key);  @override  _TopBarButtonState createState() => _TopBarButtonState();}class _TopBarButtonState extends State<TopBarButton>    with SingleTickerProviderStateMixin {  Animation<Color> _animation;  AnimationController _animationController;  @override  void initState() {    _animationController = AnimationController(      vsync: this,      duration: Duration(milliseconds: 150),    );    _animation = ColorTween(      begin: Colors.grey.withOpacity(.6),      end: Colors.blue.withOpacity(.6),    ).animate(_animationController);    _animation.addListener(() {      setState(() {});    });    super.initState();  }  @override  void didUpdateWidget(TopBarButton oldWidget) {    if (widget.isActive) {      _animationController.forward();    } else {      _animationController.reverse();    }    super.didUpdateWidget(oldWidget);  }  @override  Widget build(BuildContext context) {    return GestureDetector(      onTap: widget.onTap,      child: MouseRegion(        cursor: SystemMouseCursors.click,        child: Container(          height: 80,          child: Stack(            alignment: Alignment.center,            children: [              Padding(                padding: const EdgeInsets.symmetric(horizontal: 30),                child: Icon(                  widget.icon,                  color: _animation.value,                ),              ),              Positioned(                bottom: -1,                child: Align(                  alignment: Alignment.bottomCenter,                  child: AnimatedContainer(                    duration: Duration(milliseconds: 50),                    curve: Curves.easeInOut,                    decoration: BoxDecoration(                      color: _animation.value,                      borderRadius: BorderRadius.circular(5),                      boxShadow: [                        BoxShadow(                          color: _animation.value,                          blurRadius: 5,                          offset: Offset(0, 2),                        ),                      ],                    ),                    width: widget.isActive ? 50 : 0,                    height: 4,                  ),                ),              ),            ],          ),        ),      ),    );  }  @override  void dispose() {    _animationController.dispose();    super.dispose();  }}


widget.dart
import 'package:facebook_desktop/screens/home/components/top_bar/app_logo.dart';import 'package:facebook_desktop/screens/home/components/top_bar/avatar.dart';import 'package:facebook_desktop/screens/home/components/top_bar/button.dart';import 'package:flutter/material.dart';import 'package:flutter/widgets.dart';import 'package:flutter_feather_icons/flutter_feather_icons.dart';class TopBar extends StatefulWidget {  @override  _TopBarState createState() => _TopBarState();}class _TopBarState extends State<TopBar> {  int _selectedPage = 0;  @override  Widget build(BuildContext context) {    return Container(      color: Colors.white,      padding: const EdgeInsets.symmetric(        horizontal: 30,      ),      child: Row(        children: [          Expanded(            flex: 1,            child: Align(              alignment: Alignment.centerLeft,              child: AppLogo(),            ),          ),          Expanded(            flex: 6,            child: Row(              mainAxisAlignment: MainAxisAlignment.center,              children: [                TopBarButton(                  icon: FeatherIcons.home,                  isActive: _selectedPage == 0,                  onTap: () {                    setState(() {                      _selectedPage = 0;                    });                  },                ),                TopBarButton(                  icon: FeatherIcons.youtube,                  isActive: _selectedPage == 1,                  onTap: () {                    setState(() {                      _selectedPage = 1;                    });                  },                ),                TopBarButton(                  icon: FeatherIcons.grid,                  isActive: _selectedPage == 2,                  onTap: () {                    setState(() {                      _selectedPage = 2;                    });                  },                ),                TopBarAvatar(),                TopBarButton(                  icon: FeatherIcons.users,                  isActive: _selectedPage == 3,                  onTap: () {                    setState(() {                      _selectedPage = 3;                    });                  },                ),                TopBarButton(                  icon: FeatherIcons.zap,                  isActive: _selectedPage == 4,                  onTap: () {                    setState(() {                      _selectedPage = 4;                    });                  },                ),                TopBarButton(                  icon: FeatherIcons.smile,                  isActive: _selectedPage == 5,                  onTap: () {                    setState(() {                      _selectedPage = 5;                    });                  },                ),              ],            ),          ),          Expanded(            flex: 1,            child: Align(              alignment: Alignment.centerRight,              child: IconButton(                color: Colors.grey.withOpacity(.6),                icon: Icon(FeatherIcons.settings),                onPressed: () {},              ),            ),          ),        ],      ),    );  }}


ChatCard (Kivy, Flutter)

Анимация сдвига карточки происходит относительно родительского виджета (parent) при получении событий фокуса и анфокуса (on_enter, on_leave):

on_enter: Animation(x=root.parent.x + dp(12), d=0.4, t="out_cubic").start(root)on_leave: Animation(x=root.parent.x + dp(24), d=0.4, t="out_cubic").start(root)


И базовый класс Python:

from kivy.core.window import Windowfrom kivy.properties import StringPropertyfrom FacebookDesktop.components.cards.fake_card import FakeCardclass ChatCard(FakeCard):    avatar = StringProperty()    text = StringProperty()    name = StringProperty()    def on_enter(self):        Window.set_system_cursor("hand")    def on_leave(self):        Window.set_system_cursor("arrow")

Реализация Python/Kivy 60 строк кода, реализация Dart/Flutter 182 строки кода.

chat_card.dart
import 'package:ezanimation/ezanimation.dart';import 'package:facebook_desktop/components/user_tile.dart';import 'package:flutter/material.dart';import 'package:flutter/rendering.dart';import 'package:flutter_feather_icons/flutter_feather_icons.dart';class ChatCard extends StatefulWidget {  final String image;  final String name;  final String message;  final EdgeInsets padding;  const ChatCard({    Key key,    this.image,    this.name,    this.message,    this.padding,  }) : super(key: key);  @override  _ChatCardState createState() => _ChatCardState();}class _ChatCardState extends State<ChatCard> {  EzAnimation _animation;  @override  void initState() {    _animation = EzAnimation(      0.0,      -5.0,      Duration(milliseconds: 200),      curve: Curves.easeInOut,      context: context,    );    _animation.addListener(() {      setState(() {});    });    super.initState();  }  @override  Widget build(BuildContext context) {    return Transform.translate(      offset: Offset(_animation.value, 0),      child: MouseRegion(        cursor: SystemMouseCursors.click,        onEnter: (event) {          _animation.start();        },        onExit: (event) {          _animation.reverse();        },        child: Padding(          padding: widget.padding ?? const EdgeInsets.all(15),          child: Container(            width: 250,            padding: const EdgeInsets.all(15),            decoration: BoxDecoration(              color: Colors.white,              borderRadius: BorderRadius.circular(10),              boxShadow: [                BoxShadow(                  color: Colors.black.withOpacity(.1),                  blurRadius: 15,                  offset: Offset(0, 8),                ),              ],            ),            child: Column(              crossAxisAlignment: CrossAxisAlignment.start,              children: [                UserTile(                  name: widget.name,                  image: widget.image,                  trailing: Icon(                    FeatherIcons.messageSquare,                    color: Colors.blue,                    size: 14,                  ),                ),                SizedBox(                  height: 10,                ),                Text(                  widget.message,                  style: TextStyle(color: Colors.grey, fontSize: 12),                  maxLines: 3,                  overflow: TextOverflow.ellipsis,                ),              ],            ),          ),        ),      ),    );  }  @override  void dispose() {    _animation.dispose();    super.dispose();  }}


user_tile.dart
import 'package:facebook_desktop/screens/home/components/section.dart';import 'package:flutter/material.dart';class UserTile extends StatelessWidget {  final String name;  final String image;  final Widget trailing;  const UserTile({    Key key,    this.name,    this.image,    this.trailing,  }) : super(key: key);  @override  Widget build(BuildContext context) {    return Row(      crossAxisAlignment: CrossAxisAlignment.start,      children: [        Container(          margin: const EdgeInsets.only(right: 10),          decoration: BoxDecoration(            color: Colors.white,            borderRadius: BorderRadius.circular(10),            boxShadow: [              BoxShadow(                color: Colors.black.withOpacity(.1),                blurRadius: 5,                offset: Offset(0, 2),              ),            ],          ),          child: ClipRRect(            borderRadius: BorderRadius.circular(5),            child: Image(              image: NetworkImage(                image,              ),              fit: BoxFit.cover,              height: 50,              width: 50,            ),          ),        ),        Column(          crossAxisAlignment: CrossAxisAlignment.start,          children: [            SectionTitle(              title: name,            ),            SizedBox(              height: 5,            ),            Text(              '12 min ago',              style: TextStyle(color: Colors.grey),            ),          ],        ),        if (trailing != null)        Expanded(          child: Align(            alignment: Alignment.topRight,            child: trailing          ),        ),      ],    );  }}


Но не все так просто, как кажется. В процессе я обнаружил, что в библиотеке KivyMD отсутствуют кнопки с типом badge. В проекте на Flutter, кстати, тоже использовались кастомные кнопки. Поэтому для создания правой панели инструментов пришлось сделать такие кнопки самостоятельно.


Базовый Python класс:

from kivy.properties import StringPropertyfrom kivymd.uix.relativelayout import MDRelativeLayoutclass BadgeButton(MDRelativeLayout):    icon = StringProperty()    text = StringProperty()

И уже создать левую панель инструментов:



Даже учитывая, что мне пришлось создавать кастомные кнопки типа badge, код левой панели инструментов на Python/Kivy получился короче 58 строк кода, реализация на Dart/Flutter 97 строк.

button.dart
import 'package:flutter/material.dart';class LeftBarButton extends StatelessWidget {  final IconData icon;  final String badge;  const LeftBarButton({    Key key,    this.icon,    this.badge,  }) : super(key: key);  @override  Widget build(BuildContext context) {    return GestureDetector(      child: Stack(        children: [          Container(            padding: const EdgeInsets.all(10),            child: Icon(              icon,              color: Colors.grey.withOpacity(.6),            ),          ),          if (badge != null)            Positioned(              top: 5,              right: 2,              child: Container(                padding: const EdgeInsets.all(3),                decoration: BoxDecoration(                  borderRadius: BorderRadius.circular(100),                  color: Colors.blue,                ),                child: Text(                  badge,                  style: TextStyle(                    color: Colors.white,                    fontSize: 10,                  ),                ),              ),            )        ],      ),    );  }}


widget.dart
import 'package:facebook_desktop/screens/home/left_bar/button.dart';import 'package:flutter/material.dart';import 'package:flutter/widgets.dart';import 'package:flutter_feather_icons/flutter_feather_icons.dart';class LeftBar extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Container(      margin: const EdgeInsets.all(30),      padding: const EdgeInsets.all(5),      decoration: BoxDecoration(        color: Colors.white,        borderRadius: BorderRadius.circular(50),        boxShadow: [          BoxShadow(            color: Colors.grey.withOpacity(.1),            blurRadius: 2,            offset: Offset(0, 4),          )        ],      ),      child: Column(        mainAxisSize: MainAxisSize.min,        children: [          LeftBarButton(            icon: FeatherIcons.mail,            badge: '10',          ),          SizedBox(            height: 5,          ),          LeftBarButton(            icon: FeatherIcons.search,          ),          SizedBox(            height: 5,          ),          LeftBarButton(            icon: FeatherIcons.bell,            badge: '20',          ),        ],      ),    );  }}


Безусловно я не умаляю достоинств фреймворка Flutter. Инструмент замечательный! Я всего лишь хотел показать Python разработчикам, что они могут делать те же самые вещи, что и во Flutter, но на их любимом языке программирования с помощью фреймворка Kivy и библиотеки KivyMD. Что касается мобильных платформ, то здесь стоит признать, что Flutter превосходит Kivy в скорости работы. Но это уже уже другая статья Ссылка на репозиторий проекта Facebook Desktop Redesign built with Flutter Desktop в реализации Python/Kivy/KivyMD.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru