Простые сладкие приложения с Kivy

в 15:59, , рубрики: kivy, python, разработка мобильных приложений, Разработка под android

Простые сладкие приложения с Kivy - 1

После неудачной попытки первой версии статьи, когда материал заминусовали из-за чудовищного дизайна приведенного в статье примера программы (статью пришлось удалить), я учел все минусы и привожу более поздний вариант тестового приложения.

Возможно, для вас будет новостью, но разрабатывать мобильные приложения с функционалом, который доступен Java разработчикам, под Android с помощью фреймворка Kivy не просто просто, а очень просто! Именно этого правила я придерживаюсь, создавая свои проекты с Python + Kivy — разработка должна быть максимально простой и быстрой. Как щелчок пальцами.

На новичков подаваемая информация не расчитана, я не буду на пальцах объяснять, что, откуда и куда. Думаю, те, кто читает данную статью, обладают достаточными для понимания материала, знаниями. К тому же, Kivy, как я уже только что написал, очень простой фреймворк и вся документация с примерами использования находится в исходниках!

В прошлой статье были рассмотрены несколько экранов приложения Clean Master в реализации на Kivy. Сегодня я покажу вам один из черновиков домашней приложения, над которым работаю в свободное время.

Простые сладкие приложения с Kivy - 2

Говорят, что 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 - 3

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

Простые сладкие приложения с Kivy - 4

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

Простые сладкие приложения с Kivy - 5

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

Простые сладкие приложения с Kivy - 6

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

Давайте начнем с кастомных кнопкок, которые используются в нашем главном экране:

Простые сладкие приложения с Kivy - 7

Да, в самом Kivy нет многих стандартных элементов для построения дизайна интерфейса, которые есть в Java, но зато Kivy позволяет сконструировать и анимировать абсолютно любой виджет, который вам понадобиться самостоятельно. Все зависит от границ вашей фантазии.

Создадим в директории Libs/uix файл custombutton.py и определим в нем класс нашей кнопки:

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:

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

Кастомная кнопка для экрана локаций:

Простые сладкие приложения с Kivy - 8

custommenu.py

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)

custommenu.kv

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

imagebutton.py

from kivy.uix.image import Image
from kivy.uix.behaviors import ButtonBehavior

class ImageButton(ButtonBehavior, Image):
    pass

Далее нам потребуется класс для смены рекламных баннеров на главном экране программы. Для данного тестого примера приложения плакаты с рекламными баннерами помещены в локальную папку проекта. Создадим файл show_banners.py в директории классов приложения Libs/programclass:

show_banners.py

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:

Простые сладкие приложения с Kivy - 9

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

Простые сладкие приложения с Kivy - 10

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

Простые сладкие приложения с Kivy - 11

Реализация главного экрана приложения:

startscreen.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, 
                ) 
            )

startscreen.kv

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

Простые сладкие приложения с Kivy - 12

Простые сладкие приложения с Kivy - 13

Базовый класс Program:

program.py

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

… и получаем вот такую картинку:

Простые сладкие приложения с Kivy - 14

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

Но не смотря на все это, думаю, эта статья станет полезной для тех, кто также, как и я, любит и использует такой замечательный фремворк, как Kivy!

Автор: HeaTTheatR

Источник


  1. Илья:

    Можно ли с kivy связать парсер написанный в комплекте с библиотекой requests + lxml/beautifulsoup и заставить работать в фоне на android?

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js