OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** | |
6 * Implementation of a custom scrolling behavior. | |
7 * This behavior overrides native scrolling for an area. This area can be a | |
8 * single defined part of a page, the entire page, or several different parts | |
9 * of a page. | |
10 * | |
11 * To use this scrolling behavior you need to define a frame and the content. | |
12 * The frame defines the area that the content will scroll within. The frame and | |
13 * content must both be HTML Elements, with the content being a direct child of | |
14 * the frame. Usually the frame is smaller in size than the content. This is | |
15 * not necessary though, if the content is smaller then bouncing will occur to | |
16 * provide feedback that you are past the scrollable area. | |
17 * | |
18 * The scrolling behavior works using the webkit translate3d transformation, | |
19 * which means browsers that do not have hardware accelerated transformations | |
20 * will not perform as well using this. Simple scrolling should be fine even | |
21 * without hardware acceleration, but animating momentum and deceleration is | |
22 * unacceptably slow without it. There is also the option to use relative | |
23 * positioning (setting the left and top styles). | |
24 * | |
25 * For this to work properly you need to set -webkit-text-size-adjust to 'none' | |
26 * on an ancestor element of the frame, or on the frame itself. If you forget | |
27 * this you may see the text content of the scrollable area changing size as it | |
28 * moves. | |
29 * | |
30 * The behavior is intended to support vertical and horizontal scrolling, and | |
31 * scrolling with momentum when a touch gesture flicks with enough velocity. | |
32 */ | |
33 typedef void Callback(); | |
34 | |
35 // Helper method to await the completion of 2 futures. | |
36 void joinFutures(List<Future> futures, Callback callback) { | |
37 int count = 0; | |
38 int len = futures.length; | |
39 void helper(value) { | |
40 count++; | |
41 if (count == len) { | |
42 callback(); | |
43 } | |
44 } | |
45 for (Future p in futures) { | |
46 p.then(helper); | |
47 } | |
48 } | |
49 | |
50 class Scroller implements Draggable, MomentumDelegate { | |
51 | |
52 /** Pixels to move each time an arrow key is pressed. */ | |
53 static final ARROW_KEY_DELTA = 30; | |
54 static final SCROLL_WHEEL_VELOCITY = 0.01; | |
55 static final FAST_SNAP_DECELERATION_FACTOR = 0.84; | |
56 static final PAGE_KEY_SCROLL_FRACTION = .85; | |
57 | |
58 // TODO(jacobr): remove this static variable. | |
59 static bool _dragInProgress = false; | |
60 | |
61 /** The node that will actually scroll. */ | |
62 Element _element; | |
63 | |
64 /** | |
65 * Frame is the node that will serve as the container for the scrolling | |
66 * content. | |
67 */ | |
68 Element _frame; | |
69 | |
70 /** Touch manager to track the events on the scrollable area. */ | |
71 TouchHandler _touchHandler; | |
72 | |
73 Momentum _momentum; | |
74 | |
75 EventListenerList _onScrollerStart; | |
76 EventListenerList _onScrollerEnd; | |
77 EventListenerList _onScrollerDragEnd; | |
78 EventListenerList _onContentMoved; | |
79 EventListenerList _onDecelStart; | |
80 | |
81 /** Set if vertical scrolling should be enabled. */ | |
82 bool verticalEnabled; | |
83 | |
84 /** Set if horizontal scrolling should be enabled. */ | |
85 bool horizontalEnabled; | |
86 | |
87 /** | |
88 * Set if momentum should be enabled. | |
89 */ | |
90 bool _momentumEnabled; | |
91 | |
92 /** Set which type of scrolling translation technique should be used. */ | |
93 int _scrollTechnique; | |
94 | |
95 /** | |
96 * The maximum coordinate that the left upper corner of the content can scroll | |
97 * to. | |
98 */ | |
99 Coordinate _maxPoint; | |
100 | |
101 /** | |
102 * An offset to subtract from the maximum coordinate that the left upper | |
103 * corner of the content can scroll to. | |
104 */ | |
105 Coordinate _maxOffset; | |
106 | |
107 /** | |
108 * An offset to add to the minimum coordinate that the left upper corner of | |
109 * the content can scroll to. | |
110 */ | |
111 Coordinate _minOffset; | |
112 | |
113 /** Initialize the current content offset. */ | |
114 Coordinate _contentOffset; | |
115 | |
116 // TODO(jacobr): the function type is | |
117 // [:Function(Element, num, num)->void:]. | |
118 /** | |
119 * The function to use that will actually translate the scrollable node. | |
120 */ | |
121 Function _setOffsetFunction; | |
122 /** | |
123 * Function that returns the content size that can be specified instead of | |
124 * querying the DOM. | |
125 */ | |
126 Function _lookupContentSizeDelegate; | |
127 | |
128 Size _scrollSize; | |
129 Size _contentSize; | |
130 Coordinate _minPoint; | |
131 bool _isStopping = false; | |
132 Coordinate _contentStartOffset; | |
133 bool _started = false; | |
134 bool _activeGesture = false; | |
135 ScrollWatcher _scrollWatcher; | |
136 | |
137 Scroller(Element scrollableElem, [this.verticalEnabled = false, | |
138 this.horizontalEnabled = false, | |
139 momentumEnabled = true, | |
140 lookupContentSizeDelegate = null, | |
141 num defaultDecelerationFactor = 1, | |
142 int scrollTechnique = null, bool capture = false]) | |
143 : _momentumEnabled = momentumEnabled, | |
144 _lookupContentSizeDelegate = lookupContentSizeDelegate, | |
145 _element = scrollableElem, | |
146 _frame = scrollableElem.parent, | |
147 _scrollTechnique = scrollTechnique !== null | |
148 ? scrollTechnique : ScrollerScrollTechnique.TRANSFORM_3D, | |
149 _minPoint = new Coordinate(0, 0), | |
150 _maxPoint = new Coordinate(0, 0), | |
151 _maxOffset = new Coordinate(0, 0), | |
152 _minOffset = new Coordinate(0, 0), | |
153 _contentOffset = new Coordinate(0, 0) { | |
154 _touchHandler = new TouchHandler(this, scrollableElem.parent); | |
155 _momentum = new Momentum(this, defaultDecelerationFactor); | |
156 | |
157 Element parentElem = scrollableElem.parent; | |
158 assert(parentElem != null); | |
159 _setOffsetFunction = _getOffsetFunction(_scrollTechnique); | |
160 _touchHandler.setDraggable(this); | |
161 _touchHandler.enable(capture); | |
162 | |
163 _frame.on.mouseWheel.add((e) { | |
164 if (e.wheelDeltaY != 0 && verticalEnabled || | |
165 e.wheelDeltaX != 0 && horizontalEnabled) { | |
166 num x = horizontalEnabled ? e.wheelDeltaX : 0; | |
167 num y = verticalEnabled ? e.wheelDeltaY : 0; | |
168 throwDelta(x, y, FAST_SNAP_DECELERATION_FACTOR); | |
169 e.preventDefault(); | |
170 } | |
171 }); | |
172 | |
173 _frame.on.keyDown.add((KeyboardEvent e) { | |
174 bool handled = false; | |
175 // We ignore key events where further scrolling in that direction | |
176 // would have no impact which matches default browser behavior with | |
177 // nested scrollable areas. | |
178 | |
179 switch(e.keyCode) { | |
180 case 33: // page-up | |
181 throwDelta( | |
182 0, | |
183 _scrollSize.height * PAGE_KEY_SCROLL_FRACTION); | |
184 handled = true; | |
185 break; | |
186 case 34: // page-down | |
187 throwDelta( | |
188 0, -_scrollSize.height * PAGE_KEY_SCROLL_FRACTION); | |
189 handled = true; | |
190 break; | |
191 case 35: // End | |
192 throwTo(_maxPoint.x, _minPoint.y, | |
193 FAST_SNAP_DECELERATION_FACTOR); | |
194 handled = true; | |
195 break; | |
196 case 36: // Home | |
197 throwTo(_maxPoint.x,_maxPoint.y, | |
198 FAST_SNAP_DECELERATION_FACTOR); | |
199 handled = true; | |
200 break; | |
201 /* TODO(jacobr): enable arrow keys when the don't conflict with other | |
202 application keyboard shortcuts. | |
203 case 38: // up | |
204 handled = throwDelta( | |
205 0, | |
206 ARROW_KEY_DELTA, | |
207 FAST_SNAP_DECELERATION_FACTOR); | |
208 break; | |
209 case 40: // down | |
210 handled = throwDelta( | |
211 0, -ARROW_KEY_DELTA, | |
212 FAST_SNAP_DECELERATION_FACTOR); | |
213 break; | |
214 case 37: // left | |
215 handled = throwDelta( | |
216 ARROW_KEY_DELTA, 0, | |
217 FAST_SNAP_DECELERATION_FACTOR); | |
218 break; | |
219 case 39: // right | |
220 handled = throwDelta( | |
221 -ARROW_KEY_DELTA, | |
222 0, | |
223 FAST_SNAP_DECELERATION_FACTOR); | |
224 break; | |
225 */ | |
226 } | |
227 if (handled) { | |
228 e.preventDefault(); | |
229 } | |
230 }); | |
231 // The scrollable element must be relatively positioned. | |
232 // TODO(jacobr): this assert fires asynchronously which could be confusing. | |
233 if (_scrollTechnique == ScrollerScrollTechnique.RELATIVE_POSITIONING) { | |
234 _element.computedStyle.then((CSSStyleDeclaration style) { | |
235 assert(style.position != "static"); | |
236 }); | |
237 } | |
238 | |
239 _initLayer(); | |
240 } | |
241 | |
242 EventListenerList get onScrollerStart() { | |
243 if (_onScrollerStart === null) { | |
244 _onScrollerStart = new SimpleEventListenerList(); | |
245 } | |
246 return _onScrollerStart; | |
247 } | |
248 | |
249 EventListenerList get onScrollerEnd() { | |
250 if (_onScrollerEnd === null) { | |
251 _onScrollerEnd = new SimpleEventListenerList(); | |
252 } | |
253 return _onScrollerEnd; | |
254 } | |
255 | |
256 EventListenerList get onScrollerDragEnd() { | |
257 if (_onScrollerDragEnd === null) { | |
258 _onScrollerDragEnd = new SimpleEventListenerList(); | |
259 } | |
260 return _onScrollerDragEnd; | |
261 } | |
262 | |
263 EventListenerList get onContentMoved() { | |
264 if (_onContentMoved === null) { | |
265 _onContentMoved = new SimpleEventListenerList(); | |
266 } | |
267 return _onContentMoved; | |
268 } | |
269 | |
270 EventListenerList get onDecelStart() { | |
271 if (_onDecelStart === null) { | |
272 _onDecelStart = new SimpleEventListenerList(); | |
273 } | |
274 return _onDecelStart; | |
275 } | |
276 | |
277 | |
278 /** | |
279 * Add a scroll listener. This allows other classes to subscribe to scroll | |
280 * notifications from this scroller. | |
281 */ | |
282 void addScrollListener(ScrollListener listener) { | |
283 if (_scrollWatcher === null) { | |
284 _scrollWatcher = new ScrollWatcher(this); | |
285 _scrollWatcher.initialize(); | |
286 } | |
287 _scrollWatcher.addListener(listener); | |
288 } | |
289 | |
290 /** | |
291 * Adjust the new calculated scroll position based on the minimum allowed | |
292 * position and returns the adjusted scroll value. | |
293 */ | |
294 num _adjustValue(num newPosition, num minPosition, | |
295 num maxPosition) { | |
296 assert(minPosition <= maxPosition); | |
297 | |
298 if (newPosition < minPosition) { | |
299 newPosition -= (newPosition - minPosition) / 2; | |
300 } else { | |
301 if (newPosition > maxPosition) { | |
302 newPosition -= (newPosition - maxPosition) / 2; | |
303 } | |
304 } | |
305 return newPosition; | |
306 } | |
307 | |
308 /** | |
309 * Coordinate we would end up at if we did nothing. | |
310 */ | |
311 Coordinate get currentTarget() { | |
312 Coordinate end = _momentum.destination; | |
313 if (end === null) { | |
314 end = _contentOffset; | |
315 } | |
316 return end; | |
317 } | |
318 | |
319 Coordinate get contentOffset() => _contentOffset; | |
320 | |
321 /** | |
322 * Animate the position of the scroller to the specified [x], [y] coordinates | |
323 * by applying the throw gesture with the correct velocity to end at that | |
324 * location. | |
325 */ | |
326 void throwTo(num x, num y, [num decelerationFactor = null]) { | |
327 reconfigure(() { | |
328 final snappedTarget = _snapToBounds(x, y); | |
329 // If a deceleration factor is not specified, use the existing | |
330 // deceleration factor specified by the momentum simulator. | |
331 if (decelerationFactor == null) { | |
332 decelerationFactor = _momentum.decelerationFactor; | |
333 } | |
334 | |
335 if (snappedTarget != currentTarget) { | |
336 _momentum.abort(); | |
337 | |
338 _startDeceleration( | |
339 _momentum.calculateVelocity( | |
340 _contentOffset, | |
341 snappedTarget, | |
342 decelerationFactor), | |
343 decelerationFactor); | |
344 onDecelStart.dispatch(new Event(ScrollerEventType.DECEL_START)); | |
345 } | |
346 }); | |
347 } | |
348 | |
349 void throwDelta(num deltaX, num deltaY, [num decelerationFactor = null]) { | |
350 Coordinate start = _contentOffset; | |
351 Coordinate end = currentTarget; | |
352 int x = end.x.toInt(); | |
353 int y = end.y.toInt(); | |
354 // If we are throwing in the opposite direction of the existing momentum, | |
355 // cancel the current momentum. | |
356 if (deltaX != 0 && deltaX.isNegative() != (end.x - start.x).isNegative()) { | |
357 x = start.x; | |
358 } | |
359 if (deltaY != 0 && deltaY.isNegative() != (end.y - start.y).isNegative()) { | |
360 y = start.y; | |
361 } | |
362 x += deltaX.toInt(); | |
363 y += deltaY.toInt(); | |
364 throwTo(x, y, decelerationFactor); | |
365 } | |
366 | |
367 void setPosition(num x, num y) { | |
368 _momentum.abort(); | |
369 _contentOffset.x = x; | |
370 _contentOffset.y = y; | |
371 _snapContentOffsetToBounds(); | |
372 _setContentOffset(_contentOffset.x, _contentOffset.y); | |
373 } | |
374 /** | |
375 * Adjusted content size is a size with the combined largest height and width | |
376 * of both the content and the frame. | |
377 */ | |
378 Size _getAdjustedContentSize() { | |
379 return new Size(Math.max(_scrollSize.width, _contentSize.width), | |
380 Math.max(_scrollSize.height, _contentSize.height)); | |
381 } | |
382 | |
383 // TODO(jmesserly): these should be properties instead of get* methods | |
384 num getDefaultVerticalOffset() => _maxPoint.y; | |
385 Element getElement() => _element; | |
386 Element getFrame() => _frame; | |
387 num getHorizontalOffset() => _contentOffset.x; | |
388 | |
389 /** | |
390 * [x] Value to use as reference for percent measurement. If | |
391 * none is provided then the content's current x offset will be used. | |
392 * Returns the percent of the page scrolled horizontally. | |
393 */ | |
394 num getHorizontalScrollPercent([num x = null]) { | |
395 x = x !== null ? x : _contentOffset.x; | |
396 return (x - _minPoint.x) / (_maxPoint.x - _minPoint.x); | |
397 } | |
398 | |
399 num getMaxPointY()=> _maxPoint.y; | |
400 num getMinPointY() => _minPoint.y; | |
401 Momentum get momentum() => _momentum; | |
402 | |
403 /** | |
404 * Provide access to the touch handler that the scroller created to manage | |
405 * touch events. | |
406 */ | |
407 TouchHandler getTouchHandler() => _touchHandler; | |
408 num getVerticalOffset() => _contentOffset.y; | |
409 | |
410 /** | |
411 * [y] value is used as reference for percent measurement. If | |
412 * none is provided then the content's current y offset will be used. | |
413 */ | |
414 num getVerticalScrollPercent([num y = null]) { | |
415 y = y !== null ? y : _contentOffset.y; | |
416 return (y - _minPoint.y) / Math.max(1, _maxPoint.y - _minPoint.y); | |
417 } | |
418 | |
419 /** | |
420 * Initialize the dom elements necessary for the scrolling to work. | |
421 */ | |
422 void _initLayer() { | |
423 // The scrollable node provided to Scroller must be a direct child | |
424 // of the scrollable frame. | |
425 // TODO(jacobr): Figure out why this is failing on dartium. | |
426 // assert(_element.parent == _frame); | |
427 _setContentOffset(_maxPoint.x, _maxPoint.y); | |
428 } | |
429 | |
430 void onDecelerate(num x, num y) { | |
431 _setContentOffset(x, y); | |
432 } | |
433 | |
434 void onDecelerationEnd() { | |
435 onScrollerEnd.dispatch(new Event(ScrollerEventType.SCROLLER_END)); | |
436 _started = false; | |
437 } | |
438 | |
439 void onDragEnd() { | |
440 _dragInProgress = false; | |
441 | |
442 bool decelerating = false; | |
443 if (_activeGesture) { | |
444 if (_momentumEnabled) { | |
445 decelerating = _startDeceleration(_touchHandler.getEndVelocity()); | |
446 } | |
447 } | |
448 | |
449 onScrollerDragEnd.dispatch(new Event(ScrollerEventType.DRAG_END)); | |
450 | |
451 if (!decelerating) { | |
452 _snapContentOffsetToBounds(); | |
453 onScrollerEnd.dispatch(new Event(ScrollerEventType.SCROLLER_END)); | |
454 _started = false; | |
455 } else { | |
456 onDecelStart.dispatch(new Event(ScrollerEventType.DECEL_START)); | |
457 } | |
458 _activeGesture = false; | |
459 } | |
460 | |
461 void onDragMove() { | |
462 if (_isStopping || (!_activeGesture && _dragInProgress)) { | |
463 return; | |
464 } | |
465 | |
466 assert(_contentStartOffset != null); // Content start not set | |
467 Coordinate contentStart = _contentStartOffset; | |
468 num newX = contentStart.x + _touchHandler.getDragDeltaX(); | |
469 num newY = contentStart.y + _touchHandler.getDragDeltaY(); | |
470 newY = _shouldScrollVertically() ? | |
471 _adjustValue(newY, _minPoint.y, _maxPoint.y) : 0; | |
472 newX = _shouldScrollHorizontally() ? | |
473 _adjustValue(newX, _minPoint.x, _maxPoint.x) : 0; | |
474 if (!_activeGesture) { | |
475 _activeGesture = true; | |
476 _dragInProgress = true; | |
477 } | |
478 if (!_started) { | |
479 _started = true; | |
480 onScrollerStart.dispatch(new Event(ScrollerEventType.SCROLLER_START)); | |
481 } | |
482 _setContentOffset(newX, newY); | |
483 } | |
484 | |
485 bool onDragStart(TouchEvent e) { | |
486 if (e.touches.length > 1) { | |
487 return false; | |
488 } | |
489 bool shouldHorizontal = _shouldScrollHorizontally(); | |
490 bool shouldVertical = _shouldScrollVertically(); | |
491 bool verticalish = _touchHandler.getDragDeltaY().abs() > | |
492 _touchHandler.getDragDeltaX().abs(); | |
493 return !!(shouldVertical || shouldHorizontal && !verticalish); | |
494 } | |
495 | |
496 void onTouchEnd() { | |
497 } | |
498 | |
499 /** | |
500 * Prepare the scrollable area for possible movement. | |
501 */ | |
502 bool onTouchStart(TouchEvent e) { | |
503 reconfigure(() { | |
504 final touch = e.touches[0]; | |
505 if (_momentum.decelerating) { | |
506 e.preventDefault(); | |
507 e.stopPropagation(); | |
508 stop(); | |
509 } | |
510 _contentStartOffset = _contentOffset.clone(); | |
511 _snapContentOffsetToBounds(); | |
512 }); | |
513 return true; | |
514 } | |
515 | |
516 /** | |
517 * Recalculate dimensions of the frame and the content. Adjust the minPoint | |
518 * and maxPoint allowed for scrolling and scroll to a valid position. Call | |
519 * this method if you know the frame or content has been updated. Called | |
520 * internally on every touchstart event the frame receives. | |
521 */ | |
522 void reconfigure(Callback callback) { | |
523 _resize(() { | |
524 _snapContentOffsetToBounds(); | |
525 callback(); | |
526 }); | |
527 } | |
528 | |
529 void reset() { | |
530 stop(); | |
531 _touchHandler.reset(); | |
532 _maxOffset.x = 0; | |
533 _maxOffset.y = 0; | |
534 _minOffset.x = 0; | |
535 _minOffset.y = 0; | |
536 reconfigure(() => _setContentOffset(_maxPoint.x, _maxPoint.y)); | |
537 } | |
538 | |
539 /** | |
540 * Recalculate dimensions of the frame and the content. Adjust the minPoint | |
541 * and maxPoint allowed for scrolling. | |
542 */ | |
543 void _resize(Callback callback) { | |
544 final frameRect = _frame.rect; | |
545 Future contentSizeFuture; | |
546 | |
547 if (_lookupContentSizeDelegate !== null) { | |
548 contentSizeFuture = _lookupContentSizeDelegate(); | |
549 contentSizeFuture.then((Size size) { | |
550 _contentSize = size; | |
551 }); | |
552 } else { | |
553 contentSizeFuture = _element.rect; | |
554 contentSizeFuture.then((ElementRect rect) { | |
555 _contentSize = new Size(rect.scroll.width, rect.scroll.height); | |
556 }); | |
557 } | |
558 | |
559 joinFutures(<Future>[frameRect, contentSizeFuture], () { | |
560 _scrollSize = new Size(frameRect.value.offset.width, | |
561 frameRect.value.offset.height); | |
562 Size adjusted = _getAdjustedContentSize(); | |
563 _maxPoint = new Coordinate(-_maxOffset.x, -_maxOffset.y); | |
564 _minPoint = new Coordinate( | |
565 Math.min( | |
566 _scrollSize.width - adjusted.width + _minOffset.x, _maxPoint.x), | |
567 Math.min( | |
568 _scrollSize.height - adjusted.height + _minOffset.y, _maxPoint.y))
; | |
569 callback(); | |
570 }); | |
571 } | |
572 | |
573 Coordinate _snapToBounds(num x, num y) { | |
574 num clampX = GoogleMath.clamp(_minPoint.x, x, _maxPoint.x); | |
575 num clampY = GoogleMath.clamp(_minPoint.y, y, _maxPoint.y); | |
576 return new Coordinate(clampX, clampY); | |
577 } | |
578 | |
579 /** | |
580 * Translate the content to a new position specified in px. | |
581 */ | |
582 void _setContentOffset(num x, num y) { | |
583 _contentOffset.x = x; | |
584 _contentOffset.y = y; | |
585 _setOffsetFunction(_element, x, y); | |
586 onContentMoved.dispatch(new Event(ScrollerEventType.CONTENT_MOVED)); | |
587 } | |
588 | |
589 /** | |
590 * Enable or disable momentum. | |
591 */ | |
592 void setMomentum(bool enable) { | |
593 _momentumEnabled = enable; | |
594 } | |
595 | |
596 /** | |
597 * Sets the vertical scrolled offset of the element where [y] is the amount | |
598 * of vertical space to be scrolled, in pixels. | |
599 */ | |
600 void setVerticalOffset(num y) { | |
601 _setContentOffset(_contentOffset.x, y); | |
602 } | |
603 | |
604 /** | |
605 * Whether the scrollable area should scroll horizontally. Only | |
606 * returns true if the client has enabled horizontal scrolling, and the | |
607 * content is wider than the frame. | |
608 */ | |
609 bool _shouldScrollHorizontally() { | |
610 return horizontalEnabled && _scrollSize.width < _contentSize.width; | |
611 } | |
612 | |
613 /** | |
614 * Whether the scrollable area should scroll vertically. Only | |
615 * returns true if the client has enabled vertical scrolling. | |
616 * Vertical bouncing will occur even if frame is taller than content, because | |
617 * this is what iPhone web apps tend to do. If this is not the desired | |
618 * behavior, either disable vertical scrolling for this scroller or add a | |
619 * 'bouncing' parameter to this interface. | |
620 */ | |
621 bool _shouldScrollVertically() { | |
622 return verticalEnabled; | |
623 } | |
624 | |
625 /** | |
626 * In the event that the content is currently beyond the bounds of | |
627 * the frame, snap it back in to place. | |
628 */ | |
629 void _snapContentOffsetToBounds() { | |
630 num clampX = | |
631 GoogleMath.clamp(_minPoint.x, _contentOffset.x, _maxPoint.x); | |
632 num clampY = | |
633 GoogleMath.clamp(_minPoint.y, _contentOffset.y, _maxPoint.y); | |
634 if (_contentOffset.x != clampX || _contentOffset.y != clampY) { | |
635 _setContentOffset(clampX, clampY); | |
636 } | |
637 } | |
638 | |
639 /** | |
640 * Initiate the deceleration behavior given a flick [velocity]. | |
641 * Returns true if deceleration has been initiated. | |
642 */ | |
643 bool _startDeceleration(Coordinate velocity, | |
644 [num decelerationFactor = null]) { | |
645 if (!_shouldScrollHorizontally()) { | |
646 velocity.x = 0; | |
647 } | |
648 if (!_shouldScrollVertically()) { | |
649 velocity.y = 0; | |
650 } | |
651 assert(_minPoint != null); // Min point is not set | |
652 assert(_maxPoint != null); // Max point is not set | |
653 return _momentum.start(velocity, _minPoint, _maxPoint, _contentOffset, | |
654 decelerationFactor); | |
655 } | |
656 | |
657 Coordinate stop() { | |
658 return _momentum.stop(); | |
659 } | |
660 | |
661 /** | |
662 * Stop the deceleration of the scrollable content given a new position in px. | |
663 */ | |
664 void _stopDecelerating(num x, num y) { | |
665 _momentum.stop(); | |
666 _setContentOffset(x, y); | |
667 } | |
668 | |
669 static Function _getOffsetFunction(int scrollTechnique) { | |
670 return scrollTechnique == ScrollerScrollTechnique.TRANSFORM_3D ? | |
671 (el, x, y) { FxUtil.setTranslate(el, x, y, 0); } : | |
672 (el, x, y) { FxUtil.setLeftAndTop(el, x, y); }; | |
673 } | |
674 } | |
675 | |
676 // TODO(jacobr): cleanup this class of enum constants. | |
677 class ScrollerEventType { | |
678 static final SCROLLER_START = "scroller:scroll_start"; | |
679 static final SCROLLER_END = "scroller:scroll_end"; | |
680 static final DRAG_END = "scroller:drag_end"; | |
681 static final CONTENT_MOVED = "scroller:content_moved"; | |
682 static final DECEL_START = "scroller:decel_start"; | |
683 } | |
684 | |
685 // TODO(jacobr): for now this ignores capture. | |
686 class SimpleEventListenerList implements EventListenerList { | |
687 // Ignores capture for now. | |
688 List<EventListener> _listeners; | |
689 | |
690 SimpleEventListenerList() : _listeners = new List<EventListener>() { } | |
691 | |
692 EventListenerList add(EventListener handler, [bool useCapture = false]) { | |
693 _add(handler, useCapture); | |
694 return this; | |
695 } | |
696 | |
697 EventListenerList remove(EventListener handler, [bool useCapture = false]) { | |
698 _remove(handler, useCapture); | |
699 return this; | |
700 } | |
701 | |
702 EventListenerList addCapture(EventListener handler) { | |
703 _add(handler, true); | |
704 return this; | |
705 } | |
706 | |
707 EventListenerList removeCapture(EventListener handler) { | |
708 _remove(handler, true); | |
709 return this; | |
710 } | |
711 | |
712 void _add(EventListener handler, bool useCapture) { | |
713 _listeners.add(handler); | |
714 } | |
715 | |
716 void _remove(EventListener handler, bool useCapture) { | |
717 // TODO(jacobr): implemenet as needed. | |
718 throw 'Not implemented yet.'; | |
719 } | |
720 | |
721 bool dispatch(Event evt) { | |
722 for (EventListener listener in _listeners) { | |
723 listener(evt); | |
724 } | |
725 } | |
726 } | |
727 | |
728 class ScrollerScrollTechnique { | |
729 static final TRANSFORM_3D = 1; | |
730 static final RELATIVE_POSITIONING = 2; | |
731 } | |
OLD | NEW |