Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

AutoScroller.java 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. /*
  2. * Copyright 2000-2018 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.widget.grid;
  17. import com.google.gwt.animation.client.AnimationScheduler;
  18. import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback;
  19. import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle;
  20. import com.google.gwt.dom.client.Element;
  21. import com.google.gwt.dom.client.NativeEvent;
  22. import com.google.gwt.dom.client.TableElement;
  23. import com.google.gwt.dom.client.TableSectionElement;
  24. import com.google.gwt.event.shared.HandlerRegistration;
  25. import com.google.gwt.user.client.Event;
  26. import com.google.gwt.user.client.Event.NativePreviewEvent;
  27. import com.google.gwt.user.client.Event.NativePreviewHandler;
  28. import com.vaadin.client.WidgetUtil;
  29. import com.vaadin.client.widget.grid.selection.SelectionModelWithSelectionColumn;
  30. import com.vaadin.client.widgets.Grid;
  31. /**
  32. * A class for handling automatic scrolling vertically / horizontally in the
  33. * Grid when the cursor is close enough the edge of the body of the grid,
  34. * depending on the scroll direction chosen.
  35. *
  36. * @since 7.5.0
  37. * @author Vaadin Ltd
  38. */
  39. public class AutoScroller {
  40. /**
  41. * Callback that notifies when the cursor is on top of a new row or column
  42. * because of the automatic scrolling.
  43. */
  44. public interface AutoScrollerCallback {
  45. /**
  46. * Triggered when doing automatic scrolling.
  47. * <p>
  48. * Because the auto scroller currently only supports scrolling in one
  49. * axis, this method is used for both vertical and horizontal scrolling.
  50. *
  51. * @param scrollDiff
  52. * the amount of pixels that have been auto scrolled since
  53. * last call
  54. */
  55. void onAutoScroll(int scrollDiff);
  56. /**
  57. * Triggered when the grid scroll has reached the minimum scroll
  58. * position. Depending on the scroll axis, either scrollLeft or
  59. * scrollTop is 0.
  60. */
  61. void onAutoScrollReachedMin();
  62. /**
  63. * Triggered when the grid scroll has reached the max scroll position.
  64. * Depending on the scroll axis, either scrollLeft or scrollTop is at
  65. * its maximum value.
  66. */
  67. void onAutoScrollReachedMax();
  68. }
  69. public enum ScrollAxis {
  70. VERTICAL, HORIZONTAL
  71. }
  72. /** The maximum number of pixels per second to autoscroll. */
  73. private static final int SCROLL_TOP_SPEED_PX_SEC = 500;
  74. /**
  75. * The minimum area where the grid doesn't scroll while the pointer is
  76. * pressed.
  77. */
  78. private static final int MIN_NO_AUTOSCROLL_AREA_PX = 50;
  79. /** The size of the autoscroll area, both top/left and bottom/right. */
  80. private int scrollAreaPX = 100;
  81. /**
  82. * This class's main objective is to listen when to stop autoscrolling, and
  83. * make sure everything stops accordingly.
  84. */
  85. private class TouchEventHandler implements NativePreviewHandler {
  86. @Override
  87. public void onPreviewNativeEvent(final NativePreviewEvent event) {
  88. /*
  89. * Remember: targetElement is always where touchstart started, not
  90. * where the finger is pointing currently.
  91. */
  92. switch (event.getTypeInt()) {
  93. case Event.ONTOUCHSTART: {
  94. if (event.getNativeEvent().getTouches().length() == 1) {
  95. /*
  96. * Something has dropped a touchend/touchcancel and the
  97. * scroller is most probably running amok. Let's cancel it
  98. * and pretend that everything's going as expected
  99. *
  100. * Because this is a preview, this code is run before start
  101. * event can be passed to the start(...) method.
  102. */
  103. stop();
  104. /*
  105. * Related TODO: investigate why iOS seems to ignore a
  106. * touchend/touchcancel when frames are dropped, and/or if
  107. * something can be done about that.
  108. */
  109. }
  110. break;
  111. }
  112. case Event.ONTOUCHMOVE:
  113. event.cancel();
  114. break;
  115. case Event.ONTOUCHEND:
  116. case Event.ONTOUCHCANCEL:
  117. // TODO investigate if this works as desired
  118. stop();
  119. break;
  120. }
  121. }
  122. }
  123. /**
  124. * This class's responsibility is to scroll the table while a pointer is
  125. * kept in a scrolling zone.
  126. * <p>
  127. * <em>Techical note:</em> This class is an AnimationCallback because we
  128. * need a timer: when the finger is kept in place while the grid scrolls, we
  129. * still need to be able to make new selections. So, instead of relying on
  130. * events (which won't be fired, since the pointer isn't necessarily
  131. * moving), we do this check on each frame while the pointer is "active"
  132. * (mouse is pressed, finger is on screen).
  133. */
  134. private class AutoScrollingFrame implements AnimationCallback {
  135. /**
  136. * If the acceleration gradient area is smaller than this, autoscrolling
  137. * will be disabled (it becomes too quick to accelerate to be usable).
  138. */
  139. private static final int GRADIENT_MIN_THRESHOLD_PX = 10;
  140. /**
  141. * The speed at which the gradient area recovers, once scrolling in that
  142. * direction has started.
  143. */
  144. private static final int SCROLL_AREA_REBOUND_PX_PER_SEC = 1;
  145. private static final double SCROLL_AREA_REBOUND_PX_PER_MS = SCROLL_AREA_REBOUND_PX_PER_SEC
  146. / 1000.0d;
  147. /**
  148. * The lowest y/x-coordinate on the {@link Event#getClientY() client-y}
  149. * or {@link Event#getClientX() client-x} from where we need to start
  150. * scrolling towards the top/left.
  151. */
  152. private int startBound = -1;
  153. /**
  154. * The highest y/x-coordinate on the {@link Event#getClientY() client-y}
  155. * or {@link Event#getClientX() client-x} from where we need to
  156. * scrolling towards the bottom.
  157. */
  158. private int endBound = -1;
  159. /**
  160. * The area where the selection acceleration takes place. If &lt;
  161. * {@link #GRADIENT_MIN_THRESHOLD_PX}, autoscrolling is disabled
  162. */
  163. private final int gradientArea;
  164. /**
  165. * The number of pixels per seconds we currently are scrolling (negative
  166. * is towards the top/left, positive is towards the bottom/right).
  167. */
  168. private double scrollSpeed = 0;
  169. private double prevTimestamp = 0;
  170. /**
  171. * This field stores fractions of pixels to scroll, to make sure that
  172. * we're able to scroll less than one px per frame.
  173. */
  174. private double pixelsToScroll = 0.0d;
  175. /** Should this animator be running. */
  176. private boolean running = false;
  177. /** The handle in which this instance is running. */
  178. private AnimationHandle handle;
  179. /**
  180. * The pointer's pageY (VERTICAL) / pageX (HORIZONTAL) coordinate
  181. * depending on scrolling axis.
  182. */
  183. private int scrollingAxisPageCoordinate;
  184. /** @see #doScrollAreaChecks(int) */
  185. private int finalStartBound;
  186. /** @see #doScrollAreaChecks(int) */
  187. private int finalEndBound;
  188. private boolean scrollAreaShouldRebound = false;
  189. public AutoScrollingFrame(final int startBound, final int endBound,
  190. final int gradientArea) {
  191. finalStartBound = startBound;
  192. finalEndBound = endBound;
  193. this.gradientArea = gradientArea;
  194. }
  195. @Override
  196. public void execute(final double timestamp) {
  197. final double timeDiff = timestamp - prevTimestamp;
  198. prevTimestamp = timestamp;
  199. reboundScrollArea(timeDiff);
  200. pixelsToScroll += scrollSpeed * (timeDiff / 1000.0d);
  201. final int intPixelsToScroll = (int) pixelsToScroll;
  202. pixelsToScroll -= intPixelsToScroll;
  203. if (intPixelsToScroll != 0) {
  204. double scrollPos;
  205. double maxScrollPos;
  206. double newScrollPos;
  207. if (scrollDirection == ScrollAxis.VERTICAL) {
  208. scrollPos = grid.getScrollTop();
  209. maxScrollPos = getMaxScrollTop();
  210. } else {
  211. scrollPos = grid.getScrollLeft();
  212. maxScrollPos = getMaxScrollLeft();
  213. }
  214. if (intPixelsToScroll > 0 && scrollPos < maxScrollPos
  215. || intPixelsToScroll < 0 && scrollPos > 0) {
  216. newScrollPos = scrollPos + intPixelsToScroll;
  217. if (scrollDirection == ScrollAxis.VERTICAL) {
  218. grid.setScrollTop(newScrollPos);
  219. } else {
  220. grid.setScrollLeft(newScrollPos);
  221. }
  222. callback.onAutoScroll(intPixelsToScroll);
  223. if (newScrollPos <= 0) {
  224. callback.onAutoScrollReachedMin();
  225. } else if (newScrollPos >= maxScrollPos) {
  226. callback.onAutoScrollReachedMax();
  227. }
  228. }
  229. }
  230. reschedule();
  231. }
  232. /**
  233. * If the scroll are has been offset by the pointer starting out there,
  234. * move it back a bit
  235. */
  236. private void reboundScrollArea(double timeDiff) {
  237. if (!scrollAreaShouldRebound) {
  238. return;
  239. }
  240. int reboundPx = (int) Math
  241. .ceil(SCROLL_AREA_REBOUND_PX_PER_MS * timeDiff);
  242. if (startBound < finalStartBound) {
  243. startBound += reboundPx;
  244. startBound = Math.min(startBound, finalStartBound);
  245. updateScrollSpeed(scrollingAxisPageCoordinate);
  246. } else if (endBound > finalEndBound) {
  247. endBound -= reboundPx;
  248. endBound = Math.max(endBound, finalEndBound);
  249. updateScrollSpeed(scrollingAxisPageCoordinate);
  250. }
  251. }
  252. private void updateScrollSpeed(final int pointerPageCordinate) {
  253. final double ratio;
  254. if (pointerPageCordinate < startBound) {
  255. final double distance = pointerPageCordinate - startBound;
  256. ratio = Math.max(-1, distance / gradientArea);
  257. } else if (pointerPageCordinate > endBound) {
  258. final double distance = pointerPageCordinate - endBound;
  259. ratio = Math.min(1, distance / gradientArea);
  260. } else {
  261. ratio = 0;
  262. }
  263. scrollSpeed = ratio * SCROLL_TOP_SPEED_PX_SEC;
  264. }
  265. public void start() {
  266. running = true;
  267. reschedule();
  268. }
  269. public void stop() {
  270. running = false;
  271. if (handle != null) {
  272. handle.cancel();
  273. handle = null;
  274. }
  275. }
  276. private void reschedule() {
  277. if (running && gradientArea >= GRADIENT_MIN_THRESHOLD_PX) {
  278. handle = AnimationScheduler.get().requestAnimationFrame(this,
  279. grid.getElement());
  280. }
  281. }
  282. public void updatePointerCoords(int pageX, int pageY) {
  283. final int pageCordinate;
  284. if (scrollDirection == ScrollAxis.VERTICAL) {
  285. pageCordinate = pageY;
  286. } else {
  287. pageCordinate = pageX;
  288. }
  289. doScrollAreaChecks(pageCordinate);
  290. updateScrollSpeed(pageCordinate);
  291. scrollingAxisPageCoordinate = pageCordinate;
  292. }
  293. /**
  294. * This method checks whether the first pointer event started in an area
  295. * that would start scrolling immediately, and does some actions
  296. * accordingly.
  297. * <p>
  298. * If it is, that scroll area will be offset "beyond" the pointer (above
  299. * if pointer is towards the top/left, otherwise below/right).
  300. */
  301. private void doScrollAreaChecks(int pageCordinate) {
  302. /*
  303. * The first run makes sure that neither scroll position is
  304. * underneath the finger, but offset to either direction from
  305. * underneath the pointer.
  306. */
  307. if (startBound == -1) {
  308. startBound = Math.min(finalStartBound, pageCordinate);
  309. endBound = Math.max(finalEndBound, pageCordinate);
  310. } else {
  311. /*
  312. * Subsequent runs make sure that the scroll area grows (but
  313. * doesn't shrink) with the finger, but no further than the
  314. * final bound.
  315. */
  316. int oldTopBound = startBound;
  317. if (startBound < finalStartBound) {
  318. startBound = Math.max(startBound,
  319. Math.min(finalStartBound, pageCordinate));
  320. }
  321. int oldBottomBound = endBound;
  322. if (endBound > finalEndBound) {
  323. endBound = Math.min(endBound,
  324. Math.max(finalEndBound, pageCordinate));
  325. }
  326. final boolean startDidNotMove = oldTopBound == startBound;
  327. final boolean endDidNotMove = oldBottomBound == endBound;
  328. final boolean wasMovement = pageCordinate != scrollingAxisPageCoordinate;
  329. scrollAreaShouldRebound = (startDidNotMove && endDidNotMove
  330. && wasMovement);
  331. }
  332. }
  333. }
  334. /** The registration info for {@link #scrollPreviewHandler} */
  335. private HandlerRegistration handlerRegistration;
  336. /**
  337. * The top/left bound, as calculated from the {@link Event#getClientY()
  338. * client-y} or {@link Event#getClientX() client-x} coordinates.
  339. */
  340. private double startingBound = -1;
  341. /**
  342. * The bottom/right bound, as calculated from the {@link Event#getClientY()
  343. * client-y} or or {@link Event#getClientX() client-x} coordinates.
  344. */
  345. private int endingBound = -1;
  346. /** The size of the autoscroll acceleration area. */
  347. private int gradientArea;
  348. private Grid<?> grid;
  349. private HandlerRegistration nativePreviewHandlerRegistration;
  350. private ScrollAxis scrollDirection;
  351. private AutoScrollingFrame autoScroller;
  352. private AutoScrollerCallback callback;
  353. /**
  354. * This handler makes sure that pointer movements are handled.
  355. * <p>
  356. * Essentially, a native preview handler is registered (so that selection
  357. * gestures can happen outside of the selection column). The handler itself
  358. * makes sure that it's detached when the pointer is "lifted".
  359. */
  360. private final NativePreviewHandler scrollPreviewHandler = event -> {
  361. if (autoScroller == null) {
  362. stop();
  363. return;
  364. }
  365. final NativeEvent nativeEvent = event.getNativeEvent();
  366. int pageY = 0;
  367. int pageX = 0;
  368. switch (event.getTypeInt()) {
  369. case Event.ONMOUSEMOVE:
  370. case Event.ONTOUCHMOVE:
  371. pageY = WidgetUtil.getTouchOrMouseClientY(nativeEvent);
  372. pageX = WidgetUtil.getTouchOrMouseClientX(nativeEvent);
  373. autoScroller.updatePointerCoords(pageX, pageY);
  374. break;
  375. case Event.ONMOUSEUP:
  376. case Event.ONTOUCHEND:
  377. case Event.ONTOUCHCANCEL:
  378. stop();
  379. break;
  380. }
  381. };
  382. /**
  383. * Creates a new instance for scrolling the given grid.
  384. *
  385. * @param grid
  386. * the grid to auto scroll
  387. */
  388. public AutoScroller(Grid<?> grid) {
  389. this.grid = grid;
  390. }
  391. /**
  392. * Starts the automatic scrolling detection.
  393. *
  394. * @param startEvent
  395. * the event that starts the automatic scroll
  396. * @param scrollAxis
  397. * the axis along which the scrolling should happen
  398. * @param callback
  399. * the callback for getting info about the automatic scrolling
  400. */
  401. public void start(final NativeEvent startEvent, ScrollAxis scrollAxis,
  402. AutoScrollerCallback callback) {
  403. scrollDirection = scrollAxis;
  404. this.callback = callback;
  405. injectNativeHandler();
  406. start();
  407. startEvent.preventDefault();
  408. startEvent.stopPropagation();
  409. }
  410. /**
  411. * Stops the automatic scrolling.
  412. */
  413. public void stop() {
  414. if (handlerRegistration != null) {
  415. handlerRegistration.removeHandler();
  416. handlerRegistration = null;
  417. }
  418. if (autoScroller != null) {
  419. autoScroller.stop();
  420. autoScroller = null;
  421. }
  422. removeNativeHandler();
  423. }
  424. /**
  425. * Set the auto scroll area height or width depending on the scrolling axis.
  426. * This is the amount of pixels from the edge of the grid that the scroll is
  427. * triggered.
  428. * <p>
  429. * Defaults to 100px.
  430. *
  431. * @param px
  432. * the pixel height/width for the auto scroll area depending on
  433. * direction
  434. */
  435. public void setScrollArea(int px) {
  436. scrollAreaPX = px;
  437. }
  438. /**
  439. * Returns the size of the auto scroll area in pixels.
  440. * <p>
  441. * Defaults to 100px.
  442. *
  443. * @return size in pixels
  444. */
  445. public int getScrollArea() {
  446. return scrollAreaPX;
  447. }
  448. private void start() {
  449. /*
  450. * bounds are updated whenever the autoscroll cycle starts, to make sure
  451. * that the widget hasn't changed in size, moved around, or whatnot.
  452. */
  453. updateScrollBounds();
  454. assert handlerRegistration == null : "handlerRegistration was not null";
  455. assert autoScroller == null : "autoScroller was not null";
  456. handlerRegistration = Event
  457. .addNativePreviewHandler(scrollPreviewHandler);
  458. autoScroller = new AutoScrollingFrame((int) Math.ceil(startingBound),
  459. endingBound, gradientArea);
  460. autoScroller.start();
  461. }
  462. private void updateScrollBounds() {
  463. double startBorder = getBodyClientStart();
  464. final int endBorder = getBodyClientEnd();
  465. startBorder += getFrozenColumnsWidth();
  466. startingBound = startBorder + scrollAreaPX;
  467. endingBound = endBorder - scrollAreaPX;
  468. gradientArea = scrollAreaPX;
  469. // modify bounds if they're too tightly packed
  470. if (endingBound - startingBound < MIN_NO_AUTOSCROLL_AREA_PX) {
  471. double adjustment = MIN_NO_AUTOSCROLL_AREA_PX
  472. - (endingBound - startingBound);
  473. startingBound -= adjustment / 2;
  474. endingBound += adjustment / 2;
  475. gradientArea -= adjustment / 2;
  476. }
  477. }
  478. private void injectNativeHandler() {
  479. removeNativeHandler();
  480. nativePreviewHandlerRegistration = Event
  481. .addNativePreviewHandler(new TouchEventHandler());
  482. }
  483. private void removeNativeHandler() {
  484. if (nativePreviewHandlerRegistration != null) {
  485. nativePreviewHandlerRegistration.removeHandler();
  486. nativePreviewHandlerRegistration = null;
  487. }
  488. }
  489. private TableElement getTableElement() {
  490. final Element root = grid.getElement();
  491. final Element tablewrapper = Element.as(root.getChild(2));
  492. if (tablewrapper != null) {
  493. return TableElement.as(tablewrapper.getFirstChildElement());
  494. } else {
  495. return null;
  496. }
  497. }
  498. private TableSectionElement getTheadElement() {
  499. TableElement table = getTableElement();
  500. if (table != null) {
  501. return table.getTHead();
  502. } else {
  503. return null;
  504. }
  505. }
  506. private TableSectionElement getTfootElement() {
  507. TableElement table = getTableElement();
  508. if (table != null) {
  509. return table.getTFoot();
  510. } else {
  511. return null;
  512. }
  513. }
  514. private int getBodyClientEnd() {
  515. if (scrollDirection == ScrollAxis.VERTICAL) {
  516. return getTfootElement().getAbsoluteTop() - 1;
  517. } else {
  518. return getTableElement().getAbsoluteRight();
  519. }
  520. }
  521. private int getBodyClientStart() {
  522. if (scrollDirection == ScrollAxis.VERTICAL) {
  523. return getTheadElement().getAbsoluteBottom() + 1;
  524. } else {
  525. return getTableElement().getAbsoluteLeft();
  526. }
  527. }
  528. public double getFrozenColumnsWidth() {
  529. double value = 0;
  530. for (int i = 0; i < getRealFrozenColumnCount(); i++) {
  531. value += grid.getColumn(i).getWidthActual();
  532. }
  533. return value;
  534. }
  535. private int getRealFrozenColumnCount() {
  536. if (grid.getFrozenColumnCount() < 0) {
  537. return 0;
  538. } else if (grid
  539. .getSelectionModel() instanceof SelectionModelWithSelectionColumn) {
  540. // includes the selection column
  541. return grid.getFrozenColumnCount() + 1;
  542. } else {
  543. return grid.getFrozenColumnCount();
  544. }
  545. }
  546. private double getMaxScrollLeft() {
  547. return grid.getScrollWidth()
  548. - (getTableElement().getParentElement().getOffsetWidth()
  549. - getFrozenColumnsWidth());
  550. }
  551. private double getMaxScrollTop() {
  552. return grid.getScrollHeight() - getTfootElement().getOffsetHeight()
  553. - getTheadElement().getOffsetHeight();
  554. }
  555. }