Kivy. От создания до production один шаг. Часть 1

от автора

Буквально статью тому назад, большинством голосов, было решено начать серию уроков по созданию аналога нативного приложения, написанного для Android на Java, но с помощью фреймворка Kivy + Python. Будет рассмотрено: создание и компоновка контроллов и виджетов, углубленное исследование техники разметки пользовательского интерфейса в Kv-Language, динамическое управление элементами экранов, библиотека, предоставляющая доступ к Android Material Design, и многое другое…
Заинтересовавшихся, прошу под кат!

Итак, после безуспешных поисков подопытного кролика подходящего приложения, в меру сложного (чтобы не растягивать наш туториал до масштабов Санты Барбары) и не слишком простого (дабы осветить как можно больше технических аспектов Kivy разработки), по совету хабровчанина Roman Hvashchevsky, который согласился выступить Java консультантом наших уроков (иногда в статьях я буду приводить листинги кода оригинала, написанного на Java), я был переадресован вот сюда — и выбор был сделан:

Conversations — приложение для обмена мгновенными сообщениями для Android, используещее XMPP/Jabber протокол. Альтернатива таким программам, как WhatsApp, WeChat, Line, Facebook Messenger, Google Hangouts и Threema.

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

ссылка на оригинал статьи https://habrahabr.ru/post/314236/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *