
После неудачной попытки первой версии статьи, когда материал заминусовали из-за чудовищного дизайна приведенного в статье примера программы (статью пришлось удалить), я учел все минусы и привожу более поздний вариант тестового приложения.
Возможно, для вас будет новостью, но разрабатывать мобильные приложения с функционалом, который доступен Java разработчикам, под Android с помощью фреймворка Kivy не просто просто, а очень просто! Именно этого правила я придерживаюсь, создавая свои проекты с Python + Kivy — разработка должна быть максимально простой и быстрой. Как щелчок пальцами.
На новичков подаваемая информация не расчитана, я не буду на пальцах объяснять, что, откуда и куда. Думаю, те, кто читает данную статью, обладают достаточными для понимания материала, знаниями. К тому же, Kivy, как я уже только что написал, очень простой фреймворк и вся документация с примерами использования находится в исходниках!
В прошлой статье были рассмотрены несколько экранов приложения Clean Master в реализации на Kivy. Сегодня я покажу вам один из черновиков приложения, над которым работаю в свободное время.

Говорят, что Kivy годится только лишь для наколенных поделок и серьезное приложение сделать с его помощью не получится. Спешу вас обрадовать (или огорчить) — так говорят те, кто не умеет данный фрукт (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 и отредактируйте, функцию отправки баг репорта вручную.

Итак, в результате мы получаем дефолтный Kivy проект со следующей структурой каталогов:

Отдельно следует рассмотреть каталог Libs:

Остальные каталоги проекта в комментариях не нуждаются. Созданный проект будет иметь два экрана — главный и экран настроек:

Все что нам нужно, это использовать свой главный экран, то есть заменить файл startscreen.py в директории Libs/uix, создать новый файл разметки экрана startscreen.kv в папке Libs/uix/kv, отредактировать базовый класс program.py, ну, и добавить новые импорты и сопутствующие классы, если таковые имеются.
Давайте начнем с кастомных кнопкок, которые используются в нашем главном экране:

Да, в самом Kivy нет многих стандартных элементов для построения дизайна интерфейса, которые есть в Java, но зато Kivy позволяет сконструировать и анимировать абсолютно любой виджет, который вам понадобиться самостоятельно. Все зависит от границ вашей фантазии.
Создадим в директории Libs/uix файл custombutton.py и определим в нем класс нашей кнопки:
import os from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button from kivy.lang import Builder from kivy.properties import StringProperty, ObjectProperty, ListProperty from . imagebutton import ImageButton root = os.path.split(__file__)[0] Builder.load_file('{}/kv/custombutton.kv'.format( root if root != '' else os.getcwd()) ) class CustomButton(BoxLayout, Button): icon = StringProperty('') icon_map = StringProperty('') icon_people = StringProperty('') text = StringProperty('') button_color = ListProperty([0, 0, 0, .2]) text_color = ListProperty([0, 0, 0, .1]) events_callback = ObjectProperty(None)
Разметка кнопки в директории Libs/uix/kv — файл custombutton.kv:
#:kivy 1.9.1 <CustomButton>: id: root.text padding: 5 size_hint_y: None height: 60 on_release: root.events_callback(root.text) canvas: # Цвет кнопки Color: rgba: root.button_color Rectangle: pos: self.x + 2.5, self.y - 3 size: self.size # Тень кнопки Color: rgba: [1, 1, 1, 1] Rectangle: pos: self.pos size: self.size Image: source: root.icon size_hint: .2, 1 Label: markup: True text: root.text text_size: self.width - 70, None color: root.text_color BoxLayout: orientation: 'vertical' size_hint: .1, 1 spacing: 6 ImageButton: source: root.icon_people on_release: root.events_callback({'people': root.text}) ImageButton: source: root.icon_map on_release: root.events_callback({'map': root.text})
Кастомная кнопка для экрана локаций:

import os from kivy.uix.boxlayout import BoxLayout from kivy.lang import Builder from kivy.properties import ListProperty, StringProperty, ObjectProperty root = os.path.split(__file__)[0] Builder.load_file('{}/kv/custommenu.kv'.format( root if root != '' else os.getcwd()) ) class CustomMenuItem(BoxLayout): background_item = ListProperty([.1, .1, .1, 1]) text_color = ListProperty([.1, .1, .1, 1]) icon_item = StringProperty('') text_item = StringProperty('') id_item = StringProperty('') events_callback = ObjectProperty(None)
#:kivy 1.9.1 #:import ImageButton Libs.uix.imagebutton.ImageButton <CustomMenuItem>: orientation: 'vertical' canvas: Color: rgba: root.background_item Rectangle: pos: self.pos size: self.size ImageButton: source: root.icon_item on_release: root.events_callback(root.id_item.split('.')[0]) Label: text: root.text_item color: root.text_color font_size: '19sp'
Также мы будем использовать класс ImageButton — кнопку с изображением — для баннеров. Поскольку класс относится к UI, я поместил файл imagebutton.py в каталог Libs/uix:
from kivy.uix.image import Image from kivy.uix.behaviors import ButtonBehavior class ImageButton(ButtonBehavior, Image): pass
Далее нам потребуется класс для смены рекламных баннеров на главном экране программы. Для данного тестого примера приложения плакаты с рекламными баннерами помещены в локальную папку проекта. Создадим файл show_banners.py в директории классов приложения Libs/programclass:
import os 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) ) # Направление смены слайдов баннеров. self.directions = ('up', 'down', 'left', 'right') def show_banners(self, interval): if self.screen.ids.screen_manager.current == '': name_banner = self.choice(self.banner_list) box_banner = BoxLayout() new_banner = ImageButton( id=name_banner.split('.')[0], source='Data/Images/banners/{}'.format(name_banner), on_release=self.press_banner ) box_banner.add_widget(new_banner) name_screen = name_banner banner = self.Screen(name=name_screen) banner.add_widget(box_banner) self.screen.ids.banner_manager.add_widget(banner) effect = self.choice(self.effects_transition) direction = self.choice(self.directions) if effect != self.SwapTransition: self.screen.ids.banner_manager.transition = effect( direction=direction ) else: self.screen.ids.banner_manager.transition = effect() self.screen.ids.banner_manager.current = name_screen self.screen.ids.banner_manager.screens.pop() def press_banner(self, instance_banner): if isinstance(instance_banner, str): print(instance_banner) else: print(instance_banner.id)
Данный класс просто меняет экраны с баннерами, устанавливая их в менеджере экранов banner_manager:

