The postings on this site are the contributor's and don’t necessarily represent IBM’s positions, strategies or opinions.
March 28th, 2010 Alex Moffat Posted in GWT | 13 Comments »
The goal is to be able to handle the touchstart, touchmove, touchend and touchcancel events provided by mobile Safari by writing standard GWT code like the example below for touchstart.
touchable.addTouchStartHandler(new TouchStartHandler() {
public void onTouchStart(TouchStartEvent event) {
event.preventDefault();
addStyleDependentName("touched");
Touch t = event.touches().get(0);
offsetX = t.pageX() - getAbsoluteLeft();
offsetY = t.pageY() - getAbsoluteTop();
}
});
I’ll walk through the steps needed to get this working, concentrating on the implementation needed for the touchstart event.
The easiest place to start is by building the EventHandler and Event subclasses. The code below is the TouchStartHandler. It’s just extends EventHandler.
public interface TouchStartHandler extends EventHandler {
void onTouchStart(TouchStartEvent event);
}
The TouchStartEvent class is also simple. It implements the abstract methods getAssociatedType and dispatch defined by GWTEvent. I chose to implement a TouchEvent abstract superclass for the TouchStart, TouchMove, TouchEnd and TouchCancel classes following the pattern of the MouseDown, MouseUp etc events provided by GWT. The start of the TouchStartEvent class looks like
public class TouchStartEvent extends TouchEvent<TouchStartHandler> {
private static final Type<TouchStartHandler> TYPE =
new Type<TouchStartHandler>("touchstart", new TouchStartEvent());
public static Type<TouchStartHandler> getType() {
return TYPE;
}
// getAssociatedType and dispatch
}
Notice the value “touchstart” provided as the value of the eventName parameter to the DomEvent.Type constructor. This is used when hooking up the handlers so that they are called when touchstart events are fired by the browser. It does not have to be the native event name but things are less confusing if you follow the convention that it is.
A basic implementation of the TouchEvent superclass just provides access to the touches array of touch objects provided by the native JavaScript.
public abstract class TouchEvent<H extends EventHandler> extends DomEvent<H> {
public JsArray<Touch> touches() {
return touches(getNativeEvent());
}
private native JsArray<Touch> touches(NativeEvent nativeEvent) /*-{
return nativeEvent.touches;
}-*/;
}
I used overlay types to implement Touch so there’s no overhead. The fragment below just shows access to the pageX value.
public class Touch extends JavaScriptObject {
protected Touch() {
}
public final native int pageX() /*-{
return this.pageX;
}-*/;
Now all these pieces are in place it possible to write the code that will add support for touch events to a GWT widget. For example you would be able to extend SimplePanel to create TouchPanel and add this method.
public HandlerRegistration addTouchStartHandler(TouchStartHandler handler) {
return addDomHandler(handler, TouchStartEvent.getType());
}
As you probably expect this doesn’t work. To see why we need to take a look at the implementation of addDomHandler in the Widget class and understand a little about how GWT handles events. The critical line in addDomHandler is
sinkEvents(Event.getTypeInt(type.getName()));
This is what eventually sets the ontouchstart property of the widget’s element.
Each different type of event supported by GWT has a corresponding integer code returned by Event.getTypeInt. Each code is a single bit so that the codes can be combined with an or operation as a bit field. The bit field is used when event handlers are added or removed to know which handlers are currently set on an element. Adding a new type of event requires that Event.getTypeInt return a new unique value for that type which works with the bit field mechanism and the values used for all the existing types. If you trace through Event.getTypeInt you end up at the implementation in eventGetTypeInt in DOMImpl. It’s just a large switch statement with case statements for each type value. For example
case "load": return 0x08000;
case "losecapture": return 0x02000;
case "mousedown": return 0x00004;
The correct subclass of DOMImpl is chosen at compile time using deferred binding so to support these new events you need a subclass appropriate for mobile Safari. My post “The right way to add support for a new browser to GWT” explains how to do this.
I copied the code for eventGetTypeInt from DOMImpl into my DOMImplMobileSafari class and added new case statements for the new touch* event type values.
case "touchstart": return 0x100000;
case "touchmove": return 0x200000;
case "touchcancel": return 0x400000;
case "touchend": return 0x800000;
An alternative I experimented with was to override eventGetTypeInt with a non-native method, detect whether the native method returned an undefined value, which indicates no case statement matched, and use an extra switch statement to deal with the touch events. In my opinion the extra complexity wasn’t worth it, if GWT adds extra event types the overridden code will have to be investigated anyway to ensure the int values I’m using are still unique.
Next you need to look at the call to sinkEvents in Widget.addDomHandler. This is where the bit pattern returned from Event.getTypeInt is combined with the patterns from other events already being listened for by the widget to determine which event dispatch handlers should be registered. Tracing through the code leads to sinkEventsImpl in DOMImplStandard. This is the method that needs to be overridden in DOMImplMobileSafari. Each separate event has a corresponding if statement that checks whether the current call to sinkEventsImpl is targeting that event and if it is whether the dispatcher function should be added or removed.
if (chMask & 0x00020) elem.onmouseout = (bits & 0x00020) ?
@com.google.gwt.user.client.impl.DOMImplStandard::dispatchEvent : null;
As with the eventGetTypeInt method I just copied the existing code and added new statements to the end for the touch* bit patterns, for instance
if (chMask & 0x100000) elem.ontouchstart = (bits & 0x100000) ?
@com.google.gwt.user.client.impl.DOMImplStandard::dispatchEvent : null;
With this change the solution is complete and the addTouchStartHandler presented earlier works correctly.
March 29th, 2010 at 4:18 am
[...] This post was mentioned on Twitter by Vincent Bostoen and Rahul Garg, Christophe PHU. Christophe PHU said: RT @rahul_garg: Supporting multi-touch events with GWT on mobile Safari http://bit.ly/boXlOn [...]
March 29th, 2010 at 1:51 pm
[...] has a nice blog post about how to get multi-touch to work on mobile Safari using the touchstart, touchmove, touchend and touchcancel events. Together with HTML5 support this [...]
May 1st, 2010 at 4:16 am
Any chance of releasing the full code for this?
Buggered if i can get it to work
May 1st, 2010 at 2:28 pm
I’d like to. Unfortunately I need to check with the powers that be at IBM before I can do so, that process in underway. If you can describe the problems etc. then I can certainly help out in that way.
May 7th, 2010 at 7:51 pm
So… I basically followed the directions here and also got nothing to work. Which leads me to my big question: How did you go about debugging? You mentioned stepping through the debugger a couple of times, and I must confess I do not know how to get the debugger working with my iPad. Any help would be greatly appreciated.
May 10th, 2010 at 9:43 am
Here’s the code I created to be able to receive touch events. Hopefully this will help. I’d be glad to try and answer any questions. touch.tar.gz The stepping through the debugger I did was to discover how the original event handing was working in GWT in regular safari. Debugging on the iPad / iPhone or the simulator is frustrating. What I’ve been reduced to is adding a label to the ui and then writing simple log messages to that label. I think a better approach, which I’ve not tried yet, would be to accumulate messages in a buffer and send these back to the server. This would probably work well for tracing events. In general what I’ve tried to do is isolate the touch dependent methods from the rest of the code and then test as much as possible using Chrome / Safari with GWT Dev mode.
May 11th, 2010 at 3:24 pm
Thanks for a very interesting developer blog!
I tried your example and it seems to work fine except for when i run it in the latest ff using OOPHM (hosted). Then I get the exception:
Something other than an int was returned from JSNI method ‘@com.google.gwt.user.client.impl.DOMImpl::eventGetTypeInt(Ljava/lang/String;)’: JS value of type undefined, expected int
any ideas?
Thanks again
/Marcus
May 12th, 2010 at 3:10 am
It’s the same error (in OOPHM hosted) in the latest chrome version by the way.
May 14th, 2010 at 2:47 pm
The article is very precise and clear.
Your code too, you saved me much work.
Thanks a lot
Alessandro
May 14th, 2010 at 9:30 pm
I’m not seeing this error. What do you do in the browser to get it to happen? Thanks Alex.
May 15th, 2010 at 3:26 pm
(Alex, please drop my previous post, I did some reading which answered my 2 questions, now only the exception in oophm remains)
Thanks for your reply Alex!
I get the exception on startup in ff/ie/chrome in hosted mode (OOPHM) when I add any of your touchhandlers to the TouchableFocusPanel. The simplest way for me to reproduce the exception is:
public void onModuleLoad() {
try {
final TouchableFocusPanel tfp = new TouchableFocusPanel();
tfp.addTouchStartHandler(new TouchStartHandler() {
@Override
public void onTouchStart(TouchStartEvent event) {}
});
RootPanel.get().add(tfp);
} catch (Exception e)
{
e.printStackTrace();
}
}
Thanks again,
/Marcus
May 16th, 2010 at 9:56 pm
On FireFox the problem is certainly that you can’t use the TouchableFocusPanel. I can’t test on Chrome because I upgraded to the latest beta version and so lost access to the gwt dev mode plugin. When you add a handler for a touch event, like a TouchStartHandler, the relevant part of the code path is Widget.addDomHandler -> Event.getTypeInt -> DOM.impl.eventGetTypeInt. The final bit, eventGetTypeInt is where the string name of the event, for example touchstart, is converted to the appropriate int value. When you’re using FireFox the method invoked is DOMImpl.eventGetTypeInt, which does not handle the touchstart value. The DOMImplMobileSafari class does but that is only used by permutations targeting mobile safari. What I did to get code that could be compile to support different browsers was to use a factory instantiated by GWT.create to produce the correct type of panel with browser level events already hooked up depending on whether the target is mobile safari (touch) or other browsers (mouse)
May 17th, 2010 at 1:20 pm
Ok, I see, that makes sense. Thanks for your insights and a very interesting blog!
/Marcus