
Продолжаем серию статей о разработке мобильных приложений с фреймворком Kivy. Сегодня речь пойдет о замечательной библиотеке KivyMD — библиотеке для построения нативного интерфейса в стиле Android Material Design, написанной с использованием и для фреймворка Kivy. Откровенно говоря, лично я бесконечно рад, что отпала необходимость лепить и созерцать кривые, темные и страшные кастомные виджеты в Kivy приложениях. Используя в своих проектах библиотеку KivyMD плюс немного фантазии, вряд ли кто-то сможет визуально отличить, написана ли ваша программа на Java или с использованием фрейворка Kivy и Python.
Скачайте, распакуйте KivyMD, зайдите в корневой каталог распакованного архива и выполните установку:
python setup.py install
Далее, установите зависимости для KivyMD:
pip install kivy-garden garden install recycleview
После установки библиотеки вы можете запустить тестовый пример из скачанного вами и распакованного архива:
python kitchen_sink.py
После запуска вы увидите приложение, демонстрирующее нативные виджеты и контроллы, которые доступны вам для использования в ваших проектах:
В статье мы не будем останавливаться на каких-то конкретных виджетах библиотеки (их создание и параметры прекрасно описаны в том же самом kitchen_sink.py), а создадим простое демонстрационное приложение «Контакты» с использованием KivyMD. Наше приложение будет уметь создавать контакты и группы, а также добавлять в них созданные контакты. Ну, и попутно более детально осветим некоторые аспекты создания интерфейса приложений в Kivy:
Для простого создания дефолтного проекта на Kivy рекомендую CreatorKivyProject, детальное описание работы с которым описанно в этой статье. Итак, следуя инструкциям в статье по ссылке, проект DemoKivyContacts создан. Откроем файл по пути DemoKivyContacts/libs/uix/kv/startscreen.kv, безжалостно удалим все его содержимое и «нарисуем» стартовый экран своего приложения!

Вот так выглядит разметка данного интерфейса в Kivy-Language:
#:kivy 1.9.1 #:import CreateContact libs.uix.createcontact.CreateContact #:import CallContact libs.uix.callcontact.CallContact #:import EmptyScreen libs.uix.emptyscreen.EmptyScreen #:import Toolbar kivymd.toolbar.Toolbar #:import MDTabbedPanel kivymd.tabs.MDTabbedPanel #:import MDTab kivymd.tabs.MDTab ############################################################################### # # СТАРТОВЫЙ ЭКРАН # ############################################################################### <StartScreen>: id: root.manager Screen: name: 'root_screen' BoxLayout: #canvas: # Rectangle: # pos: self.pos # size: self.size # source: 'data/images/background.jpg' orientation: 'vertical' #################################################################### # # ACTION BAR # #################################################################### Toolbar: #canvas.before: # Rectangle: # pos: self.pos # size: self.size # source: 'data/images/background_toolbar.jpg' id: action_bar #background_color: app.data.alpha background_color: app.theme_cls.primary_color title: app.data.string_lang_contacts left_action_items: [['menu', lambda x: app.nav_drawer.toggle()]] right_action_items: [['more-vert', lambda x: None]] #################################################################### # # TABBED PANEL # #################################################################### MDTabbedPanel: id: tabs tab_display_mode: 'text' #tab_color: app.data.alpha tab_text_color: app.data.tab_text_color tab_indicator_color: app.data.tab_indicator_color MDTab: name: 'contacts' text: app.data.string_lang_contacts on_tab_press: app.on_tab_press(self.name) ScreenManager: id: screen_manager_tab_contacts Screen: name: 'empty_contacts_list' EmptyScreen: image: 'data/images/contacts.png' text: app.data.string_lang_add_contacts callback: app.show_form_create_contact disabled: False Screen: name: 'create_contact' CreateContact: MDTab: name: 'groups' text: app.data.string_lang_groups on_tab_press: app.on_tab_press(self.name) ScreenManager: id: screen_manager_tab_groups Screen: name: 'empty_groups_list' EmptyScreen: image: 'data/images/contacts.png' text: app.data.string_lang_not_groups callback: lambda: app.create_group() disabled: False Screen: name: 'call_contact' CallContact:
Как видите, наш экран использует:
Toolbar: id: action_bar background_color: app.theme_cls.primary_color title: app.data.string_lang_contacts left_action_items: [['menu', lambda x: app.nav_drawer.toggle()]] right_action_items: [['more-vert', lambda x: None]]

