Gestures - Creating Apps in Kivy (2014)

Creating Apps in Kivy (2014)

Chapter 7. Gestures

Kivy was designed from the ground up with fingers, rather than pointing devices, in mind as a primary method of input. You haven’t had much direct interaction with input devices in this book; instead, we’ve focused on touch events that are built into widgets supplied with Kivy. Buttons can be pressed, inputs can have text entered into them, and ListViews can be scrolled with a gesture.

In this chapter, you’ll delve a bit deeper into Kivy’s event system and add a few basic gestures to the app. The gestures will be swipe left and swipe right on the current weather and forecast widgets, which will switch to the other widget. The swipe down gesture will be used to refresh the weather. This is a fairly standard interaction feature in mobile computing.

The Forecast Tab

First, take some time to implement the forecast widget, so you have something to use gestures to switch between. As in Chapter 6, this should be old hat for you, so I’m just going to summarize the changes. Then I can focus on giving you new and interesting content! I recommend you try to implement these steps before referring to the example code.

1. Create a mockup of how you would like the forecast to look. Bear in mind that your widget has to fit on a narrow mobile screen. See Figure 7-1.

2. Add a ForecastLabel class to weather.kv to render individual forecast data. Put an icon, conditions, and temperature min and max on it. See Example 7-1.

3. Add a Forecast class extending BoxLayout to main.py and weather.kv. Give it a BoxLayout with a named id so that ForecastLabel objects can be added to it later. Also give it a button to switch to the CurrentWeather widget and hook up the event listener. See Example 7-1.

4. Create an update_weather method on Forecast that downloads three days’ worth of weather (remember to query the config for metrics) from Open Weather Map’s forecast page and creates three ForecastLabel objects on the BoxLayout. You’ll probably want to import and use Python’s datetime module here (see Example 7-2).

5. Create an appropriate show_forecast function on WeatherRoot, as shown in Example 7-3. Also hook up event listeners to call it from the button on CurrentWeather.

Mockup of the forecast widget

Figure 7-1. A three-day forecast

Example 7-1. ForecastLabel and Forecast classes

<ForecastLabel@BoxLayout>:

date: ""

conditions_image: ""

conditions: ""

temp_min: None

temp_max: None

canvas.before:

Color:

rgb: [0.2, 0.2, 0.2]

Line:

points: [self.pos[0], self.pos[1], self.width, self.pos[1]]

Label:

text: root.date

BoxLayout:

orientation: "vertical"

AsyncImage:

source: root.conditions_image

Label:

text: root.conditions

BoxLayout:

orientation: "vertical"

Label:

text: "Low: {}".format(root.temp_min)

Label:

text: "High: {}".format(root.temp_max)

<Forecast>:

forecast_container: forecast_container

orientation: "vertical"

Label:

size_hint_y: 0.1

font_size: "30dp"

text: "{} ({})".format(root.location[0], root.location[1])

BoxLayout:

orientation: "vertical"

id: forecast_container

BoxLayout:

orientation: "horizontal"

size_hint_y: None

height: "40dp"

Button:

text: "Current"

on_press: app.root.show_current_weather(root.location)

Example 7-2. Forecast class with update_weather method

classForecast(BoxLayout):

location = ListProperty(['New York', 'US'])

forecast_container = ObjectProperty()

def update_weather(self):

config = WeatherApp.get_running_app().config

temp_type = config.getdefault("General", "temp_type", "metric").lower()

weather_template = "http://api.openweathermap.org/data/2.5/forecast/" +

"daily?q={},{}&units={}&cnt=3"

weather_url = weather_template.format(

self.location[0],

self.location[1],

temp_type)

request = UrlRequest(weather_url, self.weather_retrieved)

def weather_retrieved(self, request, data):

data = json.loads(data.decode()) ifnotisinstance(data, dict) else data

self.forecast_container.clear_widgets()

for day indata['list']:

label = Factory.ForecastLabel()

label.date = datetime.datetime.fromtimestamp(day['dt']).strftime(

"%a %b %d")

label.conditions = day['weather'][0]['description']

