Kivy — ще простіше, ще нативнее


Продовжуємо серію статей про розробку мобільних додатків з фреймворком 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:
startscreen.kv
#: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
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 з вкладками MDTab
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:
emptyscreen.kv
#: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()

createcontact.kv
#: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

...

Після збереження даних програма створює список контактів, якщо він не створений, або додає до вже існуючого списку і виводить його на екран:

show_contacts
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) # створюємо новий екран
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:
show_contacts
#: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: 'міс' 

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:
nawdrawer.kv
#: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 App import
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 і т. д.


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.