Не библейская история



  1. И сотворил Google Android. Поселил его в саду мобильных платформ, дав ему жену — Java.
  2. И повелел Google Javе: создавай программы Androidу, красивые и быстрые, и Androidу сказал: не следует тебе брать других жен, кроме Javы. И запретил им вкушать плодов от древа познания фреймворков и языков программирования, дабы не сделались их программы медленными и неугодными пользователю.
  3. Хитрейшим же на том древе был древний змий — динамический Python. Долгое время наблюдал он за Androidом и, наконец, подстерег его прогуливающимся в тени деревьев. Тогда спросил хитрый Python Androidа: правду ли сказал тебе Google, не вкушать плодов от древа познания фреймворков и языков программирования, дабы не сделались твои программы медленными и неугодными пользователю?
  4. Точно так заповедовал мне всемогущий Google, ответил Android и прогаммы создает мне жена моя — Java.
  5. Обманул тебя Google, прошипел хитрый Python, ибо знает он, что в тот день, когда ты вкусишь плодов от древа познания фреймворков и языков программирования, прозреешь ты и потянутся к тебе другие разработчики и станут создавать программы, и появятся у тебя приложения такие же красивые и быстрые, как от жены твоей Java, и будут они кроссплатформенны!
  6. И сорвал Python плод от древа познания фреймворков и языков программирования и протянул Android`у, и тот ел.
  7. Имя того плода — Kivy.

Книга фреймворка Kivy (Глава 2, стих 1-7)



Как вы уже догадались, речь пойдет о разработке мобильных приложений для платформы Android с использованием фреймворка Kivy и языка программирования Python. На Хабре, уже есть несколько статей на эту тему, в основном — небольшие очерки, описывающие в общих чертах, что за фрукт этот Kivy, с чем его едят и пара-тройка примеров, типа Hello World и крестики-нолики.


В Интернете, можно найти не много, написанных с использованием Kivy приложений, и довольно длинный список качественных игр с 2D и 3D графикой. Отчасти это объясняется тем, что благодаря поддержке GPU acceleration, графических провайдеров PyGame, OpenGL, SDL, X11, поддержке шейдеров, — Kivy, можно сказать, по умолчанию больше ориентирован для применения в игровой индустрии, а кто-то прямо говорит, что написать более менее стоящее и красивое приложение в Kivy не удастся или вы потратите на это значительно больше времени, чем, скажем, при использовании нативного кода, не говоря уже о проектах, типа WhatsApp.


Что ж, доверять нельзя никому (мне — можно), поэтому в данной статье мы лично будем тестировать возможности Kivy в нише разработке мобильных разробток. Нет, писать приложение, типа WatsApp с нуля и говорить, насколько это легко, между двумя сигаретами и чашкой кофе, делается в Kivy, мы не будем (формат статьи не позволяет), но вот я вижу у вас установлена замечательная программа CleanMaster: анимация, кастомные контроллы, прозрачный прогресс бар — sexy, а не приложение — как раз то, что нам нужно, чтобы продемонстрировать возможности фреймворка!


Вот, как это выглядит (оригинальные экраны Clean Master):



Мы попробуем создать аналогичные, говоря языком Java, Activity: с анимацией, трансформацией, красивым прогрессом, счетчиком очищаемого кэша и пр., а в следующей статье соберем дефолтный установочный apk для Android девайса и проанализируем его плюсы и минусы. Сразу отмечу, что никакой функциональности, кроме демонстрации UI элементов, в нашем приложении не будет, а вся анимация прогресса очистки кэша, анимация подсчета STORAGE/RAM — обычная демонстрация.


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


Что нам для этого потребуется? Самые что ни на есть простые инструменты — кофе и пару пачек сигарет. На самом деле, мы просто не будем рассматривать установку Kivy и сопутствующих инструментов для сборки apk, поскольку все это можно легко найти в сети.


Итак, первым делом создадим директорию нашего проекта. Назовем его KivyCleanMasterDemo.



Структура проекта произвольная. Вы можете называть директории, как вам будет и держать файлы, где вам будет угодно, благо Python и Kivy не накладывают в этом плане никаких ограничений. Единственное условие: в корневой папке проекта должен присутствовать файл main.py — это точка входа в программу. Именно этот файл будет импортирован при запуске уже установленного apk пакета.


Итак, входим в наше приложение!



main.py
#! /usr/bin/python2.7
# -*- coding: utf-8 -*-
#
# main.py
#
# Точка входа в приложение. Запускает основной программный код program.py.
# В случае ошибки, выводит на экран окно с ее текстом.
#

from __future__ import print_function

import os
import sys
import traceback

try:
    import kivy
    kivy.require("1.9.1")

    from kivy.app import App
    from kivy.config import Config

    # Указываем пользоваться системным методом ввода, использующимся на
    # платформе, в которой запущенно приложение.
    Config.set("kivy", "keyboard_mode", "system")

    # Activity баг репорта.
    from Libs.uix.bugreporter import BugReporter
except Exception:
    print("\n\n{}".format(traceback.format_exc()))
    sys.exit(1)

__version__ = "0.0.1"

def main():
    app = None

    try:
        from program import Program  # основной класс программы

        # Запуск приложения.
        app = Program()
        app.run()
    except Exception as exc:
        print(traceback.format_exc())
        traceback.print_exc(file=open("{}/error.log".format(
            os.path.split(os.path.abspath(sys.argv[0]))[0]), "w"))

        if app:  # очищаем экран приложения от всех виджетов
            app.start_screen.clear_widgets()

        class Error(App):
            """Выводит экран с текстом ошибки."""

            def callback_report(self, *args):
                """Функция отправки баг-репорта"""

                try:
                    import webbrowser
                    import six.moves.urllib

                    txt = six.moves.urllib.parse.quote(
                        self.win_report.txt_traceback.text.encode(
                            "utf-8"))
                    url = "https://github.com/HeaTTheatR/KivyCleanMasterDemo"                           "/issues/new?body=" + txt
                    webbrowser.open(url)
                except Exception:
                    sys.exit(1)

            def build(self):
                self.win_report = BugReporter(
                    callback_report=self.callback_report, txt_report=str(exc),
                    icon_background="Data/Images/logo.png")
                return self.win_report

        Error().run()

if __name__ in ("__main__", "__android__"):
    main()

Здесь все просто и в дополнительных комментариях нет необходимости. Нас интересует код:


from program import Program # основной класс программы

app = Program()
app.run()  # запуск приложения

Из main.py двигаемся по коду далее. Рассмотрим основной класс нашего демо приложения из модуля program.py



program .py
#! /usr/bin/python2.7
# -*- coding: utf-8 -*-
#
# program.py
#
# Основной рограммный код приложения.
#

try:
    import kivy
    kivy.require("1.9.1")

    from kivy.app import App
    from kivy.uix.button import Button
    from kivy.uix.screenmanager import Screen, FadeTransition
    from kivy.clock import Clock
    from kivy.core.window import Window

    # Импорт классов для манипуляции с Activity приложения.
    # У нас их, как вы помните, три.
    from Libs.uix.about import About
    from Libs.uix.startscreen import StartScreen
    from Libs.uix.junkfiles import JunkFiles

    # Классы, управляющие переключением Activity и отрисовкой анимации.
    from Libs.programclass import ShowScreens, AnimationProgress
except Exception:
    import traceback
    raise Exception(traceback.format_exc())

__version__ = "0.0.1"

class Program(App, ShowScreens, AnimationProgress):
    """Функционал программы"""

    # Для десктопа.
    title = "Clean Master"  # заголовок окна программы
    icon = "Data/Images/logo.png"  # иконка приложения

    def __init__(self, **kvargs):
        super(Program, self).__init__(**kvargs)
        # Привязывает события клавиатуры/кнопок девайса к функции-обработчику.
        Window.bind(on_keyboard=self.on_events)

        # Для области видимомти в пакете programclass.
        self.About = About
        self.Clock = Clock
        self.JunkFiles = JunkFiles
        self.prog_dir = self.directory
        self.new_color =             [0.1568627450980392, 0.34509803921568627, 0.6784313725490196]

    def build(self):
        # Главное Activity программы.
        self.start_screen = StartScreen(events_callback=self.on_events)

        # Привязываем Activity на изменение размеров экрана приложения
        # к функции вычисления координат и отрисовки эллипсов прогресса
        # (для десктопа).
        self.start_screen.body_storage_ram.bind(pos=self.animation_storage_ram)
        self.start_screen.body_storage_ram.bind(size=self.animation_storage_ram)

        # Запуск анимации прогресса подсчета STORAGE/RAM.
        Clock.schedule_interval(self.calc_elliptical_length, .03)
        return self.start_screen

    def on_events(self, *args):
        """Обработчик событий приложения."""

        try:
            _args = args[0]  # события приложения - имя либо идентификатор контролла
            event = _args if isinstance(_args, str) else _args.id
        except AttributeError:
            event = args[1]  # события клавиатуры, кнопок девайса - код нажатай клавиши

        if event == "About":  # выводим Activity About
            self.show_about()
        elif event == "on_previous" or event == 27:  # возврат в к предыдущему Activity
            self.back_screen()
        elif event == "JUNK FILES":  # выводим Activity JUNK FILES
            self.show_junk_files()
            self.Clock.unschedule(self.calc_elliptical_length)  # прерываем анимацию подсчета STORAGE/RAM
            self.Clock.schedule_interval(self.animation_clean, 0.2)  # запуск анимации прогресса очистки
        elif event == "STOP":   # прерываем анимацию JUNK FILES
            Clock.unschedule(self.animation_clean)
            self.back_screen()

    def show_new_screen(self, instance_new_screen, string_new_name_screen):
        """Устанавливает новый экран."""

        # Если пытаются открыть один и тот же экран, например, About в About.
        name_current_screen = self.start_screen.screen_manager.current
        if name_current_screen == string_new_name_screen:
            return

        self.start_screen.screen_manager.add_widget(screen)  # добавляем Activity в экранный менеджер
        self.start_screen.screen_manager.transition = FadeTransition()  # запускаем анимацию смены экрана
        self.start_screen.screen_manager.current = string_new_name_screen  # выводим Activity на экран
        self.start_screen.action_previous.title = string_new_name_screen   # новое имя Activity в ActionBar
        self.start_screen.action_previous.app_icon = "Data/Images/arrow_left.png"

По сути, данный класс устанавливает главный экран приложения, запускает анимацию прогресса подсчета STORAGE/RAM, отслеживает события и переключает экраны нашего демо приложения.


Главное Activity приложения выводится на экран в нижеследующем куске кода:


# Главное Activity программы.
self.start_screen = StartScreen(events_callback=self.on_events)
return self.start_screen

Однако прежде чем посмотреть, как выглядит файл разметки интерфейса стартового Activity, несколько слов о построении UI в Kivy.


Макет интерфейса в Kivy можно построить двумя способами — непосредственно в коде:


root = MyRootWidget()
box = BoxLayout()
byt1 = Button()
byt2 = Button()

box.add_widget(btn1)
box.add_widget(btn2)
root.add_widget(box)

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


Совсем другое дело UI построенный с использованием специального языка разметки — Kv Language, очень похожего на QML в Qt, где иерархия виджетов выделяется с помощью идентов и наглядно видно, к какому layout относится тот или иной контролл, а управление свойствами осуществляется в программном коде в одноименном классе через идентификатор виджета:


<MyRootWidget@BoxLayout>:
    Button:
        id: btn1
    Button:
        text: "Текст кнопки btn2"

class MyRootWidget(BoxLayout):
    def __init__(self. **kvargs):
        super(MyRootWidget, self).__init__(**kvargs)

        self.ids.btn1.text = "Текст кнопки btn1"

Что ж, базовую информацию для чтения макетов интерфейса мы дали (у Java программистов вообще не должно возникнуть трудностей на этот счет), давайте теперь посмотрим, как выглядит стартовое Activity, созданное в Kivy:



А вот разметка данного Activity в Kv Language:



startscreen.kv
#:kivy 1.9.1

<StartScreen>
    ActionBar:
        id: action_bar
        canvas:
            Color:
                rgb: root.color_blue
            Rectangle:
                pos: self.pos
                size: self.size
        ActionView:
            ActionPrevious:
                id: action_previous
                app_icon: "Data/Images/previous_app_icon.png"
                previous_image: "Data/Images/previous_image.png"
                with_previous: True
                on_press: root.events_callback("on_previous")
            ActionOverflow:
                id: action_overflow
                overflow_image: "Data/Images/overflow_image.png"
    # Менеджер экранов.
    ScreenManager:
        id: screen_manager
        # Текущий стартовый экран.
        Screen:
            id: screen
            # Тело STORAGE/RAM
            FloatLayout:
                id: float_layout
                canvas:
                    Color:
                        rgb: root.color_blue
                    Rectangle:
                        pos: self.pos
                        size: self.size
                    # Статическая окружность STORAGE
                    Color:
                        rgba: root.color_ellipse_static
                    Line:
                        width: 3.
                        circle:
                            (self.center_x / 1.5, self.center_y / .65,                             min(self.width, self.height) / 4.5, 220, 500, 50)
                    # Динамическая оружность прогресса STORAGE
                    Color:
                        rgba: 1.0, 1.0, 1.0, 1
                    Line:
                        width: 3.
                    # Статическая окружность RAM
                    Color:
                        rgba: root.color_ellipse_static
                    Line:
                        width: 3.
                        circle:
                            (self.center_x / .65, self.center_y / .69,                             min(self.width, self.height) / 7, 220, 500, 50)
                    # Динамическая оружность прогресса RAM
                    Color:
                        rgba: 1.0, 1.0, 1.0, 1
                    Line:
                        width: 3.
                # Цифры процента прогресса STORAGE
                Image:
                    id: storage_numeral_one
                    size_hint: .14, .14
                    pos_hint: {"center_x": .28, "center_y": .76}
                    allow_stretch: True
                Image:
                    id: storage_numeral_two
                    size_hint: .14, .14
                    pos_hint: {"center_x": .39, "center_y": .76}
                    allow_stretch: True
                Image:
                    size_hint: .06, .06
                    pos_hint: {"center_x": .47, "center_y": .82}
                    allow_stretch: True
                    source: "Data/Images/percent.png"
                # Цифры процента прогресса RAM
                Image:
                    id: ram_numeral_one
                    size_hint: .07, .07
                    pos_hint: {"center_x": .74, "center_y": .72}
                    source: "Data/Images/3.png"
                    allow_stretch: True
                Image:
                    id: ram_numeral_two
                    size_hint: .07, .07
                    pos_hint: {"center_x": .80, "center_y": .72}
                    source: "Data/Images/4.png"
                    allow_stretch: True
                Image:
                    size_hint: .05, .05
                    pos_hint: {"center_x": .85, "center_y": .74}
                    allow_stretch: True
                    source: "Data/Images/percent.png"
                # Подписи окружностей прогресса
                Label:
                    text: "STORAGE"
                    bold: True
                    pos_hint: {"center_x": .34, "center_y": .63}
                    color: root.color_label
                Label:
                    text: "124.40MB/704.99MB"
                    bold: True
                    pos_hint: {"center_x": .34, "center_y": .68}
                    color: root.color_label
                    font_size: "10sp"
                Label:
                    text: "RAM"
                    bold: True
                    pos_hint: {"center_x": .78, "center_y": .63}
                    color: root.color_label
                # Строка состояния today_cleaned
                Label:
                    size_hint: 1, .05
                    text: "Today cleaned: 0.0B Total: 0.0B"
                    pos_hint: {"top": .45}
                    canvas:
                        Color:
                            rgba: 1.0, 1.0, 1.0, 0.3
                        Rectangle:
                            pos: self.pos
                            size: self.size
                # Бокс кнопок меню "JUNK FILES", "MEMORY BOOST"
                # "APP MANAGER", "SECURITY & PRIVACY"
                GridLayout:
                    id: body_buttons_menu
                    size_hint: 1, .4
                    cols: 2
                    padding: 10
                    spacing: 5
                    # Фоновый цвет меню
                    canvas.before:
                        Color:
                            rgb: 1.0, 1.0, 1.0
                        Rectangle:
                            pos: self.pos
                            size: self.size
                        # Линии, рзбивающие пункты меню
                        Color:
                            rgb: 0, 0, 0
                        Line:
                            points:
                                [0, self.size[1] / 1.9, self.size[0], self.size[1] / 1.9]
                            width: 1
                        Line:
                            points:
                                [self.size[0] / 2, 0, self.size[0] / 2, self.size[1] / 1.001]
                            width: 1

Как вы могли заметить, Kv Language — это почти Python, в том смысле, что помимо разметки UI элементов, вы можете проводить в kv файлах вычисления, дергать колбэки и передавать в них параметры, импортировать и использовать стандартные или внешние модули Python.


Теперь давайте взглянем на управляющий нашим макетом класс, поскольку видно, что а файле разметки созданы не все элементы: например, отсутствует спиннер в ActionBar, а под кнопки меню создан только корневой бокс.



startscreen.py
#! /usr/bin/python2.7
# -*- coding: utf-8 -*-
#
# startscreen.py
#
# Главный экран программы.
#

from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.image import Image
from kivy.uix.button import Button
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.actionbar import ActionItem
from kivy.lang import Builder
from kivy.properties import ObjectProperty, ListProperty

class ImageButton(ButtonBehavior, Image):
    pass

class MyOwnActionButton(Button, ActionItem):
    pass

class StartScreen(BoxLayout):
    events_callback = ObjectProperty(None)
    """Функция обработки сигналов экрана."""

    color_blue = ListProperty(
        [0.1607843137254902, 0.34901960784313724, 0.6784313725490196])
    """Синий background экрана. """

    color_label= ListProperty(
        [0.6784313725490196, 0.7294117647058823, 0.8392156862745098, 1])
    """Цвет подписей эллипсов STORAGE/RAM. """

    color_ellipse_static= ListProperty(
        [0.38823529411764707, 0.5254901960784314, 0.7764705882352941, 1])
    """Цвет статических эллипсов STORAGE/RAM. """

    Builder.load_file("Libs/uix/kv/startscreen.kv")
    """Макет интерфейса"""

    def __init__(self, **kvargs):
        super(StartScreen, self).__init__(**kvargs)
        self.orientation = "vertical"

        # Виждеты стартового экрана.
        self.layouts = self.ids
        self.body_storage_ram = self.ids.float_layout
        self.screen_manager = self.ids.screen_manager
        self.action_previous = self.ids.action_previous
        self.background_action_bar = self.ids.action_bar.canvas.children[3]
        self.ellips_storage = self.body_storage_ram.canvas.children[8]
        self.ellips_ram = self.body_storage_ram.canvas.children[14]
        self._action_overflow = self.ids.action_overflow

        self.create_spinner_items()
        self.create_menu_buttons()

    def create_spinner_items(self):
        """Создает кнопки для выпадающего списка меню ActionBar."""

        for item_name in ["Settings", "Update", "Like Us",
                          "Feedback", "FAQ", "About"]:
            item_button =                 MyOwnActionButton(
                    text=item_name, id=item_name,
                    on_press=self.events_callback, color=[.1, .1, .1, 1],
                    background_normal="Data/Images/background_action_item.png",
                    background_down="Data/Images/background_down.png",
                    on_release=lambda *args: self._action_overflow._dropdown.select(
                        self.on_release_select_item_spinner()))
            self._action_overflow.add_widget(item_button)

    def create_menu_buttons(self):
        """Создает кнопки и подписи меню."""

        name_path_buttons_menu = {
            "JUNK FILES": "Data/Images/clean_cache.png",
            "MEMORY BOOST": "Data/Images/clean_memory.png",
            "APP MANAGER": "Data/Images/clean_apk.png",
            "SECURITY & PRIVACY": "Data/Images/clean_privacy.png"}

        for name_button in name_path_buttons_menu.keys():
            item_box = BoxLayout(orientation="vertical")
            item_label = Label(text=name_button, color=[.1, .1, .1, 1])
            item_button =                 ImageButton(source=name_path_buttons_menu[name_button],
                            id=name_button, on_press=self.events_callback)
            item_box.add_widget(item_button)
            item_box.add_widget(item_label)
            self.ids.body_buttons_menu.add_widget(item_box)

    def on_release_select_item_spinner(self):
        """Вешается на release событие кнопок спиннера ActionBar.
        В противноном случае, список не будет автоматически скрываться."""

        pass

Данный класс имеет всего две функции, задача которых создать кнопки для выпадающего списка ActionBar, кнопки и подписи меню. Также класс инициализирует для дальнейшего использования в коде атрибуты — виджеты, полученные по их идентификаторам из файла разметки startscreen.kv.


Для дальнейшей работы нам понадобиться кастомный виджет кнопка...



… который используется в двух следующих Activity. Он небольшой и поможет лучше разобраться во взаимодействии разметки интерфейса и программного кода, который манипулирует его свойствами.



custombutton.kv


#:kivy 1.9.1

<CustomButton@Button>
    # Унаследованые от виджета Button свойства.
    # Ключевое слово root - это экземпляр класса CustomButton.
    id: root.id
    text: root.button_text
    background_normal: "Data/Images/background_action_item.png"
    background_down: "Data/Images/background_down.png"
    size_hint_y: None
    text_size: root.width - 150, root.height
    valign: "middle"
    height: root.button_height
    color: 0.1, 0.1, 0.1, 1
    on_press: if callable(root.event_callback): root.event_callback(root.id)
    # Иконка действия очистки справа
    Image:
        source: root.icon
        size_hint_y: None
        height: root.icon_height
        pos: root.x - 25, root.y + self.height / 2
    # Иконка прогресса очистки слева
    Image:
        source: root.icon_load
        size_hint_y: None
        pos: root.width / 2 - 25, root.y
        size: root.size

custombutton.py


#! /usr/bin/python2.7
# -*- coding: utf-8 -*-
#
# custombutton.py
#

from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.properties import StringProperty, NumericProperty, ObjectProperty
Builder.load_file("Libs/uix/kv/custombutton.kv")

class CustomButton(Button):
    id = StringProperty("")
    button_height = NumericProperty(65)
    button_text = StringProperty("")
    icon = StringProperty("")
    icon_height = NumericProperty(30)
    icon_load = StringProperty("Data/Images/loading.gif")
    event_callback = ObjectProperty(None)

Ну, и раз у нас уже готов спиннер, кастомный виджет кнопка и в главном классе Program в функции on_events есть обработка события "About"...



… давайте создадим это Activity и его управляющий класс.



about.kv
#:kivy 1.9.1
<About>:
    orientation: "vertical"
    ScrollView:
        GridLayout:
            cols: 1
            size_hint_y: None
            spacing: 10
            padding: 10
            height: self.minimum_height
            canvas:
                Color:
                    rgb: root.about_background
                Rectangle:
                    pos: self.pos
                    size: self.size
            # Логотип приложения
            Image:
                source: "Data/Images/logo.png"
                size_hint_y: None
            # Версия приложения
            Label:
                text: "Clean Master 5.4.0.1395"
                size_hint_y: None
                height: "10pt"
                color: 0.1, 0.1, 0.1, 1
                italic: True
            # Бокс меню share
            GridLayout:
                id: box_share
                cols: 1
                size_hint_y: None
                height: self.minimum_height
            # Текст лицензии
            Label:
                text: root.text_license
                text_size: self.size
                font_size: dp(12)
                valign: "top"
                size_hint_y: None
                height: root.height / 2.5
                color: 0.1, 0.1, 0.1, 1
    # Бокс копирайта
    BoxLayout:
        orientation: "vertical"
        size_hint: 1, .2
        canvas:
            Color:
                rgb: root.about_background
            Rectangle:
                pos: self.pos
                size: self.size
            Color:
                rgb: 0.5843137254901961, 0.5843137254901961, 0.5843137254901961
            Line:
                points: [0, self.size[1], self.size[0], self.size[1]]
                width: 1
        Label:
            text: "Copyright 2016 Demo Clean Master\nby HeaTTheatR"
            halign: "center"
            color: 0.1, 0.1, 0.1, 1

about.py
#! /usr/bin/python2.7
# -*- coding: utf-8 -*-
#
# about.py
#

from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
from kivy.properties import ObjectProperty, StringProperty, ListProperty
from .custombutton import CustomButton

class About(BoxLayout):
    events_callback = ObjectProperty(None)
    """Функция обработки сигналов экрана."""

    text_license = StringProperty("Clean Master")
    about_background = ListProperty(
        [0.7294117647058823, 0.7686274509803922, 0.8470588235294118])

    Builder.load_file("Libs/uix/kv/about.kv")
    """Макеты интерфейса"""

    def __init__(self, **kvargs):
        super(About, self).__init__(**kvargs)
        self.create_button_share(self.ids.box_share)

    def create_button_share(self, box_share):
        """Добавляет кнопки меню share в макет.

        :type box_share: <'kivy.weakproxy.WeakProxy'>
        :param box_share: <'kivy.uix.gridlayout.GridLayout'>

        """

        about_items_share = {
            "Share this app": "Data/Images/about_share.png",
            "Like us on Facebook": "Data/Images/about_facebook.png",
            "Join our beta testing group": "Data/Images/google_plus.png",
            "Help us with localization": "Data/Images/about_localization.png",
            "For Business Cooperation": "Data/Images/skype_icon.png"}

        for name_item in about_items_share.keys():
                box_share.add_widget(
                    CustomButton(icon_load="Data/Images/previous_image.png",
                                 icon=about_items_share[name_item],
                                 button_text=name_item, button_height=45,
                                 icon_height=25))

Теперь мы можем открывать выпадающий список в ActionBar, выбрать пункт About и соответственно перейти в выбранный экран. Я позволил себе некоторые вольности относительно текста About, заменив его лицензией GNU GPL, но, думаю, для нашей демонстрации это не существенно.



Вернуться к стартовому экрану мы можем нажав стрелочку слева в ActionBar. Вызов данного колбэка мы записали в макете startscreen.kv:



И поймали его в главном классе Program в функции on_events:



Обратите внимание на функции show_about и back_screen — это функции из класса ShowScreens одноименного модуля пакета programclass, которые дергаются в on_events. Данный класс открывает три макета нашего демо приложения и переключается между ними.



Можно, конечно, было написать эти функции в главном классе Program, но я не люблю, когда код приобретает вид трехэтажного здания, да и вносить правки значительно легче, когда все лежит на полочках на виду, а не теряется в десятках функций и классов.


Давайте посмотрим на класс ShowScreens.


ShowScreens
#! /usr/bin/python2.7
# -*- coding: utf-8 -*-
#
# ShowScreens.py
#

class ShowScreens(object):
    """Выводит новые экраны."""

    background_action_bar =         [0.1568627450980392, 0.34509803921568627, 0.6784313725490196]

    def show_about(self):
        try:
            text_license = open("{}/LICENSE".format(self.prog_dir)).read()
        except Exception:
            text_license = "Clean Master"

        # Прерываем анимацию очистки, если About открыт из экрана "JUNK FILES".
        if self.start_screen.layouts.screen.manager.current == "JUNK FILES":
            self.Clock.unschedule(self.animation_clean)
            self.start_screen.background_action_bar.rgb = self.background_action_bar

            # Удаляем иконки анимации процесса из пунктов "Memory boost" и
            # "Cache junk".
            self.screen_junk.button_memory_bust.remove_widget(
                self.screen_junk.button_memory_bust_icon_state)
            self.screen_junk.button_cache_junk.remove_widget(
                self.screen_junk.button_cache_junk_icon_state)

        screen_about =             self.About(events_callback=self.on_events, text_license=text_license)
        self.show_new_screen(screen_about, "About")

    def show_junk_files(self):
        self.set_default_tick_rgb()
        self.screen_junk = self.JunkFiles(events_callback=self.on_events)
        self.show_new_screen(self.screen_junk, "JUNK FILES")

    def back_screen(self):
        """Вызывается при событии ActionPrevious в ActionBar.
        Устанавливает предыдущий и удаляет из списка текущий экран."""

        current_screen = self.start_screen.screen_manager.current

        if current_screen in ("About", "JUNK FILES"):
            # Если открыт экран процесса очистки, останавливаем
            # процесс анимации.
            if current_screen == "JUNK FILES":
                self.Clock.unschedule(self.animation_clean)
            # Если возвращаемся на главный экран, запускаем анимацию
            # подсчета STORAGE/RAM.
            self.Clock.schedule_interval(self.calc_elliptical_length, .03)

        if len(self.start_screen.screen_manager.screens) != 1:
            self.start_screen.screen_manager.screens.pop()

        self.start_screen.screen_manager.current =             self.start_screen.screen_manager.screen_names[-1]
        self.start_screen.action_previous.title =             self.start_screen.screen_manager.current

        if current_screen in ("About", "JUNK FILES"):
            # Устанавливаем цвет в actionbar, который на момент открытия экрана
            # About, использовался в экране "JUNK FILES".
            self.start_screen.background_action_bar.rgb = self.new_color
            # Возвращаем "родной", синий цвет в actionbar.
            if self.start_screen.screen_manager.screen_names[-1] !=                     "JUNK FILES" or current_screen == "":
                self.start_screen.background_action_bar.rgb =                     self.background_action_bar
        # Возвращение иконки previous стартового экрана в actionbar.
        if self.start_screen.screen_manager.screens[-1].name !=                 "JUNK FILES":
            self.start_screen.action_previous.app_icon =                 "Data/Images/previous_app_icon.png"

Так! Ну, и, собственно, осталось реализовать Activity JUNK FILES и управление анимацией приложения. Построенное Activity JUNK FILES у нас будет выглядеть следующим образом:



Создадим файлы junkfiles.py и junkfiles.kv:



junkfiles.kv
#:kivy 1.9.1

<JunkFiles>
    orientation:  "vertical"
    FloatLayout:
        id: float_layout
        canvas:
            Color:
                rgb:
                    0.1607843137254902, 0.34901960784313724, 0.6784313725490196
            Rectangle:
                pos: self.pos
                size: self.size
        # Цифровое табло прогресса
        BoxLayout:
            pos_hint: {"center_x": .5, "center_y": .75}
            size_hint: .7, 1
            Image:
                id: storage_numeral_one
                size_hint: .52, .52
                source: "Data/Images/6.png"
            Image:
                id: storage_numeral_two
                size_hint: .52, .52
                source: "Data/Images/5.png"
            Image:
                id: point
                size_hint: .25, .25
                pos_hint: {"center_y": .21}
                source: "Data/Images/dot.png"
            Image:
                id: numeral_float
                size_hint: .52, .52
                source: "Data/Images/8.png"
            Image:
                size_hint: .22, .22
                pos_hint: {"center_y": .48}
                source: "Data/Images/gb.png"
        # Линия прогресса
        ProgressLine:
            id: progress_line
            size_hint: 1, .1
            pos_hint: {"center_y": .05}
        Label:
            id: progress_label
            text: "Scanning:"
            text_size: root.width - 20, root.height
            pos_hint: {"center_y": .05}
            valign: "middle"
    ScrollView:
        size_hint: 1, .6
        canvas.before:
            Color:
                rgb: 1.0, 1.0, 1.0,
            Rectangle:
                pos: self.pos
                size: self.size
        GridLayout:
            id: grid_layout
            cols: 1
            size_hint_y: None
            height: self.minimum_height
    BoxLayout:
        size_hint_y: None
        height: 80
        padding: 10, 10
        canvas.before:
            Color:
                rgb: 1.0, 1.0, 1.0,
            Rectangle:
                pos: self.pos
                size: self.size
        Button:
            id: button_stop
            text: "STOP"
            font_size: "19sp"
            bold: True
            markup: True
            background_normal: "Data/Images/stop_progress.png"
            background_down: "Data/Images/stop_progress_down.png"
            color: 0.1, 0.1, 0.1, 1

junkfiles.py
#! /usr/bin/python2.7
# -*- coding: utf-8 -*-
#
# junkfiles.py
#
# Экран процесса очистки.
#

from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
from kivy.properties import ObjectProperty

from .progressline import ProgressLine
from .custombutton import CustomButton

class JunkFiles(BoxLayout):
    events_callback = ObjectProperty(None)
    """Функция обработки сигналов экрана."""

    Builder.load_file("Libs/uix/kv/junkfikes.kv")
    """Макеты интерфейса"""

    def __init__(self, **kvargs):
        super(JunkFiles, self).__init__(**kvargs)
        self.create_custom_button()

        # Виждеты экрана очистки.
        self.layouts = self.ids
        self.button_memory_bust = self.layouts.grid_layout.children[0]
        self.button_cache_junk = self.layouts.grid_layout.children[1]
        self.button_memory_bust_icon_state = self.button_memory_bust.children[0]
        self.button_cache_junk_icon_state = self.button_cache_junk.children[0]
        self.progress_line = self.layouts.progress_line
        self.progress_label = self.layouts.progress_label
        self.button_stop = self.layouts.button_stop
        self.background = self.ids.float_layout.canvas.children[0]

    def create_custom_button(self):
        """Создает список кнопок с именем и иконкой действий очистки."""

        junk_files_items = {"Memory boost": "Data/Images/memory_boost.png",
                            "Cache junk": "Data/Images/cache_junk.png"}

        for action_clean in junk_files_items.keys():
            path_to_icon_action = junk_files_items[action_clean]
            self.ids.grid_layout.add_widget(
                CustomButton(id=action_clean, icon=path_to_icon_action,
                             button_text=action_clean,
                             on_press=self.events_callback))
        self.ids.button_stop.bind(
            on_press=lambda *args: self.events_callback("STOP"))

Обратите внимание на кастомный виджет ProgressLine из junkfiles.kv:



Мы импортировали его в junkfiles.py:



#! /usr/bin/python2.7
# -*- coding: utf-8 -*-
#
# progressline.py
#

from kivy.uix.widget import Widget
from kivy.utils import get_color_from_hex
from kivy.graphics import Color, Line

class ProgressLine(Widget):
    """Линия прогресса."""

    bar_value_percent = 0
    color = "#ffffff56"

    def __init__(self, **kwargs):
        super(ProgressLine, self).__init__(**kwargs)
        self.bind(pos=self.redraw)
        self.bind(size=self.redraw)

    def redraw(self, *args):
        """Отрисовка новых координат линии прогресса."""

        with self.canvas:
            self.canvas.clear()
            line_width = float(self.height) / 2 + 1
            new_y = self.y + line_width
            new_x = self.x + self.width * self.bar_value_percent / 100
            Color(*get_color_from_hex(self.color))
            Line(points=[self.x, new_y, new_x, new_y], width=line_width,
                 cap="none")


Теперь осталось создать класс, управляющий анимацией нашего демо приложения.



AnimationProgress.py
#! /usr/bin/python2.7
# -*- coding: utf-8 -*-
#
# AnimationProgress.py
#

from random import randint

class AnimationProgress(object):
    """Анимации прогрессов приложения."""

    def __init__(self):
        self.set_default_tick_rgb()
        self.scan_packages = range(100)

    def animation_storage_ram(self, *args):
        """Анимация эллипсов прогресса STORAGE/RAM."""

        if isinstance(args[0], int):  # при отрисовке прогресса
            elliptical_length_storage = elliptical_length_ram = args[0]
        else:  # при изменении размера окна приложения
            elliptical_length_storage = 317
            elliptical_length_ram = 401

        if self.tick <= 34:
            self.start_screen.ellips_storage.circle =                 ((self.start_screen.body_storage_ram.center_x / 1.5,
                  self.start_screen.body_storage_ram.center_y / .65,
                  min(self.start_screen.body_storage_ram.width,
                      self.start_screen.body_storage_ram.height) / 4.5,
                  220, elliptical_length_storage, 50))
        if self.tick <= 65:
            self.start_screen.ellips_ram.circle =                 ((self.start_screen.body_storage_ram.center_x / .65,
                  self.start_screen.body_storage_ram.center_y / .69,
                  min(self.start_screen.body_storage_ram.width,
                      self.start_screen.body_storage_ram.height) / 7,
                  220, elliptical_length_ram, 50))

    def animation_clean(self, interval):
        # Меняем иконки анимации процесса из пунктов "Memory boost" и
        # "Cache junk" на иконки-галочки.
        if int(self.tick) == 50:
            self.screen_junk.button_memory_bust_icon_state.source =                 "Data/Images/app_uninatall.png"
        elif int(self.tick) == 99:
            self.screen_junk.button_cache_junk_icon_state.source =                 "Data/Images/app_uninatall.png"

            # Устанавливаем цвет и текст кнопки STOP.
            self.screen_junk.button_stop.background_normal =                 "Data/Images/done_progress.png"
            self.screen_junk.button_stop.text =                 "CLEAN JUNK {}MB".format(self.tick)
            self.screen_junk.button_stop.color = [1.0, 1.0, 1.0, 1]

        # Смена фона Activity.
        self.set_new_color()
        self.screen_junk.background.rgb = self.new_color
        self.start_screen.background_action_bar.rgb = self.new_color

        # Вычисление и установка линии прогресса.
        value = (self.tick * 100) / 100
        print value
        self.screen_junk.progress_line.bar_value_percent = value
        self.screen_junk.progress_line.redraw()

        self.screen_junk.progress_label.text =             "Scanning: org.package {}".format(self.scan_packages[self.tick])
        self.animation_percent(
            self.screen_junk.layouts, self.animation_clean, iteration=100)

    def animation_percent(self, layout, callback, iteration=65):
        """
        Анимация процентов циферблата.

        :type layout: <class 'Libs.uix.startscreen.StartScreen'> and
                      <class 'Libs.uix.junkfiles.JunkFiles'>;
        :param callback: animation_clean and calc_elliptical_length;

        """

        self.tick += 1
        if self.tick == iteration:
            self.set_default_tick_rgb()
            self.Clock.unschedule(callback)
            return

        numeral_one, numeral_two = divmod(self.tick, 10)

        if self.tick <= 34 or iteration != 65:
            layout.storage_numeral_one.source =                 "Data/Images/{}.png".format(int(numeral_one))
            layout.storage_numeral_two.source =                 "Data/Images/{}.png".format(int(numeral_two))
            try:
                layout.numeral_float.source =                     "Data/Images/{}.png".format(randint(1, 9))
            except AttributeError:
                pass
        try:
            if self.tick <= 65:
                layout.ram_numeral_one.source =                     "Data/Images/{}.png".format(int(numeral_one))
                layout.ram_numeral_two.source =                     "Data/Images/{}.png".format(int(numeral_two))
        except AttributeError:
            pass

    def calc_elliptical_length(self, interval):
        """Вычисление координат эллипсов прогресса стартового Activity."""

        elliptical_length = ((self.tick * 500) // 178) + 222

        self.animation_storage_ram(elliptical_length)
        self.animation_percent(
            self.start_screen.layouts, self.calc_elliptical_length)

    def set_default_tick_rgb(self):
        """Устанавливаем дефолтный цвет фона макета в Activity JUNK FILES."""

        self.tick = 9
        self.R = 41.
        self.G = 89.
        self.B = 173.

    def set_new_color(self):
        """Устанавливаем новый цвет фона макета в Activity JUNK FILES."""

        self.R += 2
        self.G += 1
        self.B -= 1
        self.new_color = self.R / 255., self.R / 255., self.B / 255.

Теперь осталось посмотреть результаты:



KivyCleanMasterDemo доступен на githab — https://github.com/HeaTTheatR/KivyCleanMasterDemo


Поделиться с друзьями
-->

Комментарии (20)


  1. ri_gilfanov
    17.05.2016 05:23
    +1

    На какие платформы получилось собрать и запустить приложение?


  1. HeaTTheatR
    17.05.2016 05:27
    -1

    Приложение писал в и собирал на Linux машине. Сборку делал под Android. iOS не имею, поэтому собрать под эту платформу не могу. Вообще Kivy кроссплатформеный фреймворк, по заявлениям разработчиков, работает везде и даже на стиральных машинках.

    P.S.
    О машинках я пошутил :)


    1. ri_gilfanov
      17.05.2016 06:01

      Было бы интересно в будущем увидеть примеры кроссплатформенности кода из Kivy как в плане мобильных приложений, так и настольных.

      Пока что самым простым и кроссплатформенным вариантом для настольных приложений можно считать CPython и библиотеку Tkinter (всходит в стандартную) — ничего дополнительно ставить не надо, из исходников работает в основных настольных ОС, со сборкой .exe под Windows проблем обычно не возникает.

      Если с Kivy без многочасовых танцев с бубном можно один и тот же код собрать под основные настольные и/или мобильные платформы — было бы питонистам счастье.


  1. ivlis
    17.05.2016 07:04
    -2

    Вы четвертый Андроид, меню в стиле второго и такое маленькое разрешение специально в археологических раскопках отыскали?


    1. Merlen_Gross
      17.05.2016 08:26
      +4

      Конечно. Вашей лопатой откопали.


  1. c4simba
    17.05.2016 10:53
    +1

    Всегда в кроссплатформенных фреймворках смущал объем собранного приложения. Как тут обстоят дела?


    1. HeaTTheatR
      17.05.2016 14:34

      Пакет, который я собирал, Kivy взвесил мне на 8 Мб. Но! При сборке можно указывать, какие модули и библиотеки не включать в пакет. Таким образом, у меня получались пакеты 3-3.5 Мб.


    1. ShashkovS
      17.05.2016 14:36

      7 метров — минимум. А дальше — сколько нарисуете.


    1. barbados
      17.05.2016 15:57

      печально, т.к. там в apk вставляется питон, 6-7мб минимум


      1. HeaTTheatR
        17.05.2016 16:08

        Не совсем так. Как я уже говорил, большая половина этого объема — библиотеки, от которых при компиляции можно избавиться.


        1. barbados
          17.05.2016 16:15
          +2

          ждем следующую статью :)


  1. vladbarcelo
    17.05.2016 14:36
    +1

    Вы бы лучше поподробнее процесс компиляции описали, это пока что самое сложное в работе с этим фреймворком.


    1. HeaTTheatR
      17.05.2016 14:39
      +1

      Процесс компиляции опишу в следующей статье.


  1. ogoNEKto
    17.05.2016 14:36

    Почему-то в статье ни слова, что не только пайтон — альтарнатива джаве под андроид.
    Вполне годные приложения собираются в тех же Delphi XE буквально за два клика.
    Для ищущих альтернативу грех не попробовать.


  1. themtrx
    17.05.2016 16:08

    А не могли бы Вы подсказать, как у этого фреймворка обстоят дела с WebRTC? Интересует клиентская часть, взаимодействие с камерой/микрофоном устройства. А то что-то гугл ничего путного не даёт, как и официальная документация проекта.


    1. HeaTTheatR
      17.05.2016 16:19

      Вы можете подключить и использовать сторонний модуль player, который предоставляет некоторые возможности по использованию камеры, акселерометра и др. функций девайса. Также можно использовать библиотеку PyJnius, и дергать все доступные для Java разработчиков Android API.


      1. themtrx
        17.05.2016 20:09

        Благодарю.


  1. nikolay_karelin
    17.05.2016 18:48

    А почему такие странные импорты — все завернуто в try: и потом ловится самое общее исключение???

    Я правильно понимаю, что собранный пакет уже должен содержать все нужные части Kivy и можно не настолько жесткую обработку ошибок импорта делать?


    1. HeaTTheatR
      17.05.2016 18:59

      Все необходимые библиотеки уже будут включены в установочный пакет. Насчет импортов… Не забывайте, что Kivy — это еще и десктоп и приложение могут запускать из исходников на машинах, где Kivy может не быть или он установлен с ошибками.


  1. kronk
    24.05.2016 18:33

    В пятой заповеди «Обнанул тебя Google» нужно исправить на Обманул. Не могу в личку так как ридонли.