Не библейская история
- И сотворил Google Android. Поселил его в саду мобильных платформ, дав ему жену — Java.
- И повелел Google Jav
е: создавай программы Android
у, красивые и быстрые, и Androidу сказал: не следует тебе брать других жен, кроме Jav
ы. И запретил им вкушать плодов от древа познания фреймворков и языков программирования, дабы не сделались их программы медленными и неугодными пользователю. - Хитрейшим же на том древе был древний змий — динамический Python. Долгое время наблюдал он за Android
ом и, наконец, подстерег его прогуливающимся в тени деревьев. Тогда спросил хитрый Python Android
а: правду ли сказал тебе Google, не вкушать плодов от древа познания фреймворков и языков программирования, дабы не сделались твои программы медленными и неугодными пользователю? - Точно так заповедовал мне всемогущий Google, ответил Android и прогаммы создает мне жена моя — Java.
- Обманул тебя Google, прошипел хитрый Python, ибо знает он, что в тот день, когда ты вкусишь плодов от древа познания фреймворков и языков программирования, прозреешь ты и потянутся к тебе другие разработчики и станут создавать программы, и появятся у тебя приложения такие же красивые и быстрые, как от жены твоей Java, и будут они кроссплатформенны!
- И сорвал Python плод от древа познания фреймворков и языков программирования и протянул Android`у, и тот ел.
- Имя того плода — 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 пакета.
Итак, входим в наше приложение!
#! /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
#! /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:
#: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, а под кнопки меню создан только корневой бокс.
#! /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 и его управляющий класс.
#: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
#! /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.
#! /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:
#: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
#! /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")
Теперь осталось создать класс, управляющий анимацией нашего демо приложения.
#! /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)
HeaTTheatR
17.05.2016 05:27-1Приложение писал в и собирал на Linux машине. Сборку делал под Android. iOS не имею, поэтому собрать под эту платформу не могу. Вообще Kivy кроссплатформеный фреймворк, по заявлениям разработчиков, работает везде и даже на стиральных машинках.
P.S.
О машинках я пошутил :)ri_gilfanov
17.05.2016 06:01Было бы интересно в будущем увидеть примеры кроссплатформенности кода из Kivy как в плане мобильных приложений, так и настольных.
Пока что самым простым и кроссплатформенным вариантом для настольных приложений можно считать CPython и библиотеку Tkinter (всходит в стандартную) — ничего дополнительно ставить не надо, из исходников работает в основных настольных ОС, со сборкой .exe под Windows проблем обычно не возникает.
Если с Kivy без многочасовых танцев с бубном можно один и тот же код собрать под основные настольные и/или мобильные платформы — было бы питонистам счастье.
ivlis
17.05.2016 07:04-2Вы четвертый Андроид, меню в стиле второго и такое маленькое разрешение специально в археологических раскопках отыскали?
c4simba
17.05.2016 10:53+1Всегда в кроссплатформенных фреймворках смущал объем собранного приложения. Как тут обстоят дела?
HeaTTheatR
17.05.2016 14:34Пакет, который я собирал, Kivy взвесил мне на 8 Мб. Но! При сборке можно указывать, какие модули и библиотеки не включать в пакет. Таким образом, у меня получались пакеты 3-3.5 Мб.
barbados
17.05.2016 15:57печально, т.к. там в apk вставляется питон, 6-7мб минимум
HeaTTheatR
17.05.2016 16:08Не совсем так. Как я уже говорил, большая половина этого объема — библиотеки, от которых при компиляции можно избавиться.
vladbarcelo
17.05.2016 14:36+1Вы бы лучше поподробнее процесс компиляции описали, это пока что самое сложное в работе с этим фреймворком.
ogoNEKto
17.05.2016 14:36Почему-то в статье ни слова, что не только пайтон — альтарнатива джаве под андроид.
Вполне годные приложения собираются в тех же Delphi XE буквально за два клика.
Для ищущих альтернативу грех не попробовать.
themtrx
17.05.2016 16:08А не могли бы Вы подсказать, как у этого фреймворка обстоят дела с WebRTC? Интересует клиентская часть, взаимодействие с камерой/микрофоном устройства. А то что-то гугл ничего путного не даёт, как и официальная документация проекта.
HeaTTheatR
17.05.2016 16:19Вы можете подключить и использовать сторонний модуль player, который предоставляет некоторые возможности по использованию камеры, акселерометра и др. функций девайса. Также можно использовать библиотеку PyJnius, и дергать все доступные для Java разработчиков Android API.
nikolay_karelin
17.05.2016 18:48А почему такие странные импорты — все завернуто в try: и потом ловится самое общее исключение???
Я правильно понимаю, что собранный пакет уже должен содержать все нужные части Kivy и можно не настолько жесткую обработку ошибок импорта делать?HeaTTheatR
17.05.2016 18:59Все необходимые библиотеки уже будут включены в установочный пакет. Насчет импортов… Не забывайте, что Kivy — это еще и десктоп и приложение могут запускать из исходников на машинах, где Kivy может не быть или он установлен с ошибками.
kronk
24.05.2016 18:33В пятой заповеди «Обнанул тебя Google» нужно исправить на Обманул. Не могу в личку так как ридонли.
ri_gilfanov
На какие платформы получилось собрать и запустить приложение?