label.conditions_image = "http://openweathermap.org/img/w/{}.png".format(

day['weather'][0]['icon'])

label.temp_min = day['temp']['min']

label.temp_max = day['temp']['max']

self.forecast_container.add_widget(label)

Example 7-3. show_forecast method

def show_forecast(self, location=None):

self.clear_widgets()

if self.forecast isNone:

self.forecast = Factory.Forecast()

if location isnotNone:

self.forecast.location = location

self.forecast.update_weather()

self.add_widget(self.forecast)

As Figure 7-2 shows, the forecast widget isn’t beautiful, but it gets the job done.

The rendered forecast picture

Figure 7-2. Seattle is going to get snow

Recording Gestures

The mathematics behind computational gesture recognition are quite complicated. Luckily, the Kivy developers have supplied a basic gesture library that allows you to record a gesture using an example tool, and then match user input gestures against the recording to test if it’s the same gesture. Sound simple? It’s not hard, but the process is a bit involved.

The first thing you want to do is to record the encoded representation of the three gestures you want to recognize. Do this using the gesture_board.py example that ships with Kivy. Hopefully you already have the Kivy source code checked out, either because you built it from scratch inChapter 1 or because you’ve been diligently reading through the source when you get stuck trying to understand something and the documentation is insufficient. If not, check it out with the command git clone http://github.com/kivy/kivy.

GETTING GIT

If you use Linux, Git is probably preinstalled, since Linux is a developer’s operating system. Git was originally designed for the distributed development of the Linux kernel and is an integral part of modern Linux development culture. If it’s not installed, it’ll be a simple process to install it using your distribution’s package manager.

If you’ve configured Mac OS for development by installing Apple’s XCode, you will also have Git installed. You’re going to need to do this anyway if you want to deploy to iOS devices, and XCode contains a dazzling number of other tools that may or may not be useful in your future endeavors as a developer on an Apple machine.

If you use Windows, you’ll want to install the MSysGit package. Windows was once considered a second-class citizen for Git usage, but MSysGit changes this. In addition to giving you the Git version control tool, it also provides an easy installer for the powerful bash command-line shell. You can (and should) use this shell instead of the default Windows command prompt for all your development, not just Git.

Then, in a terminal, cd into the kivy/examples/gestures directory. Run the command python gesture_board.py.

This will pop up a familiar black window. It looks like the default blank Kivy window with no widgets on it. This is a bit misleading, though, because it does have a widget that is designed to recognize Kivy gestures. The demo comes with four gestures preinstalled. If you draw a clockwise circular shape on the screen, as shown in Figure 7-3, you should get some output on the terminal similar to Example 7-4.

The mouse cursor made a circular gesture

Figure 7-3. A circular gesture

Example 7-4. Output after drawing a circular gesture

gesture representation: <a very long random string of characters>

cross: 0.3540801575248498

check: -2.4878193071394503

circle: 0.9506637523286905

square: 0.7579676451240299

(0.9506637523286905, <kivy.gesture.Gesture object at 0x7f513df07450>)

circle

Read this output from bottom to top. The last line of the output indicates that the app (correctly) guessed that I drew a circle. The line above that indicates the actual gesture object that was matched and with what confidence. The four lines of text above that provide the matching scores for the four sample gestures. Unless I happened to draw exactly the same circle that whoever recorded these gestures did, I will not get a score of 1.0, but the closer it is to 1.0 the more likely it is that my drawing is a representation of the same gesture.

The first line of text, which I snipped out of this example to save you from having to read a page full of gibberish (and O’Reilly from having to print it), is a textual representation of the gesture I drew. Don’t bother trying to interpret it; it’s meaningless in this form. However, this line (likely several lines since it wraps across your terminal, but there’s no newline character, so treat it as a single line) is the information you need to record if you want to create your own gestures and match them.

So, we’ll do that next. There are three gestures you want to record: a left-to-right horizontal line, a right-to-left horizontal line, and a bottom-to-top vertical line. They are pretty simple gestures. Create a new Python file named gesture_box.py. Put each string inside a dictionary namedgesture_strings, as summarized in Example 7-5, bearing in mind that your strings will be both different and longer.

