Kivy. Від створення до production один крок. Частина 1


Буквально статті тому, більшістю голосів було вирішено розпочати серію уроків по створенню аналога нативного програми, написаної для Android на Java, але з допомогою фреймворку Kivy + Python. Буде розглянуто: створення і компонування контроллов і віджетів, поглиблене дослідження техніки розмітки користувальницького інтерфейсу в Kv-Language, динамічне управління елементами екранів, бібліотека, що надає доступ до Android Material Design, і багато іншого...
Зацікавилися, прошу під кат!
Отже, після безуспішних пошуків піддослідного кролика відповідного додатку, в міру складного (щоб не розтягувати наш туторіал до масштабів Санти Барбари) і не занадто простого (щоб висвітлити якомога більше технічних аспектів Kivy розробки), за порадою хабровчанина Roman Hvashchevsky, який погодився виступити Java консультантом наших уроків (іноді в статтях я буду приводити лістинги коду оригіналу, написаного на Java), я був переадресований сюди — і вибір був зроблений:

Conversations — додаток для обміну миттєвими повідомленнями для Android, используещее XMPP/Jabber протокол. Альтернатива такими програмами, як WhatsApp, WeChat, Line, Facebook Messenger, Google Hangouts і Threema.
Саме на основі цієї програми будуть побудовані наші уроки, а ближче до релізу до кінця фінальної статті у нас буде свій creepy земноводно-фруктовий тондем пітона, жаби і фрукта Jabber-Python-Kivy — PyConversations і заповітна apk-шечка, зібрана з Python3!
Сподіваюся, чаєм і сигаретами ви запаслися, тому що ми починаємо! Як завжди, вам знадобитися, якщо ще не обзавелися, Майстер створення нового проекту для Kivy додатків. Клонувати його в своїх лабораторіях, відкрийте кореневу директорію майстра в терміналі і виконайте команду:
python3 main.py PyConversations шлях/до/місцем/розташування/створюваного/проекту -repo https://github.com/User/PyConversations -autor Easy -mail gorodage@gmail.com

Звісно, сам фреймворк Kivy, про встановлення якого можна прочитати тут. Ну, а чудову бібліотеку KivyMD для створення нативного інтерфейсу в стилі Android Material Design ви, звичайно ж, вже знайшли по посиланню в репозиторії Майстра створення нового проекту.
Тепер вирушайте на PornHub github і форкните/ клонуйте/скачайте ріпу PyConversations, тому що проект, який ми з вами почали, буде не маленький, і по ходу виходу нових статей, він буде обростати новими функціями, класами і файлами. В іншому випадку, вже у другій статті ви будете курити бамбук дивуватися, чому у вас нічого не працює.
Отже, проект створено:

Для сьогоднішньої статті я взяв перші чотири Activity офіційного додатку Conversations (Activity регистарции нового аккаунта), які ми з вами зараз будемо створювати:

Однак перш, ніж почати, щоб ми з вами розуміли один одного, вам варто ознайомитися з базовими правилами і поняттями...

Створення та керування динамічними класами

Базове уявлення динамічного класу на простому прикладі:
from kivy.app App import 
from kivy.uix.boxlayout import BoxLayout 
from kivy.lang import Builder 
from kivy.properties import StringProperty 

