Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(211)

Side by Side Diff: client/touch/Momentum.dart

Issue 9382027: Move client/{base, observable, layout, touch, util, view} to samples/ui_lib . (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: '' Created 8 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « client/touch/Math.dart ('k') | client/touch/ScrollWatcher.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 * Implementations can be used to simulate the deceleration of an element within
7 * a certain region. To use this behavior you need to provide an initial
8 * velocity that is meant to represent the gesture that is initiating this
9 * deceleration. You also provide the bounds of the region that the element
10 * exists in, and the current offset of the element within that region. The
11 * transitions will have the element decelerate to rest, or stretch past the
12 * offset boundaries and then come to rest.
13 *
14 * This is primarily designed to solve the problem of slow scrolling in mobile
15 * safari. You can use this along with the [Scroller] behavior to make a
16 * scrollable area scroll the same way it would in a native application.
17 *
18 * Implementations of this interface do not maintain any references to HTML
19 * elements, and therefore cannot do any redrawing of elements. They only
20 * calculates where the element should be on an interval. It is the delegate's
21 * responsibility to redraw the element when the onDecelerate callback is
22 * invoked. It is recommended that you move the element with a hardware
23 * accelerated method such as using 'translate3d' on the element's
24 * -webkit-transform style property.
25 */
26 interface Momentum default TimeoutMomentum {
27
28 Momentum(MomentumDelegate delegate, [num defaultDecelerationFactor]);
29
30 bool get decelerating();
31
32 num get decelerationFactor();
33
34 /**
35 * Transition end handler. This function must be invoked after any transition
36 * that occurred as a result of a call to the delegate's onDecelerate callback.
37 */
38 void onTransitionEnd();
39
40 /**
41 * Start decelerating.
42 * The [velocity] passed should be in terms of number of pixels / millisecond.
43 * [minCoord] and [maxCoord] specify the content's scrollable boundary.
44 * The current offset of the element within its boundaries is specified by
45 * [initialOffset].
46 * Returns true if deceleration has been initiated.
47 */
48 bool start(Coordinate velocity, Coordinate minCoord, Coordinate maxCoord,
49 Coordinate initialOffset, [num decelerationFactor]);
50
51 /**
52 * Calculate the velocity required to transition between coordinates [start]
53 * and [target] optionally specifying a custom [decelerationFactor].
54 */
55 Coordinate calculateVelocity(Coordinate start, Coordinate target,
56 [num decelerationFactor]);
57
58 /** Stop decelerating and return the current velocity. */
59 Coordinate stop();
60
61 /** Aborts decelerating without dispatching any notification events. */
62 void abort();
63
64 /** null if no transition is in progress. */
65 Coordinate get destination();
66 }
67
68 /**
69 * Momentum Delegate interface.
70 * You are required to implement this interface in order to use the
71 * Momentum behavior.
72 */
73 interface MomentumDelegate {
74 /**
75 * Callback for a deceleration step. The delegate is responsible for redrawing
76 * the element in its new position specified in px.
77 */
78 void onDecelerate(num x, num y);
79
80 /**
81 * Callback for end of deceleration.
82 */
83 void onDecelerationEnd();
84 }
85
86 class BouncingState {
87 static final NOT_BOUNCING = 0;
88 static final BOUNCING_AWAY = 1;
89 static final BOUNCING_BACK = 2;
90 }
91
92 class _Move {
93 final num x;
94 final num y;
95 final num vx;
96 final num vy;
97 final num time;
98
99 _Move(this.x, this.y, this.vx, this.vy, this.time);
100 }
101
102 /**
103 * Secant method root solver helper class.
104 * We use http://en.wikipedia.org/wiki/Secant_method
105 * falling back to the http://en.wikipedia.org/wiki/Bisection_method
106 * if it doesn't appear we are converging properlty.
107 * TODO(jacobr): simplify the code so we don't have to use this solver
108 * class at all.
109 */
110 class Solver {
111
112 static num solve(num fn(num), num targetY, num startX,
113 [int maxIterations = 50]) {
114 num lastX = 0;
115 num lastY = fn(lastX);
116 num deltaX;
117 num deltaY;
118 num minX = null;
119 num maxX = null;
120 num x = startX;
121 num delta = startX;
122 for (int i = 0; i < maxIterations; i++) {
123 num y = fn(x);
124 if (y.round() == targetY.round()) {
125 return x;
126 }
127 if (y > targetY) {
128 maxX = x;
129 } else {
130 minX = x;
131 }
132
133 num errorY = targetY - y;
134 deltaX = x - lastX;
135 deltaY = y - lastY;
136 lastX = x;
137 lastY = y;
138 // Avoid divide by zero and as a hack just repeat the previous delta.
139 // Obviously this is a little dangerous and we might not converge.
140 if (deltaY != 0) {
141 delta = errorY * deltaX / deltaY;
142 }
143 x += delta;
144 if (minX != null && maxX != null
145 && (x > minX || x < maxX)) {
146 // Fall back to binary search.
147 x = (minX + maxX) / 2;
148 }
149 }
150 window.console.warn('''Could not find an exact solution. LastY=${lastY},
151 targetY=${targetY} lastX=$lastX delta=$delta deltaX=$deltaX
152 deltaY=$deltaY''');
153 return x;
154 }
155 }
156
157 /**
158 * Helper class modeling the physics of a throwable scrollable area along a
159 * single dimension.
160 */
161 class SingleDimensionPhysics {
162 /** The number of frames per second the animation should run at. */
163 static final _FRAMES_PER_SECOND = 60;
164
165 /**
166 * The spring coefficient for when the element has passed a boundary and is
167 * decelerating to change direction and bounce back. Each frame, the velocity
168 * will be changed by x times this coefficient, where x is the current stretch
169 * value of the element from its boundary. This will end when velocity reaches
170 * zero.
171 */
172 static final _PRE_BOUNCE_COEFFICIENT = 7.0 / _FRAMES_PER_SECOND;
173
174 /**
175 * The spring coefficient for when the element is bouncing back from a
176 * stretched offset to a min or max position. Each frame, the velocity will
177 * be changed to x times this coefficient, where x is the current stretch
178 * value of the element from its boundary. This will end when the stretch
179 * value reaches 0.
180 */
181 static final _POST_BOUNCE_COEFFICIENT = 7.0 / _FRAMES_PER_SECOND;
182
183 /**
184 * The number of milliseconds per animation frame.
185 */
186 static final _MS_PER_FRAME = 1000.0 / _FRAMES_PER_SECOND;
187
188 /**
189 * The constant factor applied to velocity at each frame to simulate
190 * deceleration.
191 */
192 static final _DECELERATION_FACTOR = 0.97;
193
194
195 static final _MAX_VELOCITY_STATIC_FRICTION = 0.08 * _MS_PER_FRAME;
196 static final _DECELERATION_FACTOR_STATIC_FRICTION = 0.92;
197
198 /**
199 * Minimum velocity required to start or continue deceleration, in
200 * pixels/frame. This is equivalent to 0.25 px/ms.
201 */
202 static final _MIN_VELOCITY = 0.25 * _MS_PER_FRAME;
203
204 /**
205 * Minimum velocity during a step, in pixels/frame. This is equivalent to 0.01
206 * px/ms.
207 */
208 static final _MIN_STEP_VELOCITY = 0.01 * _MS_PER_FRAME;
209
210 /**
211 * Boost the initial velocity by a certain factor before applying momentum.
212 * This just gives the momentum a better feel.
213 */
214 static final _INITIAL_VELOCITY_BOOST_FACTOR = 1.25;
215
216 /**
217 * Additional deceleration factor to apply for the current move only. This
218 * is helpful for cases such as scroll wheel scrolling where the default
219 * amount of deceleration is inadequate.
220 */
221 num customDecelerationFactor = 1;
222 num _minCoord;
223 num _maxCoord;
224
225 /** The bouncing state. */
226 int _bouncingState;
227
228
229 num velocity;
230 num _currentOffset;
231
232 /**
233 * constant used when guessing at the velocity required to throw to a specific
234 * location. Chosen arbitrarily. All that really matters is that the velocity
235 * is large enough that a throw gesture will occur.
236 */
237 static final _VELOCITY_GUESS = 20;
238
239 SingleDimensionPhysics() : _bouncingState = BouncingState.NOT_BOUNCING {
240 }
241
242 void configure(num minCoord, num maxCoord,
243 num initialOffset, num customDecelerationFactor_,
244 num velocity_) {
245 _bouncingState = BouncingState.NOT_BOUNCING;
246 _minCoord = minCoord;
247 _maxCoord = maxCoord;
248 _currentOffset = initialOffset;
249 this.customDecelerationFactor = customDecelerationFactor_;
250 _adjustInitialVelocityAndBouncingState(velocity_);
251 }
252
253 num solve(num initialOffset, num targetOffset,
254 num customDecelerationFactor_) {
255 initialOffset = initialOffset.round();
256 targetOffset = targetOffset.round();
257 if (initialOffset == targetOffset) {
258 return 0;
259 }
260 return Solver.solve((num velocity_) {
261 // Don't specify min and max coordinates as we don't need to bother
262 // with the simulating bouncing off the edges.
263 configure(null, null, initialOffset.round(),
264 customDecelerationFactor_, velocity_);
265 stepAll();
266 return _currentOffset;
267 },
268 targetOffset,
269 targetOffset > initialOffset ? _VELOCITY_GUESS : -_VELOCITY_GUESS);
270 }
271
272 /**
273 * Helper method to calculate initial velocity.
274 * The [velocity] passed here should be in terms of number of
275 * pixels / millisecond. Returns the adjusted x and y velocities.
276 */
277 void _adjustInitialVelocityAndBouncingState(num v) {
278 velocity = v * _MS_PER_FRAME * _INITIAL_VELOCITY_BOOST_FACTOR;
279
280 if (velocity.abs() < _MIN_VELOCITY) {
281 if (_minCoord !== null && _currentOffset < _minCoord) {
282 velocity = (_minCoord - _currentOffset) * _POST_BOUNCE_COEFFICIENT;
283 velocity = Math.max(velocity, _MIN_STEP_VELOCITY);
284 _bouncingState = BouncingState.BOUNCING_BACK;
285 } else if (_maxCoord !== null && _currentOffset > _maxCoord) {
286 velocity = (_currentOffset - _maxCoord) * _POST_BOUNCE_COEFFICIENT;
287 velocity = -Math.max(velocity, _MIN_STEP_VELOCITY);
288 _bouncingState = BouncingState.BOUNCING_BACK;
289 }
290 }
291 }
292
293 /**
294 * Apply deceleration.
295 */
296 void _adjustVelocity() {
297 num speed = velocity.abs();
298 velocity *= _DECELERATION_FACTOR;
299 if (customDecelerationFactor != null) {
300 velocity *= customDecelerationFactor;
301 }
302 // This isn't really how static friction works but it is a plausible
303 // approximation.
304 if (speed < _MAX_VELOCITY_STATIC_FRICTION) {
305 velocity *= _DECELERATION_FACTOR_STATIC_FRICTION;
306 }
307
308 num stretchDistance;
309 if (_minCoord !== null && _currentOffset < _minCoord) {
310 stretchDistance = _minCoord - _currentOffset;
311 } else {
312 if (_maxCoord !== null && _currentOffset > _maxCoord) {
313 stretchDistance = _maxCoord - _currentOffset;
314 }
315 }
316 if (stretchDistance != null) {
317 if (stretchDistance * velocity < 0) {
318 _bouncingState = _bouncingState == BouncingState.BOUNCING_BACK ?
319 BouncingState.NOT_BOUNCING : BouncingState.BOUNCING_AWAY;
320 velocity += stretchDistance * _PRE_BOUNCE_COEFFICIENT;
321 } else {
322 _bouncingState = BouncingState.BOUNCING_BACK;
323 velocity = stretchDistance > 0 ?
324 Math.max(stretchDistance * _POST_BOUNCE_COEFFICIENT,
325 _MIN_STEP_VELOCITY) :
326 Math.min(stretchDistance * _POST_BOUNCE_COEFFICIENT,
327 -_MIN_STEP_VELOCITY);
328 }
329 } else {
330 _bouncingState = BouncingState.NOT_BOUNCING;
331 }
332 }
333
334 void step() {
335 // It is common for scrolling to be disabled so in these cases we want to
336 // avoid needless calculations.
337 if (velocity !== null) {
338 _currentOffset += velocity;
339 _adjustVelocity();
340 }
341 }
342
343 void stepAll() {
344 while(!isDone()) {
345 step();
346 }
347 }
348
349 /**
350 * Whether or not the current velocity is above the threshold required to
351 * continue decelerating.
352 */
353 bool isVelocityAboveThreshold(num threshold) {
354 return velocity.abs() >= threshold;
355 }
356
357 bool isDone() {
358 return _bouncingState == BouncingState.NOT_BOUNCING &&
359 !isVelocityAboveThreshold(_MIN_STEP_VELOCITY);
360 }
361 }
362
363 /**
364 * Implementation of a momentum strategy using webkit-transforms
365 * and timeouts.
366 */
367 class TimeoutMomentum implements Momentum {
368
369 SingleDimensionPhysics physicsX;
370 SingleDimensionPhysics physicsY;
371 Coordinate _previousOffset;
372 Queue<_Move> _moves;
373 num _stepTimeout;
374 bool _decelerating;
375 MomentumDelegate _delegate;
376 int _nextY;
377 int _nextX;
378 Coordinate _minCoord;
379 Coordinate _maxCoord;
380 num _customDecelerationFactor;
381 num _defaultDecelerationFactor;
382
383 TimeoutMomentum(this._delegate, [num defaultDecelerationFactor = 1])
384 : _defaultDecelerationFactor = defaultDecelerationFactor,
385 _decelerating = false,
386 _moves = new Queue<_Move>(),
387 physicsX = new SingleDimensionPhysics(),
388 physicsY = new SingleDimensionPhysics();
389
390 /**
391 * Calculate and return the moves for the deceleration motion.
392 */
393 void _calculateMoves() {
394 _moves.clear();
395 num time = TimeUtil.now();
396 while (!physicsX.isDone() || !physicsY.isDone()) {
397 _stepWithoutAnimation();
398 time += SingleDimensionPhysics._MS_PER_FRAME;
399 if (_isStepNecessary()) {
400 _moves.add(new _Move(_nextX, _nextY,
401 physicsX.velocity,
402 physicsY.velocity, time));
403 _previousOffset.y = _nextY;
404 _previousOffset.x = _nextX;
405 }
406 }
407 }
408
409 bool get decelerating() => _decelerating;
410 num get decelerationFactor() => _customDecelerationFactor;
411
412 /**
413 * Checks whether or not an animation step is necessary or not. Animations
414 * steps are not necessary when the velocity gets so low that in several
415 * frames the offset is the same.
416 * Returns true if there is movement to be done in the next frame.
417 */
418 bool _isStepNecessary() {
419 return _nextY != _previousOffset.y || _nextX != _previousOffset.x;
420 }
421
422 /**
423 * The [TouchHandler] requires this function but we don't need to do
424 * anything here.
425 */
426 void onTransitionEnd() {
427 }
428
429 Coordinate calculateVelocity(Coordinate start_, Coordinate target,
430 [num decelerationFactor = null]) {
431 return new Coordinate(
432 physicsX.solve(start_.x, target.x, decelerationFactor),
433 physicsY.solve(start_.y, target.y, decelerationFactor));
434 }
435
436 bool start(Coordinate velocity, Coordinate minCoord, Coordinate maxCoord,
437 Coordinate initialOffset, [num decelerationFactor = null]) {
438 _customDecelerationFactor = _defaultDecelerationFactor;
439 if (decelerationFactor !== null) {
440 _customDecelerationFactor = decelerationFactor;
441 }
442
443 if (_stepTimeout !== null) {
444 Env.cancelRequestAnimationFrame(_stepTimeout);
445 _stepTimeout = null;
446 }
447
448 assert (_stepTimeout === null);
449 assert(minCoord.x <= maxCoord.x);
450 assert(minCoord.y <= maxCoord.y);
451 _previousOffset = initialOffset.clone();
452 physicsX.configure(minCoord.x, maxCoord.x, initialOffset.x,
453 _customDecelerationFactor, velocity.x);
454 physicsY.configure(minCoord.y, maxCoord.y, initialOffset.y,
455 _customDecelerationFactor, velocity.y);
456 if (!physicsX.isDone() || !physicsY.isDone()) {
457 _calculateMoves();
458 if (!_moves.isEmpty()) {
459 num firstTime = _moves.first().time;
460 _stepTimeout = Env.requestAnimationFrame(
461 _step, null, firstTime);
462 _decelerating = true;
463 return true;
464 }
465 }
466 _decelerating = false;
467 return false;
468 }
469
470 /**
471 * Update the x, y values of the element offset without actually moving the
472 * element. This is done because we store decimal values for x, y for
473 * precision, but moving is only required when the offset is changed by at
474 * least a whole integer.
475 */
476 void _stepWithoutAnimation() {
477 physicsX.step();
478 physicsY.step();
479 // TODO(jacobr): double.round() should return an int, see b/5121907
480 _nextX = physicsX._currentOffset.round().toInt();
481 _nextY = physicsY._currentOffset.round().toInt();
482 }
483
484 /**
485 * Calculate the next offset of the element and animate it to that position.
486 */
487 void _step(num timestamp) {
488 _stepTimeout = null;
489
490 // Prune moves that are more than 1 frame behind when we have more
491 // available moves.
492 num lastEpoch = timestamp - SingleDimensionPhysics._MS_PER_FRAME;
493 while (!_moves.isEmpty() && _moves.first() !== _moves.last()
494 && _moves.first().time < lastEpoch) {
495 _moves.removeFirst();
496 }
497
498 if (!_moves.isEmpty()) {
499 final move = _moves.removeFirst();
500 _delegate.onDecelerate(move.x, move.y);
501 if (!_moves.isEmpty()) {
502 num nextTime = _moves.first().time;
503 assert(_stepTimeout === null);
504 _stepTimeout = Env.requestAnimationFrame(_step, null, nextTime);
505 } else {
506 stop();
507 }
508 }
509 }
510
511 void abort() {
512 _decelerating = false;
513 _moves.clear();
514 if (_stepTimeout !== null) {
515 Env.cancelRequestAnimationFrame(_stepTimeout);
516 _stepTimeout = null;
517 }
518 }
519
520 Coordinate stop() {
521 final wasDecelerating = _decelerating;
522 _decelerating = false;
523 Coordinate velocity;
524 if (!_moves.isEmpty()) {
525 final move = _moves.first();
526 // This is a workaround for the ugly hacks that get applied when a user
527 // passed a velocity in to this Momentum implementation.
528 num velocityScale = SingleDimensionPhysics._MS_PER_FRAME *
529 SingleDimensionPhysics._INITIAL_VELOCITY_BOOST_FACTOR;
530 velocity = new Coordinate(
531 move.vx / velocityScale, move.vy / velocityScale);
532 } else {
533 velocity = new Coordinate(0, 0);
534 }
535 _moves.clear();
536 if (_stepTimeout !== null) {
537 Env.cancelRequestAnimationFrame(_stepTimeout);
538 _stepTimeout = null;
539 }
540 if (wasDecelerating) {
541 _delegate.onDecelerationEnd();
542 }
543 return velocity;
544 }
545
546 Coordinate get destination() {
547 if (!_moves.isEmpty()) {
548 final lastMove = _moves.last();
549 return new Coordinate(lastMove.x, lastMove.y);
550 } else {
551 return null;
552 }
553 }
554 }
OLDNEW
« no previous file with comments | « client/touch/Math.dart ('k') | client/touch/ScrollWatcher.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698