diff options
-rw-r--r-- | client/src/main/java/com/vaadin/client/widgets/Escalator.java | 261 |
1 files changed, 140 insertions, 121 deletions
diff --git a/client/src/main/java/com/vaadin/client/widgets/Escalator.java b/client/src/main/java/com/vaadin/client/widgets/Escalator.java index 25e83592d7..4c78311ea7 100644 --- a/client/src/main/java/com/vaadin/client/widgets/Escalator.java +++ b/client/src/main/java/com/vaadin/client/widgets/Escalator.java @@ -30,9 +30,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import com.google.gwt.animation.client.Animation; -import com.google.gwt.animation.client.AnimationScheduler; -import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback; -import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle; import com.google.gwt.core.client.Duration; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray; @@ -48,6 +45,7 @@ import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.TableCellElement; +import com.google.gwt.dom.client.TableElement; import com.google.gwt.dom.client.TableRowElement; import com.google.gwt.dom.client.TableSectionElement; import com.google.gwt.dom.client.Touch; @@ -55,6 +53,7 @@ import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.logging.client.LogConfiguration; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.RequiresResize; import com.google.gwt.user.client.ui.RootPanel; @@ -319,16 +318,10 @@ public class Escalator extends Widget implements RequiresResize, * {@link com.google.gwt.dom.client.NativeEvent NativeEvent} isn't * properly populated with the correct values. */ - private final static class CustomTouchEvent extends - JavaScriptObject { + private final static class CustomTouchEvent extends NativeEvent { protected CustomTouchEvent() { } - public native NativeEvent getNativeEvent() - /*-{ - return this; - }-*/; - public native int getPageX() /*-{ return this.targetTouches[0].pageX; @@ -338,6 +331,11 @@ public class Escalator extends Widget implements RequiresResize, /*-{ return this.targetTouches[0].pageY; }-*/; + + public native boolean isCancelable() + /*-{ + return this.cancelable; + }-*/; } private final Escalator escalator; @@ -389,6 +387,7 @@ public class Escalator extends Widget implements RequiresResize, final List<Double> speeds = new ArrayList<Double>(); final ScrollbarBundle scroll; double position, offset, velocity, prevPos, prevTime, delta; + double scrollMax; boolean run, vertical; public Movement(boolean vertical) { @@ -401,11 +400,15 @@ public class Escalator extends Widget implements RequiresResize, speeds.clear(); prevPos = pagePosition(event); prevTime = Duration.currentTimeMillis(); + scrollMax = scroll.getScrollSize() - scroll.getOffsetSize(); + delta = 0; } public void moveTouch(CustomTouchEvent event) { double pagePosition = pagePosition(event); - if (pagePosition > -1) { + run = false; + // skip grids without scroll + if (scrollMax > 1) { delta = prevPos - pagePosition; double now = Duration.currentTimeMillis(); double ellapsed = now - prevTime; @@ -414,14 +417,23 @@ public class Escalator extends Widget implements RequiresResize, // storing again if (speeds.size() > 0 && !validSpeed(speeds.get(0))) { speeds.clear(); - run = true; } speeds.add(0, velocity); prevTime = now; prevPos = pagePosition; + position = scroll.getScrollPos(); } } + public void validate(Movement other) { + // We don't move the scroll if no delta, scroll position + // has reached the edge, or movement in one direction is + // insignificant. + run = delta != 0 && inScrollRange(position + delta) + && Math.abs(other.delta / delta) < F_AXIS; + if (!run) delta = 0; + } + public void endTouch(CustomTouchEvent event) { // Compute average speed velocity = 0; @@ -438,24 +450,24 @@ public class Escalator extends Widget implements RequiresResize, // Enable or disable inertia movement in this axis run = validSpeed(velocity); if (run) { - event.getNativeEvent().preventDefault(); + event.preventDefault(); } } - void validate(Movement other) { - if (!run || other.velocity > 0 - && Math.abs(velocity / other.velocity) < F_AXIS) { - delta = offset = 0; - run = false; + void stepAnimation(double progress) { + if (run) { + double p = position + offset * progress; + scroll.setScrollPos(p); + run = inScrollRange(p); } } - void stepAnimation(double progress) { - scroll.setScrollPos(position + offset * progress); + boolean inScrollRange(double p) { + return p > 0 && p < scrollMax; } int pagePosition(CustomTouchEvent event) { - JsArray<Touch> a = event.getNativeEvent().getTouches(); + JsArray<Touch> a = event.getTouches(); return vertical ? a.get(0).getPageY() : a.get(0).getPageX(); } @@ -470,6 +482,11 @@ public class Escalator extends Widget implements RequiresResize, public void onUpdate(double progress) { xMov.stepAnimation(progress); yMov.stepAnimation(progress); + if (!xMov.run && !yMov.run) { + // Stop animation as soon as we reach the border, + // so as we do not wait to move the external scroll. + cancel(); + } } @Override @@ -494,14 +511,19 @@ public class Escalator extends Widget implements RequiresResize, }; public void touchStart(final CustomTouchEvent event) { - if (event.getNativeEvent().getTouches().length() == 1) { + // Consider only one-finger gestures over the body. + if (eventOnBody(escalator, event) + && event.getTouches().length() == 1) { if (yMov == null) { yMov = new Movement(true); xMov = new Movement(false); + // Mark this as a touch device. Useful for + // fix hover styles in iOS. + escalator.bodyElem.addClassName("touch"); } if (animation.isRunning()) { acceleration += F_ACC; - event.getNativeEvent().preventDefault(); + event.preventDefault(); animation.cancel(); } else { acceleration = 1; @@ -510,6 +532,7 @@ public class Escalator extends Widget implements RequiresResize, yMov.startTouch(event); touching = true; } else { + // Cancel to allow multi-finger gestures like zoom. touching = false; animation.cancel(); acceleration = 1; @@ -517,14 +540,22 @@ public class Escalator extends Widget implements RequiresResize, } public void touchMove(final CustomTouchEvent event) { - if (touching) { + if (touching && event.isCancelable()) { xMov.moveTouch(event); yMov.moveTouch(event); xMov.validate(yMov); yMov.validate(xMov); - event.getNativeEvent().preventDefault(); - moveScrollFromEvent(escalator, xMov.delta, yMov.delta, - event.getNativeEvent()); + if (xMov.run) { + xMov.scroll.setScrollPosByDelta(xMov.delta); + } + if (yMov.run) { + yMov.scroll.setScrollPosByDelta(yMov.delta); + } + if (xMov.run || yMov.run) { + // If we move the scroll prevent default, otherwise + // pass the control to the device. + event.preventDefault(); + } } } @@ -534,7 +565,7 @@ public class Escalator extends Widget implements RequiresResize, yMov.endTouch(event); xMov.validate(yMov); yMov.validate(xMov); - // Adjust duration so as longer movements take more duration + // Adjust duration so as longer movements take bigger duration boolean vert = !xMov.run || yMov.run && Math.abs(yMov.offset) > Math.abs(xMov.offset); double delta = Math.abs((vert ? yMov : xMov).offset); @@ -556,28 +587,47 @@ public class Escalator extends Widget implements RequiresResize, } } + public static boolean eventOnBody(Escalator escalator, NativeEvent event) { + Element e = event.getEventTarget().<Element>cast(); + // Consider the event if it comes from an element in the body, + // of from the main table (when setting position by code) + return TableElement.is(e) + || escalator.bodyElem.isOrHasChild(e); + } + public static void moveScrollFromEvent(final Escalator escalator, final double deltaX, final double deltaY, final NativeEvent event) { - if (!Double.isNaN(deltaX)) { - escalator.horizontalScrollbar.setScrollPosByDelta(deltaX); + // Prevent scrolling on Headers/Footers + if (!eventOnBody(escalator, event)) { + return; } - if (!Double.isNaN(deltaY)) { - escalator.verticalScrollbar.setScrollPosByDelta(deltaY); - } + boolean movex = !Double.isNaN(deltaX); + boolean movey = !Double.isNaN(deltaY); + if (movex || movey) { + escalator.bodyElem.addClassName("scrolling"); + if (movex) { + escalator.horizontalScrollbar.setScrollPosByDelta(deltaX); + } + if (movey) { + escalator.verticalScrollbar.setScrollPosByDelta(deltaY); + } + escalator.body.domSorter.reschedule(); - /* - * TODO: only prevent if not scrolled to end/bottom. Or no? UX team - * needs to decide. - */ - final boolean warrantedYScroll = deltaY != 0 - && escalator.verticalScrollbar.showsScrollHandle(); - final boolean warrantedXScroll = deltaX != 0 - && escalator.horizontalScrollbar.showsScrollHandle(); - if (warrantedYScroll || warrantedXScroll) { - event.preventDefault(); + /* + * TODO: only prevent if not scrolled to end/bottom. Or no? UX + * team needs to decide. In touch devices movement is not + * prevented when the edge is reached. + */ + final boolean warrantedYScroll = deltaY != 0 + && escalator.verticalScrollbar.showsScrollHandle(); + final boolean warrantedXScroll = deltaX != 0 + && escalator.horizontalScrollbar.showsScrollHandle(); + if (warrantedYScroll || warrantedXScroll) { + event.preventDefault(); + } } } } @@ -1083,6 +1133,10 @@ public class Escalator extends Widget implements RequiresResize, private double defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT; + private boolean autodetectRowHeightLaterQueued = false; + + private Element detectionTr, cellElem; + public AbstractRowContainer( final TableSectionElement rowContainerElement) { root = rowContainerElement; @@ -1884,41 +1938,45 @@ public class Escalator extends Widget implements RequiresResize, } public void autodetectRowHeightLater() { - Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() { - @Override - public void execute() { - if (defaultRowHeightShouldBeAutodetected && isAttached()) { - autodetectRowHeightNow(); - defaultRowHeightShouldBeAutodetected = false; + if (!autodetectRowHeightLaterQueued) { + autodetectRowHeightLaterQueued = true; + Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() { + @Override + public void execute() { + if (isAttached()) { + autodetectRowHeightLaterQueued = false; + autodetectRowHeightNow(); + } } - } - }); + }); + } } public void autodetectRowHeightNow() { - if (!isAttached()) { - // Run again when attached - defaultRowHeightShouldBeAutodetected = true; + if (!defaultRowHeightShouldBeAutodetected && !isAttached()) { return; } + if (detectionTr == null) { + detectionTr = DOM.createTR(); + detectionTr.setClassName(getStylePrimaryName() + "-row"); + cellElem = DOM.createElement(getCellElementTagName()); + cellElem.setClassName(getStylePrimaryName() + "-cell"); + cellElem.setInnerText("Ij"); + detectionTr.appendChild(cellElem); + } - final Element detectionTr = DOM.createTR(); - detectionTr.setClassName(getStylePrimaryName() + "-row"); - - final Element cellElem = DOM.createElement(getCellElementTagName()); - cellElem.setClassName(getStylePrimaryName() + "-cell"); - cellElem.setInnerText("Ij"); - - detectionTr.appendChild(cellElem); root.appendChild(detectionTr); - double boundingHeight = WidgetUtil - .getRequiredHeightBoundingClientRectDouble(cellElem); - defaultRowHeight = Math.max(1.0d, boundingHeight); + double boundingHeight = WidgetUtil.getRequiredHeightBoundingClientRectDouble(cellElem); root.removeChild(detectionTr); - if (root.hasChildNodes()) { - reapplyDefaultRowHeights(); - applyHeightByRows(); + // Height lesser than 1px causes serious performance problems, skip this hit. + if (boundingHeight >= 1 && defaultRowHeight != boundingHeight) { + defaultRowHeight = boundingHeight; + defaultRowHeightShouldBeAutodetected = false; + if (root.hasChildNodes()) { + reapplyDefaultRowHeights(); + applyHeightByRows(); + } } } @@ -2342,62 +2400,23 @@ public class Escalator extends Widget implements RequiresResize, setTopRowLogicalIndex(topRowLogicalIndex + diff); } - private class DeferredDomSorter { - private static final int SORT_DELAY_MILLIS = 50; - - // as it happens, 3 frames = 50ms @ 60fps. - private static final int REQUIRED_FRAMES_PASSED = 3; - - private final AnimationCallback frameCounter = new AnimationCallback() { - @Override - public void execute(double timestamp) { - framesPassed++; - boolean domWasSorted = sortIfConditionsMet(); - if (!domWasSorted) { - animationHandle = AnimationScheduler.get() - .requestAnimationFrame(this); - } else { - waiting = false; - } - } - }; - - private int framesPassed; - private double startTime; - private AnimationHandle animationHandle; - - /** <code>true</code> if a sort is scheduled */ - public boolean waiting = false; + private class DeferredDomSorter extends Timer implements ScheduledCommand { + // 1000/50 (@50fps) + private static final int SORT_DELAY_MILLIS = 20; public void reschedule() { - waiting = true; - resetConditions(); - animationHandle = AnimationScheduler.get() - .requestAnimationFrame(frameCounter); - } - - private boolean sortIfConditionsMet() { - boolean enoughFramesHavePassed = framesPassed >= REQUIRED_FRAMES_PASSED; - boolean enoughTimeHasPassed = (Duration.currentTimeMillis() - startTime) >= SORT_DELAY_MILLIS; - boolean notTouchActivity = !scroller.touchHandlerBundle.touching; - boolean conditionsMet = enoughFramesHavePassed - && enoughTimeHasPassed && notTouchActivity; - - if (conditionsMet) { - resetConditions(); - sortDomElements(); - } + schedule(SORT_DELAY_MILLIS); + } - return conditionsMet; + @Override + public void run() { + Scheduler.get().scheduleFinally(this); } - private void resetConditions() { - if (animationHandle != null) { - animationHandle.cancel(); - animationHandle = null; - } - startTime = Duration.currentTimeMillis(); - framesPassed = 0; + @Override + public void execute() { + sortDomElements(); + bodyElem.removeClassName("scrolling"); } } @@ -6072,7 +6091,7 @@ public class Escalator extends Widget implements RequiresResize, public void scrollToRowAndSpacer(final int rowIndex, final ScrollDestination destination, final int padding) throws IllegalArgumentException { - Scheduler.get().scheduleFinally(new ScheduledCommand() { + Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { validateScrollDestination(destination, padding); @@ -6496,7 +6515,7 @@ public class Escalator extends Widget implements RequiresResize, @Override public boolean isWorkPending() { - return body.domSorter.waiting || verticalScrollbar.isWorkPending() + return body.domSorter.isRunning() || verticalScrollbar.isWorkPending() || horizontalScrollbar.isWorkPending() || layoutIsScheduled; } |