Example 7-5. Summary of three gestures

gesture_strings = {

'left_to_right_line': 'eNp91XtsU...<snip>...4hSE=',

'right_to_left_line': 'eNp91UlME...<snip>...Erg==',

'bottom_to_top_line': 'eNp91HlsD...<snip>...NMndJz'

}

Now you need to write some code to convert those strings back into the format that Kivy recognizes as gestures. In the same gesture_box.py file, add some imports and construct a GestureDatabase, as shown in Example 7-6.

Example 7-6. Constructing a GestureDatabase

fromkivy.gestureimport GestureDatabase

gestures = GestureDatabase()

for name, gesture_string ingesture_strings.items():

gesture = gestures.str_to_gesture(gesture_string)

gesture.name = name

gestures.add_gesture(gesture)

Now you have a module-level database of gestures. This database can compare gestures the user makes to its three stored gestures and tell us if the user input matches any of them.

Touch Events

The next step is to record the gestures that the user makes on the screen. My plan is to create a new widget that listens to input touch events, distinguishes gestures, and fires new events for each gesture it recognizes. In short, you’re about to learn a lot more about Kivy’s event system!

Start by creating a new class named GestureBox in gesture_box.py. Make it extend BoxLayout (remember to import the BoxLayout class, since it isn’t available in this file yet).

You’ve worked with higher-level events in most of the previous chapters. Now it’s time to get a little closer to the hardware. Kivy is designed with touchscreens in mind, so you’ll be working with touch events, even if you’re using a mouse.

There are three different kinds of touch events:

§ touch_down, which corresponds to the user touching a finger on the touchscreen or pressing the mouse button.

§ touch_move, which corresponds to the user dragging a finger after touching it onto the screen, or dragging the mouse with the button down. There may be multiple (or no) move events between a down and an up event.

§ touch_up, which corresponds to lifting the finger off the screen or releasing the mouse button.

Kivy has full multitouch support, so it’s possible for more than one touch_down-initiated touch event to be active at one time. The down, move, and up events are linked for each touch, such that you can access in the touch_move and touch_up events data that was set earlier in thetouch_down event. You do this by passing a touch argument into the event handlers that contains data about the touch itself, and to which you can add arbitrary data in typical Pythonic fashion.

To recognize a gesture, you’ll need to start recording each individual event in the touch_down handler, add the data points for each call to touch_move, and then do the gesture calculations when all data points have been received in the touch_up handler. Refer to Example 7-7.

Example 7-7. GestureBox down, up, and move events

classGestureBox(BoxLayout):

def on_touch_down(self, touch):

touch.ud['gesture_path'] = [(touch.x, touch.y)]

super(GestureBox, self).on_touch_down(touch)

def on_touch_move(self, touch):

touch.ud['gesture_path'].append((touch.x, touch.y))

super(GestureBox, self).on_touch_move(touch)

def on_touch_up(self, touch):

print(touch.ud['gesture_path'])

super(GestureBox, self).on_touch_up(touch)

As with other Kivy events, you can hook up event handlers in your Python code by implementing on_eventname_. The touch object passed into each event has a ud (short for user data) dictionary that is the same for each event. Construct a gesture_path list in the down event, append each subsequent motion event to it in the move event, and then simply print it in the up event. Each event also calls its super method so that the buttons at the bottom of the screen still work.

Now, to test this code, modify the main.py file to include a from gesture_box import GestureBox import. Then make the CurrentWeather and Forecast widgets extend this new class instead of BoxLayout. The layout functionality will be the same. However, if you now drag the mouse on these widgets, you will see a collection of points output on the terminal when you release the button.

Recognizing Gestures

Now that you have the points that the user created available, you can construct a Gesture object. Then you simply have to invite the GestureDatabase object to detect whether or not the user’s input matches any of the gestures you created. Example 7-8 demonstrates.

Example 7-8. Recognizing if a gesture occurred

def on_touch_up(self, touch):

