/*
* Copyright 2013 Daniel Kurka
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.googlecode.mgwt.ui.client.widget.menu.swipe;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.core.shared.GWT;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.HasCloseHandlers;
import com.google.gwt.event.logical.shared.HasOpenHandlers;
import com.google.gwt.event.logical.shared.OpenEvent;
import com.google.gwt.event.logical.shared.OpenHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.uibinder.client.UiFactory;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.Widget;
import com.googlecode.mgwt.dom.client.event.animation.TransitionEndEvent;
import com.googlecode.mgwt.dom.client.event.animation.TransitionEndHandler;
import com.googlecode.mgwt.dom.client.event.tap.TapEvent;
import com.googlecode.mgwt.dom.client.event.tap.TapHandler;
import com.googlecode.mgwt.dom.client.recognizer.swipe.SwipeEndEvent;
import com.googlecode.mgwt.dom.client.recognizer.swipe.SwipeEndHandler;
import com.googlecode.mgwt.dom.client.recognizer.swipe.SwipeEvent.DIRECTION;
import com.googlecode.mgwt.dom.client.recognizer.swipe.SwipeMoveEvent;
import com.googlecode.mgwt.dom.client.recognizer.swipe.SwipeMoveHandler;
import com.googlecode.mgwt.dom.client.recognizer.swipe.SwipeStartEvent;
import com.googlecode.mgwt.dom.client.recognizer.swipe.SwipeStartHandler;
import com.googlecode.mgwt.ui.client.util.CssUtil;
import com.googlecode.mgwt.ui.client.widget.touch.TouchDelegate;
/**
* Considered experimental and might change, use at your own risk
*/
public class SwipeMenu extends Composite implements HasOpenHandlers<SwipeMenu>,
HasCloseHandlers<SwipeMenu> {
private static final SwipeMenuAppearance APPEARANCE = GWT.create(SwipeMenuAppearance.class);
protected static final int THRESHOLD = 40;
@UiField
protected FlowPanel main;
@UiField
protected SimplePanel menu;
@UiField
protected SimplePanel content;
@UiField
protected FlowPanel wrap;
@UiField(provided = true)
protected final SwipeMenuAppearance appearance;
private enum STATE {
OPEN, CLOSED, ANIMATING_TO_CLOSE, SWIPING_TO_OPEN, SWIPING_TO_CLOSE, ANIMATING_TO_OPEN
}
private STATE state;
private DIRECTION currentDirection;
private int maxDistance;
private int startX;
int position = Integer.MAX_VALUE;
private TouchDelegate touchContainer;
public SwipeMenu() {
this(APPEARANCE, /*toggleAutomatically*/ true);
}
public SwipeMenu(boolean toggleAutomatically) {
this(APPEARANCE, toggleAutomatically);
}
public SwipeMenu(SwipeMenuAppearance appearance) {
this(appearance, /*toggleAutomatically*/ true);
}
public SwipeMenu(SwipeMenuAppearance appearance, boolean toggleAutomatically) {
this.appearance = appearance;
state = STATE.CLOSED;
initWidget(appearance.uiBinder().createAndBindUi(this));
touchContainer = new TouchDelegate(main);
initHandlers(toggleAutomatically);
}
@Override
public HandlerRegistration addOpenHandler(OpenHandler<SwipeMenu> handler) {
return addHandler(handler, OpenEvent.getType());
}
@Override
public HandlerRegistration addCloseHandler(CloseHandler<SwipeMenu> handler) {
return addHandler(handler, CloseEvent.getType());
}
public void setMenuDisplay(Widget w) {
menu.setWidget(w);
}
public void setContentDisplay(Widget w) {
content.setWidget(w);
}
public void open() {
open(true);
}
public boolean isOpen() {
return state == STATE.OPEN;
}
public void open(boolean animate) {
// TODO deal with animating
if (animate) {
openMenuWithAnimation(200);
} else {
state = STATE.OPEN;
openMenu();
}
}
public void close() {
close(true);
}
public void close(boolean animate) {
// TODO deal with animating
if (animate) {
closeMenuWithAnimation(200);
} else {
state = STATE.CLOSED;
closeMenu();
}
}
@UiFactory
protected static SwipeMenuAppearance getAppearance() {
return APPEARANCE;
}
private void initHandlers(boolean toggleAutomatically) {
if (toggleAutomatically) {
touchContainer.addSwipeStartHandler(new SwipeStartHandler() {
@Override
public void onSwipeStart(SwipeStartEvent event) {
handleSwipeStart(event);
}
});
touchContainer.addSwipeMoveHandler(new SwipeMoveHandler() {
@Override
public void onSwipeMove(SwipeMoveEvent event) {
handleSwipeMove(event);
}
});
touchContainer.addSwipeEndHandler(new SwipeEndHandler() {
@Override
public void onSwipeEnd(SwipeEndEvent event) {
handleSwipeEnd(event);
}
});
touchContainer.addTapHandler(new TapHandler() {
@Override
public void onTap(TapEvent event) {
handleTap(event);
}
});
}
wrap.addDomHandler(new TransitionEndHandler() {
@Override
public void onTransitionEnd(TransitionEndEvent event) {
handleTransitionEnd();
}
}, TransitionEndEvent.getType());
}
private void updatePosition(int position) {
if (position > 0) {
position = 0;
}
if (position < -menu.getOffsetWidth()) {
position = -menu.getOffsetWidth();
}
CssUtil.translate(wrap.getElement(), position, 0);
this.position = position;
}
@Override
protected void onLoad() {
super.onLoad();
closeMenu();
// work around for CSS Resource style injection
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
closeMenu();
}
});
}
private void closeMenu() {
CloseEvent.fire(this, this);
wrap.addStyleName(appearance.css().closed());
wrap.removeStyleName(appearance.css().opened());
CssUtil.resetTransForm(wrap.getElement());
}
private void openMenu() {
OpenEvent.fire(this, this);
wrap.removeStyleName(appearance.css().closed());
wrap.addStyleName(appearance.css().opened());
CssUtil.resetTransForm(wrap.getElement());
if (position == 0) {
handleTransitionEnd();
}
}
private void openMenuWithAnimation(int time) {
state = STATE.ANIMATING_TO_OPEN;
CssUtil.setTransitionDuration(wrap.getElement(), time);
openMenu();
}
private void closeMenuWithAnimation(int time) {
state = STATE.ANIMATING_TO_CLOSE;
CssUtil.setTransitionDuration(wrap.getElement(), time);
closeMenu();
}
private void handleSwipeStart(SwipeStartEvent event) {
if (state == STATE.CLOSED && event.getDirection() == DIRECTION.LEFT_TO_RIGHT) {
// ignore?
if (event.getTouch().getPageX() > 40) {
return;
}
currentDirection = DIRECTION.LEFT_TO_RIGHT;
maxDistance = event.getDistance();
state = STATE.SWIPING_TO_OPEN;
updatePosition(-menu.getOffsetWidth() + event.getDistance());
}
if (state == STATE.OPEN && event.getDirection() == DIRECTION.RIGHT_TO_LEFT) {
state = STATE.SWIPING_TO_CLOSE;
currentDirection = DIRECTION.RIGHT_TO_LEFT;
startX = event.getTouch().getPageX();
maxDistance = event.getDistance();
updatePosition(-event.getDistance());
}
}
private void handleSwipeMove(SwipeMoveEvent event) {
if (state == STATE.SWIPING_TO_OPEN) {
if (event.getDistance() > maxDistance) {
maxDistance = event.getDistance();
}
// not ideal but covers most use cases
if (maxDistance - event.getDistance() > 40) {
// started moving in the wrong dir
currentDirection = DIRECTION.RIGHT_TO_LEFT;
} else if (maxDistance - event.getDistance() > 0) {
currentDirection = DIRECTION.LEFT_TO_RIGHT;
}
updatePosition(-menu.getOffsetWidth() + event.getDistance());
}
if (state == STATE.SWIPING_TO_CLOSE) {
if (event.getTouch().getPageX() - startX > 0) {
currentDirection = DIRECTION.LEFT_TO_RIGHT;
updatePosition(0);
return;
}
if (event.getDistance() > maxDistance) {
maxDistance = event.getDistance();
}
// not ideal but covers most use cases
if (maxDistance - event.getDistance() > 40) {
// started moving in the wrong dir
currentDirection = DIRECTION.LEFT_TO_RIGHT;
} else if (maxDistance - event.getDistance() > 0) {
currentDirection = DIRECTION.RIGHT_TO_LEFT;
}
updatePosition(-event.getDistance());
}
}
private void handleSwipeEnd(SwipeEndEvent event) {
if (state == STATE.SWIPING_TO_OPEN) {
if (event.getDistance() > THRESHOLD && currentDirection == DIRECTION.LEFT_TO_RIGHT) {
openMenuWithAnimation(150);
} else {
closeMenuWithAnimation(150);
}
}
if (state == STATE.SWIPING_TO_CLOSE) {
if (event.getDistance() > THRESHOLD && currentDirection == DIRECTION.RIGHT_TO_LEFT) {
closeMenuWithAnimation(150);
} else {
openMenuWithAnimation(150);
}
}
}
private void handleTap(TapEvent event) {
if (state == STATE.OPEN) {
if (event.getStartX() > menu.getOffsetWidth()) {
closeMenuWithAnimation(200);
}
}
}
private void handleTransitionEnd() {
switch (state) {
case ANIMATING_TO_CLOSE:
state = STATE.CLOSED;
CssUtil.setTransitionDuration(wrap.getElement(), 0);
break;
case ANIMATING_TO_OPEN:
state = STATE.OPEN;
CssUtil.setTransitionDuration(wrap.getElement(), 0);
break;
default:
break;
}
}
}