
Буквально статью тому назад, большинством голосов, было решено начать серию уроков по созданию аналога нативного приложения, написанного для 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. |
Именно на основе данного приложения будут построены наши уроки, а ближе к релизу к концу финальной статьи у нас будет свой пресмыкающийся земноводно-фруктовый тондем питона, жабы и фрукта 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 import App
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
# которым назначен идентификатор.
#
# Так, обратившись через идентификатор '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() # запуск приложенияfrom kivy.app import App
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()from kivy.app import App
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()from kivy.app import App
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()from kivy.app import App
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()from kivy.app import App
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()from kivy.app import App
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 import App
from kivy.lang import Builder
Activity = '''
#: import MDFlatButton kivymd.button.MDFlatButton
ScreenManager:
Screen:
name: 'Screen one' # имя экрана
MDFlatButton:
text: 'I`m Screen one with 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 two with 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. Начнем с первого:

#: 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
markup: 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:

<?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 import App
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]
markup: True
# Область, ограничивающая текст.
text_size: dp(self.size[0] - 10), self.size[1]
# Вертикальное выравнивание текста:
# 'bottom', 'middle', 'center' или 'top'.
valign: 'top'С областью, ограничивающую текст, можете поэкспериментировать на примере ниже:
from kivy.app import App
from kivy.uix.label import Label
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 оригинала:
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 нашего проета:

#: 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
markup: 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 == '' or username.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())]
]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 == '' or username.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:

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