if 'gesture_path' intouch.ud:

gesture = Gesture() 1

gesture.add_stroke(touch.ud['gesture_path']) 2

gesture.normalize() 3

match = gestures.find(gesture, minscore=0.90) 4

if match:

print("{} happened".format(match[1].name)) 5

super(GestureBox, self).on_touch_up(touch)

1

You’ll also want a from kivy.gesture import Gesture at the top of the file.

2

The add_stroke method accepts the list of (x, y) tuples that have been attached to the user data in the down and move methods.

3

Normalizing a gesture basically forces it to be converted to a unit box. This means that gestures that have the same shape, but different sizes, will match at each point.

4

minscore represents how confident the algorithm is that the gesture matches anything in the database. It will return the highest-matching gesture, provided the match is higher than this score.

5

The match object returns a tuple of (score, gesture). The name of the gesture was stored with each gesture when the GestureDatabase was populated. Here, you just print it.

Firing Events

Of course, it isn’t much use to just print that a gesture happened. It would be better to fire a new event that other classes can interpret. This is reminiscent of how you listen for press events rather than touch_down events on a button.

So, time to create a new event! Instead of creating a single on_gesture event, I’m going to dynamically create a new event type for each of the three gestures I’ve defined. I’ll end up with on_left_to_right_line, on_right_to_left_line, and on_bottom_to_top_line events. This might not be the most sensible design, but it does make it trivial to respond to individual gestures from inside the KV language file.

The first step is to register the new event type with the EventDispatcher. Every widget extends the EventDispatcher class, so we can do this trivially inside a new __init__ method on the GestureBox class in gesture_box.py, as shown in Example 7-9.

You also need to add default event handlers for each of those events, also shown in Example 7-9. The default activity can be to do nothing, but the methods still need to exist.

Example 7-9. Registering new event types

def __init__(self, **kwargs):

for name ingesture_strings:

self.register_event_type('on_{}'.format(name))

super(GestureBox, self).__init__

(**kwargs)

def on_left_to_right_line(self):

pass

def on_right_to_left_line(self):

pass

def on_bottom_to_top_line(self):

pass

Then all you have to do to fire an event is call the self.dispatch method, optionally passing any arguments that you would like to arrive with the event. In Example 7-10 I don’t pass any arguments, since gestures are binary (either they happened or they didn’t) and there is no data to supply. If you created a generic on_gesture event instead of different events for each gesture, you might pass in the gesture name as an argument. In this case, the call would be self.dispatch(on_gesture, gesture.name).

Example 7-10. Firing an event with self.dispatch

if match:

self.dispatch('on_{}'.format(match[1].name))

Now that your events are firing reliably, you can add a few event handlers to your KV language file, as shown in Example 7-11 and Example 7-12.

Example 7-11. Handling the gesture events on CurrentWeather

<CurrentWeather>:

orientation: "vertical"

on_right_to_left_line: app.root.show_forecast(root.location)

on_bottom_to_top_line: root.update_weather()

Example 7-12. Handling gesture events on the Forecast widget

<Forecast>:

forecast_container: forecast_container

orientation: "vertical"

on_left_to_right_line: app.root.show_current_weather(root.location)

on_bottom_to_top_line: root.update_weather()

File It All Away

At this point, you have a fully functional weather app with the current conditions and a three-day forecast. You can switch between the two views with a simple gesture. There are a few areas of further exploration that you can study:

§ The Locations, CurrentWeather, and Forecast classes all contain a BoxLayout that has several properties (orientation, size_hint_y, and height) in common. Refactor the KV code so that these properties are stored in a dynamic class named ButtonBar.

§ For practice, add a new gesture (maybe down or a curve or circle) that does something interesting. If you want to get fancy, look into multitouch gestures. There isn’t a lot of documentation on this, but it is doable.

§ Experiment with creating new events. Perhaps you could add something to the update_weather method to fire an event if the temperature changes.

§ You can make the GestureBox initializer dynamically create the default event handlers by calling setattr on self and attaching an empty method to it. This way, you won’t have to create a new default handler if you add a new gesture.