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

Ссылаемся на собственные атрибуты и методы внутри 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 

<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 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()

Использование методов с префиксом 'on_' внутри 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 
#: 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 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()

Использование Activity без корневого класса:
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()

Использование ids в Activity без корневого класса:
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. Начнем с первого:



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



Оригинальная разметка 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 оригинала:


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 
            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())] 
        ]

Оригинальный управляющий класс 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 == '' 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:



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.