You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

TouchScrollDelegate.java 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. /*
  2. * Copyright 2000-2021 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.client.ui;
  17. import java.util.ArrayList;
  18. import java.util.Arrays;
  19. import java.util.HashSet;
  20. import java.util.List;
  21. import java.util.logging.Logger;
  22. import com.google.gwt.animation.client.Animation;
  23. import com.google.gwt.core.client.Duration;
  24. import com.google.gwt.core.client.GWT;
  25. import com.google.gwt.dom.client.Element;
  26. import com.google.gwt.dom.client.NativeEvent;
  27. import com.google.gwt.dom.client.Node;
  28. import com.google.gwt.dom.client.NodeList;
  29. import com.google.gwt.dom.client.Style;
  30. import com.google.gwt.dom.client.Touch;
  31. import com.google.gwt.event.dom.client.ScrollHandler;
  32. import com.google.gwt.event.dom.client.TouchStartEvent;
  33. import com.google.gwt.event.dom.client.TouchStartHandler;
  34. import com.google.gwt.event.shared.HandlerRegistration;
  35. import com.google.gwt.user.client.Event;
  36. import com.google.gwt.user.client.Event.NativePreviewEvent;
  37. import com.google.gwt.user.client.Event.NativePreviewHandler;
  38. import com.google.gwt.user.client.ui.Widget;
  39. import com.vaadin.client.BrowserInfo;
  40. /**
  41. * Provides one finger touch scrolling for elements with once scrollable
  42. * elements inside. One widget can have several of these scrollable elements.
  43. * Scrollable elements are provided in the constructor. Users must pass
  44. * touchStart events to this delegate, from there on the delegate takes over
  45. * with an event preview. Other touch events needs to be sunken though.
  46. * <p>
  47. * This is bit similar as Scroller class in GWT expenses example, but ideas
  48. * drawn from iscroll.js project:
  49. * <ul>
  50. * <li>uses GWT event mechanism.
  51. * <li>uses modern CSS trick during scrolling for smoother experience:
  52. * translate3d and transitions
  53. * </ul>
  54. * <p>
  55. * Scroll event should only happen when the "touch scrolling actually ends".
  56. * Later we might also tune this so that a scroll event happens if user stalls
  57. * her finger long enought.
  58. *
  59. * TODO static getter for active touch scroll delegate. Components might need to
  60. * prevent scrolling in some cases. Consider Table with drag and drop, or drag
  61. * and drop in scrollable area. Optimal implementation might be to start the
  62. * drag and drop only if user keeps finger down for a moment, otherwise do the
  63. * scroll. In this case, the draggable component would need to cancel scrolling
  64. * in a timer after touchstart event and take over from there.
  65. *
  66. * TODO support scrolling horizontally
  67. *
  68. * TODO cancel if user add second finger to the screen (user expects a gesture).
  69. *
  70. * TODO "scrollbars", see e.g. iscroll.js
  71. *
  72. * TODO write an email to sjobs ät apple dot com and beg for this feature to be
  73. * built into webkit. Seriously, we should try to lobbying this to webkit folks.
  74. * This sure ain't our business to implement this with javascript.
  75. *
  76. * TODO collect all general touch related constant to better place.
  77. *
  78. * @author Matti Tahvonen, Vaadin Ltd
  79. */
  80. public class TouchScrollDelegate implements NativePreviewHandler {
  81. private static final double FRICTION = 0.002;
  82. private static final double DECELERATION = 0.002;
  83. private static final int MAX_DURATION = 1500;
  84. private int origY;
  85. private HashSet<Element> scrollableElements;
  86. private Element scrolledElement;
  87. private int origScrollTop;
  88. private HandlerRegistration handlerRegistration;
  89. private double lastAnimatedTranslateY;
  90. private int lastClientY;
  91. private int deltaScrollPos;
  92. private boolean transitionOn = false;
  93. private int finalScrollTop;
  94. private List<Element> layers;
  95. private boolean moved;
  96. private ScrollHandler scrollHandler;
  97. private static TouchScrollDelegate activeScrollDelegate;
  98. private static final boolean ANDROID_WITH_BROKEN_SCROLL_TOP = BrowserInfo
  99. .get().isAndroidWithBrokenScrollTop();
  100. /**
  101. * A helper class for making a widget scrollable. Uses native scrolling if
  102. * supported by the browser, otherwise registers a touch start handler
  103. * delegating to a TouchScrollDelegate instance.
  104. */
  105. public static class TouchScrollHandler implements TouchStartHandler {
  106. private static final String SCROLLABLE_CLASSNAME = "v-scrollable";
  107. private TouchScrollDelegate delegate;
  108. private final boolean requiresDelegate = BrowserInfo.get()
  109. .requiresTouchScrollDelegate();
  110. private Widget widget;
  111. /**
  112. * Constructs a scroll handler. You must call
  113. * {@link #init(Widget, Element...)} before using the scroll handler.
  114. */
  115. public TouchScrollHandler() {
  116. }
  117. /**
  118. * Attaches the scroll handler to the widget. This method must be called
  119. * before calling any other methods in this class.
  120. *
  121. * @param widget
  122. * The widget that contains scrollable elements
  123. * @param scrollables
  124. * The elements of the widget that should be scrollable.
  125. *
  126. * @deprecated Use {@link GWT#create(Class)} and
  127. * {@link #init(Widget, Element...)} instead of this
  128. * constructor to enable overriding.
  129. */
  130. @Deprecated
  131. public TouchScrollHandler(Widget widget, Element... scrollables) {
  132. this();
  133. init(widget, scrollables);
  134. }
  135. /**
  136. * Attaches the scroll handler to the widget. This method must be called
  137. * once before calling any other method in this class.
  138. *
  139. * @param widget
  140. * The widget that contains scrollable elements
  141. * @param scrollables
  142. * The elements of the widget that should be scrollable.
  143. */
  144. public void init(Widget widget, Element... scrollables) {
  145. this.widget = widget;
  146. if (requiresDelegate()) {
  147. delegate = new TouchScrollDelegate();
  148. widget.addDomHandler(this, TouchStartEvent.getType());
  149. } else {
  150. delegate = null;
  151. }
  152. setElements(scrollables);
  153. }
  154. @Override
  155. public void onTouchStart(TouchStartEvent event) {
  156. assert delegate != null;
  157. delegate.onTouchStart(event);
  158. }
  159. public void debug(Element e) {
  160. getLogger().info("Classes: " + e.getClassName() + " overflow: "
  161. + e.getStyle().getProperty("overflow") + " w-o-s: "
  162. + e.getStyle().getProperty("WebkitOverflowScrolling"));
  163. }
  164. /**
  165. * Registers the given element as scrollable.
  166. */
  167. public void addElement(Element scrollable) {
  168. scrollable.addClassName(SCROLLABLE_CLASSNAME);
  169. if (requiresDelegate()) {
  170. delegate.scrollableElements.add(scrollable);
  171. }
  172. }
  173. /**
  174. * Unregisters the given element as scrollable. Should be called when a
  175. * previously-registered element is removed from the DOM to prevent
  176. * memory leaks.
  177. */
  178. public void removeElement(Element scrollable) {
  179. scrollable.removeClassName(SCROLLABLE_CLASSNAME);
  180. if (requiresDelegate()) {
  181. delegate.scrollableElements.remove(scrollable);
  182. }
  183. }
  184. /**
  185. * Registers the given elements as scrollable, removing previously
  186. * registered scrollables from this handler.
  187. *
  188. * @param scrollables
  189. * The elements that should be scrollable
  190. */
  191. public void setElements(Element... scrollables) {
  192. if (requiresDelegate()) {
  193. for (Element e : delegate.scrollableElements) {
  194. e.removeClassName(SCROLLABLE_CLASSNAME);
  195. }
  196. delegate.scrollableElements.clear();
  197. }
  198. for (Element e : scrollables) {
  199. addElement(e);
  200. }
  201. }
  202. /**
  203. * Checks if a delegate for scrolling is required or if the native
  204. * scrolling of the device should be used. By default, relies on
  205. * {@link BrowserInfo#requiresTouchScrollDelegate()}, override to change
  206. * the behavior.
  207. *
  208. * @return true if a Javascript delegate should be used for scrolling,
  209. * false to use the native scrolling of the device
  210. */
  211. protected boolean requiresDelegate() {
  212. return requiresDelegate;
  213. }
  214. /**
  215. * @return The widget this {@link TouchScrollHandler} is connected to.
  216. */
  217. protected Widget getWidget() {
  218. return widget;
  219. }
  220. }
  221. /**
  222. * Makes the given elements scrollable, either natively or by using a
  223. * TouchScrollDelegate, depending on platform capabilities.
  224. *
  225. * @param widget
  226. * The widget that contains scrollable elements
  227. * @param scrollables
  228. * The elements inside the widget that should be scrollable
  229. * @return A scroll handler for the given widget.
  230. */
  231. public static TouchScrollHandler enableTouchScrolling(Widget widget,
  232. Element... scrollables) {
  233. TouchScrollHandler handler = GWT.create(TouchScrollHandler.class);
  234. handler.init(widget, scrollables);
  235. return handler;
  236. }
  237. public TouchScrollDelegate(Element... elements) {
  238. setElements(elements);
  239. }
  240. public void setScrollHandler(ScrollHandler scrollHandler) {
  241. this.scrollHandler = scrollHandler;
  242. }
  243. public static TouchScrollDelegate getActiveScrollDelegate() {
  244. return activeScrollDelegate;
  245. }
  246. /**
  247. * Has user moved the touch.
  248. *
  249. * @return
  250. */
  251. public boolean isMoved() {
  252. return moved;
  253. }
  254. /**
  255. * Forces the scroll delegate to cancels scrolling process. Can be called by
  256. * users if they e.g. decide to handle touch event by themselves after all
  257. * (e.g. a pause after touch start before moving touch -> interpreted as
  258. * long touch/click or drag start).
  259. */
  260. public void stopScrolling() {
  261. handlerRegistration.removeHandler();
  262. handlerRegistration = null;
  263. if (moved) {
  264. moveTransformationToScrolloffset();
  265. } else {
  266. activeScrollDelegate = null;
  267. }
  268. }
  269. public void onTouchStart(TouchStartEvent event) {
  270. if (activeScrollDelegate == null && event.getTouches().length() == 1) {
  271. NativeEvent nativeEvent = event.getNativeEvent();
  272. doTouchStart(nativeEvent);
  273. } else {
  274. /*
  275. * Touch scroll is currenly on (possibly bouncing). Ignore.
  276. */
  277. }
  278. }
  279. private void doTouchStart(NativeEvent nativeEvent) {
  280. if (transitionOn) {
  281. momentum.cancel();
  282. }
  283. Touch touch = nativeEvent.getTouches().get(0);
  284. if (detectScrolledElement(touch)) {
  285. getLogger().info("TouchDelegate takes over");
  286. nativeEvent.stopPropagation();
  287. handlerRegistration = Event.addNativePreviewHandler(this);
  288. activeScrollDelegate = this;
  289. origY = touch.getClientY();
  290. yPositions[0] = origY;
  291. eventTimeStamps[0] = getTimeStamp();
  292. nextEvent = 1;
  293. origScrollTop = getScrollTop();
  294. getLogger().info("ST" + origScrollTop);
  295. moved = false;
  296. // event.preventDefault();
  297. // event.stopPropagation();
  298. }
  299. }
  300. private int getScrollTop() {
  301. if (ANDROID_WITH_BROKEN_SCROLL_TOP) {
  302. if (scrolledElement.getPropertyJSO("_vScrollTop") != null) {
  303. return scrolledElement.getPropertyInt("_vScrollTop");
  304. }
  305. return 0;
  306. }
  307. return scrolledElement.getScrollTop();
  308. }
  309. private void onTransitionEnd() {
  310. if (finalScrollTop < 0) {
  311. animateToScrollPosition(0, finalScrollTop);
  312. finalScrollTop = 0;
  313. } else if (finalScrollTop > getMaxFinalY()) {
  314. animateToScrollPosition(getMaxFinalY(), finalScrollTop);
  315. finalScrollTop = getMaxFinalY();
  316. } else {
  317. moveTransformationToScrolloffset();
  318. }
  319. }
  320. private void animateToScrollPosition(int to, int from) {
  321. int dist = Math.abs(to - from);
  322. int time = getAnimationTimeForDistance(dist);
  323. if (time <= 0) {
  324. time = 1; // get animation and transition end event
  325. }
  326. getLogger().info("Animate " + time + " " + from + " " + to);
  327. int translateTo = -to + origScrollTop;
  328. int fromY = -from + origScrollTop;
  329. if (ANDROID_WITH_BROKEN_SCROLL_TOP) {
  330. fromY -= origScrollTop;
  331. translateTo -= origScrollTop;
  332. }
  333. translateTo(time, fromY, translateTo);
  334. }
  335. private int getAnimationTimeForDistance(int dist) {
  336. return 350; // 350ms seems to work quite fine for all distances
  337. // if (dist < 0) {
  338. // dist = -dist;
  339. // }
  340. // return MAX_DURATION * dist / (scrolledElement.getClientHeight() * 3);
  341. }
  342. /**
  343. * Called at the end of scrolling. Moves possible translate values to
  344. * scrolltop, causing onscroll event.
  345. */
  346. private void moveTransformationToScrolloffset() {
  347. if (ANDROID_WITH_BROKEN_SCROLL_TOP) {
  348. scrolledElement.setPropertyInt("_vScrollTop", finalScrollTop);
  349. if (scrollHandler != null) {
  350. scrollHandler.onScroll(null);
  351. }
  352. } else {
  353. for (Element el : layers) {
  354. Style style = el.getStyle();
  355. style.setProperty("webkitTransform", "translate3d(0,0,0)");
  356. }
  357. scrolledElement.setScrollTop(finalScrollTop);
  358. }
  359. activeScrollDelegate = null;
  360. handlerRegistration.removeHandler();
  361. handlerRegistration = null;
  362. }
  363. /**
  364. * Detects if a touch happens on a predefined element and the element has
  365. * something to scroll.
  366. *
  367. * @param touch
  368. * @return
  369. */
  370. private boolean detectScrolledElement(Touch touch) {
  371. Element target = touch.getTarget().cast();
  372. for (Element el : scrollableElements) {
  373. if (el.isOrHasChild(target)
  374. && el.getScrollHeight() > el.getClientHeight()) {
  375. scrolledElement = el;
  376. layers = getElements(scrolledElement);
  377. return true;
  378. }
  379. }
  380. return false;
  381. }
  382. public static ArrayList<Element> getElements(Element scrolledElement2) {
  383. NodeList<Node> childNodes = scrolledElement2.getChildNodes();
  384. ArrayList<Element> l = new ArrayList<>();
  385. for (int i = 0; i < childNodes.getLength(); i++) {
  386. Node item = childNodes.getItem(i);
  387. if (item.getNodeType() == Node.ELEMENT_NODE) {
  388. l.add((Element) item);
  389. }
  390. }
  391. return l;
  392. }
  393. private void onTouchMove(NativeEvent event) {
  394. if (!moved) {
  395. double l = (getTimeStamp() - eventTimeStamps[0]);
  396. getLogger().info(l + " ms from start to move");
  397. }
  398. boolean handleMove = readPositionAndSpeed(event);
  399. if (handleMove) {
  400. int deltaScrollTop = origY - lastClientY;
  401. int finalPos = origScrollTop + deltaScrollTop;
  402. if (finalPos > getMaxFinalY()) {
  403. // spring effect at the end
  404. int overscroll = (deltaScrollTop + origScrollTop)
  405. - getMaxFinalY();
  406. overscroll = overscroll / 2;
  407. if (overscroll > getMaxOverScroll()) {
  408. overscroll = getMaxOverScroll();
  409. }
  410. deltaScrollTop = getMaxFinalY() + overscroll - origScrollTop;
  411. } else if (finalPos < 0) {
  412. // spring effect at the beginning
  413. int overscroll = finalPos / 2;
  414. if (-overscroll > getMaxOverScroll()) {
  415. overscroll = -getMaxOverScroll();
  416. }
  417. deltaScrollTop = overscroll - origScrollTop;
  418. }
  419. quickSetScrollPosition(deltaScrollTop);
  420. moved = true;
  421. event.preventDefault();
  422. event.stopPropagation();
  423. }
  424. }
  425. private void quickSetScrollPosition(int deltaY) {
  426. deltaScrollPos = deltaY;
  427. if (ANDROID_WITH_BROKEN_SCROLL_TOP) {
  428. deltaY += origScrollTop;
  429. translateTo(-deltaY);
  430. } else {
  431. translateTo(-deltaScrollPos);
  432. }
  433. }
  434. private static final int EVENTS_FOR_SPEED_CALC = 3;
  435. public static final int SIGNIFICANT_MOVE_THRESHOLD = 3;
  436. private int[] yPositions = new int[EVENTS_FOR_SPEED_CALC];
  437. private double[] eventTimeStamps = new double[EVENTS_FOR_SPEED_CALC];
  438. private int nextEvent = 0;
  439. private Animation momentum;
  440. /**
  441. *
  442. * @param event
  443. * @return
  444. */
  445. private boolean readPositionAndSpeed(NativeEvent event) {
  446. Touch touch = event.getChangedTouches().get(0);
  447. lastClientY = touch.getClientY();
  448. int eventIndx = nextEvent++;
  449. eventIndx = eventIndx % EVENTS_FOR_SPEED_CALC;
  450. eventTimeStamps[eventIndx] = getTimeStamp();
  451. yPositions[eventIndx] = lastClientY;
  452. return isMovedSignificantly();
  453. }
  454. private boolean isMovedSignificantly() {
  455. return moved ? moved
  456. : Math.abs(origY - lastClientY) >= SIGNIFICANT_MOVE_THRESHOLD;
  457. }
  458. private void onTouchEnd() {
  459. if (!moved) {
  460. activeScrollDelegate = null;
  461. handlerRegistration.removeHandler();
  462. handlerRegistration = null;
  463. return;
  464. }
  465. int currentY = origScrollTop + deltaScrollPos;
  466. int maxFinalY = getMaxFinalY();
  467. int pixelsToMove;
  468. int finalY;
  469. int duration = -1;
  470. if (currentY > maxFinalY) {
  471. // we are over the max final pos, animate to end
  472. pixelsToMove = maxFinalY - currentY;
  473. finalY = maxFinalY;
  474. } else if (currentY < 0) {
  475. // we are below the max final pos, animate to beginning
  476. pixelsToMove = -currentY;
  477. finalY = 0;
  478. } else {
  479. double pixelsPerMs = calculateSpeed();
  480. // we are currently within scrollable area, calculate pixels that
  481. // we'll move due to momentum
  482. getLogger().info("pxPerMs" + pixelsPerMs);
  483. pixelsToMove = (int) (0.5 * pixelsPerMs * pixelsPerMs / FRICTION);
  484. if (pixelsPerMs < 0) {
  485. pixelsToMove = -pixelsToMove;
  486. }
  487. finalY = currentY + pixelsToMove;
  488. if (finalY > maxFinalY + getMaxOverScroll()) {
  489. finalY = getMaxFinalY() + getMaxOverScroll();
  490. int fixedPixelsToMove = finalY - currentY;
  491. pixelsToMove = fixedPixelsToMove;
  492. } else if (finalY < 0 - getMaxOverScroll()) {
  493. finalY = -getMaxOverScroll();
  494. int fixedPixelsToMove = finalY - currentY;
  495. pixelsToMove = fixedPixelsToMove;
  496. } else {
  497. duration = (int) (Math.abs(pixelsPerMs / DECELERATION));
  498. }
  499. }
  500. if (duration == -1) {
  501. // did not keep in side borders or was outside borders, calculate
  502. // a good enough duration based on pixelsToMove.
  503. duration = getAnimationTimeForDistance(pixelsToMove);
  504. }
  505. if (duration > MAX_DURATION) {
  506. getLogger().info("Max animation time. " + duration);
  507. duration = MAX_DURATION;
  508. }
  509. finalScrollTop = finalY;
  510. if (Math.abs(pixelsToMove) < 3 || duration < 20) {
  511. getLogger().info("Small 'momentum' " + pixelsToMove + " | "
  512. + duration + " Skipping animation,");
  513. moveTransformationToScrolloffset();
  514. return;
  515. }
  516. int translateTo = -finalY + origScrollTop;
  517. int fromY = -currentY + origScrollTop;
  518. if (ANDROID_WITH_BROKEN_SCROLL_TOP) {
  519. fromY -= origScrollTop;
  520. translateTo -= origScrollTop;
  521. }
  522. translateTo(duration, fromY, translateTo);
  523. }
  524. private double calculateSpeed() {
  525. if (nextEvent < EVENTS_FOR_SPEED_CALC) {
  526. getLogger().info("Not enough data for speed calculation");
  527. // not enough data for decent speed calculation, no momentum :-(
  528. return 0;
  529. }
  530. int idx = nextEvent % EVENTS_FOR_SPEED_CALC;
  531. final int firstPos = yPositions[idx];
  532. final double firstTs = eventTimeStamps[idx];
  533. idx += EVENTS_FOR_SPEED_CALC;
  534. idx--;
  535. idx = idx % EVENTS_FOR_SPEED_CALC;
  536. final int lastPos = yPositions[idx];
  537. final double lastTs = eventTimeStamps[idx];
  538. // speed as in change of scrolltop == -speedOfTouchPos
  539. return (firstPos - lastPos) / (lastTs - firstTs);
  540. }
  541. /**
  542. * Note positive scrolltop moves layer up, positive translate moves layer
  543. * down.
  544. */
  545. private void translateTo(double translateY) {
  546. for (Element el : layers) {
  547. Style style = el.getStyle();
  548. style.setProperty("webkitTransform",
  549. "translate3d(0px," + translateY + "px,0px)");
  550. }
  551. }
  552. /**
  553. * Note positive scrolltop moves layer up, positive translate moves layer
  554. * down.
  555. *
  556. * @param duration
  557. */
  558. private void translateTo(int duration, final int fromY, final int finalY) {
  559. if (duration > 0) {
  560. transitionOn = true;
  561. momentum = new Animation() {
  562. @Override
  563. protected void onUpdate(double progress) {
  564. lastAnimatedTranslateY = (fromY
  565. + (finalY - fromY) * progress);
  566. translateTo(lastAnimatedTranslateY);
  567. }
  568. @Override
  569. protected double interpolate(double progress) {
  570. return 1 + Math.pow(progress - 1, 3);
  571. }
  572. @Override
  573. protected void onComplete() {
  574. super.onComplete();
  575. transitionOn = false;
  576. onTransitionEnd();
  577. }
  578. @Override
  579. protected void onCancel() {
  580. int delta = (int) (finalY - lastAnimatedTranslateY);
  581. finalScrollTop -= delta;
  582. moveTransformationToScrolloffset();
  583. transitionOn = false;
  584. }
  585. };
  586. momentum.run(duration);
  587. }
  588. }
  589. private int getMaxOverScroll() {
  590. return ANDROID_WITH_BROKEN_SCROLL_TOP ? 0
  591. : scrolledElement.getClientHeight() / 3;
  592. }
  593. private int getMaxFinalY() {
  594. return scrolledElement.getScrollHeight()
  595. - scrolledElement.getClientHeight();
  596. }
  597. @Override
  598. public void onPreviewNativeEvent(NativePreviewEvent event) {
  599. int typeInt = event.getTypeInt();
  600. if (transitionOn) {
  601. /*
  602. * TODO allow starting new events. See issue in onTouchStart
  603. */
  604. event.cancel();
  605. if (typeInt == Event.ONTOUCHSTART) {
  606. doTouchStart(event.getNativeEvent());
  607. }
  608. return;
  609. }
  610. switch (typeInt) {
  611. case Event.ONTOUCHMOVE:
  612. if (!event.isCanceled()) {
  613. onTouchMove(event.getNativeEvent());
  614. if (moved) {
  615. event.cancel();
  616. }
  617. }
  618. break;
  619. case Event.ONTOUCHEND:
  620. case Event.ONTOUCHCANCEL:
  621. if (!event.isCanceled()) {
  622. if (moved) {
  623. event.cancel();
  624. }
  625. onTouchEnd();
  626. }
  627. break;
  628. case Event.ONMOUSEMOVE:
  629. if (moved) {
  630. // no debug message, mobile safari generates these for some
  631. // compatibility purposes.
  632. event.cancel();
  633. }
  634. break;
  635. default:
  636. getLogger().info(
  637. "Non touch event:" + event.getNativeEvent().getType());
  638. event.cancel();
  639. break;
  640. }
  641. }
  642. public void setElements(Element[] elements) {
  643. scrollableElements = new HashSet<>(Arrays.asList(elements));
  644. }
  645. /**
  646. * Long calculation are not very efficient in GWT, so this helper method
  647. * returns timestamp in double.
  648. *
  649. * @return
  650. */
  651. public static double getTimeStamp() {
  652. return Duration.currentTimeMillis();
  653. }
  654. private static Logger getLogger() {
  655. return Logger.getLogger(TouchScrollDelegate.class.getName());
  656. }
  657. }