MDTabbedPanel: id: tabs tab_display_mode: 'text' tab_text_color: app.data.tab_text_color tab_indicator_color: app.data.tab_indicator_color MDTab: name: 'contacts' text: app.data.string_lang_contacts on_tab_press: app.on_tab_press(self.name) ScreenManager: id: screen_manager_tab_contacts Screen: name: 'empty_contacts_list' EmptyScreen: image: 'data/images/contacts.png' text: app.data.string_lang_add_contacts callback: app.show_form_create_contact disabled: False Screen: name: 'create_contact' CreateContact: MDTab: name: 'groups' text: app.data.string_lang_groups on_tab_press: app.on_tab_press(self.name) ScreenManager: id: screen_manager_tab_groups Screen: name: 'empty_groups_list' EmptyScreen: image: 'data/images/contacts.png' text: app.data.string_lang_not_groups callback: lambda: app.create_group

Эти виджеты библиотеки KivyMD мы импортировали в самом начале файла разметки startscreen.kv:
#:import Toolbar kivymd.toolbar.Toolbar #:import MDTabbedPanel kivymd.tabs.MDTabbedPanel #:import MDTab kivymd.tabs.MDTab
Данные инструкции в Kivy-Language аналогичны импорту в python сценариях:
from kivymd.toolbar import Toolbar from kivymd.tabs import MDTabbedPanel from kivymd.tabs import MDTab
К слову, в kv-файле вы можете включать другие файлы разметки, если интерфейс, например, слишком сложный:
#:include your_kv_file.kv
У нас имеются две вкладки на MDTabbedPanel — «Контакты» и «Группы». Первая («Контакты») будет содержать виджет ScreenManager (менеджер экранов), в котором мы разместим два, говоря языком Java, Activity:
MDTab: name: 'contacts' text: app.data.string_lang_contacts on_tab_press: app.on_tab_press(self.name) ScreenManager: id: screen_manager_tab_contacts Screen: name: 'empty_contacts_list' EmptyScreen: image: 'data/images/contacts.png' text: app.data.string_lang_add_contacts callback: app.show_form_create_contact disabled: False Screen: name: 'create_contact' CreateContact:
Как вы могли заметить, ScreenManager должен включать один или несколько виджетов Screen (экранов), которые будут содержать наш контент (Activity). В нашем случае это EmptyScreen (пустой экран) и CreateContact (форма создания нового контакта):

Переключаться между данными Activity мы будем по их именам:
Screen: name: 'empty_contacts_list' … Screen: name: 'create_contact' …
… используя объект ScreenManager...
ScreenManager: id: screen_manager_tab_contacts
… в программном коде по его идентификатору из созданной нами разметки:

