Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Converting GWT Click Events to Touch Events

Tags:

tablet

touch

gwt

I am working on a Big Project and i have a lot of GWT code written. Now i am working on making the project fully compatible with Tablets like iPad and Android Tablets.

As a part of this, i have noticed that touch devices takes 300ms delay to handle click events. In this project, writing touch events again is a very tedious task. I have done a lot of researches in this and found the Google Fast Buttons API used in Google Voice Application. I tried that and its working good but requires a lot of coding and JSNI.

My question is, Is there anything else available in your knowledge to easily overcome this delay?

like image 521
Vijay Sarin Avatar asked Mar 07 '12 06:03

Vijay Sarin


4 Answers

Here is a pure java implementation of the fast button.It doesn't include a single line of JNSI

package com.apollo.tabletization.shared.util;

import java.util.Date;

import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Touch;
import com.google.gwt.event.dom.client.HasAllTouchHandlers;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Widget;

/** Implementation of Google FastButton {@link http://code.google.com/mobile/articles/fast_buttons.html} */
public class FastButton extends Composite {

  private boolean touchHandled = false;
  private boolean clickHandled = false;
  private boolean touchMoved = false;
  private int startY;
  private int startX;
  private int timeStart;

  public FastButton(Widget child) {
    // TODO - messages
    assert (child instanceof HasAllTouchHandlers) : "";
      assert (child instanceof HasClickHandlers) : "";
        initWidget(child);
        sinkEvents(Event.TOUCHEVENTS | Event.ONCLICK);
  }

  @Override
  public Widget getWidget() {
    return super.getWidget();
  }

  @Override
  public void onBrowserEvent(Event event) {
    timeStart = getUnixTimeStamp();
    switch (DOM.eventGetType(event)) {
      case Event.ONTOUCHSTART:
        {
          onTouchStart(event);
          break;
        }
      case Event.ONTOUCHEND:
        {
          onTouchEnd(event);
          break;
        }
      case Event.ONTOUCHMOVE:
        {
          onTouchMove(event);
          break;
        }
      case Event.ONCLICK:
        {
          onClick(event);
          return;
        }
    }

    super.onBrowserEvent(event);
  }

  private void onClick(Event event) {
    event.stopPropagation();

    int timeEnd = getUnixTimeStamp();
    if(touchHandled) {
      //Window.alert("click via touch: "+ this.toString() + "..." +timeStart+"---"+timeEnd);
      touchHandled = false;
      clickHandled = true;
      super.onBrowserEvent(event);
    }
    else {  
      if(clickHandled) {

        event.preventDefault();
      }
      else {
        clickHandled = false;
        //Window.alert("click nativo: "+ this.toString()+ "..." +(timeStart-timeEnd)+"==="+timeStart+"---"+timeEnd);
        super.onBrowserEvent(event);
      }
    }
  }

  private void onTouchEnd(Event event)  {
    if (!touchMoved) {
      touchHandled = true;
      fireClick();
    }
  }

  private void onTouchMove(Event event)  {
    if (!touchMoved) {
      Touch touch = event.getTouches().get(0);
      int deltaX = Math.abs(startX - touch.getClientX()); 
      int deltaY = Math.abs(startY - touch.getClientY());

      if (deltaX > 5 || deltaY > 5) {
        touchMoved = true;
      }
    }
  }

  private void onTouchStart(Event event) {
    Touch touch = event.getTouches().get(0);
    this.startX = touch.getClientX();
    this.startY = touch.getClientY();               
    touchMoved = false;
  }

  private void fireClick() {
    NativeEvent evt = Document.get().createClickEvent(1, 0, 0, 0, 0, false,
        false, false, false);
    getElement().dispatchEvent(evt);
  }

  private int getUnixTimeStamp() {
    Date date = new Date();
    int iTimeStamp = (int) (date.getTime() * .001);
    return iTimeStamp;
  }
}  
like image 128
sunnychayen Avatar answered Nov 03 '22 22:11

sunnychayen


I have tried to use the above answers and comments to take a stab at this implementation.

I have also posteds a sample GWT project which can be used for easy comparison:

http://gwt-fast-touch-press.appspot.com/

https://github.com/ashtonthomas/gwt-fast-touch-press

