KIVY FOR DESPERATE MOBILE CODERS
Marco Federighi, Coder @ Nephila
Cross-platform Python Framework for NUI Development
http://kivy.org/docs/gettingstarted/installation.html
Kivy app life cycle
import kivy
kivy.require('1.7.2')
from kivy.app import App
from kivy.uix.label import Label
class DoAlmostNothingApp(App):
def build(self):
self.title = 'Hello Django Beer!'
return Label(text='Stupid label')
if __name__ == '__main__':
DoAlmostNothingApp().run()
$ python main.py
Run your application (Desktop)
# Syntax of a rule definition. Note that several Rules can share the same
# definition (as in CSS). Note the braces: they are part of the definition.
<Rule1,Rule2>:
# .. definitions ..
<Rule3>:
# .. definitions ..
# Syntax for creating a root widget
RootClassName:
# .. definitions ..
# Syntax for creating a dynamic class
<NewWidget@BaseClass>:
# .. definitions ..
# Syntax for create a template
[TemplateName@BaseClass1,BaseClass2]:
# .. definitions ..
root = MyRootWidget()
box = BoxLayout()
box.add_widget(Button())
box.add_widget(Button())
root.add_widget(box)
MyRootWidget:
BoxLayout:
Button:
Button:
It's the same thing!
Where to place your Kv code:
class PythonClass(object):
def __init__(self, num=1.0):
super(PythonClass, self).__init__()
self.num = num
class KivyClass(EventDispatcher):
num = NumericProperty(1.0)
It's not the same thing!
Kivy handles:
buildozer init
To create a buildozer config in your project folder:
buildozer -v android debug
To make the build for Android:
Hint: Edit properly the buildozer.spec
The .apk will be put in /bin/ folder created by buildozer
Hint: check your libs version and dependencies as descrived in docs
<MenuScreen>:
BoxLayout:
orientation: 'vertical'
Button:
id: 'login_btn'
text: 'Login'
on_press: root.goto()
Button:
text: 'Settings'
on_press: root.manager.current = 'settings'
<SettingsScreen>:
BoxLayout:
orientation: 'vertical'
Label:
text: 'Host:'
TextInput:
id: host
multiline: False
Label:
text: 'Port:'
TextInput:
id: port
multiline: False
Button:
text: 'Back to menu'
size_hint_y: None
height: '48dp'
on_press: root.save_config(host.text, port.text); root.manager.current = 'menu'
<CameraScreen>:
BoxLayout:
orientation: 'vertical'
Button:
text: 'Snapshot'
on_press: root.make_photo()
size_hint_y: None
height: '48dp'
Button:
id: back_button
text: 'Back'
size_hint_y: None
height: '48dp'
on_press: root.manager.current = 'messages'
<MessageScreen>:
BoxLayout:
orientation: 'vertical'
Button:
id: btn_send
text: 'Send'
size_hint_y: None
height: '48dp'
on_press: root.send_message(message_text.text); message_text.text=""
Button:
id: btn_makephoto
text: 'Photo'
size_hint_y: None
height: '48dp'
on_press: root.manager.current = 'photo'
TextInput:
id: message_text
multiline: True
Button:
text: 'Back to menu'
size_hint_y: None
height: '48dp'
on_press: root.manager.current = 'menu'
<LoginScreen>:
orientation: 'vertical'
GridLayout:
rows: 1
cols: 1
GridLayout:
rows: 3
cols: 2
Label:
text: 'Username:'
TextInput:
id: username
multiline: False
Label:
text: 'Password:'
TextInput:
id: password
password: True
multiline: False
Button:
id: send_button
text: 'Send'
size_hint_y: None
height: '48dp'
on_press: root.login(username.text, password.text)
Button:
id: back_button
text: 'Back to the menu'
size_hint_y: None
height: '48dp'
on_press: root.manager.current = 'menu'
....
from plyer import camera
import base64
import json
import urllib
import os
__version__ = '1.0.0'
Builder.load_file('./client.kv')
default_config = {'host': 'localhost', 'port': 8000}
...
....
class MenuScreen(Screen):
def goto(self):
if not os.path.exists(os.path.join(App.get_running_app().user_data_dir, "usr_auth")):
if not os.path.exists(os.path.join(App.get_running_app().user_data_dir, "client_config")):
with open(os.path.join(App.get_running_app().user_data_dir, "client_config"), 'wb') as f:
json.dump(default_config, f)
sm.current = 'login'
else:
sm.current = 'messages'
class SettingsScreen(Screen):
def save_config(self, host, port):
if host and port:
with open(os.path.join(App.get_running_app().user_data_dir, 'client_config'), 'wb') as f:
config = {'host': host, 'port': port}
json.dump(config, f)
...
....
class CameraScreen(Screen):
def make_photo(self):
try:
self.add_widget(CameraLayout())
except:
cam = Camera(resolution=(640, 480), play=True)
if cam.texture:
cam.texture.save()
sm.current = 'messages'
class CameraLayout(FloatLayout):
def __init__(self, **kwargs):
super(CameraLayout, self).__init__(**kwargs)
self.lblCam = Label(text="Click to take a picture!")
self.add_widget(self.lblCam)
def on_touch_down(self, e):
try:
camera.take_picture('/storage/sdcard0/snapshot.jpg', self.done)
except:
pass
def done(self, e):
sm.current = 'messages'
...
....
class MessageScreen(Screen):
def send_message(self, text):
if text.strip():
self.pb = None
def loading(request, current_size, total_size):
self.pb.value += current_size
if self.pb.value >= total_size:
self.popup.dismiss()
if os.path.exists('/storage/sdcard0/snapshot.jpg'):
os.remove('/storage/sdcard0/snapshot.jpg')
with open(os.path.join(App.get_running_app().user_data_dir, 'usr_auth'), 'rb') as f:
auth_data = json.load(f)
headers = {
'Content-type': 'application/x-www-form-urlencoded',
'Authorization': 'Token {0}'.format(auth_data['token'])
}
data_to_send = {'message': text}
if os.path.exists('/storage/sdcard0/snapshot.jpg'):
data_to_send['photo'] = base64.b64encode(open('/storage/sdcard0/snapshot.jpg', 'rb').read())
params = urllib.urlencode(data_to_send)
with open(os.path.join(App.get_running_app().user_data_dir, 'client_config'), 'rb') as ff:
config = json.load(ff)
req = UrlRequest('http://{0}:{1}/messages/'.format(config['host'], config['port']), req_body=params,
req_headers=headers, timeout=10, on_progress=loading)
self.pb = ProgressBar() #100
self.popup = Popup(title='Sending message', content=self.pb, size_hint=(0.7, 0.3))
self.popup.open()
...
....
class LoginScreen(Screen):
def login(self, user, pwd):
if user.strip() and pwd.strip():
def loading(request, current_size, total_size):
self.pb.value += current_size
if self.pb.value >= total_size:
self.popup.dismiss()
def save_auth(req, result):
if 'token' in result:
with open(os.path.join(App.get_running_app().user_data_dir, 'usr_auth'), 'wb') as f:
json.dump(result, f)
sm.current = 'messages'
params = json.dumps({'username': user, 'password': pwd})
headers = {'Content-type': 'application/json',
'Accept': 'application/json'}
with open(os.path.join(App.get_running_app().user_data_dir, 'client_config'), 'rb') as ff:
config = json.load(ff)
req = UrlRequest('http://{0}:{1}/api-token-auth/'.format(config['host'], config['port']), req_body=params,
req_headers=headers, timeout=10, on_success=save_auth, on_progress=loading)
self.pb = ProgressBar()
self.popup = Popup(title='Get token from the server...', content=self.pb, size_hint=(0.7, 0.3))
self.popup.open()
...
....
sm = ScreenManager(transition=SlideTransition())
sm.add_widget(MenuScreen(name='menu'))
sm.add_widget(SettingsScreen(name='settings'))
sm.add_widget(MessageScreen(name='messages'))
sm.add_widget(CameraScreen(name='photo'))
sm.add_widget(LoginScreen(name='login'))
class ClientApp(App):
def build(self):
self.title = 'Message Board'
return sm
def on_pause(self):
return True
def on_resume(self):
pass
if __name__ == '__main__':
ClientApp().run()
...
Me and my colleagues realized a simple demo of a 2d horizontal shooter, it has ugly graphic and no sound, but it's made in Kivy and it runs on Android.
Source: https://github.com/nephilahacks/spider-eats-the-kiwi
https://github.com/fmarco