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?
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;
}
}
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
}
}
}
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.
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
Related
I have two buttons set up in an interactive fiction game, each press calls a new line of text. The problem is, every time I press on the button, I get two logged debug messages informing me of the click and my game moves two sections of text.
I've tried many different things to try to work around this including trying to alter the submit input in project settings and many different code forms. Any help would be greatly appreciated.
Here's my current code:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class AdventureGame : MonoBehaviour
{
[SerializeField]
private Text _textComponent;
[SerializeField]
private State _startingState;
private State state;
[SerializeField]
private Button _input0Button;
[SerializeField]
private Button _input1Button;
[SerializeField]
private Text _choices1;
[SerializeField]
private Text _choices2;
private bool _buttonOnePressed;
private bool _buttonTwoPressed;
void Start()
{
state = _startingState;
_textComponent.text = state.GetStateStory();
_input0Button.onClick.AddListener(Input0Button);
_input1Button.onClick.AddListener(Input1Button);
_buttonOnePressed = false;
_buttonTwoPressed = false;
}
void Update()
{
ManageState();
}
private void ManageState()
{
if (state._choice == true)
{
_choices1.text = state.GetChoiceOne();
_choices2.text = state.GetChoiceTwo();
_textComponent.text = state.GetStateStory();
_input0Button.gameObject.SetActive(true);
_input1Button.gameObject.SetActive(true);
if(_buttonOnePressed == true)
{
StartCoroutine(WaitForItOne());
}
else if(_buttonTwoPressed == true)
{
StartCoroutine(WaitForItTwo());
}
}
else if (state._choice == false)
{
_choices1.text = state.GetChoiceOne();
_choices2.text = state.GetChoiceTwo();
_textComponent.text = state.GetStateStory();
_input0Button.gameObject.SetActive(true);
_input1Button.gameObject.SetActive(false);
if(_buttonOnePressed == true)
{
StartCoroutine(WaitForItOne());
}
}
}
private void ManageChoiceOne()
{
_buttonOnePressed = false;
State[] _newState = state.GetNextStatesArray();
state = _newState[0];
}
private void ManageChoiceTwo()
{
_buttonTwoPressed = false;
State[] _newState = state.GetNextStatesArray();
state = _newState[1];
}
public void Input0Button()
{
Debug.Log("Input 0 pressed");
_buttonOnePressed = true;
}
public void Input1Button()
{
Debug.Log("Input 1 pressed");
_buttonTwoPressed = true;
}
IEnumerator WaitForItOne()
{
yield return new WaitForSeconds(3.0f);
ManageChoiceOne();
}
IEnumerator WaitForItTwo()
{
yield return new WaitForSeconds(3.0f);
ManageChoiceTwo();
}
}
First of all you keep starting new Coroutines each frame as long as e.g. _buttonOnePressed == true .. you wait 3 seconds before you finally unset this flag!
Then for the double call make sure the callbacks are not configured also in the Inspector! It seems like you have them once in the Inspector and additionally add them in your Start method so they are called twice!
Note that you won't see the callbacks added on runtime in the Inspector!
Why are you even using Update here at all? It is quite redundant to poll the state and the bool values and constantly check and handle their states in each frame. I would rather simply start the routine in the button method itself and make the whole code event driven instead!
(optionally) To give the user better feedback I would additionally in the meantime during the 3 seconds make the buttons non-interactable .. keep them active but not clickable:
// Remove state;
// Remove _buttonOnePressed
// Remove _buttonTwoPressed
private void Start()
{
// Either remove this two lines or the callbacks set in the Inspector
_input0Button.onClick.AddListener(Input0Button);
_input1Button.onClick.AddListener(Input1Button);
ManageState(_startingState);
}
// Remove Update
// This will be called only when actually needed
// since the state is passed in as parameter you don't need the private field
private void ManageState(State state)
{
// These happen in both cases anyway
_choices1.text = state.GetChoiceOne();
_choices2.text = state.GetChoiceTwo();
_textComponent.text = state.GetStateStory();
_input0Button.gameObject.SetActive(true);
// Here directly use the state flag
// since the button is disabled when needed
// there is no need for having different "states"
// since anyway only the according button(s) is(are) available
_input1Button.gameObject.SetActive(state._choice);
}
// (optional) Flag for avoiding concurrent routines
// Should be impossible since buttons get disabled but you never know
private bool alreadyHandlingButton;
private IEnumerator ManageChoice(bool isButtonOne)
{
// (optional) Skip if another routine running
if(alreadyHandlingButton) yield break;
// (optional) Block other routines just in cade
alreadyHandlingButton = true;
// Disable interactions
_input0Button.interactable = false;
_input1Button.interactable = false;
// This is the same for both buttons
yield return new WaitForSeconds(3f);
State[] _newState = state.GetNextStatesArray();
var state = _newState[isButtonOne ? 0 : 1];
// Only call ManageState when the state is actually changing
ManageState(state);
// (optional) Allow a new routine
alreadyHandlingButton = false;
// Enable interactions
_input0Button.interactable = true;
_input1Button.interactable = true;
}
public void Input0Button()
{
// (optional) Ignore if other button is already handled
if(alreadyHandlingButton) return;
Debug.Log("Input 0 pressed");
StartCoroutine(ManageChoice(true));
}
public void Input1Button()
{
// (optional) Ignore if other button is already handled
if(alreadyHandlingButton) return;
Debug.Log("Input 1 pressed");
StartCoroutine(ManageChoice(false));
}
I have a window resizeHandler, it is working fine. Except that I'm just interested in the final dimension of the window, in other words, I'm interested in the dimension of the window when the mouse button is released. Is there a way I can listen to window mouse events?
I've written a piece of code that accomplishes my goal but I'd prefer something more obvious. resizeRequestId is a field:
private void processResize() {
final Timer timer = new Timer() {
final Size windowSize = getWindowDimention();
final int requestId = ++resizeRequestId;
#Override
public void run() {
final boolean isLatestRequest = requestId == resizeRequestId;
if (isLatestRequest) {
//DO SOMETHING WITH windowSize
}
}
};
timer.schedule(100);
}
The browser doesn't pass along events that happen outside of the page, and the window resize counts as outside the page. That said, you still get a hook for resize actions of the entire browser window:
Window.addResizeHandler(new ResizeHandler() {
public void onResize(ResizeEvent event) {
//get, process window resize behavior
}
});
For some browsers and some resizes, you'll get lots of events as the mouse moves, and for others, you'll only get the complete one. In firefox, for example, the resize handle in the corner sends every change that is made, while the side handles each send only once the user releases the mouse. Minimizing and maximizing the window also result in a single event.
Colin is right.
Moreover, if you do a lot of calculations "on resize" (e.g. forceLayout), it is a good idea to add a Timer. This way the calculations will be fired once every...10th of second?
final Timer resizeTimer = new Timer() {
#Override
public void run() {
mainPanel.forceLayout();
}
};
Window.addResizeHandler(new ResizeHandler() {
public void onResize(ResizeEvent event) {
int height = event.getHeight();
mainPanel.setHeight(height + "px");
resizeTimer.cancel();
resizeTimer.schedule(100);
}
});
That's it
Purpose
I have implemented the "reorder items from a ListView" paradigm following the Android 3.0 Drag & drop framework.
Issue
Seen on android 4.0.3 and 4.0.4. Not tested on other releases.
Drag an item of the ListView and drop it after the last item of the ListView.
The shadow box is cleared and the ListView invalidated. That is the nominal behaviour and happens 90% of the time.
But for the remaining 10%, what happens is:
NB: all of the effects listed below are seen AFTER onDrag(View, DragEvent) has returned for ACTION_DRAG_ENDED, that is to say the entire drag & drop process seems to be completed before the problems occur.
the shadow box is displayed for a mere 10 seconds before disappearance. Calling view.invalidate() in onDrag(View, DragEvent) handler for ACTION_DRAG_ENDED will correct that but won't fix the hang or reboot problem (see below)
some unusual traces in logcat:
I/ViewRootImpl( 954): Reporting drop result: true I/InputQueue-JNI(
210): Sending finished signal for input channel 'drag (client)' since
it is being unregistered while an input message is still in progress.
I/InputQueue-JNI( 210): Ignoring finish signal on channel that is no
longer registered. W/WindowManager( 210): Drag is in progress but
there is no drag window handle. I/ViewRootImpl( 954): Reporting drop
result: true
on some device, Android hangs or reboots. It depends of the device but the effect is always the same.
Source code
import android.annotation.SuppressLint;
import android.app.ListActivity;
import android.content.ClipData;
import android.content.ClipDescription;
import android.os.Bundle;
import android.view.DragEvent;
import android.view.View;
import android.view.View.DragShadowBuilder;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
public class TestReorderActivity extends ListActivity {
#SuppressLint("NewApi")
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ListView listView = getListView();
String[] listeStrings = { "France", "United States", "Russia" };
listView.setAdapter(new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, listeStrings));
listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
#Override
public boolean onItemLongClick(AdapterView<?> adapter, View view,
int id, long position) {
ClipData data = ClipData.newPlainText(" ",
Long.toString(position));
DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(
view);
if (view.startDrag(data, shadowBuilder/*
* new
* MyDragShadowBuilder(view)
*/, view, 0)) {
return true;
}
return false;
}
});
listView.setOnDragListener(new ListView.OnDragListener() {
#Override
public boolean onDrag(View view, DragEvent event) {
final int action = event.getAction();
switch (action) {
case DragEvent.ACTION_DRAG_STARTED:
if (event.getClipDescription().hasMimeType(
ClipDescription.MIMETYPE_TEXT_PLAIN)) {
// accept drag
return true;
} else {
// reject drag
return false;
}
case DragEvent.ACTION_DRAG_ENTERED:
// entered drag
return true;
case DragEvent.ACTION_DRAG_LOCATION:
// location is returned as event.getX() and event.getY()
return true;
case DragEvent.ACTION_DRAG_EXITED:
// cancel drag.
return true;
case DragEvent.ACTION_DROP:
// item dropped
processDrop(event);
return true;
case DragEvent.ACTION_DRAG_ENDED:
default: // unknown case
return true;
}
}
private boolean processDrop(DragEvent event) {
ClipData data = event.getClipData();
if ((data != null) && (data.getItemCount() > 0)) {
ClipData.Item item = data.getItemAt(0);
CharSequence value = item.getText();
long position = Long.valueOf(value.toString());
int x = (int) event.getX();
int y = (int) event.getY();
ListView listView = getListView();
int newPosition = listView.pointToPosition(x, y);
if (newPosition > ListView.INVALID_POSITION) {
swap(position, newPosition);
return true;
}
}
return false;
}
});
}
// swap(long, long) here
}
the reason is that: your screen upload action_move too lazily.
in normal case , action move is uploading very frequently even if your finger don't move on the screen. but some phone screens are not so sensitive.
you can modify the threshold of your phone. It needs kernel support.
this is because : unregisterInputChannel fail to get the lock, dead lock occur.
PopupPanel is a class within GWT written (akhem) a long time ago (which is why it sucks so much) that allows for showing popups with content. One of the options is autoHide where if there's a certain event outside of the popup it closes the popup. It works well on anything EXCEPT Safari Mobil (SM). Reason is SM doesn't fire click events on touch. It fires touch events. PopupPanel is hard coded to look for ClickEvents.
Specifically, the code says:
case Event.ONMOUSEDOWN:
...
if (!eventTargetsPopupOrPartner && autoHide) {
hide(true);
...
Obviously this isn't complete and it should also include Event.ONTOUCHSTART
Problem is, all the methods and fields are private so I cannot add this functionality. That's a big boo-boo on the part of GWT team but not really a concern since I could just make my own class and copy contents of PopupPanel. The big problem is that nativeEventPreview doesn't capture Touch Events!
I tried adding the following to Event Preview the following:
private static NativePreviewHandler nativePreviewHandler = new NativePreviewHandler() {
public void onPreviewNativeEvent(NativePreviewEvent event) {
Event nativeEvent = Event.as(event.getNativeEvent());
switch (nativeEvent.getTypeInt()) {
case Event.ONTOUCHSTART:
case Event.ONMOUSEDOWN:
EventTarget target = nativeEvent.getEventTarget();
if (!Element.is(target) || !popup.getElement().isOrHasChild(Element.as(target))) {
popup.hide();
} break;
}
}
};
Where 'popup' is the PopupPanel I'm trying to get to close on outside touch events.
Sad thing is it works for mouse down when testing in any other browser on Earth, but not on iPad.
Another thing I tried was adding a TouchStartHandler to the glass of the PopupPanel (the gray looking thing behind it). I war hoping that I could catch the touch events that way, but I was unable to get events to fire on glass since it's attached to DOM in some funny way. My code:
private static class ProperPopupPanel extends PopupPanel {
public ProperPopupPanel() {
super();
}
void setHideOnGlassTouch() {
setGlassEnabled(true);
TouchableLabeThatDoesntCrashOnWrap glass = new TouchableLabeThatDoesntCrashOnWrap(getGlassElement());
glass.addTouchStartHandler(new TouchStartHandler() {
#Override
public void onTouchStart(TouchStartEvent event) {
hide();
}
});
glass.addClickHandler(new ClickHandler() {
#Override
public void onClick(ClickEvent event) {
hide();
}
});
}
private class TouchableLabeThatDoesntCrashOnWrap extends Label {
public TouchableLabeThatDoesntCrashOnWrap(Element element) {
super(element);
super.onAttach();
}
}
}
In my mind this should work, but it doesn't. I don't know why. Any ideas or suggestions are welcome. Thanks.
Not enough GWT users here... well I made my own class that adds touch handlers through JSNI ...
/**
* Overwrite of the usual PopupPanel with a modification that this one
* works well on touch-enabled browsers.
* #author McTrafik
*/
public class ProperPopupPanel extends PopupPanel {
////////////////////////////////////////////////////////////
/////////// OVERRIDES //////////////////////////////////////
////////////////////////////////////////////////////////////
public ProperPopupPanel() {
super();
setTouchListener();
}
#Override
public void hide() {
super.hide();
removeTouchListener();
}
#Override
public void show() {
super.show();
addTouchListener();
}
////////////////////////////////////////////////////////////
/////////// NANDLERS ///////////////////////////////////////
////////////////////////////////////////////////////////////
protected JavaScriptObject touchHandler;
/**
* Handle a touch event that happened while the popup is open.
* #param event - The event to handle
*/
protected void handleTouchEvent(Event event) {
// Check to see if the events should be firing in the first place.
if (!isShowing()) {
removeTouchListener();
return;
}
// Check if the event happened within the popup
EventTarget target = event.getEventTarget();
if (!Element.is(target) || !getElement().isOrHasChild(Element.as(target))) {
// Stop event if the popup is modal
if (isModal()) event.preventDefault();
// Close the popup if the event happened outside
if (isAutoHideEnabled()) {
hide(true);
removeTouchListener();
}
}
};
/**
* Create a touchHandler that knows how to point to this instance.
* Without it there's a cast exception that happens.
*/
protected native void setTouchListener() /*-{
var caller = this;
this.#[package].ProperPopupPanel::touchHandler = function(event) {
caller.#[package].ProperPopupPanel::handleTouchEvent(Lcom/google/gwt/user/client/Event;)(event);
}
}-*/;
/**
* Add a touch listener that will listen to touch events.
*/
protected native void addTouchListener() /*-{
$doc.addEventListener(
"touchstart",
this.#[package].ProperPopupPanel::touchHandler,
true
);
$doc.addEventListener(
"MozTouchDown",
this.#[package].ProperPopupPanel::touchHandler,
true
);
}-*/;
/**
* Remove the touch listeners
*/
protected native void removeTouchListener() /*-{
$doc.removeEventListener(
"touchstart",
this.#[package].ProperPopupPanel::touchHandler,
true
);
$doc.removeEventListener(
"MozTouchDown",
this.#[package].ProperPopupPanel::touchHandler,
true
);
}-*/;
}
I have a Composite that I want to be able to enable/disable programatically. The Control.setEnabled(boolean enabled) method works fine, but it does not give any visual information that the widget(s) are disabled.
What I would like to do is to have the disabled state mean the widgets are greyed out. Right now they just enter a weird state where the user is unable to click or perform any action on them.
A Composite is a container control that hold other controls using a layout - you can't see a composite really, you can only see the controls it holds. To disable and visually see then disabled, you'll have to call setEnabled(false) on all the children, assuming they're not containers too. Basically, to have to enable/disable the leaf widgets and you will see visual indication.
The reason you can't do anything with the widgets when disabling the Composite is because the Composite is eating all the events. Although the child widgets are not getting the events forwarded, they know nothing about the state of their parent, so they aren't greyed out.
The problem was indeed that I was disabling the composite and not the controls inside it. What I ended up doing was something like this:
public void recursiveSetEnabled(Control ctrl, boolean enabled) {
if (ctrl instanceof Composite) {
Composite comp = (Composite) ctrl;
for (Control c : comp.getChildren())
recursiveSetEnabled(c, enabled);
} else {
ctrl.setEnabled(enabled);
}
}
The other solutions posted here are pretty primitive. They have several shortcomings:
Even if a control is disabled to start with, it will become enabled if its control tree is disabled and then enabled. You probably want to keep such a control disabled.
Sometimes nested controls should remain enabled when their control tree is disabled.
It is useful to distinguish between two different disabled states:
Disabled state, with no information to show. This should be clearly visually indicated to the user.
Displaying information, but read-only state. It is useful to be able to copy text in text fields in this state.
The code below solves these problems. It is the ultimate enabler/disabler for SWT.
It keeps track of the modified controls by tagging them with Widget.setData, so that it only enables the contols it previously has disabled. It handles different kinds of controls differently in tree states: DISABLED, READ_ONLY and EDITABLE.
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.forms.widgets.ExpandableComposite;
public class GuiEnabler {
/**
* Used to set the enable state of a tree of controls.
*/
public enum EnableState {
/**
* The control is disabled, for when there is no information to show in
* it. All controls, including labels, are disabled.
*/
DISABLED,
/**
* For when there is information to show in the control, but it should
* be read-only. Controls are disabled, except Text which is
* non-editable, and Lables, which are enabeled.
*/
READ_ONLY,
/**
* All controls are enabled and editable.
*/
EDITABLE
}
private static final String ENABLED_KEY = GuiEnabler.class.getName() + ".disabled";
private static final String EDITABLE_KEY = GuiEnabler.class.getName() + ".read_only";
/**
* Disables or makes read-only {#code control} and all its child controls (recursively).
* Also restores the state of controls previously disabled by this method. The action
* performed on the controls is determined by {#link EnableState enableState}.
*
* #param excluded These controls (and their children) are not modified by
* the method.
*/
public static void recursiveUpdateEnableState(Control control, EnableState enableState, Control... excluded) {
updateEnabledState(control, enableState, new HashSet<>(Arrays.asList(excluded)));
}
/**
* See {#link GuiEnabler#recursiveUpdateEnableState(Control, EnableState, Control...)}.
*/
public static void updateEnabledState(Control control, EnableState enableState, Set<Control> excluded) {
if (excluded.contains(control)) {
return;
} else if (control instanceof ExpandableComposite) {
updateEnabledState(((ExpandableComposite) control).getClient(), enableState, excluded);
} else if (control instanceof Composite && !(control instanceof Combo)) {
for (Control child : ((Composite) control).getChildren()) {
updateEnabledState(child, enableState, excluded);
}
} else {
updateControl(control, enableState);
}
}
/**
* Updates a single control to have its proper state for enableState.
*/
private static void updateControl(Control control, EnableState enableState) {
if (enableState == EnableState.DISABLED) {
makeDisabled(control);
} else if (enableState == EnableState.READ_ONLY) {
if (control instanceof Text) {
makeNonEditable((Text) control);
makeEnabled(control);
} if (control instanceof Label) {
makeEnabled(control);
} else {
makeDisabled(control);
}
} else if (enableState == EnableState.EDITABLE) {
makeEnabled(control);
if (control instanceof Text) makeEditable((Text) control);
}
}
private static void makeEnabled(Control control) {
if (control.getData(ENABLED_KEY) != null) {
control.setData(ENABLED_KEY, null);
control.setEnabled(true);
}
}
private static void makeDisabled(Control control) {
if (control.getEnabled()) {
control.setData(ENABLED_KEY, "marked");
control.setEnabled(false);
}
}
private static void makeEditable(Text text) {
if (text.getData(EDITABLE_KEY) != null) {
text.setData(EDITABLE_KEY, null);
text.setEditable(true);
}
}
private static void makeNonEditable(Text text) {
if (text.getEditable()) {
text.setData(EDITABLE_KEY, "marked");
text.setEditable(false);
}
}
}
One limitation of this is that even in disabled state it is still possible to change the active tab in a TabFolder control.
In other words, you need to write code like this, given a Composite c:
for (Control child : c.getChildren())
child.setEnabled(false);