Преобразование событий GWT Click в сенсорные события - PullRequest
12 голосов
/ 07 марта 2012

Я работаю над большим проектом, и у меня написано много кода GWT.Сейчас я работаю над тем, чтобы сделать проект полностью совместимым с планшетами, такими как iPad и Android.

Как часть этого, я заметил, что сенсорные устройства требуют 300 мс задержки для обработки событий щелчка.В этом проекте повторное написание сенсорных событий - очень утомительная задача.Я провел много исследований в этой области и обнаружил, что Google Fast Buttons API используется в приложении Google Voice.Я попробовал это, и это хорошо работает, но требует много кодирования и JSNI.

Мой вопрос: есть ли что-нибудь еще в ваших знаниях, чтобы легко преодолеть эту задержку?

Ответы [ 4 ]

14 голосов
/ 08 марта 2012

Вот чистая Java-реализация быстрой кнопки. Она не содержит ни одной строки 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;
  }
}  
10 голосов
/ 17 декабря 2012

Я попытался использовать приведенные выше ответы и комментарии, чтобы попробовать эту реализацию.

Я также опубликовал пример проекта GWT, который можно использовать для простого сравнения:

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

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

Обратите внимание, что вы сможете увидеть сэкономленное время, только если вы находитесь на мобильном устройстве (или устройствах, которые обрабатывают сенсорные события и не просто возвращаются к onClick).

Я добавил 3 быстрые кнопки и 3 обычные кнопки. Вы можете легко увидеть улучшение, когда на старых мобильных устройствах, а иногда и меньше, на более новых (Samsung Galaxy Nexus показывал задержки всего около 100 мс, в то время как iPad первого поколения превышал 400 мс почти каждый раз). Самое большое улучшение - это когда вы пытаетесь быстро и последовательно нажимать на поля (здесь не совсем кнопки, но их можно адаптировать)

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:
 * /8255135/preobrazovanie-sobytii-gwt-click-v-sensornye-sobytiya
 *
 * 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
    }
  }

}
6 голосов
/ 05 ноября 2012

Я думаю, что код из предыдущего ответа, как написано, имеет несколько проблем, в частности, когда их несколько касаний.

(ПРИМЕЧАНИЕ: я смотрю на код, который я написал, используя библиотеку Elemental в качестве ссылки,поэтому некоторые вызовы могут быть разными в пользовательской библиотеке).

a) Код не фильтрует касания, направленные на кнопку;он вызывает TouchEvent.getTouches ().Вы хотите вызвать TouchEvent.getTargetTouches () при сенсорном запуске и сенсорном движении, чтобы получить прикосновения только для вашей кнопки.Вы хотите вызвать TouchEvent.getChangedTouches () на touchend, чтобы получить конечное касание.

b) Код не учитывает мультитач.На Touchstart вы можете проверить, доступно ли одно касание, и выручить, если их больше одного.Кроме того, при запуске прикосновения спрятать идентификатор касания, затем использовать его в touchmove и touchend, чтобы найти идентификатор касания в возвращаемом массиве (в случае, если пользователь позже коснулся другого пальца).Вы также можете упростить и проверить наличие нескольких касаний касания и касания, а затем снова внести залог.

в) Я считаю, что вам нужно вызывать stopPropagation при запуске касания, поскольку вы обрабатываете событие.Я не вижу, где они вызывают event.stopPropagation для события touchstart. Вы можете видеть, что это происходит в обработчиках щелчков, но не в touchstart.Это предотвращает автоматическое превращение касания в щелчок браузером, что может привести к многократным щелчкам.

Существует также более простой способ.Если вас не волнует перетаскивание, начинающееся с кнопки, тогда вы можете просто вызвать свою логику щелчка в событии сенсорного запуска (и убедиться, что вы проверяете на одно касание и вызывать event.stopPropagation) и игнорировать сенсорное перемещение и касание.Все, что касается сенсорного перемещения и касания, - это случай, когда можно начать перетаскивание на кнопку.

1 голос
/ 10 апреля 2013

Также попробуйте FastClick

FastClick - это простая и простая в использовании библиотека для устранения задержки 300 мс между физическим касанием и срабатыванием события щелчка в мобильных браузерах. Цель состоит в том, чтобы сделать ваше приложение менее чувствительным и более отзывчивым, избегая при этом какого-либо вмешательства в вашу текущую логику.

FastClick разработан FT Labs, частью Financial Times.

Библиотека была развернута как часть FT Web App и опробована и протестирована в следующих мобильных браузерах:

  • Мобильное Safari на iOS 3 и выше
  • Chrome на iOS 5 и выше
  • Chrome на Android (ICS)
  • Opera Mobile 11,5 и выше
  • Браузер Android начиная с Android 2
  • PlayBook OS 1 и выше

FastClick не присоединяет никаких слушателей в настольных браузерах, поскольку это не нужно. Те, которые были проверены:

  • Safari
  • Chrome
  • Internet Explorer
  • Firefox
  • Opera
...