Lecture 21 Multi-Touch
In this short chapter, you will practice working with touch interaction by implementing support for multi-touch gestures, or the ability to detect two or more different “contacts” (fingers) independently. Multi-touch is actually a pretty awesome interaction mode; it’s very “sci-fi”.
Specifically, you will be implementing an simple animated app that graphically tracks the location of all 5 of your fingers.
The code for this tutorial can be found at https://github.com/info448/lab-multitouch. Note that the starter code builds on the example presented in Lecture 15.
The emulator doesn’t support multi-touch, so you will need to run this project on a physical device.
21.1 Identifying Fingers
Android lets you react to multiple touches by responding to ACTION_POINTER_DOWN
events, which occur when a second “pointer” (finger) joins the gesture (after the ACTION_DOWN
event).
The first finger starts the gesture with an
ACTION_DOWN
event. then subsequent fingers produceACTION_POINTER_DOWN
events.Similarly, there are
ACTION_POINTER_UP
events for removing fingers, until the last finger is removed which causes theACTION_UP
event.
Practice: In MainActivity.java
, add further cases to the onTouchEvent()
callback and log out when subsequent fingers are placed and lifted.
Here the tricky part: each finger that is currently “down” can cause events independently. That is, if we move a finger, then an ACTION_MOVE
event will occur. Similarly, if we remove a finger, then an ACTION_POINTER_UP
event will occur. So the question is, how do we know which finger caused the event?
Underneath the hood, pointers are stored in a list (think: an ArrayList
), so each has a pointer index (a number representing their index in that list). But these indices can change as you interact with the device. For example, lifting up a finger will could cause that pointer to be removed from the list, thus moving all of the other pointers up an index. In fact, the index is allowed to change between each event—while they often stay in order, there is no assurance that they will. The exact behavior of these indices is not specified or enforced by the framework, so we need to treat those values as unstable and cannot use them to “track” particular fingers.
However, each pointer that comes down is assigned a consistent pointer id number that we can refer to it by. This id will be associated with that finger for the length of time that contact is being made. In general, the first finger down will be id 0
, and no matter what happens to the list order that pointer’s id will stay the same.
Practice: track pointer ids using the following procedure:
When a Touch event occurs, determine the pointer index which caused the event. Do this by calling the
getActionIndex()
method on theMotionEvent
object. Note that this only actually applies toPOINTER_DOWN
orPOINTER_UP
events, otherwise it will just return0
(for the “main finger” of the event).Get the unique pointer id for whatever finger caused the event. Do this by calling the
getPointerId(pointerIndex)
method on theMotionEvent
. This will give you the unique id associated with the event’s finger.
21.2 Drawing Touches
Once you know which pointer has gone up and down, you can respond to it by modifying the drawing displayed by the App. Add the falling functionality to the the DrawingSurfaceView
:
Add an instance variable
touches
that is aHashMap
mapping pointer ids (Integers
) toBall
objects. This will track a single “ball” for each touch.Add a method
addTouch()
that takes in a pointer id as well as thex,y
coordinates of the touch point. This method should add a newBall
(at the given coordinates) to thetouches
map (with the given pointer id).Because this method will need to work across threads in the
DrawingSurfaceView
, you should make sure the method issynchronized
(specifying that keyword in the method signature).Call this method on the drawing View from
MainActivity
when a new finger is put down—including the first finger! This may be from two different types of events.- Pass the pointer index as a parameter to the
getX()
andgetY()
methods to determine the coordinates of that particular pointer!
- Pass the pointer index as a parameter to the
Add a method
removeTouch()
that takes in a pointer id, and removes theBall
that corresponds to that touch.This method should also be
synchronized
.Call this method on the drawing View from
MainActivity
when a finger is lifted—including the last finger!
Modify the
render()
method so that the View draws each of theBall
objects in theHashMap
at their stored location. You can use gold paint for this. Recall that you can get an iterable sequence of the values for aHashMap
using the.values()
method.
This should cause your app to show a small ball underneath each finger that goes down, with the balls disappearing when a finger is lifted. Make sure each ball is big enough to see!
21.3 Moving Fingers
Now we just need to handle finger movements. With a MOVE
action, the event doesn’t track which finger has moved—instead, the pointer index of that action is always going to be 0
(the main pointer), even if a different finger moved!
However, the event does include information about how many pointers are involved in it (that is: how many pointers are currently down): we can get that number with the MotionEvent#getPointerCount()
method. We don’t know which pointer index each finger has, but we do know that they will be consecutive indices (because they are stored in a list). Moreover—as was the case previously—each pointer will have its own x
and y
coordinates, representing the current position of that pointer (this may or may not have “moved” from the previous event).
Thus we can just loop through all of the pointer indices and get the pointer id for each one. We can then specify that we want the corresponding Ball to update its position to match the “current” pointer position. Again, most of the Balls will not have moved, but we know at least one of them did and so we will just update everything to make sure it works!
Add a method
moveTouch()
to the drawing View that takes in a pointer id (e.g., Ball id), and the “latest”x,y
coordinates for that Ball. Update the appropriate Ball’s position to reflect these latest coordinates (use.get()
to access aHashMap
value by its key).- This method should again be
synchronized
.
- This method should again be
In
MainActivity
, when aMOVE
event occurs, loop through all of the pointer indices in the event. Get the pointer id andx,y
coordinates of each, and use those to call themoveTouch()
method on the drawing View. You will be “moving” all of the balls, even if most are just moving to the same place they were.
And with that, you should have multi-touch tracking! Try adding and removing fingers in unique orders, moving them around, and make sure that the Balls follow the contacts.
- Note that tracking individual ids in this way is more commonly used to make sure you’re ignoring extra multiple touches. See the docs or this developer blog post for details.
21.4 Other Multi-Touch Gestures
We can respond to common multi-touch gestures (like “pinch to scale”) by using another kind of GestureDetector called a ScaleGestureDetector
. As before, we subclass the simple version (ScaleGestureDetector.SimpleOnScaleGestureListener
), and fill in the onScale()
method. You can get the “scale factor” from the gesture with the .getScaleFactor()
method. As a further bonus exercise, you might try to use the gesture to scale the size of a single ball.