Возможно, для вас будет новостью, но разрабатывать мобильные приложения с функционалом, который доступен Java разработчикам, под Android с помощью фреймворка Kivy не просто просто, а очень просто! Именно этого правила я придерживаюсь, создавая свои проекты с Python + Kivy — разработка должна быть максимально простой и быстрой. Как щелчок пальцами.
На новичков подаваемая информация не расчитана, я не буду на пальцах объяснять, что, откуда и куда. Думаю, те, кто читает данную статью, обладают достаточными для понимания материала, знаниями. К тому же, Kivy, как я уже только что написал, очень простой фреймворк и вся документация с примерами использования находится в исходниках, достаточно в PyCharm кликнуть левой кнопкой мыши, зажав Ctrl, по интересующему виджету — и вы получите полную справку с примерами и описанием атрибутов!
В прошлой статье были рассмотрены несколько экранов приложения Clean Master в реализации на Kivy. Сегодня я покажу вам один из черновиков домашней страницы тестового приложения, над которым мы работаем для одного стартапа. Вот так это будет выглядеть после запуска примера:
Ну, что ж, а теперь приступим! Нам понадобиться: кофе-сигареты, террариум с третьим Python-ом, то ли птица, то ли фрукт — Kivy и немного мозгов. Наличие последних приветствуется! Заходим на github и качаем Мастер создания нового проекта для фреймворка Kivy + Python3 (да, я полностью отказался от использования Python2). Распаковываем, переходим в папку с мастером и запускаем:
python3 main.py name_project path_to_project -repo repo_project_on_github
если у проекта имеется репозиторий на github.
Или
python3 main.py name_project path_to_project
если репозитория не github не имеется.
В этом случае после создания откройте файл проекта main.py и отредактируйте, функцию отправки баг репорта вручную. А лучше заводите для своих будущих проектов репозитории на github.
Итак, в результате мы получаем дефолтный Kivy проект со следующей структурой каталогов:
Отдельно следует рассмотреть каталог Libs:
Остальные каталоги проекта в комментариях не нуждаются. Созданный проект будет иметь два экрана — главный и экран настроек:
Все что нам нужно, это использовать свой главный экран, то есть заменить файл startscreen.py в директории Libs/uix, создать новый файл разметки экрана startscreen.kv в папке Libs/uix/kv, ну, и добавить новые импорты и сопутствующие классы, если таковые имеются.
Давайте начнем с кастомных кнопкок, которые используются в нашем главном экране:
Создадим в директории Libs/uix файл custombutton.py и определим в нем класс нашей кнопки:
import os
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.properties import StringProperty
root = os.path.split(__file__)[0]
Builder.load_file('{}/kv/custombutton.kv'.format(
root if root != '' else os.getcwd())
)
class CustomButton(Button):
icon_button = StringProperty('')
Разметка кнопки в директории Libs/uix/kv — файл custombutton.kv:
#:kivy 1.9.1
<CustomButton>:
background_normal: 'Data/Images/none.png'
background_down: 'Data/Images/shadows/shadow_btn.png'
size_hint: None, 1
width: 150
# Рамка
Image:
source: 'Data/Images/rectangle.png'
pos: root.x + 2, root.y
height: root.height
width: root.width - 5
# Иконка
Image:
source: root.icon_button
pos: root.x + 16, root.y + 16
size: 115, 115
Также мы будем использовать класс ImageButton — кнопку с изображением — для баннеров. Поскольку класс относится к UI, я поместил файл imagebutton.py в каталог Libs/uix:
from kivy.uix.image import Image
from kivy.uix.behaviors import ButtonBehavior
class ImageButton(ButtonBehavior, Image):
pass
Далее нам потребуется класс для смены рекламных баннеров на главном экране программы. Создадим его в директории классов приложения Libs/programclass — файл show_banners.py :
import os
from random import choice
from kivy.uix.boxlayout import BoxLayout
from Libs.uix.imagebutton import ImageButton
class ShowBanners(object):
'''Меняет и выводит на главном экране рекламные баннеры.'''
def __init__(self):
self.banner_list = os.listdir(
'{}/Data/Images/banners'.format(self.directory)
)
def show_banners(self, interval):
if self.screen.screen_manager.current == '':
name_banner = choice(self.banner_list)
box_banner = BoxLayout(padding=5)
new_banner = ImageButton(
id=name_banner.split('.')[0],
source='Data/Images/banners/{}'.format(name_banner),
on_release=self.on_banner
)
box_banner.add_widget(new_banner)
name_screen = name_banner
banner = self.Screen(name=name_screen)
banner.add_widget(box_banner)
self.screen.banner_manager.add_widget(banner)
effect = choice(self.effects_transition)
self.screen.banner_manager.transition = effect()
self.screen.banner_manager.current = name_screen
self.screen.banner_manager.screens.pop()
def on_banner(self, instance_banner):
if isinstance(instance_banner, str):
print(instance_banner)
else:
print(instance_banner.id)
Данный класс просто меняет экраны с баннерами, устанавливая их в менеджере экранов banner_manager:
Не забываем добавить импорт созданного класса в библиотеку классов programclass в файле инициализации:
Вот так, примерно, это будет выглядеть:
Для демонстрации я выставил очень маленькую задержку смены рекламных баннеров. Кроме того, в примере всего два баннера. Набор шейдеров для анимаций смены афиш мы импортируем в базовом файле program.py:
Так, ну, и перед тем, как, собствено, создать и заменить стандартные startscreen.py и startscreen.kv, создадим еще один класс, реализующий поиск магазин из поисковой строки:
class SearchShop(object):
'''Поиск магазина по ключу из строки поиска главного экрана.'''
def search_shop(self):
key_name_shop = self.screen.text_input_search.text.lower()
if key_name_shop != '':
if key_name_shop in self.shops:
print('Find shop {}'.format(key_name_shop.upper()))
else:
self.KDialog(title=self.name_program).show(
text=self.core.string_lang_shop_not_found.format(
key_name_shop
)
)
self.screen.text_input_search.text = ''
Опять же, добавляем импорт созданного класса в библиотеку классов programclass в файл инициализации.
Реализация главного экрана приложения:
import os
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.actionbar import ActionItem
from kivy.uix.gridlayout import GridLayout
from kivy.uix.screenmanager import Screen
from kivy.uix.scrollview import ScrollView
from kivy.core.window import Window
from kivy.animation import Animation
from kivy.lang import Builder
from kivy.properties import (
ObjectProperty, ListProperty, StringProperty, BooleanProperty,
NumericProperty
)
from Libs.uix.custombutton import CustomButton
from Libs.uix.garden.stiffscroll import StiffScrollEffect
__version__ = '0.0.1'
root = os.path.split(__file__)[0]
root = root if root != '' else os.getcwd()
class MyOwnActionButton(Button, ActionItem):
markup = BooleanProperty(True)
minimum_width = NumericProperty(150)
class StartScreen(BoxLayout):
events_callback = ObjectProperty(None)
'''Функция обработки сигналов экрана.'''
core = ObjectProperty(None)
'''module 'Libs.programdata' '''
color_action_bar = ListProperty(
[0.4, 0.11764705882352941, 0.2901960784313726, 0.5607843137254902]
)
'''Цвет ActionBar.'''
color_body_program = ListProperty(
[0.15294117647058825, 0.0392156862745098, 0.11764705882352941, 1]
)
'''Цвет фона экранов программы.'''
color_text_action_item = StringProperty('')
'''Цвет текста для кнопок ActionItem в ActionBar.'''
title_previous = StringProperty('')
'''Заголоок ActionBar.'''
text_search_shop = StringProperty('')
'''Текст подсказки в поле ввода поиска магазинов.'''
text_choice_shop = StringProperty('')
'''Текст списка магазинов.'''
banner = StringProperty('')
'''Текст над рекламным баннером.'''
Builder.load_file('{}/kv/startscreen.kv'.format(root))
def __init__(self, **kvargs):
super(StartScreen, self).__init__(**kvargs)
self.screen_manager = self.ids.screen_manager
self.banner_manager = self.ids.banner_manager
self.icons_list_manager = self.ids.icons_list_manager
self.action_view = self.ids.action_view
self.action_previous = self.ids.action_previous
self.text_input_search = self.ids.text_input_search
self.label_list_icons = self.ids.label_list_icons
for name_item_spinner in [
self.core.string_lang_settings, self.core.string_lang_plugin,
self.core.string_lang_license, self.core.string_lang_exit_key]:
item_button = MyOwnActionButton(
text='[color={}]{}'.format(
self.color_text_action_item, name_item_spinner),
id=name_item_spinner, on_press=self.events_callback,
background_normal='Data/Images/bgd_action_item_normal.png',
background_down='Data/Images/bgd_action_item_down.png',
on_release=lambda *args:
self.ids.action_overflow._dropdown.select(
self.on_release_select_item_spinner()
)
)
self.ids.action_overflow.add_widget(item_button)
screen = Screen(name='shops')
scroll_icons = ScrollView(
effect_cls=StiffScrollEffect, size_hint_y=None,
pos=(Window.size[0], 0), height=150
)
box_shops = GridLayout(rows=1, size_hint_x=None)
box_shops.bind(minimum_width=box_shops.setter("width"))
# Логотипы магазинов.
for logo_shops in os.listdir('{}/Data/Images/shops'.format(
self.core.prog_path)):
box_shops.add_widget(
CustomButton(
id=logo_shops.split('.')[0],
on_release=self.events_callback,
icon_button='Data/Images/shops/{}'.format(logo_shops)
)
)
scroll_icons.add_widget(box_shops)
screen.add_widget(scroll_icons)
self.icons_list_manager.add_widget(screen)
self.icons_list_manager.current = 'shops'
self.animation_scroll_icons(scroll_icons)
def animation_scroll_icons(self, instance_scroll_icons):
'''Анимация скролла иконок с логотипами магазинов.'''
animate = Animation(x=0)
animate += Animation(x=0)
animate.start(instance_scroll_icons)
def on_release_select_item_spinner(self):
'''Вешается на release событие кнопок спиннера ActionBar.
В противноном случае, список не будет автоматически скрываться.
'''
pass
#: kivy 1.9.1
<StartScreen>
orientation: 'vertical'
canvas:
Color:
rgb: root.color_body_program
Rectangle:
pos: self.pos
size: self.size
ActionBar:
id: action_bar
canvas:
Color:
rgb: root.color_action_bar
Rectangle:
pos: self.pos
size: self.size
ActionView:
id: action_view
ActionPrevious:
id: action_previous
app_icon: 'Data/Images/none.png'
title: root.title_previous
previous_image: 'Data/Images/logo.png'
with_previous: True
on_release: root.events_callback('previous')
ActionOverflow:
id: action_overflow
overflow_image: 'Data/Images/overflow.png'
ScreenManager:
id: screen_manager
size_hint: 1, 8
Screen:
BoxLayout:
orientation: 'vertical'
BoxLayout:
spacing: 5
padding: 5
size_hint: 1, .15
TextInput:
id: text_input_search
hint_text: root.text_search_shop
background_normal: 'Data/Images/bgd_text_input.png'
background_active: 'Data/Images/bgd_text_input.png'
size_hint: .85, 1
multiline: False
ImageButton:
source: 'Data/Images/search_button.png'
size_hint: .15, 1
on_release: root.events_callback('search_shop')
Label:
id: label_list_icons
text: root.text_choice_shop
bold: True
size_hint: 1, .15
font_size: dp(25)
ScreenManager:
id: icons_list_manager
size_hint: 1, .6
Label:
text: root.banner
bold: True
size_hint: 1, .15
font_size: dp(25)
ScreenManager:
id: banner_manager
Screen:
BoxLayout:
padding: 5
ImageButton:
source: 'Data/Images/banners/obi_banner.png'
on_release: root.events_callback('obi_banner')
Базовый класс Program:
import os
import sys
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.scrollview import ScrollView
from kivy.uix.screenmanager import (
Screen, SlideTransition, SwapTransition, FadeTransition, WipeTransition,
FallOutTransition, RiseInTransition
)
from kivy.core.window import Window
from kivy.config import ConfigParser
from kivy.clock import Clock
from kivy.properties import ObjectProperty, NumericProperty
from kivy.uix.settings import SettingsWithSidebar
from Libs.uix.kdialog import KDialog, BDialog
from Libs.uix.startscreen import StartScreen
from Libs.uix.customsettings import CustomSettings
from Libs.uix.custombutton import CustomButton
from Libs.uix.garden.stiffscroll import StiffScrollEffect
from Libs.uix.garden.moretransitions import (
PixelTransition, RippleTransition, BlurTransition, RotateTransition
)
# Классы программы.
from Libs import programclass as prog_class
from Libs import programdata as core
from Libs.manifest import Manifest
__version__ = '0.0.1'
class Program(App, prog_class.ShowPlugin, prog_class.ShowBanners,
prog_class.SearchShop):
'''Функционал программы.'''
start_screen = ObjectProperty(None)
''':attr:`start_screen` is a :class:`~Libs.uix.startscreen.StartScreen`'''
screen = ObjectProperty(None)
''':attr:`screen` is a :class:`~Libs.uix.startscreen.StartScreen`'''
window_text_size = NumericProperty(15)
def __init__(self, **kvargs):
super(Program, self).__init__(**kvargs)
Window.bind(on_keyboard=self.events_program)
# Для области видимомти в programclass.
self.Screen = Screen
self.Clock = Clock
self.effects_transition = (
SlideTransition, SwapTransition, FadeTransition, WipeTransition,
FallOutTransition, RiseInTransition, RippleTransition,
PixelTransition, BlurTransition, RotateTransition
)
# Список магазинов.
self.shops = [shop.split('.')[0].lower() for shop in os.listdir(
'{}/Data/Images/shops'.format(core.prog_path))]
# Список лкоаций дома.
self.locations = [
location.split('.')[0].lower() for location in os.listdir(
'{}/Data/Images/locations'.format(core.prog_path))
]
# ----------------------------------
self.KDialog = KDialog
self.BDialog = BDialog
# ----------------------------------
self.Manifest = Manifest
self.core = core
self.name_program = core.string_lang_title
# ----------------------------------
self.shop = False # выбранный магазин
self.open_dialog = False # открыто диалоговое окно
def build_config(self, config):
config.adddefaultsection('General')
config.setdefault('General', 'language', 'Русский')
config.setdefault('General', 'hint', '1')
config.adddefaultsection('Theme')
config.setdefault('Theme', 'theme', 'default')
config.setdefault('Theme', 'edit_theme', '0')
config.setdefault('Theme', 'create_theme', '0')
def build_settings(self, settings):
CustomSettings(
background_sections=core.color_action_bar,
background_color=core.color_body_program,
background_color_title=core.color_action_bar,
button_close_background_down='Data/Images/shadows/shadow_btn.png',
color_text_title=core.separator_color, settings_obj=settings
)
general_settings = open('{}/Data/Settings/general.json'.format(
core.prog_path)).read()
general_data = general_settings.format(
language=core.string_lang_setting_language,
title=core.string_lang_setting_language_title,
desc=core.string_lang_setting_language_desc,
russian=core.string_lang_setting_language_russian,
english=core.string_lang_setting_language_english)
settings.add_json_panel(
core.string_lang_general, self.config, data=general_data
)
def build(self):
self.settings_cls = SettingsWithSidebar
self.title = self.name_program # заголовок окна программы
self.icon = 'Data/Images/logo.png' # иконка окна программы
self.use_kivy_settings = False
self.config = ConfigParser()
self.config.read('{}/program.ini'.format(core.prog_path))
self.set_var_from_file_settings()
# Главный экран программы.
self.start_screen = StartScreen(
color_action_bar=core.color_action_bar,
color_body_program=core.color_body_program,
color_text_action_item=core.theme_text_color_action_item,
text_search_shop=core.string_lang_search_shop,
text_choice_shop=core.string_lang_choice_shop,
events_callback=self.events_program,
core=core, banner=core.string_lang_banner
)
self.screen = self.start_screen
if self.hint:
Clock.schedule_once(
lambda *args: self.show_hints(
core.string_lang_check_shops,
core.string_lang_check_shops_label), 1
)
Clock.schedule_interval(self.show_banners, 1)
return self.start_screen
def set_var_from_file_settings(self):
'''Установка значений переменных из файла настроек program.ini.'''
self.language = core.select_locale[
self.config.get('General', 'language')
]
self.hint = self.config.getint('General', 'hint')
def show_hints(self, hint_text, check_text):
'''Выводит окно подсказок.'''
def answer_callback(answer):
hint = int(answer[1])
if hint:
self.config.set('General', 'hint', '0')
self.config.write()
self.hint = False
KDialog(title=self.name_program, answer_callback=answer_callback,
separator_color=self.core.separator_color).show(
text=hint_text, check_text=check_text, auto_dismiss=True,
text_button_ok=core.string_lang_next, check=True, param='query'
)
def events_program(self, *args):
'''Обработка событий программы.'''
if len(args) == 2: # нажата ссылка
event = args[1].encode('utf-8')
else: # нажата кнопка программы
try:
_args = args[0]
event = _args if isinstance(_args, str) else _args.id
except AttributeError: # нажата кнопка девайса
event = args[1]
if event == core.string_lang_settings:
self.open_settings()
elif event == core.string_lang_exit_key:
self.exit_program()
elif event == core.string_lang_plugin:
self.show_plugins()
elif event in self.locations:
print(event)
elif event == 'search_shop':
self.search_shop()
elif event == 'obi_banner':
self.press_banner(event)
elif event == 'previous' or event in (1001, 27):
self.back_screen(event)
elif event in self.shops:
self.set_list_icons_locations(event)
return True
def back_screen(self, event):
'''Менеджер экранов.'''
# Возврат из списка локаций.
if self.screen.screen_manager.current == '' and self.shop:
self.screen.icons_list_manager.current = 'shops'
self.screen.action_previous.title = self.name_program
self.screen.label_list_icons.text = core.string_lang_choice_shop
self.screen.action_previous.previous_image = 'Data/Images/logo.png'
self.screen.action_previous.app_icon = 'Data/Images/none.png'
self.shop = None
return
# Нажата BackKey на главном экране.
if self.screen.screen_manager.current == '':
if event in (1001, 27):
self.exit_program()
return
if len(self.screen.screen_manager.screens) != 1:
self.screen.screen_manager.screens.pop()
self.screen.screen_manager.current = self.screen.screen_manager.screen_names[-1]
# Устанавливаем имя предыдущего экрана.
self.screen.action_previous.title = self.screen.screen_manager.current
def set_list_icons_locations(self, name_select_icon):
'''Меняет логотипы магазинов на главном экране на иконки локаций
помещения при выборе одного из магазина.'''
self.shop = name_select_icon
screen = Screen(name='locations')
scroll_icons = ScrollView(
effect_cls=StiffScrollEffect, size_hint_y=None, pos=(0, 0),
height=150
)
box_locations = GridLayout(rows=1, size_hint_x=None)
box_locations.bind(minimum_width=box_locations.setter("width"))
# Логотипы локаций.
for logo_location in os.listdir('{}/Data/Images/locations'.format(
core.prog_path)):
box_locations.add_widget(
CustomButton(
id=logo_location.split('.')[0],
on_release=self.events_program,
icon_button='Data/Images/locations/{}'.format(
logo_location
)
)
)
self.screen.label_list_icons.text = core.string_lang_choice_location
self.screen.action_previous.previous_image = 'atlas://data/images/defaulttheme/previous_normal'
self.screen.action_previous.app_icon = 'Data/Images/shops/{}.png'.format(self.shop)
if self.hint:
self.show_hints(
core.string_lang_check_locations,
core.string_lang_check_shops_label
)
scroll_icons.add_widget(box_locations)
screen.add_widget(scroll_icons)
self.screen.icons_list_manager.add_widget(screen)
self.screen.icons_list_manager.transition = SwapTransition()
self.screen.icons_list_manager.current = 'locations'
self.screen.action_previous.title = core.string_name_shop.format(
self.shop.capitalize()
)
self.screen.icons_list_manager.screens.pop()
def show_about(self):
def events_callback(instance_label, text_link):
def answer_callback(answer):
pass
pass
ADialog(events_callback=events_callback,
name_program=self.name_program)
def exit_program(self, *args):
def dismiss(*args):
self.open_dialog = False
def answer_callback(answer):
if answer == core.string_lang_yes:
sys.exit(0)
dismiss()
if not self.open_dialog:
KDialog(answer_callback=answer_callback, on_dismiss=dismiss,
separator_color=core.separator_color,
title=self.name_program).show(
text=core.string_lang_exit,
text_button_ok=core.string_lang_yes,
text_button_no=core.string_lang_no, param='query',
auto_dismiss=True
)
self.open_dialog = True
def on_config_change(self, config, section, key, value):
'''Вызывается при выборе одного из пункта настроек программы.'''
def select_callback(*args):
pass
if key == 'language':
# self.self.language = value
print(value)
def on_pause(self):
'''Ставит приложение на 'паузу' при выхоже из него.
В противном случае запускает программу по заново'''
return True
def on_resume(self):
print('on_resume')
def on_stop(self):
print('on_stop')
Оставляем за бортом (не рассмотренными) файл локализации — Data/Language/russian.txt, расфасовку графических ресурсов и запускаем тестовый пример:
python3 main.py
Немного не то, что было заявлено в начале статьи. Открываем файл Data/Themes/default/default.ini и правим тему:
[color]
color_action_bar = [0.4, 0.11764705882352941, 0.2901960784313726, 0.5607843137254902]
color_body_program = [0.15294117647058825, 0.0392156862745098, 0.11764705882352941, 1]
separator_color = [1.0, 1.0, 1.0, 1.0]
text_color = #ffffffff
text_color_action_item = #00000000
key_text_color = #661e4a8f
link_color = #2fbfe0ff
Сохраняем и запускаем повторно:
Видео обзор примера:
P.S.
Возможно некорректное отображение некоторых окон, созданных с помощью библиотеки kdialog — находится в разработке.
P.S.S
Спасибо за внимание!
Комментарии (9)
Zifix
16.07.2016 06:40+4Вполне неплохо для начала нулевых, а у нас тут в 2016 уже два года как Material Design завезли!
HeaTTheatR
16.07.2016 08:28-3То есть, давайте сделаем 1001-ое приложение на Android, которые отличается от других только цветовой палитрой и размером кнопок?
Zifix
16.07.2016 08:31+2Вы не поверите, но существует огромадное число вариантов, как в рамках заданного стиля сделать уникальное приложение. Но если навыков дизайна нет, то стандартные контролы, которые правильно себя ведут — это тоже хорошо, гораздо лучше, чем что-либо чужеродное для ОС.
HeaTTheatR
16.07.2016 09:05-5Возможно для вас это будет будет дикостью, но еще со времен старой доброй Symbian я всегда уходил от всего "родного" для ОС. И, как пользователь Android, меня всегда раздражают однотипные приложения. Такое ощущение, что у авторов напрочь отсутствует фантазия. А оказывается, что они просто придерживаются единого стиля, "родного" для ОС. Это мое мнение.
Zifix
16.07.2016 09:13+1Пользователи простили бы вам неродной стиль, будь он красивым, но увы, ваши рамки устарели лет на 15, а градиенты на 10 :/
HeaTTheatR
16.07.2016 09:20-1Так это статья и не пособие по дизайну. Ключевое слово здесь — Kivy. К тому же это — "один из черновиков домашней страницы тестового приложения". Ключевое слово здесь — "черновиков".
bigfatbrowncat
16.07.2016 10:31Я вам не как разработчик под Андроид с четырехлетним стажем сейчас скажу, а как простой пользователь.
Если бы я увидел скриншот вашего приложения на Play Market, то пожалел бы о потраченных на загрузку страницы двух секундах.
Я стал бы пользоваться приложением с таким оформлением только если бы это было совершенно необходимо для меня (например, магазин с уникальным товаром, который больше нигде не найдешь). И всё равно удалил бы при первой возможности. Потому что это ужас. Я даже в школе на Delphi под Windows 95 старался оформлять лучше. Простите, если резок.
braska
Аааа, мои глаза. Я вообще не питонист и под мобилки не пишу. Но искренне не понимаю чем руководствуются люди, разрабатывая под Android, и не следуя официальным гайдам.