
Буквально статью тому назад, большинством голосов, было решено начать серию уроков по созданию аналога нативного приложения, написанного для 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.