| 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 |