Вступление
Такая замечательная вещь как SL4A(Scripting Level for Android) уже давно не является новостью. С каждым новым релизом SL4A возможности API для доступа/управления смартфоном растут. Еще до недавних пор создание пользовательского интерфейса ограничивалось средствами webView и стандартными диалоговыми окнами. Но в версии r5 появился новый, как заявили разработчики, пока что экспериментальный, способ создания пользовательского интерфейса — fullScreenUI.
FullScreenUI позволяет создавать интерфейс, используя стандартные виджеты Android-а (кнопки, текстовые поля, радиокнопки, и проч.), а также обрабатывать события от них. На примере создания простого секундомера я хочу продемонстрировать возможности этого API.
Я рассчитываю, что вы уже знакомы с SL4A(если нет — то Хабре достаточно много полезной и интересной информации).
Что получится
Вот скрины конечного результата:
Разметка
Для начала создадим разметку нашего интерфейса. Это стандартная Android-овская xml-разметка(подробнее о ней можно узнать на http://developer.android.com/guide/topics/ui/index.html). Конечно же SL4A не поддерживает всех тонкостей разметки и еще недавно не поддерживал очень важного типа разметки RelativeLayout, но с версии r6 эта возможность стала поддерживаться.
Рассмотрим саму разметку:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:id="@+id/MainWidget"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
xmlns:android="http://schemas.android.com/apk/res/android">
android:background="#ff000000"
<TextView
android:id="@+id/display"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:textColor="#0bda51"
android:text="00:00:00.000"
android:textStyle="bold"
android:gravity="center"
android:textSize="60dp" />
<Button
android:id="@+id/startbutton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@id/display"
android:layout_alignParentLeft="true"
android:textSize="40dp"
android:layout_toLeftOf = "@id/center"/>
<Button
android:id="@+id/center"
android:layout_below="@id/display"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_centerHorizontal="true" />
<Button
android:id="@+id/stopbutton"
android:layout_width="0pdp"
android:layout_height="wrap_content"
android:enabled="false"
android:textSize="40dp"
android:layout_below="@id/display"
android:layout_alignParentRight="true"
android:layout_toRightOf = "@id/center"/>
<TextView
android:id="@+id/info"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_below="@id/stopbutton"
android:textColor="#FFFFFF"
android:text=""
android:textStyle="bold"
android:layout_alignParentBottom="true"
android:textSize="30dp"
android:layout_alignParentBottom="true"/>
</RelativeLayout>
Если вы даже первый раз видите эту разметку, но имеете некий опыт работы с HTML или XML — там все должно быть понятно. Если говорить о поддержке RelativeLayout, то полноценное использование этого типа разметки стала возможным после того, как стали поддерживаться атрибуты такого типа, как
android:layout_alignParentBottom="true"
— нижний край виджета будет выровнян по нижнем крае родительского виджета(аналагочно дляlayout_alignParentTop
,layout_alignParentLeft
,layout_alignParentRight
)android:layout_below="id/display"
– разместит виджет под виджетом с указаным idandroid:layout_toRightOf = "id/center"
— разместит виджет слева от виджета с указаным idandroid:layout_centerHorizontal="true"
– разместит по центру родительского виджета- и т.д
Button с id «center» созданный только для того, чтобы кнопки «Старт» и «Стоп» растянулись до него(он размещен по центру). Вот что должно получится:
Код
Собственно сам код секундомера. Некоторый тривиальный или дублирующийся код опущен. Полная версия: http://pastebin.com/z4H2p7Wq
Да простит меня сообщество Python-а за некрасивый код и за глобальные переменные, просто с этим языком программирования я познакомился совсем недавно.
#StopWatch.py
#------------Ресурсы------------
layout="""<?xml version="1.0" encoding="utf-8"?>
...
</RelativeLayout>
"""
rCircle_label = "Круг"
rStart_label = "Старт"
rClear_label = "Очистить"
rStop_label = "Стоп"
#---------------------
import android, os, datetime
#Глобальные переменные
starttime=datetime.datetime.now()
runed = False #если True то таймер запущен
lastcircle = 0
cleared = True #если True, то показания секундомера очищены
def format_time(tm):
hours = int(tm.seconds / 3600)
minuts = int((tm.seconds - hours*3600)/60)
seconds=tm.seconds - hours*3600 - minuts*60
microseconds = round(tm.microseconds/1000)
return "{0:0>02}:{1:0>02}:{2:0>02}.{3:0>03}".format(hours,minuts,seconds,microseconds)
#Возвращает разницу времени в виде строки. Если now =0, то разница с текущим временем
def timediff(prev, now=0):
if not now: now=datetime.datetime.now()
diff=now-prev
return format_time(diff)
def stopwatch_start():
global runed,lastcircle,starttime
runed=True
starttime=datetime.datetime.now()
lastcircle = starttime
droid.fullSetProperty("startbutton","text",rCircle_label)
droid.fullSetProperty("stopbutton","enabled","true")
def stopwatch_circle():
#код опущен
# t - сформированная строка с временем круга
lastdata = droid.fullQueryDetail("info").result['text']
newdata = lastdata+"n"+t;
droid.fullSetProperty("info","text",newdata)
lastcircle = datetime.datetime.now()
def stopwatch_stop():
#код опущен
def stopwatch_clear():
#код опущен
def eventloop():
while True:
event=droid.eventWait(50).result
if runed:
droid.fullSetProperty("display","text",timediff(starttime))
if event != None:
if event["name"]=="key":
droid.vibrate(30)
if event["data"]["key"] == '4':
return
elif event["data"]["key"]=='24' and cleared:
if runed:
stopwatch_circle()
else:
stopwatch_start()
elif event["data"]["key"]=='25' and runed:
stopwatch_stop()
elif event["name"]=="click":
droid.vibrate(30)
id=event["data"]["id"]
if id=="startbutton" and not runed:
stopwatch_start()
elif id=="stopbutton" and runed:
stopwatch_stop()
elif id=="stopbutton" and not runed:
stopwatch_clear()
elif id=="startbutton" and runed:
stopwatch_circle()
elif event["name"]=="screen":
if event["data"]=="destroy":
return
droid = android.Android()
try:
print(droid.fullShow(layout))
droid.fullKeyOverride([24,25],True)
droid.fullSetProperty("MainWidget","background","#ff000000")
droid.fullSetProperty("startbutton","text",rStart_label)
droid.fullSetProperty("stopbutton","text",rStop_label)
eventloop()
finally:
droid.fullDismiss()
Разбор кода
Итак, чтобы отобразить созданный нами в виде xml-разметки интерфейс надо передать строку с разметкой в функцию droid.fullShow
. Можно конечно создать отдельный файл с разметкой и потом его прочитать, но в случае, когда разметка несложная, как у меня, я просто присвоил ее переменной layout
. В отладочных целях результат, возвращаемыйdroid.fullShow
можно вывести на консоль:
print(droid.fullShow(layout))
Если в разметке были ошибки, или поддерживаемые атрибуты, будет выведено соответствующее сообщение. После вызова этой функции, если разметка была корректной, она отобразится на экране аппарата. Чтобы убрать ее нужно вызвать функцию:
droid.fullDismiss()
Если эта функция не будет вызвана по каким то причинам как например аварийное завершения программы, то созданный интерфейс не очистится автоматически, а просто останется, поэтому важно воспользоваться конструкцией try finally
Следующая строчка:
droid.fullKeyOverride([24,25],True)
заменяет стандартное поведение при нажатии клавиш с кодами 24 и 25(это клавиши громкости, коды других клавиш здесь). Что это значит? Это значит, что если наш скрипт запущен, и нажата одна из этих клавиш — то стандартное действие этих клавиш не будет выполнено(громкость звука не будет изменена в данном конкретном случае).
Для изменения свойств виджетов из сценария имеется функция droid.fullSetProperty
, которая принимает три параметра: id виджета, название свойства, присваиваемое значение. Например этой строчкой
droid.fullSetProperty("startbutton","text",rStart_label)
мы меняем надпись на кнопке.
Для запроса свойств droid.fullQueryDetail
, принимает одно значение — id виджета. Для примера, получить значение свойства — text
текстового поля с id info
можно так:
droid.fullQueryDetail("info").result['text']
Обработка сообщений
Все действия пользователя, как например нажатие на кнопку, или клавишу, в сценарий попадают в виде сообщений.
Обработка сообщений удобно реализовать в отдельной функции. У меня функции eventloop()
. Для обработки сообщений имеется ряд функций. Для этого примера удобно было использовать: droid.eventWait
. Эта функция останавливает сценарий, пока не будет получено некое сообщение. Принимает необязательный параметр — максимальное время ожидания в мс. Если за это время сообщение не было получено, сценарий продолжит выполнение, а результатом функции будет объект None
. Если же сообщение получено, то результатом выполнения
event=droid.eventWait(50).result
будет ассоциативный массив с именем события и информацией о нем.
Для начала проверяем значение event["name"]
, если оно равно «key», то была нажата кнопка, код которой можно узнать из event["data"]["key"]
. Если же оно равно «click», то было нажатия на кнопку(или другой виджет), id которого можно узнать из event["data"]["id"]
.
Я надеюсь, что остальной код вполне понятный и не требует обяснений.
Итог
С появлением fullScreenUI в sl4a разработчики на Python, Perl, JRuby, Lua, BeanShell, JavaScript теперь могут конкурировать с разработчиками на Java. Хотя и поддержка fullScreenUI в sl4a еще далека от идеала, но все же создавать достаточно хороший, быстрый графический интерфейс для своих сценариев уже можно. Разработчики постоянно занимаются усовершенствованием данного API, что очень радует.
Автор: RomanGotsiy