Please note that you will only be able to see the time saved if you are on a mobile device (or devices that handles the touch events and doesn't just fall back to onClick).

I have added 3 fast buttons and 3 normal buttons. You can easily see an improvement when on older mobile devices and sometimes less so on newer (Samsung Galaxy Nexus only showed delays of around 100ms while the 1st gen iPad was over 400ms almost every time). The biggest improvement is when you try to rapidly and consecutively click the boxes (not really buttons here but can be adapted)

package io.ashton.fastpress.client.fast;

import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Touch;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Widget;

/**
 *
 * GWT Implementation influenced by Google's FastPressElement:
 * https://developers.google.com/mobile/articles/fast_buttons
 *
 * Using Code examples and comments from:
 * http://stackoverflow.com/questions/9596807/converting-gwt-click-events-to-touch-events
 *
 * The FastPressElement is used to avoid the 300ms delay on mobile devices (Only do this if you want
 * to ignore the possibility of a double tap - The browser waits to see if we actually want to
 * double top)
 *
 * The "press" event will occur significantly fast (around 300ms faster). However the biggest
 * improvement is from enabling fast consecutive touches.
 *
 * If you try to rapidly touch one or more FastPressElements, you will notice a MUCH great
 * improvement.
 *
 * NOTE: Different browsers will handle quick swipe or long hold/drag touches differently.
 * This is an edge case if the user is long pressing or pressing while dragging the finger
 * slightly (but staying on the element) - The browser may or may not fire the event. However,
 * the browser will always fire the regular tap/press very quickly.
 *
 * TODO We should be able to embed fastElements and have the child fastElements NOT bubble the event
 * So we can embed the elements if needed (???)
 *
 * @author ashton
 *
 */
public abstract class FastPressElement extends Composite implements HasPressHandlers {

  private boolean touchHandled = false;
  private boolean clickHandled = false;
  private boolean touchMoved = false;
  private boolean isEnabled = true;
  private int touchId;
  private int flashDelay = 75; // Default time delay in ms to flash style change

  public FastPressElement() {
    // Sink Click and Touch Events
    // I am not going to sink Mouse events since
    // I don't think we will gain anything

    sinkEvents(Event.ONCLICK | Event.TOUCHEVENTS); // Event.TOUCHEVENTS adds all (Start, End,
                                                   // Cancel, Change)

  }

  public FastPressElement(int msDelay) {
    this();
    if (msDelay >= 0) {
      flashDelay = msDelay;
    }
  }

  public void setEnabled(boolean enabled) {
    if (enabled) {
      onEnablePressStyle();
    } else {
      onDisablePressStyle();
    }
    this.isEnabled = enabled;
  }

  /**
   * Use this method in the same way you would use addClickHandler or addDomHandler
   *
   */
  @Override
  public HandlerRegistration addPressHandler(PressHandler handler) {
    // Use Widget's addHandler to ensureHandlers and add the type/return handler
    // We don't use addDom/BitlessHandlers since we aren't sinkEvents
    // We also aren't even dealing with a DomEvent
    return addHandler(handler, PressEvent.getType());
  }

  /**
   *
   * @param event
   */
  private void firePressEvent(Event event) {
    // This better verify a ClickEvent or TouchEndEvent
    // TODO might want to verify
    // (hitting issue with web.bindery vs g.gwt.user package diff)
    PressEvent pressEvent = new PressEvent(event);
    fireEvent(pressEvent);
  }

  /**
   * Implement the handler for pressing but NOT releasing the button. Normally you just want to show
   * some CSS style change to alert the user the element is active but not yet pressed
   *
   * ONLY FOR STYLE CHANGE - Will briefly be called onClick
   *
   * TIP: Don't make a dramatic style change. Take note that if a user is just trying to scroll, and
   * start on the element and then scrolls off, we may not want to distract them too much. If a user
   * does scroll off the element,
   *
   */
  public abstract void onHoldPressDownStyle();

  /**
   * Implement the handler for release of press. This should just be some CSS or Style change.
   *
   * ONLY FOR STYLE CHANGE - Will briefly be called onClick
   *
   * TIP: This should just go back to the normal style.
   */
  public abstract void onHoldPressOffStyle();

  /**
   * Change styling to disabled
   */
  public abstract void onDisablePressStyle();

  /**
   * Change styling to enabled
   *
   * TIP:
   */
  public abstract void onEnablePressStyle();

  @Override
  public Widget getWidget() {
    return super.getWidget();
  }

  @Override
  public void onBrowserEvent(Event event) {
    switch (DOM.eventGetType(event)) {
      case Event.ONTOUCHSTART: {
        if (isEnabled) {
          onTouchStart(event);
        }
        break;
      }
      case Event.ONTOUCHEND: {
        if (isEnabled) {
          onTouchEnd(event);
        }
        break;
      }
      case Event.ONTOUCHMOVE: {
        if (isEnabled) {
          onTouchMove(event);
        }
        break;
      }
      case Event.ONCLICK: {
        if (isEnabled) {
          onClick(event);
        }
        return;
      }
      default: {
        // Let parent handle event if not one of the above (?)
        super.onBrowserEvent(event);
      }
    }

  }

  private void onClick(Event event) {
    event.stopPropagation();

    if (touchHandled) {
      // if the touch is already handled, we are on a device
      // that supports touch (so you aren't in the desktop browser)

      touchHandled = false;// reset for next press
      clickHandled = true;//

      super.onBrowserEvent(event);

    } else {
      if (clickHandled) {
        // Not sure how this situation would occur
        // onClick being called twice..
        event.preventDefault();
      } else {
        // Press not handled yet

        // We still want to briefly fire the style change
        // To give good user feedback
        // Show HoldPress when possible
        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
          @Override
          public void execute() {
            // Show hold press
            onHoldPressDownStyle();

            // Now schedule a delay (which will allow the actual
            // onTouchClickFire to executed
            Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
              @Override
              public boolean execute() {
                // Clear the style change
                onHoldPressOffStyle();
                return false;
              }
            }, flashDelay);
          }
        });

        clickHandled = false;
        firePressEvent(event);

      }
    }
  }

  private void onTouchStart(Event event) {

    onHoldPressDownStyle(); // Show style change

    // Stop the event from bubbling up
    event.stopPropagation();

    // Only handle if we have exactly one touch
    if (event.getTargetTouches().length() == 1) {
      Touch start = event.getTargetTouches().get(0);
      touchId = start.getIdentifier();
      touchMoved = false;
    }

  }

  /**
   * Check to see if the touch has moved off of the element.
   *
   * NOTE that in iOS the elasticScroll may make the touch/move cancel more difficult.
   *
   * @param event
   */
  private void onTouchMove(Event event) {

    if (!touchMoved) {
      Touch move = null;

      for (int i = 0; i < event.getChangedTouches().length(); i++) {
        if (event.getChangedTouches().get(i).getIdentifier() == touchId) {
          move = event.getChangedTouches().get(i);
        }
      }

      // Check to see if we moved off of the original element

      // Use Page coordinates since we compare with widget's absolute coordinates
      int yCord = move.getPageY();
      int xCord = move.getPageX();

      boolean yTop = getWidget().getAbsoluteTop() > yCord; // is y above element
      boolean yBottom = (getWidget().getAbsoluteTop() + getWidget().getOffsetHeight()) < yCord; // y
                                                                                                // below
      boolean xLeft = getWidget().getAbsoluteLeft() > xCord; // is x to the left of element
      boolean xRight = (getWidget().getAbsoluteLeft() + getWidget().getOffsetWidth()) < xCord; // x
                                                                                               // to
                                                                                               // the
                                                                                               // right
      if (yTop || yBottom || xLeft || xRight) {
        touchMoved = true;
        onHoldPressOffStyle();// Go back to normal style
      }

    }

  }

  private void onTouchEnd(Event event) {
    if (!touchMoved) {
      touchHandled = true;
      firePressEvent(event);
      event.preventDefault();
      onHoldPressOffStyle();// Change back the style
    }
  }

}
like image 29
Ashton Thomas Avatar answered Nov 03 '22 20:11

