Секундомер под Android на Python + sl4a + fullScreenUI

в 12:42, , рубрики: android development, python, sl4a, Разработка под android, метки: , ,

Вступление

Такая замечательная вещь как SL4A(Scripting Level for Android) уже давно не является новостью. С каждым новым релизом SL4A возможности API для доступа/управления смартфоном растут. Еще до недавних пор создание пользовательского интерфейса ограничивалось средствами webView и стандартными диалоговыми окнами. Но в версии r5 появился новый, как заявили разработчики, пока что экспериментальный, способ создания пользовательского интерфейса — fullScreenUI.
FullScreenUI позволяет создавать интерфейс, используя стандартные виджеты Android-а (кнопки, текстовые поля, радиокнопки, и проч.), а также обрабатывать события от них. На примере создания простого секундомера я хочу продемонстрировать возможности этого API.

Я рассчитываю, что вы уже знакомы с SL4A(если нет — то Хабре достаточно много полезной и интересной информации).

Что получится

Вот скрины конечного результата:
Секундомер под Android на Python + sl4a + fullScreenUI Секундомер под Android на Python + sl4a + fullScreenUI

Разметка

Для начала создадим разметку нашего интерфейса. Это стандартная 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" – разместит виджет под виджетом с указаным id
  • android:layout_toRightOf = "id/center" — разместит виджет слева от виджета с указаным id
  • android:layout_centerHorizontal="true" – разместит по центру родительского виджета
  • и т.д

Button с id «center» созданный только для того, чтобы кнопки «Старт» и «Стоп» растянулись до него(он размещен по центру). Вот что должно получится:

Секундомер под Android на Python + sl4a + fullScreenUI

Код

Собственно сам код секундомера. Некоторый тривиальный или дублирующийся код опущен. Полная версия: 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

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