Продолжаем серию статей о разработке мобильных приложений с фреймворком 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 contactnMail 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 contactnMail 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 и т.д.
Автор: HeaTTheatR