package org.vaadin.touchkit.gwt.client.ui;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.Window.Navigator;
import com.google.gwt.user.client.ui.ComplexPanel;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.BrowserInfo;
public class VNavigationManager extends ComplexPanel {
public interface AnimationListener {
public void animationDidEnd();
public void animationWillStart();
}
class PlaceHolder extends Widget {
private DivElement el = Document.get().createDivElement();
public PlaceHolder() {
setElement(Document.get().createDivElement());
setStyleName(CLASSNAME + "-placeholder");
getElement().appendChild(el);
}
public void moveToNextPosition() {
setPosition(getElement().getStyle(), -currentWrapperPos + 1);
}
public void setHTML(String innerText) {
el.setInnerHTML(innerText);
}
}
private static final String CONTAINER_CLASSNAME = "v-touchkit-navpanel-container";
private static final String WRAPPER_CLASSNAME = "v-touchkit-navpanel-wrapper";
private static final String CLASSNAME = "v-touchkit-navpanel";
private Widget currentView;
private Widget prevView;
private Widget nextView;
private DivElement wrapper = Document.get().createDivElement();
private List<AnimationListener> animationListeners = new ArrayList<AnimationListener>();
static boolean rerendering = false;
int currentWrapperPos = 0;
private PlaceHolder _placeHolder;
private boolean transitionPending;
/**
* Flag to indicate whether ios6 scrolling workaround is needed. See #9754
*/
private boolean needsIos6ScrollingWorkaround;
/**
* Flag used for #9754 workaround. True when previous and next are in
* correct place for animating/sliding.
*
* @see #needsIos6ScrollingWorkaround
*/
private boolean preparedForTranslation3d;
private String width;
private String pendingWidth;
private Collection<Widget> detachAfterAnimation = new ArrayList<Widget>();
public VNavigationManager() {
setElement(Document.get().createDivElement());
setStyleName(CLASSNAME);
wrapper.setClassName(WRAPPER_CLASSNAME);
getElement().appendChild(wrapper);
hookTransitionEndListener(Css3Propertynames.transitionEnd(),
wrapper);
getElement().setTabIndex(-1);
}
private void add(Widget child, int pos) {
Element createContainerElement = createContainerElement();
add(child, createContainerElement);
setPosition(child, pos);
}
public void addAnimationListener(AnimationListener animationListener) {
animationListeners.add(animationListener);
}
private void animateHorizontally(final int views) {
animateHorizontally(views, true);
}
private void animateHorizontally(final int views, final boolean lockClient) {
prepareForAnimation();
if (lockClient) {
transitionPending = true;
fireAnimationWillStart();
}
currentWrapperPos += views;
Style style = wrapper.getStyle();
// ensure animation are "on" (from css), they might not be on due
// setHorizontalOffset
style.setProperty(Css3Propertynames.transition(), "");
setLeftUsingTranslate3d(style, currentWrapperPos);
if(!BrowserInfo.get().isWebkit()) {
// FIXME FF && IE10 don't fire transition end events properly for some reason
new Timer() {
@Override
public void run() {
onTransitionEnd();
}}.schedule(300);
}
}
private Element createContainerElement() {
DivElement el = Document.get().createDivElement();
el.setClassName(CONTAINER_CLASSNAME);
moveAside(Element.as(el));
wrapper.appendChild(el);
return el.cast();
}
private void fireAnimationDidEnd() {
for (AnimationListener l : animationListeners) {
l.animationDidEnd();
}
}
private void fireAnimationWillStart() {
for (AnimationListener l : animationListeners) {
l.animationWillStart();
}
}
public Widget getNextView() {
return nextView;
}
private int getPixelWidth() {
int offsetWidth = getOffsetWidth();
return offsetWidth;
}
private PlaceHolder getPlaceHolder() {
if (_placeHolder == null) {
_placeHolder = new PlaceHolder();
Element container = wrapper.cast();
add(_placeHolder, container);
}
return _placeHolder;
}
public Widget getPreviousView() {
return prevView;
}
private void hidePlaceHolder() {
if (_placeHolder != null) {
moveAside(_placeHolder.getElement());
}
}
private native void hookTransitionEndListener(String eventName,
DivElement el)
/*-{
var me = this;
el.addEventListener(eventName,function(event) {
if(event.target == el) {
$entry(
me.@org.vaadin.touchkit.gwt.client.ui.VNavigationManager::onTransitionEnd()()
);
}
},false);
}-*/;
private void initIosScroollHack() {
needsIos6ScrollingWorkaround = Navigator.getUserAgent().contains(
" OS 6_")
&& Navigator.getUserAgent().contains(" afari");
// Disable hack if "fullscreen", the hack disturbs e.g. SwipeView a
// LOT as it slows down "warming up" the hardware accelerated layer
if (needsIos6ScrollingWorkaround
&& getOffsetWidth() == RootPanel.get().getOffsetWidth()) {
needsIos6ScrollingWorkaround = false;
}
}
private void moveAside(com.google.gwt.dom.client.Element element) {
element.getStyle().setOpacity(0);
element.getStyle().setTop(100, Unit.PCT);
}
private void moveAside(Widget p) {
com.google.gwt.dom.client.Element parentElement = (p).getElement()
.getParentElement();
moveAside(parentElement);
}
public void navigateBackward() {
animateHorizontally(1);
if (nextView != null) {
nextView.removeFromParent();
}
nextView = currentView;
currentView = prevView;
prevView = null;
}
public void navigateForward() {
animateHorizontally(-1);
if (prevView != null) {
prevView.removeFromParent();
}
prevView = currentView;
currentView = nextView;
nextView = null;
}
/**
* Navigates to a placeholder component that mimics VNavigationView by
* default. During the animation developers can commonly make a server visit
* and fetch real content for new view. The given string is used in the
* placeholder as a caption.
*
* @param placeHolderCaption
*/
public void navigateToPlaceholder(String placeHolderCaption) {
preparePlaceHolder(placeHolderCaption);
animateHorizontally(-1);
detachAfterAnimation.add(prevView);
prevView = currentView;
currentView = null;
}
private void onTransitionEnd() {
new Timer() {
@Override
public void run() {
hidePlaceHolder();
}
}.schedule(160);
transitionPending = false;
fireAnimationDidEnd();
if (pendingWidth != null) {
setWidth(pendingWidth);
pendingWidth = null;
}
for (Widget w : detachAfterAnimation) {
if (w != null) {
w.removeFromParent();
}
}
detachAfterAnimation.clear();
prepareIos6ForScrolling();
}
private void prepareForAnimation() {
// ensure focus is removed from possibly focused fields
getElement().focus();
if (needsIos6ScrollingWorkaround && !preparedForTranslation3d) {
prepareForAnimation(prevView);
prepareForAnimation(nextView);
preparedForTranslation3d = true;
}
}
private RegExp regExp3dValues = RegExp.compile("\\(([^,]+)");
private void prepareForAnimation(Widget p) {
if (p != null) {
Style style = p.getElement().getParentElement().getStyle();
String property = style.getProperty(Css3Propertynames
.transform());
MatchResult exec = regExp3dValues.exec(property);
style.setProperty(Css3Propertynames.transform(),
"translate3d(" + exec.getGroup(1) + ",0,0)");
}
}
private void prepareForScrolling(Widget p) {
if (p != null) {
// we'll swift the element to place where even ios 6 webkit don't
// sink events for it
Style style = p.getElement().getParentElement().getStyle();
String property = style.getProperty(Css3Propertynames.transform());
MatchResult exec = regExp3dValues.exec(property);
style.setProperty(
Css3Propertynames.transform(),
"translate3d(" + exec.getGroup(1) + ",-"
+ Window.getClientHeight() + "px,0)");
}
}
private void prepareIos6ForScrolling() {
if (needsIos6ScrollingWorkaround && preparedForTranslation3d) {
prepareForScrolling(prevView);
prepareForScrolling(nextView);
preparedForTranslation3d = false;
}
}
private void preparePlaceHolder(String placeholdercaption) {
getPlaceHolder().setHTML(placeholdercaption);
getPlaceHolder().moveToNextPosition();
if (nextView != null) {
moveAside(nextView);
}
}
@Override
public boolean remove(Widget w) {
com.google.gwt.dom.client.Element wrapperElement = w.getElement()
.getParentElement();
boolean removed = super.remove(w);
if (removed) {
wrapper.removeChild(wrapperElement);
}
return removed;
}
public void removeAnimationListener(AnimationListener animationListener) {
animationListeners.remove(animationListener);
}
int lastWidth;
public void resetPositionsAndChildSizes() {
if (getPixelWidth() == lastWidth) {
return;
}
lastWidth = getPixelWidth();
// update positions. Not set with percentages as ios safari bugs
// occasionally with percentages in translate3d.
/*
* Disable animation for while.
*/
wrapper.getStyle().setProperty(Css3Propertynames.transition(), "none");
currentWrapperPos = 0;
animateHorizontally(0, false);
transitionPending = false;
if (currentView != null) {
setPosition(currentView, -currentWrapperPos);
// client.handleComponentRelativeSize((Widget) currentView);
}
if (prevView != null) {
setPosition(prevView, -currentWrapperPos - 1);
// client.handleComponentRelativeSize((Widget) prevView);
}
if (nextView != null) {
setPosition(nextView, -currentWrapperPos + 1);
// client.handleComponentRelativeSize((Widget) nextView);
}
wrapper.getStyle().setProperty(Css3Propertynames.transition(), "");
prepareIos6ForScrolling();
}
public void setCurrentWidget(Widget w) {
if (currentView == null) {
// No currentView => no animation,
// = placeholder navigation done, or this is the first view
if (w == nextView) {
// turns out we're actually going to show the nextView
// null to avoid problems, we're going to show it w/o animation
setNextWidget(null);
} else if (w == prevView) {
// turns out we're actually going to show the prevView
// null to avoid problems, we're going to show it w/o animation
setPreviousWidget(null);
}
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
initIosScroollHack();
}
});
add(w, -currentWrapperPos);
currentView = w;
} else if (nextView == w) {
navigateForward();
} else if (prevView == w) {
navigateBackward();
} else {
if (currentView == w) {
return;
}
// We'll navigate forward since we don't know better -
// explicit direction should be added to the API
setNextWidget(w);
navigateForward();
}
}
public void setHorizontalOffset(int deltaX, boolean animate) {
final Style style = wrapper.getStyle();
if (!animate) {
style.setProperty(Css3Propertynames.transition(), "none");
}
prepareForAnimation();
setPosition(style, currentWrapperPos + deltaX
/ (double) getPixelWidth());
if (!animate) {
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
public void execute() {
style.setProperty(Css3Propertynames.transition(), "");
}
});
}
}
/**
* @param style
* @param pos
* multiple of panel width
*/
private void setLeftUsingTranslate3d(Style style, double pos) {
style.setProperty(Css3Propertynames.transform(),
"translate3d(" + Math.round((int) (pos * getPixelWidth()))
+ "px,0,0)");
}
public void setNextWidget(Widget w) {
if (nextView == w) {
return;
}
if (nextView != null) {
remove(nextView);
}
if (w != null) {
if (w.getParent() != null) {
throw new RuntimeException("P Component already has a parent "
+ w.getElement().getId() + " parent"
+ w.getParent().getElement().getId());
}
add(w, -currentWrapperPos + 1);
}
nextView = w;
}
private void setPosition(Style style, double pos) {
if (style != null) {
style.setTop(0, Unit.PCT);
// style.setLeft(pos * getOffsetWidth(), Unit.PX);
setLeftUsingTranslate3d(style, pos);
style.setOpacity(1);
}
}
/**
* Sets the position of given widget in "widget strip".
*
* @param widget
* the widget inside this component whose position is to be
* modified
* @param pos
* 0 means current, negative are left from current, positive are
* on the right
*/
private void setPosition(Widget widget, int pos) {
if (widget != null && widget.getParent() == this) {
setPosition((widget).getElement().getParentElement().getStyle(),
pos);
}
}
public void setPreviousWidget(Widget w) {
if (prevView == w) {
return;
}
if (prevView != null) {
remove(prevView);
}
if (w != null) {
if (w.getParent() != null) {
throw new RuntimeException("P Component already has a parent "
+ w.getElement().getId() + " parent"
+ w.getParent().getElement().getId());
}
add(w, -currentWrapperPos - 1);
}
prevView = w;
}
private void slideFromLeft() {
animateHorizontally(1);
}
}