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