… и переключая Activity посредством передачи аттрибуту current имени нового экрана:
self.manager_tab_contacts.current = 'create_contact'
Теперь «нарисуем» наши Activity — EmptyScreen (пустой экран) и CreateContact (форму создания нового контакта). Создадим файлы разметки интерфейса в директории проекта DemoKivyContacts/libs/uix/kv emptyscreen.kv и createcontact.kv и одноименные python сценарии в директории DemoKivyContacts/libs/uix для управления и передачи параметров созданным виджетам EmptyScreen и CreateContact:
#:kivy 1.9.1 #:import MDLabel kivymd.label.MDLabel #:import MDFloatingActionButton kivymd.button.MDFloatingActionButton <EmptyScreen>: id: empty_screen Image: source: root.image pos_hint: {'center_x': .5, 'center_y': .6} opacity: .5 MDLabel: id: label font_style: 'Headline' theme_text_color: 'Primary' color: app.data.text_color text: root.text halign: 'center' MDFloatingActionButton: id: float_act_btn icon: 'plus' size_hint: None, None size: dp(56), dp(56) opposite_colors: True elevation_normal: 8 pos_hint: {'center_x': .9, 'center_y': .1} background_color: app.data.floating_button_color background_color_down: app.data.floating_button_down_color disabled: root.disabled on_release: root.callback()
#:kivy 1.9.1 #:import SingleLineTextField kivymd.textfields.SingleLineTextField #:import MDIconButton kivymd.button.MDIconButton #:import MDFlatButton kivymd.button.MDFlatButton #:import MDFloatingActionButton kivymd.button.MDFloatingActionButton <CreateContact>: orientation: 'vertical' FloatLayout: size_hint: 1, .3 Image: id: avatar pos_hint: {'center_y': .5} source: 'data/images/avatar_empty.png' MDFloatingActionButton: icon: 'plus' size_hint: None, None size: dp(56), dp(56) opposite_colors: True elevation_normal: 8 pos_hint: {'center_x': .9, 'center_y': .20} background_color: app.data.floating_button_color background_color_down: app.data.floating_button_down_color on_release: app.choice_avatar_contact() BoxLayout: orientation: 'vertical' padding: 5, 5 size_hint: 1, .3 BoxLayout: MDIconButton: icon: 'account' disabled: True SingleLineTextField: id: name_field hint_text: 'ИФО' BoxLayout: MDIconButton: icon: 'phone' disabled: True SingleLineTextField: id: number_field hint_text: 'Номер' BoxLayout: MDIconButton: icon: 'email' disabled: True SingleLineTextField: id: email_field hint_text: 'E-mail' Widget: size_hint: 1, .3 AnchorLayout: anchor_x: 'right' anchor_y: 'bottom' size_hint: 1, None height: dp(40) MDFlatButton: id: button_ok text: 'OK' on_release: app.save_info_contact()
emptyscreen.py
from kivy.uix.floatlayout import FloatLayout from kivy.properties import StringProperty, ObjectProperty, BooleanProperty class EmptyScreen(FloatLayout): image = StringProperty() text = StringProperty() callback = ObjectProperty() disabled = BooleanProperty()
createcontact.py
from kivy.uix.boxlayout import BoxLayout class CreateContact(BoxLayout): pass
В EmptyScreen мы использовали еще один виджет из библиотеки KivyMD — MDFloatingActionButton, который стоит описать. Та самая назойливая муха, которую многим пользователям хочется прихлопнуть:

MDFloatingActionButton: id: float_act_btn icon: 'plus' size_hint: None, None size: dp(56), dp(56) opposite_colors: True # иконка белого/черного цветов elevation_normal: 8 # длинна тени pos_hint: {'center_x': .9, 'center_y': .1} # самое нужное место на экране, которое кнопка обязательно закроет background_color: app.data.floating_button_color background_color_down: app.data.floating_button_down_color disabled: root.disabled on_release: root.callback()
В CreateContact используются виджеты библиотеки KivyMD:
MDIconButton:

MDIconButton — это кнопка с векторной иконкой. Полный набор официальных иконок от Google смотрите по ссылке. Все они используются и доступны в KivyMD.
SingleLineTextField:

MDFlatButton:

Будет вызывать функцию сохранения введенных пользователем данных:
MDFlatButton: … on_release: app.save_info_contact()
Получать введенную пользователем информацию из полей SingleLineTextField мы будем по уже описанному способу выше — по их id из аттрибута text:
DemoKivyContacts/libs/uix/kv/createcontact.kv

DemoKivyContacts/libs/programclass/showformcreatecontact.py
def show_form_create_contact(self, *args): '''Выводит на экран форму для создания нового контакта.''' self.manager_tab_contacts.current = 'create_contact' # <class 'libs.uix.createcontact.CreateContact'> self._form_create_contact = \ self.manager_tab_contacts.current_screen.children[0] ... def save_info_contact(self): '''Сохраняет информацию о новом контакте.''' name_contact = self._form_create_contact.ids.name_field.text number_contact = self._form_create_contact.ids.number_field.text mail_contact = self._form_create_contact.ids.email_field.text ...
После сохранения данных программа создает список контактов, если он не создан, или добавляет новый к уже существующему списку и выводит его на экран:

def show_contacts(self, info_contacts): ''' :type info_contacts: dict; :param info_contacts: { 'Name contact': ['Number contact\nMail contact', 'path/to/avatar'] }; ''' if not self._contacts_items: # Создаем список контактов. self._contacts_list = ContactsList() self._contacts_items = Lists( dict_items=info_contacts, flag='three_list_custom_icon', right_icons=self.data.right_icons, events_callback=self._event_contact_item ) button_add_contact = Builder.template( 'ButtonAdd', disabled=False, events_callback=self.show_form_create_contact ) self._contacts_list.add_widget(self._contacts_items) self._contacts_list.add_widget(button_add_contact) self.add_screens( 'contact_list', self.manager_tab_contacts, self._contacts_list ) else: # Добавляет контакт к существующему списку # и выводит список на экран. self._add_contact_item(info_contacts) self.manager_tab_contacts.current = 'contact_list'
Обратите внимание на функцию add_screens — программное добавление нового Activity и установка его в качестве текущего экрана:
DemoKivyContacts/program.py
def add_screens(self, name_screen, screen_manager, new_screen): screen = Screen(name=name_screen) # cоздаем новый экран screen.add_widget(new_screen) # добавляем Activity в созданный экран screen_manager.add_widget(screen) # добавляем экран в менеджер экранов screen_manager.current = name_screen # указываем менеджеру имя Activity, которое должно стать текущим экраном приложения
Я написал небольшую (пока топорную) обвязку для создания списков MDList — DemoKivyContacts/libs/uix/lists.py
Создать пункт списка с иконкой слева и векторными иконками справа можно достаточно легко, создав экземпляр класса Lists c нужными параметрами.
def show_contacts(self, info_contacts): ''' :type info_contacts: dict; :param info_contacts: { 'Name contact': ['Number contact\nMail contact', 'path/to/avatar'] }; ''' … self._contacts_items = Lists( dict_items=info_contacts, flag='three_list_custom_icon', right_icons=self.data.right_icons, events_callback=self._event_contact_item )

Далее список self._contacts_items кидаете на любой требуемый виджет.
При создании пункта списка мы передали параметру events_callback функцию _event_contact_item для обработки событий путнкта:
def _event_contact_item(self, *args): '''События пункта списка контактов.''' def end_call(): self.screen.current = 'root_screen' instanse_button = args[0] if type(instanse_button) == RightButton: name_contact, name_event = instanse_button.id.split(', ') if name_event == 'call': self.screen.current = 'call_contact' data_contact = self.info_contacts[name_contact] call_screen = self.screen.current_screen.children[0] call_screen.name_contact = name_contact call_screen.number_contact = data_contact[0].split('\n')[0] call_screen.avatar = data_contact[1] call_screen.callback = end_call elif name_event == 'groups': self._show_names_groups(name_contact) else: name_contact, name_event = args
Идентификаторы событий 'call' и 'group' — это имена иконок, которые мы указали в параметре right_icons:
DemoKivyContacts/libs/programdata.py
… right_icons = ['data/images/call.png', 'data/images/groups.png']
При нажатии на иконку звонка откроется экран имитации исходящего вызова:
def _event_contact_item(self, *args): def end_call(): self.screen.current = 'root_screen' … if name_event == 'call': self.screen.current = 'call_contact' call_screen = self.screen.current_screen.children[0] … call_screen.callback = end_call

Все виджеты в нем уже описаны, поэтому просто приведу макет разметки данного Activity:
#:kivy 1.9.1 #:import MDIconButton kivymd.button.MDIconButton #:import MDFloatingActionButton kivymd.button.MDFloatingActionButton #:import MDLabel kivymd.label.MDLabel <CallContact>: id: call_contact Widget: id: title_line canvas: Color: rgba: app.theme_cls.primary_color Rectangle: size: self.size pos: self.pos size_hint_y: None height: root.height * 30 // 100 # 30% от высоты экрана pos: 0, call_contact.height - self.size[1] Widget: canvas: Ellipse: pos: self.pos size: 150, 150 source: root.avatar if root.avatar else 'data/logo/kivy-icon-128.png' pos: (call_contact.width // 2) - 75, call_contact.height * 61 // 100 BoxLayout: orientation: 'vertical' size_hint: 1, None height: 50 pos: self.pos[0], call_contact.height * 45 // 100 MDLabel: id: name_contact font_style: 'Headline' theme_text_color: 'Primary' color: app.data.text_color text: root.name_contact if root.name_contact else 'Abonent' halign: 'center' MDLabel: id: number_contact font_style: 'Subhead' theme_text_color: 'Primary' color: app.data.text_color text: root.number_contact if root.number_contact else '12345' halign: 'center' BoxLayout: size_hint: None, None height: 60 width: volume.width + dialpad.width + account.width + mic.width pos: (call_contact.width // 2) - (self.width // 2), call_contact.height * 18 // 100 MDIconButton: id: volume icon: 'volume-mute' MDIconButton: id: dialpad icon: 'dialpad' MDIconButton: id: account icon: 'account' MDIconButton: id: mic icon: 'mic' MDFloatingActionButton: id: phone_end icon: 'phone-end' size_hint: None, None size: dp(56), dp(56) opposite_colors: True # иконка белого/черного цветов elevation_normal: 8 # длинна тени pos_hint: {'center_x': .5, 'center_y': .1} background_color: app.data.floating_button_color_end_call background_color_down: app.data.floating_button_down_color_end_call on_release: root.callback()
Виджет CallContact унаследован от FloatLayout:
from kivy.uix.floatlayout import FloatLayout from kivy.properties import StringProperty, ObjectProperty class CallContact(FloatLayout): callback = ObjectProperty(lambda: None) avatar = StringProperty(None) name_contact = StringProperty(None) number_contact = StringProperty(None)
Это значит, что все виджеты и контроллы в нем будут накладываться друг на друга, отчего в разметке я использовал процентное указание их позиций относительно высоты главного экрана:
pos: self.pos[0], call_contact.height * 45 // 100
Теперь, когда вы знаете, как работает ScreenManager, давайте еще раз взглянем на управляющий класс стартового Activity:
from kivy.uix.screenmanager import ScreenManager from kivy.properties import ObjectProperty class StartScreen(ScreenManager): events_callback = ObjectProperty(lambda: None) '''Функция обработки сигналов экрана.'''
и скелет разметки:
<StartScreen>: Screen: name: 'root_screen' … # Экран с вкладками — MDTabbedPanel Screen: name: 'call_contact' CallContact:
То есть, при нажатии кнопки вызова в пункте списка контактов мы открываем Activity имитации исходящего вызова и закрываем его при нажатии кнопки «Отбой»:
def _event_contact_item(self, *args): def end_call(): self.screen.current = 'root_screen' … if name_event == 'call': self.screen.current = 'call_contact' call_screen = self.screen.current_screen.children[0] … call_screen.callback = end_call
Процесс создания группы мы не будем рассматривать, так как он аналогичен процессу создания нового контакта. А остановимся на виджете NavigationDrawer:

Для использования панели NavigationDrawer мы должны создать ее разметку и управляющий класс, унаследованный от NavigationDrawer:
#:kivy 1.9.1 <NavDrawer>: NavigationDrawerIconButton: icon: 'settings' text: app.data.string_lang_settings on_release: app.events_program(self.text) NavigationDrawerIconButton: icon: 'view-module' text: app.data.string_lang_plugin on_release: app.events_program(self.text) NavigationDrawerIconButton: icon: 'info' text: app.data.string_lang_license on_release: app.events_program(self.text) NavigationDrawerIconButton: icon: 'collection-text' text: 'About' on_release: app.events_program(self.text) NavigationDrawerIconButton: icon: 'close-circle' text: app.data.string_lang_exit_key on_release: app.events_program(app.data.string_lang_exit_key)
DemoKivyContacts/program.py
from kivy.app import App from kivy.properties import ObjectProperty from kivymd.navigationdrawer import NavigationDrawer class NavDrawer(NavigationDrawer): events_callback = ObjectProperty() class Program(App): nav_drawer = ObjectProperty() def __init__(self, **kvargs): super(Program, self).__init__(**kvargs) def build(self): self.nav_drawer = NavDrawer(title=data.string_lang_menu)
На этом пока все. С полным сценарием проекта вы можете ознакомиться на github.
P.S
Без сомнения библиотека KivyMD является отличным дополнением к фреймворку Kivy! Надеюсь, вы ее освоите и будете применять в своих проектах.
У меня есть предложение поменять формат статей о разработке мобильных приложений с помощью Kivy: возьмем уже готовое нативное приложение для Android, написанное на Java и создадим аналогичное, но написанное на Python с использованием фреймворка Kivy, по ходу действия освещая весь процесс разработки с нуля: как создаются виджеты и контроллы в Kivy, как использовать динамические классы, что есть FloatLayout и т.д.