Не забываем добавить импорт созданного класса в библиотеку классов programclass в файле инициализации:

Набор шейдеров для анимаций смены афиш мы импортируем в базовом файле program.py:

Реализация главного экрана приложения:
import os from kivy.uix.boxlayout import BoxLayout from kivy.lang import Builder from kivy.properties import ObjectProperty, ListProperty, StringProperty from Libs.uix.custombutton import CustomButton root = os.path.split(__file__)[0] root = root if root != '' else os.getcwd() 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_tabbed_panel = ListProperty( [0.15294117647058825, 0.0392156862745098, 0.11764705882352941, 1] ) '''Цвет фона tabbed panel.''' title_previous = StringProperty('') '''Заголовок ActionBar.''' tabbed_text = StringProperty('') '''Текст пунктов кастомной tabbed panel.''' Builder.load_file('{}/kv/startscreen.kv'.format(root)) def __init__(self, **kvargs): super(StartScreen, self).__init__(**kvargs) self.ids.custom_tabbed.bind(on_ref_press=self.events_callback) # Cписок магазинов. for name_shop in self.core.dict_shops.keys(): self.ids.shops_list.add_widget( CustomButton( text=self.core.dict_shops[name_shop], icon='Data/Images/shops/{}.png'.format(name_shop), icon_people='Data/Images/people.png', icon_map='Data/Images/mapmarker.png', events_callback=self.events_callback, ) )
#: kivy 1.9.1 #: import StiffScrollEffect Libs.uix.garden.stiffscroll.StiffScrollEffect <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/logo.png' title: root.title_previous with_previous: False on_release: root.events_callback('navigation_drawer') ActionButton: icon: 'Data/Images/trash_empty.png' ActionButton: icon: 'Data/Images/search.png' Label: id: custom_tabbed text: root.tabbed_text bold: True markup: True size_hint: 1, .35 text_size: self.width - 40, None canvas.before: Color: rgb: root.color_tabbed_panel Rectangle: pos: self.pos size: self.size ScreenManager: id: screen_manager size_hint: 1, 8 Screen: ScreenManager: id: banner_manager size_hint: 1, .38 pos_hint: {'top': 1} ScrollView: effect_cls: StiffScrollEffect size_hint_y: None height: root.height // 1.8 pos_hint: {'top': .62} GridLayout: id: shops_list cols: 1 spacing: 5 padding: 5 size_hint_y: None height: self.minimum_height
Как вы могли заметить, я не использую TabbedPanel, так как считаю ее стандартную реализацию в Android не слишком красивой. Она была заменена на Label + ref:


Базовый класс Program:
import os import sys from random import choice from kivy.app import App from kivy.uix.screenmanager import Screen, SlideTransition, SwapTransition from kivy.core.window import Window from kivy.config import ConfigParser from kivy.clock import Clock from kivy.utils import get_hex_from_color, get_color_from_hex from kivy.properties import ObjectProperty, NumericProperty from Libs.uix.kdialog import KDialog, BDialog, Dialog from Libs.uix.startscreen import StartScreen from Libs.uix.custommenu import CustomMenuItem from Libs.uix.navigationmenu import NavigationMenu from Libs.uix.garden.navigationdrawer import NavigationDrawer # Классы программы. from Libs import programclass as prog_class from Libs import programdata as core from Libs.manifest import Manifest # Графика для диалоговых окон. Dialog.background_image_buttons = core.image_buttons Dialog.background_image_shadows = core.image_shadows Dialog.background = core.decorator class Program(App, prog_class.ShowPlugin, prog_class.ShowBanners, prog_class.SearchShop, prog_class.ShowLicense, prog_class.ShowLocations): '''Функционал программы.''' 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.CustomMenuItem = CustomMenuItem self.KDialog = KDialog self.BDialog = BDialog self.Manifest = Manifest self.SwapTransition = SwapTransition self.choice = choice self.get_color_from_hex = get_color_from_hex self.get_hex_from_color = get_hex_from_color self.core = core self.name_program = core.string_lang_title self.navigation_drawer = NavigationDrawer(side_panel_width=230) self.current_open_tab = core.string_lang_tabbed_menu_shops self.shop = False # выбранный магазин self.open_dialog = False # открыто диалоговое окно self.effects_transition = (SlideTransition, SwapTransition) # Список магазинов. self.shops = core.dict_shops.keys() # Список локаций. self.locations = [ location.split('.')[0].lower() for location in os.listdir( '{}/Data/Images/locations'.format(core.prog_path))] def build_config(self, config): config.adddefaultsection('General') config.setdefault('General', 'language', 'Русский') config.setdefault('General', 'theme', 'default') def build(self): 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_tabbed_panel=core.color_tabbed_panel, tabbed_text=core.string_lang_tabbed_menu.format( TEXT_SHOPS=core.string_lang_tabbed_menu_shops, TEXT_LOCATIONS=core.string_lang_tabbed_menu_locations, COLOR_TEXT_SHOPS=get_hex_from_color(core.color_action_bar), COLOR_TEXT_LOCATIONS=core.theme_text_color), title_previous=self.name_program[1:], events_callback=self.events_program, core=core ) self.screen = self.start_screen navigation_panel = NavigationMenu( events_callback=self.events_program, items=core.dict_navigation_items ) Clock.schedule_interval(self.show_banners, 4) self.navigation_drawer.add_widget(navigation_panel) self.navigation_drawer.anim_type = 'slide_above_anim' self.navigation_drawer.add_widget(self.start_screen) return self.navigation_drawer def set_var_from_file_settings(self): '''Установка значений переменных из файла настроек program.ini.''' self.language = core.select_locale[ self.config.get('General', 'language') ] def set_current_item_tabbed_panel(self, color_current_tab, color_tab): self.screen.ids.custom_tabbed.text = \ core.string_lang_tabbed_menu.format( TEXT_SHOPS=core.string_lang_tabbed_menu_shops, TEXT_LOCATIONS=core.string_lang_tabbed_menu_locations, COLOR_TEXT_SHOPS=color_tab, COLOR_TEXT_LOCATIONS=color_current_tab ) def events_program(self, *args): '''Обработка событий программы.''' if self.navigation_drawer.state == 'open': self.navigation_drawer.anim_to_state('closed') if len(args) == 2: # нажата ссылка event = args[1] else: # нажата кнопка программы try: _args = args[0] event = _args if isinstance(_args, str) else _args.id except AttributeError: # нажата кнопка девайса event = args[1] if core.PY2: if isinstance(event, unicode): event = event.encode('utf-8') if event == core.string_lang_settings: pass elif event == core.string_lang_exit_key: self.exit_program() elif event == core.string_lang_license: self.show_license() 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 == 'navigation_drawer': self.navigation_drawer.toggle_state() elif event == core.string_lang_tabbed_menu_locations: self.show_locations() elif event == core.string_lang_tabbed_menu_shops: self.back_screen(event) elif event == 'obi_banner': self.press_banner(event) elif event in (1001, 27): self.back_screen(event) elif event in self.shops: print(event) return True def back_screen(self, event): '''Менеджер экранов.''' # Нажата BackKey на главном экране. if self.screen.ids.screen_manager.current == '': if event in (1001, 27): self.exit_program() return if len(self.screen.ids.screen_manager.screens) != 1: self.screen.ids.screen_manager.screens.pop() self.screen.ids.screen_manager.current = \ self.screen.ids.screen_manager.screen_names[-1] # Устанавливаем имя предыдущего экрана. #self.screen.ids.action_previous.title = self.screen.ids.screen_manager.current # Устанавливаем активный пункт в item_tabbed_panel. self.set_current_item_tabbed_panel( core.theme_text_color, get_hex_from_color(core.color_action_bar) ) 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_color=get_color_from_hex(core.theme_text_black_color), title=self.name_program).show( text=core.string_lang_exit.format(core.theme_text_black_color), 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_pause(self): '''Ставит приложение на 'паузу' при выхоже из него. В противном случае запускает программу заново''' return True def on_resume(self): print('on_resume') def on_stop(self): print('on_stop')
Оставляя за бортом (не рассмотренными) файл локализации — Data/Language/russian.txt, расфасовку графических ресурсов, цветовую схему приложения в файле Data/Themes/default/default.ini, создание данных в programdata.py, меню Navigation Drawer — все это несложные мелочи, запускаем тестовый пример:
python3 main.py
… и получаем вот такую картинку:

Ввиду плохого качества gif-ки, анимация экранов плохо просматривается, но это не столь важно. Те, кто будет тестировать пример из исходников проекта на девайсах, сразу выделят недочеты: я пока не реализовал алгоритм, по которому размеры рекламных баннеров будут подгоняться под все разрешения экранов без искажения пропорций; также, ввиду отсутствия возможности протестировать приложение хотя в эмуляторе не удалось найти оптимальный размер шрифта в программе, отсутствует анимация кнопок, хромает библиотека для работы с диалоговыми окнами kdialog, поскольку пока находится в разработке.
Но не смотря на все это, думаю, эта статья станет полезной для тех, кто также, как и я, любит и использует такой замечательный фремворк, как Kivy!