Builder.load_string("' 
#: import MDFlatButton kivymd.button.MDFlatButton 

# Дані інструкції Kivy-Language аналогічні імпорту в python сценаріях: 
# from kivymd.button import MDFlatButton 
# 
# В kv-файлі ви можете включати інші файли розмітки, 
# якщо інтерфейс, наприклад, надто складний: #: include your_kv_file.kv 
#
# Стандартні віджети і контроллы, що надаються Kivy з коробки,
# не потрібно імпортувати у Activity — просто використовуйте їх.

# Всі елементи даного Activity будуть розташовуватися в BoxLayout - 
# віджеті, від якого успадковано базовий клас. 
<StartScreen> 

MDFlatButton: 
id: button 
text: 'Press Me' 
size_hint_x: 1 # відносна ширина контролла - від 0 до 1 
pos_hint: {'y': .5} # положення контролла щодо вертикалі 'y' кореневого віджету 

# Подія контролла. 
on_release: 
# Ключове слово 'root' - це інстанси базового класу розмітки, 
# через який ви можете отримати доступ до всіх його методів та атрибутів. 
root.set_text_on_button() 
"') 
# Builder.load_file('path/to/kv-file'), 
# якщо розмітка Activity знаходиться у файлі. 

class StartScreen(BoxLayout): 
"'Базовий клас."' 

new_text_for_button = StringProperty() 
# В Kivy ви повинні явно вказувати тип атрибутів: 
# 
# StringProperty; 
# NumericProperty; 
# BoundedNumericProperty; 
# ObjectProperty; 
# DictProperty; 
# ListProperty; 
# OptionProperty; 
# AliasProperty; 
# BooleanProperty; 
# ReferenceListProperty; 
# 
# в іншому випадку ви отримаєте помилку 
# при встановленні значень цих атрибутів. 
# 
# Наприклад, якщо не вказувати тип: 
# 
# new_text_for_button =" 
# 
# буде порушено виняток - 
# TypeError: object.__init__() takes no parameters. 

def set_text_on_button(self): 
self.ids.button.text = self.new_text_for_button 
# ids - це словник усіх об'єктів Activity 
# яким призначено id. 
# 
# Так, звернувшись через ідентифікатор 'button' - self.ids.button - 
# до об'єкта кнопки, ми отримуємо доступ 
# до всіх його методів та атрибутів. 

# Будь атрибут, инициализировванный як Properties, 
# автоматично отримує метод в базовому класі з префіксом 'on_', 
# який буде викликаний як тільки даний атрибут отримає нове значення. 
def on_new_text_for_button(self, instance, value): 
print(instance, value) 

class Program(App): 
def build(self): 
"'Метод, що викликається при старті програми. 
Повинен повертати об'єкт створюваного Activity."' 

return StartScreen(new_text_for_button='This new text') 

if __name__ in ('__main__', '__android__'): 
Program().run() # запуск програми

Посилаємося на власні атрибути і методи всередині Activity:
from kivy.app App import 
from kivy.uix.boxlayout import BoxLayout 
from kivy.lang import Builder 
from kivy.properties import StringProperty 

Builder.load_string("' 
#: import MDFlatButton kivymd.button.MDFlatButton 

<StartScreen> 

MDFlatButton: 
id: button 
text: 'Press Me' 
size_hint_x: 1 
pos_hint: {'y': .5} 

on_release: 
# Через ключове слово 'self' ми можемо посилатися 
# на власні атрибути та методи поточного віджета. 
self.text = root.new_text_for_button 
"') 

class StartScreen(BoxLayout): 
new_text_for_button = StringProperty() 

def on_new_text_for_button(self, instance, value): 
print(instance, value) 

class Program(App): 
def build(self): 
return StartScreen(new_text_for_button='This new text') 

if __name__ in ('__main__', '__android__'): 
Program().run()

Використання id контроллов і віджетів всередині Activity:
from kivy.app App import 
from kivy.uix.boxlayout import BoxLayout 
from kivy.lang import Builder 
from kivy.properties import StringProperty 

Builder.load_string("' 
#: import MDFlatButton kivymd.button.MDFlatButton 

<StartScreen> 
orientation: 'vertical' 

MDFlatButton: 
id: button 
text: 'Press Me' 
size_hint: 1, 1 
pos_hint: {'center_y': .5} 

on_release: 
# Отримуємо доступ через id до атрибутів і методів другої кнопки. 
# Зверніть увагу, що всередині розмітки ми можемо виконувати код Python 
# точно так, як і в звичайному Python сценарії. 
button_two.text = 'Id: "button_two: " {}'.format(root.new_text_for_button) 

MDFlatButton: 
id: button_two 
text: 'Id: "button_two: " Old text' 
size_hint: 1, 1 
pos_hint: {'center_y': .5} 
"') 

class StartScreen(BoxLayout): 
new_text_for_button = StringProperty() 

class Program(App): 
def build(self): 
return StartScreen(new_text_for_button='This new text') 

if __name__ in ('__main__', '__android__'): 
Program().run()

Використання методів з префіксом 'on_' всередині Activity:
from kivy.app App import 
from kivy.uix.boxlayout import BoxLayout 
from kivy.lang import Builder 
from kivy.properties import StringProperty 

Builder.load_string("' 
#: import MDFlatButton kivymd.button.MDFlatButton 
#: import snackbar kivymd.snackbar 

<StartScreen> 
orientation: 'vertical' 

MDFlatButton: 
id: button 
text: 'Press Me' 
size_hint: 1, 1 
pos_hint: {'center_y': .5} 

on_release: 
button_two.text = 'Id: "button_two: " {}'.format(root.new_text_for_button) 

MDFlatButton: 
id: button_two 
text: 'Id: "button_two: " Old text' 
size_hint: 1, 1 
pos_hint: {'center_y': .5} 

on_text: 
# Подія на зміни значення атрибуту 'text'. 
snackbar.make (О, Боже! Мій текст тільки що змінили!') 
"') 

class StartScreen(BoxLayout): 
new_text_for_button = StringProperty() 

class Program(App): 
def build(self): 
return StartScreen(new_text_for_button='This new text') 

if __name__ in ('__main__', '__android__'): 
Program().run()

Використання атрибутів і методів з головного класу додатки всередині Activity:
from kivy.app App import
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
from kivy.properties import StringProperty

Builder.load_string("'
#: import MDFlatButton kivymd.button.MDFlatButton

<StartScreen>
MDFlatButton:
# Через лкючевое слово 'app' — екземпляр програми -
# отримуємо доступ до методів і атрибутів,
# инициальзированным в головному класі програми,
# успадкованому від kivy.app.App.
text: app.string_attribute
size_hint_x: 1
pos_hint: {'y': .5}
"')

class StartScreen(BoxLayout):
pass

class Program(App):
string_attribute = StringProperty('String from App')

def build(self):
return StartScreen()

if __name__ in ('__main__', '__android__'):
Program().run()

Використання Activity без кореневого класу:
from kivy.app App import 
from kivy.lang import Builder 

Activity = "' 
<MyScreen@FloatLayout>: 

Label: 
text: 'Text 1' 

BoxLayout: 
MyScreen: 
"' 

class Program(App): 
def build(self): 
return Builder.load_string(Activity) 

if __name__ in ('__main__', '__android__'): 
Program().run()

Використання ids Activity без кореневого класу:
from kivy.app App import 
from kivy.lang import Builder 

Activity = "' 
#: import MDFlatButton kivymd.button.MDFlatButton 

# Зверніть увагу, якщо ми не використовуємо базовий клас,
# ми повинні вказати, базовий віджет. У поточному прикладі - FloatLayout.
<MyScreen@FloatLayout>: 
Label: 
id: label_1 
text: 'Text 1' 

BoxLayout: 
orientation: 'vertical' 

MyScreen: 
id: my_screen 

MDFlatButton: 
text: 'Press me' 
size_hint_x: 1 

on_press: 
my_screen.ids.label_1.text = 'New text' 
"' 

class Program(App): 
def build(self): 
return Builder.load_string(Activity) 

if __name__ in ('__main__', '__android__'): 
Program().run()

Для розуміння того, про що я буду розповідати далі, цього поки що досить, решта буду пояснювати в окопі по дорозі. Що ж, давайте почнемо зі стартового Activity нашого проекту. Відкрийте файл start_screen.kv. У дереві проекту він, як всі інші Activity програми, розміщується в директорії libs/uix/kv/activity:

І Activity виглядає так:
#: kivy 1.9.1
#: import Toolbar kivymd.toolbar.Toolbar
#: import NoTransition kivy.uix.screenmanager.NoTransition

<StartScreen>:
orientation: 'vertical'

Toolbar:
id: action_bar
background_color: app.theme_cls.primary_color # колір встановленої теми
title: app.title
opposite_colors: True # чорна або біла іконка
elevation: 10 # довжина тіні
# Іконки зліва - 
# left_action_items: [['name-icon', function], ...]
# Іконки праворуч - 
# right_action_items: [['name-icon', function], ...]

ScreenManager:
id: root_manager
transition: NoTransition() # ефект зміни Activity

Introduction:
id: introduction
# Викликається при виведенні поточного Activity на екран.
on_enter: self._on_enter(action_bar, app)

CreateAccount:
id: create_account
on_enter: self._on_enter(action_bar, app, root_manager)

AddAccount:
id: add_account
on_enter: self._on_enter(action_bar, app)
# Викликається при закритті поточного Activity.
on_leave: action_bar.title = app.data.string_lang_create_account

AddAccountOwn:
id: add_account_own_provider
on_enter: self._on_enter(action_bar, app, root_manager)
on_leave: action_bar.title = app.title; action_bar.left_action_items = []

А ось більш наочно:

Тепер відкриємо базовий клас Activity StartScreen, який знаходиться по дорозі libs/uix/kv/activity/baseclass:

startscreen.py:
from kivy.uix.boxlayout import BoxLayout

class StartScreen(BoxLayout):
pass

Як бачите, клас порожній, але успадкований від контейнера BoxLayout, який розміщує в собі віджети вертикально, або горизонтально залежно від параметра 'orientation' — 'vertical' або 'horizontal' (типово 'horizontal'). Ось ще більш докладна схема Activity StartScreen:

Базовий клас Activity StartScreen, ми успадкували від BoxLayout, в самій розмітці оголосили його орієнтацію нетрадиційну вертикальну, і помістили в його контейнер ToolBar і менеджер екранів ScreenManager. ScreenManager — це теж свого роду контейнер, в який ми поміщаємо екрани Screen з створеними Activity і надалі встановлюємо їх на екран просто нызывая їх по іменах. Наприклад:
from kivy.app App import 
from kivy.lang import Builder 

Activity = "' 
#: import MDFlatButton kivymd.button.MDFlatButton 

ScreenManager: 

Screen: 
name: 'Screen one' # ім'я екрану

MDFlatButton: 
text: 'i'm Screen with one Button' 
size_hint: 1, 1 
on_release: 
root.current = 'Screen two' # зміна екрану

Screen: 
name: 'Screen two' 

BoxLayout: 
orientation: 'vertical' 

Image: 
source: 'data/logo/kivy-icon-128.png' 

MDFlatButton: 
text: 'i'm Screen with two Button' 
size_hint: 1, 1 
on_release: root.current = 'Screen one' 
"' 

class Program(App): 
def build(self): 
return Builder.load_string(Activity) 

if __name__ in ('__main__', '__android__'): 
Program().run()

Наш ScreenManager містить чотири екрану з Activity: Introduction, CreateAccount, AddAccount і AddAccountOwn. Почнемо з першого:

Introduction.kv
#: kivy 1.9.1 
#: import MDFlatButton kivymd.button.MDFlatButton 

# Стартове Activity програми. 

<Introduction>: 
name: 'Start screen' 

BoxLayout: 
orientation: 'vertical' 
padding: dp(5), dp(20) 

Image: 
source: 'data/images/logo.png' 
size_hint: None, None 
size: dp(150), dp(150) 
pos_hint: {'center_x': .5} 

Label: 
text: app.data.string_lang_introduction 
мова: True 
color: app.data.text_color 
text_size: dp(self.size[0] - 10), self.size[1] 
size_hint_y: None 
valign: 'top' 
height: dp(250) 

Widget: 

BoxLayout: 

MDFlatButton: 
text: app.data.string_lang_create_account 
on_release: app.screen_root_manager.current = 'Create account' 

MDFlatButton: 
text: app.data.string_lang_own_provider 
theme_text_color: 'Primary' 
on_release: 
app.delete_textfield_and_set_check_in_addaccountroot
()
app.screen_root_manager.current = 'Add account own provider'

Ось, що представляє дане Activity на екрані пристрою (я дозволив собі деякі вільності, але, мені здалося, що так буде краще):

Ось оригінал Java:

Оригінальна розмітка Activity в Java
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/color_background_primary">

<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:minHeight="256dp"
android:orientation="vertical"
android:paddingBottom="10dp"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/welcome_header"
android:textColor="?attr/color_text_primary"
android:textSize="?attr/TextSizeHeadline"
android:textStyle="bold"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/welcome_text"
android:textColor="?attr/color_text_primary"
android:textSize="?attr/TextSizeBody"/>
<Button
android:id="@+id/create_account"
style="?android:attr/borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:text="@string/create_account"
android:textColor="@color/accent"/>
<Button
android:id="@+id/use_own_provider"
style="?android:attr/borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:text="@string/use_own_provider"
android:textColor="?attr/color_text_secondary"/>
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/linearLayout"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:padding="8dp"
android:src="@drawable/main_logo"/>
</RelativeLayout>
<TextView
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:textColor="?attr/color_text_secondary"
android:textSize="@dimen/fineprint_size"
android:maxLines="1"
android:text="@string/free_for_six_month"
android:layout_centerHorizontal="true"/>
</RelativeLayout>
</ScrollView>

Нижче наводиться схема Activity Introduction:

Тепер хотілося б пройти за атрибутами віджетів:
BoxLayout:
...

padding: dp(5), dp(20) # відступи вмісту від країв контейнера — ліворуч/праворуч і зверху/знизу

Image:
...

# Як випливає з назви параметра,це підказка - відносний
# розмір віджету від 0 до 1 (.1, .5, .01 тощо). Якщо ми бажаємо
# вказати конкретні розміри, ми повинні поставити в size_hint
# значення None, після чого вказати фіксований розмір.
# Наприклад, вкажемо ширину зображення:
#
# size_hint_x: None
# width: 250
#
# або висоту
#
# size_hint_y: None
# height: 50
#
# або, як у коді Activity, і ширину і висоту відразу.
# За замовчуванням параметр size_hint має значення (1, 1),
# тобто, займає всю доступну йому в контейнері площа.
size_hint: None, None
size: dp(150), dp(150)
# Відносне положення віджета від ценру по осі 'x'
# Також є 'жестское' положення, яке задається в параметрі
# pos, наприклад, pos: 120, 90.
pos_hint: {'center_x': .5}

За відносними положеннями і розмірами віджета можете поекспериментувати на прикладі нижче:
from kivy.app App import
from kivy.lang import Builder

Activity = "'
FloatLayout:

Button:
text: "We Will"
pos: 100, 100
size_hint: .2, .4

Button:
text: "Wee Wiill"
pos: 280, 200
size_hint: .4, .2

Button:
text: "ROCK YOU!!"
pos_hint: {'x': .3, 'y': .6}
size_hint: .5, .2
"'

class Program(App):
def build(self):
return Builder.load_string(Activity)

if __name__ in ('__main__', '__android__'):
Program().run()

Далі по атрибутів:
Label:
...

# Вказує, чи використовувати markdown теги в тексті
# або залишити as is.
# Підтримуваних тегів трохи:
# [b][/b]
# [i][/i]
# [u][/u]
# [s][/s]
# [font=<str>][/font]
# [size=<integer>][/size]
# [color=#<color>][/color]
# [ref=<str>][/ref]
# [anchor=<str>]
# [sub][/sub]
# [sup][/sup]
мова: True
# Область, обмежує текст.
text_size: dp(self.size[0] - 10), self.size[1]
# Вертикальне вирівнювання тексту:
# 'bottom', 'middle', 'center' або 'top'.
valign: 'top'

З областю, що обмежує текст, можете поекспериментувати на прикладі нижче:
from kivy.app App import 
from kivy.uix.label Label import 

class LabelTextSizeTest(App): 
def build(self): 
return Label( 
text='Область тексту, обмежена прямокутником\n' * 50, 
text_size=(250, 300), # поэксперементируйте з цими значеннями 
line_height=1.5 
) 

if __name__ == '__main__': 
LabelTextSizeTest().run()

Далі по Activity:
Widget:

В контексті використовується як аналог в Java:
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>

Далі:
BoxLayout:

MDFlatButton:
text: app.data.string_lang_create_account
# Установка Activity з ім'ям 'Create account'.
on_release: app.screen_root_manager.current = 'Create account'

MDFlatButton:
text: app.data.string_lang_own_provider
# Для встановлення свого цывета тексту на кнопці
# дайте параметру theme_text_color значення 'Custom'
# і далі вказуйте колір - text_color: .7, .2, .2, 1
theme_text_color: 'Primary'
on_release:
# Виклик функції з основного класу програми.
# Можна було реалізувати прямо тут, але, коскольку
# я вважаю, що зайвий код в розмітці відволікає
# від розуміння дерева Activity, було вирішено його винести.
app.delete_textfield_and_set_check_in_addaccountroot()
app.screen_root_manager.current = 'Add account own provider'

Так. У нас залишився не розглянутим ще одне питання. Повернемося до розмітки Activity StartScreen:
Introduction:
id: introduction
# Викликається при виведенні поточного Activity на екран.
on_enter: self._on_enter(action_bar, app)

тобто, як тільки Activity буде виведено на екран, виконується код події on_enter. Давайте подивимося, що робить метод _on_enter в базовому класі Activity (файл libs/uix/kv/activity/baseclass/introduction.py):

from kivy.uix.screenmanager import Screen

class Introduction(Screen):
def _on_enter(self, instance_toolbar, instance_program):
instance_toolbar.left_action_items = []
instance_toolbar.title = instance_program.title

Метод _on_enter видаляє іконку в ToolBar зліва, встановлюючи значення left_action_items, як порожній список, і змінює підпис ToolBar на ім'я програми.
Для прикладу наведу керуючий клас з Java оригіналу:
WelcomeActivity
package eu.siacs.conversations.ui; 

import android.app.ActionBar; 
import android.app.Activity; 
import android.content.Intent; 
import android.content.pm.ActivityInfo; 
import android.os.Bundle; 
import android.view.View; 
import android.widget.Button; 

import eu.siacs.conversations.R; 

public class WelcomeActivity extends Activity { 

@Override 
protected void onCreate(final Bundle savedInstanceState) { 
if (getResources().getBoolean(R. bool.portrait_only)) { 
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 
} 
final ActionBar ab = getActionBar(); 
if (ab != null) { 
ab.setDisplayShowHomeEnabled(false); 
ab.setDisplayHomeAsUpEnabled(false); 
} 
super.onCreate(savedInstanceState); 
setContentView(R. layout.welcome); 
final Button createAccount = (Button) findViewById(R. id.create_account); 
createAccount.setOnClickListener(new View.OnClickListener() { 
@Override 
public void onClick(View v) { 
Intent intent = new Intent(WelcomeActivity.this, MagicCreateActivity.class); 
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); 
startActivity(intent); 
} 
}); 
final Button useOwnProvider = (Button) findViewById(R. id.use_own_provider); 
useOwnProvider.setOnClickListener(new View.OnClickListener() { 
@Override 
public void onClick(View v) { 
startActivity(new Intent(WelcomeActivity.this, EditAccountActivity.class)); 
} 
}); 

} 

}

Так. З цим розібралися. У нас є Activity і дві юзабельные кнопки. Почнемо з першої:

При натисканні на кнопку буде виведено Activity CreateAccount:
MDFlatButton:
text: app.data.string_lang_create_account
on_release: app.screen_root_manager.current = 'Create account'

Activity CreateAccount (Kivy):

Activity CreateAccount (original):

Відкриємо Activity CreateAccount нашого проет:

createaccount.kv
#: kivy 1.9.1 
#: import SingleLineTextField kivymd.textfields.SingleLineTextField 
#: import snackbar kivymd.snackbar 

# Activity реєстрації нового аккаунта. 
# Викликається по події кнопки 'Create account' стартового Activity. 

<CreateAccount>: 
name: 'Create account' 

BoxLayout: 
orientation: 'vertical' 
padding: dp(5), dp(20) 

Image: 
source: 'data/images/logo.png' 
size_hint: None, None 
size: dp(150), dp(150) 
pos_hint: {'center_x': .5} 

Label: 
text: app.data.string_lang_enter_user_name 
мова: True 
color: app.data.text_color 
text_size: dp(self.size[0] - 10), self.size[1] 
size_hint_y: None 
valign: 'top' 
height: dp(215) 

Widget: 
size_hint_y: None 
height: dp(10) 

SingleLineTextField: 
id: username 
hint_text: 'Username' 
message: 'username@conversations.im' 
message_mode: 'persistent' 
on_text: app.check_len_login_in_textfield(self) 

Widget: 

BoxLayout: 

MDFlatButton: 
text: app.data.string_lang_next 
on_release: 
if username.text == " username or.text.isspace(): \ 
snackbar.make(app.data.string_lang_not_valid_username) 
else: app.screen_root_manager.current = 'Add account'

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

Заголовок і іконка в ToolBar встановлюються в базовому класі Activity CreateAccount в методі _on_enter:
from kivy.uix.screenmanager import Screen 

class CreateAccount(Screen): 

def _on_enter(self, instance_toolbar, instance_program, instance_screenmanager): 
instance_toolbar.title = instance_program.data.string_lang_create_account 
instance_toolbar.left_action_items = [ 
['chevron-left', lambda x: instance_program.back_screen( 
instance_screenmanager.previous())] 
]

Оригінальний керуючий клас MagicCreateActivity на Java
package eu.siacs.conversations.ui; 

import android.content.Intent; 
import android.content.pm.ActivityInfo; 
import android.os.Bundle; 
import android.text.Editable; 
import android.text.TextWatcher; 
import android.view.View; 
import android.widget.Button; 
import android.widget.EditText; 
import android.widget.TextView; 
import android.widget.Toast; 

import java.security.SecureRandom; 

import eu.siacs.conversations.Config; 
import eu.siacs.conversations.R; 
import eu.siacs.conversations.entities.Account; 
import eu.siacs.conversations.xmpp.jid.InvalidJidException; 
import eu.siacs.conversations.xmpp.jid.Jid; 

public class MagicCreateActivity extends XmppActivity implements TextWatcher { 

private TextView mFullJidDisplay; 
private EditText mUsername; 
private SecureRandom mRandom; 

private static final String CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456780+-/#$!?"; 
private static final int PW_LENGTH = 10; 

@Override 
protected void refreshUiReal() { 

} 

@Override 
void onBackendConnected() { 

} 

@Override 
protected void onCreate(final Bundle savedInstanceState) { 
if (getResources().getBoolean(R. bool.portrait_only)) { 
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 
} 
super.onCreate(savedInstanceState); 
setContentView(R. layout.magic_create); 
mFullJidDisplay = (TextView) findViewById(R. id.full_jid); 
mUsername = (EditText) findViewById(R. id.username); 
mRandom = new SecureRandom(); 
Button next = (Button) findViewById(R. id.create_account); 
next.setOnClickListener(new View.OnClickListener() { 
@Override 
public void onClick(View v) { 
String username = mUsername.getText().toString(); 
if (username.contains("@") || username.length() < 3) { 
mUsername.setError(getString(R. string.invalid_username)); 
mUsername.requestFocus(); 
} else { 
mUsername.setError(null); 
try { 
Jid jid = Jid.fromParts(username.toLowerCase(), Config.MAGIC_CREATE_DOMAIN, null); 
Account account = xmppConnectionService.findAccountByJid(jid); 
if (account == null) { 
account = new Account(jid, createPassword()); 
account.setOption(Account.OPTION_REGISTER, true); 
account.setOption(Account.OPTION_DISABLED, true); 
account.setOption(Account.OPTION_MAGIC_CREATE, true); 
xmppConnectionService.createAccount(account); 
} 
Intent intent = new Intent(MagicCreateActivity.this, EditAccountActivity.class); 
intent.putExtra("jid", account.getJid().toBareJid().toString()); 
intent.putExtra("init", true); 
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 
Toast.makeText(MagicCreateActivity.this, R. string.secure_password_generated, Toast.LENGTH_SHORT).show(); 
startActivity(intent); 
} catch (InvalidJidException e) { 
mUsername.setError(getString(R. string.invalid_username)); 
mUsername.requestFocus(); 
} 
} 
} 
}); 
mUsername.addTextChangedListener(this); 
} 

private String createPassword() { 
StringBuilder builder = new StringBuilder(PW_LENGTH); 
for(int i = 0; i < PW_LENGTH; ++i) { 
builder.append(CHARS.charAt(mRandom.nextInt(CHARS.length() - 1))); 
} 
return builder.toString(); 
} 

@Override 
public void beforeTextChanged(CharSequence s, int start, int count, int after) { 

} 

@Override 
public void onTextChanged(CharSequence s, int start, int before, int count) { 

} 

@Override 
public void afterTextChanged(Editable s) { 
if (s.toString().trim().length() > 0) { 
try { 
mFullJidDisplay.setVisibility(View.VISIBLE); 
Jid jid = Jid.fromParts(s.toString().toLowerCase(), Config.MAGIC_CREATE_DOMAIN, null); 
mFullJidDisplay.setText(getString(R. string.your_full_jid_will_be, jid.toString())); 
} catch (InvalidJidException e) { 
mFullJidDisplay.setVisibility(View.INVISIBLE); 
} 

} else { 
mFullJidDisplay.setVisibility(View.INVISIBLE); 
} 
} 
}

… викликаному події on_enter (коли Activity було виведено на екран):
<StartScreen>: 
...

ScreenManager: 
...

CreateAccount: 
on_enter: self._on_enter(action_bar, app, root_manager) 

...

Також нас цікавить подія on_text, коли змінюється значення текстового поля:
<CreateAccount>:
...

SingleLineTextField: 
...

on_text: app.check_len_login_in_textfield(self)

Метод check_len_login_in_textfield з головного класу додатка:

def check_len_login_in_textfield(self, instance_textfield):
# Якщо введене значення в полі більше 20 символів.
if len(instance_textfield.text) > 20:
instance_textfield.text = instance_textfield.text[:20]
# Змінюємо значення підписи під текстовим полем згідно
# введеним користувачем у текстове поле даними.
instance_textfield.message = 'username@conversations.im' \
if instance_textfield.text == " \
else '{}@conversations.im'.format(instance_textfield.text)


Отже, якщо дані текстового поля коректні, виводимо Activity AddAccount:
MDFlatButton:
...

on_release:
if ...
...
else: app.screen_root_manager.current = 'Add account'

В іншому випадку виводимо повідомлення про некоректних даних:
MDFlatButton:
...

on_release:
if username.text == " username or.text.isspace(): \
snackbar.make(app.data.string_lang_not_valid_username)
...


Ну, і, нарешті, у нас залишилося останнє Activity...
Original:

Kivy:

Так, це одне Activity. З другого, при його виведенні на екран, ми просто програмно видаляємо «зайве» текстове поле.
<StartScreen>:
...

ScreenManager:
...

AddAccount:
id: add_account
on_enter: self._on_enter(action_bar, app)
on_leave: action_bar.title = app.data.string_lang_create_account
AddAccountOwn:
id: add_account_own_provider
on_enter: self._on_enter(action_bar, app, root_manager)
on_leave: action_bar.title = app.title; action_bar.left_action_items = []


У файлах розмітки ми створили шаблони Activity:
<AddAccount>:
name: 'Add account'

AddAccountRoot:
id: add_account_root

<AddAccountOwn>:
name: 'Add account own provider'

AddAccountRoot:
id: add_account_root

«успадкувавши їх Activity AddAccountRoot:

Activity AddAccountRoot
#: kivy 1.9.1
#: import progress libs.uix.dialogs.dialog_progress
#: import MDFlatButton kivymd.button.MDFlatButton
#: import SingleLineTextField kivymd.textfields.SingleLineTextField
#: import MDCheckbox kivymd.selectioncontrols.MDCheckbox

# Activity реєстрації нового аккаунта на сервері.

<AddAccountRoot@BoxLayout>:
canvas:
Color:
rgba: app.data.background
Rectangle:
size: self.size
pos: self.pos

orientation: 'vertical'
padding: dp(10), dp(10)

BoxLayout:
id: box
canvas:
Color:
rgba: app.data.rectangle
Rectangle:
size: self.size
pos: self.pos
Color:
rgba: app.data.list_color
Rectangle:
size: self.size[0] - 2, self.size[1] - 2
pos: self.pos[0] + 1, self.pos[1] + 1

orientation: 'vertical'
size_hint_y: None
padding: dp(10), dp(10)
spacing: dp(15)
height: app.window.height // 2

SingleLineTextField:
id: username
hint_text: 'Username'
on_text:
if self.message != ": app.check_len_login_in_textfield(self)

SingleLineTextField:
id: password
hint_text: 'Password'
password: True

BoxLayout:
id: box_check
size_hint_y: None
height: dp(40)

MDCheckbox:
id: check
size_hint: None, None
size: dp(40), dp(40)
active: True
on_state:
if self.active: box.add_widget(confirm_password)
else: box.remove_widget(confirm_password)
if username.message != ": confirm_password.hint_text = 'Confirm password'

Label:
text: 'Register new account on server'
valign: 'middle'
color: app.data.text_color
size_hint_x: .9
text_size: self.size[0] - 10, self.size[1]

SingleLineTextField:
id: confirm_password
password: True

Widget:

Widget:

BoxLayout:
padding: dp(0), dp(10)

MDFlatButton:
text: app.data.string_lang_cancel
theme_text_color: 'Primary'
on_release:
if app.screen.ids.root_manager.current == 'Add account own provider': \
app.screen.ids.root_manager.current = 'Start screen'; \
app.screen.ids.action_bar.title = app.title
else: \
app.screen.ids.root_manager.current = 'Create account';
app.screen.ids.action_bar.title = app.data.string_lang_create_account

MDFlatButton:
text: app.data.string_lang_next
on_release:
instance_progress, instance_text_wait = \
progress(text_wait=app.data.string_lang_text_wait.format(app.data.text_color_hex), \
events_callback=lambda x: instance_progress.dismiss())


Будь віджет в Kivy має властивість canvas. Тому ви можете малювати на ньому все, що завгодно: від примітивних фігур до накладання текстур. В даному Activity я намалював прямокутник спочатку сірим кольором, потім зверху наклав прямокутник білого кольору, але меншим розміром (малювати просто лінії, обчислюючи їх координати було лінь). Вийшла рамка.
При активації чекбокса нижнє текстове поле видаляється:
MDCheckbox:
...

on_state:
# True/False — активний/неактивний
if self.active: box.add_widget(confirm_password)
else: box.remove_widget(confirm_password)

...

Коли Activity AddAccount виводиться на екран, встановлюємо значення текстових полів і їх фокус:
from kivy.uix.screenmanager import Screen
from kivy.clock import Clock

class AddAccount(Screen):

def _on_enter(self, instance_toolbar, instance_program):
instance_toolbar.title = self.name
self.ids.add_account_root.ids.username.focus = True
# Виконується одноразово через заданий інтервал часу.
Clock.schedule_once(instance_program.set_text_on_textfields, .5)

Головний клас програми:
def set_focus_on_textfield(self, interval=0, instance_textfield=None, focus=True):
if instance_textfield: instance_textfield.focus = focus

def set_text_on_textfields(self, interval):
add_account_root = self.screen.ids.add_account.ids.add_account_root
field_username = add_account_root.ids.username
field_password = add_account_root.ids.password
field_confirm_password = add_account_root.ids.confirm_password
field_username.text = self.screen.ids.create_account.ids.username.text.lower()
field_password.focus = True
password = self.generate_password()
field_password.text = password
field_confirm_password.text = password

Clock.schedule_once(
lambda x: self.set_focus_on_textfield(
instance_textfield=field_password, focus=False), .5
)
Clock.schedule_once(
lambda x: self.set_focus_on_textfield(
instance_textfield=field_username), .5
)

Що ж! Чотири запланованих Activity готові, пальці втомилися, голова розболілася. Це я про себе. Тому на сьогодні поки що все. Оскільки неможливо в рамках однієї статті висвітлити всі питання, описати всі параметри віджетів Kivy і нюанси, вони будуть розглянуті в наступних статтях, тому не соромтеся, задавайте питання.
Швидше за все, у другій частині статті буде розглянута архітектура самого проекту PyConversations та ваші питання відносно першої частини, якщо такі будуть. До зустрічі!
PyConversations на github.

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

0 коментарів

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