Ashton Thomas


I think the code from the prior answer as written has a few problems, in particular when their are multiple touches.

(NOTE: I'm looking at code I wrote using the Elemental library as reference, so some of the calls might be different in the user library).

a) The code is not filtering touches aimed at the button; it calls TouchEvent.getTouches(). You want to call TouchEvent.getTargetTouches() on touchstart and touchmove to get the the touches just for your button. You want to call TouchEvent.getChangedTouches() on touchend to get the end touch.

b) The code does not take into account multitouch. On touchstart, you can check that a single touch is available and bail out if there is more than one. Also, on touchstart, stash away the id of touch, then use this in touchmove and touchend to find your touch id in the array that is returned (in case the user has touched another finger later on). You can also simplify and check for multiple touches on touchmove and touchend and bail again there.

c) I believe you need to call stopPropagation on touchstart, since you are handling the event. I don't see where they call event.stopPropagation on the touchstart event You can see that this happens in the click handlers, but not the touchstart. This prevents the touch from being turned into a click automatically by the browser, which would cause multiple clicks.

There is also a simpler way. If you don't care about dragging starting on a button, then you can simply call your click logic in the touchstart event (and make sure you check for single touch, and call event.stopPropagation ) and ignore touchmove and touchend. All the touchmove and touchend stuff is to handle the case of allowing dragging to start on the button.

like image 6
Ezward Avatar answered Nov 03 '22 21:11

Ezward


Also Try FastClick

FastClick is a simple, easy-to-use library for eliminating the 300ms delay between a physical tap and the firing of a click event on mobile browsers. The aim is to make your application feel less laggy and more responsive while avoiding any interference with your current logic.

FastClick is developed by FT Labs, part of the Financial Times.

The library has been deployed as part of the FT Web App and is tried and tested on the following mobile browsers:

  • Mobile Safari on iOS 3 and upwards
  • Chrome on iOS 5 and upwards
  • Chrome on Android (ICS)
  • Opera Mobile 11.5 and upwards
  • Android Browser since Android 2
  • PlayBook OS 1 and upwards

FastClick doesn't attach any listeners on desktop browsers as it is not needed. Those that have been tested are:

  • Safari
  • Chrome
  • Internet Explorer
  • Firefox
  • Opera
like image 1
Vijay Sarin Avatar answered Nov 03 '22 20:11

Vijay Sarin