@@ -321,19 +321,91 @@ class SimulationDrivenScrollActivity extends ScrollActivity {
321
321
}
322
322
}
323
323
324
+ /// A simulation of motion at a constant velocity.
325
+ ///
326
+ /// Models a particle that follows Newton's law of inertia,
327
+ /// with no forces acting on the particle, and no end to the motion.
328
+ ///
329
+ /// See also [GravitySimulation] , which adds a constant acceleration
330
+ /// and a stopping point.
331
+ class InertialSimulation extends Simulation { // TODO(upstream)
332
+ InertialSimulation (double initialPosition, double velocity)
333
+ : _x0 = initialPosition, _v = velocity;
334
+
335
+ final double _x0;
336
+ final double _v;
337
+
338
+ @override
339
+ double x (double time) => _x0 + _v * time;
340
+
341
+ @override
342
+ double dx (double time) => _v;
343
+
344
+ @override
345
+ bool isDone (double time) => false ;
346
+
347
+ @override
348
+ String toString () => '${objectRuntimeType (this , 'InertialSimulation' )}('
349
+ 'x₀: ${_x0 .toStringAsFixed (1 )}, dx₀: ${_v .toStringAsFixed (1 )})' ;
350
+ }
351
+
352
+ /// A simulation of the user impatiently scrolling to the end of a list.
353
+ ///
354
+ /// The position [x] is in logical pixels, and time is in seconds.
355
+ ///
356
+ /// The motion is meant to resemble the user scrolling the list down
357
+ /// (by dragging up and flinging), and if the list is long then
358
+ /// fling-scrolling again and again to keep it moving quickly.
359
+ ///
360
+ /// In that scenario taken literally, the motion would repeatedly slow down,
361
+ /// then speed up again with a fresh drag and fling. But doing that in
362
+ /// response to a simulated drag, as opposed to when the user is actually
363
+ /// dragging with their own finger, would feel jerky and not a good UX.
364
+ /// Instead this takes a smoothed-out approximation of such a trajectory.
365
+ class ScrollToEndSimulation extends InertialSimulation {
366
+ factory ScrollToEndSimulation (ScrollPosition position) {
367
+ final startPosition = position.pixels;
368
+ final estimatedEndPosition = position.maxScrollExtent;
369
+ final velocityForMinDuration = (estimatedEndPosition - startPosition)
370
+ / (minDuration.inMilliseconds / 1000.0 );
371
+ assert (velocityForMinDuration > 0 );
372
+ final velocity = clampDouble (velocityForMinDuration, 0 , topSpeed);
373
+ return ScrollToEndSimulation ._(startPosition, velocity);
374
+ }
375
+
376
+ ScrollToEndSimulation ._(super .initialPosition, super .velocity);
377
+
378
+ /// The top speed to move at, in logical pixels per second.
379
+ ///
380
+ /// This will be the speed whenever the estimated distance to be traveled
381
+ /// is long enough to take at least [minDuration] at this speed.
382
+ ///
383
+ /// This is chosen to equal the top speed that can be produced
384
+ /// by a fling gesture in a Flutter [ScrollView] ,
385
+ /// which in turn was chosen to equal the top speed of
386
+ /// an (initial) fling gesture in a native Android scroll view.
387
+ static const double topSpeed = 8000 ;
388
+
389
+ /// The desired duration of the animation when traveling short distances.
390
+ ///
391
+ /// The speed will be chosen so that traveling the estimated distance
392
+ /// will take this long, whenever that distance is short enough
393
+ /// that that means a speed of at most [topSpeed] .
394
+ static const minDuration = Duration (milliseconds: 300 );
395
+ }
396
+
324
397
/// An activity that animates a scroll view smoothly to its end.
325
398
///
326
399
/// In particular this drives the "scroll to bottom" button
327
400
/// in the Zulip message list.
328
- class ScrollToEndActivity extends DrivenScrollActivity {
329
- ScrollToEndActivity (
330
- super .delegate, {
331
- required super .from,
332
- required super .to,
333
- required super .duration,
334
- required super .curve,
335
- required super .vsync,
336
- });
401
+ class ScrollToEndActivity extends SimulationDrivenScrollActivity {
402
+ /// Create an activity that animates a scroll view smoothly to its end.
403
+ ///
404
+ /// The [delegate] is required to also implement [ScrollPosition] .
405
+ ScrollToEndActivity (ScrollActivityDelegate delegate)
406
+ : super .simulation (delegate,
407
+ vsync: (delegate as ScrollPosition ).context.vsync,
408
+ ScrollToEndSimulation (delegate as ScrollPosition ));
337
409
338
410
ScrollPosition get _position => delegate as ScrollPosition ;
339
411
@@ -488,20 +560,20 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext {
488
560
489
561
/// Scroll the position smoothly to the end of the scrollable content.
490
562
///
491
- /// This method only works well if [maxScrollExtent] is accurate
492
- /// and does not change during the animation.
493
- /// (For example, this works if there is no content in forward slivers,
494
- /// so that [maxScrollExtent] is always zero.)
495
- /// The animation will attempt to travel to the value [maxScrollExtent] had
496
- /// at the start of the animation, even if that ends up being more or less far
497
- /// than the actual extent of the content.
563
+ /// This is similar to calling [animateTo] with a target of [maxScrollExtent] ,
564
+ /// except that if [maxScrollExtent] changes over the course of the animation
565
+ /// (for example due to more content being added at the end,
566
+ /// or due to the estimated length of the content changing as
567
+ /// different items scroll into the viewport),
568
+ /// this animation will carry on until it reaches the updated value
569
+ /// of [maxScrollExtent] , not the value it had at the start of the animation.
570
+ ///
571
+ /// The animation is typically handled by a [ScrollToEndActivity] .
498
572
void scrollToEnd () {
499
- final target = maxScrollExtent;
500
-
501
573
final tolerance = physics.toleranceFor (this );
502
- if (nearEqual (pixels, target , tolerance.distance)) {
574
+ if (nearEqual (pixels, maxScrollExtent , tolerance.distance)) {
503
575
// Skip the animation; jump right to the target, which is already close.
504
- jumpTo (target );
576
+ jumpTo (maxScrollExtent );
505
577
return ;
506
578
}
507
579
@@ -513,30 +585,7 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext {
513
585
return ;
514
586
}
515
587
516
- /// The top speed to move at, in logical pixels per second.
517
- ///
518
- /// This will be the speed whenever the distance to be traveled
519
- /// is long enough to take at least [minDuration] at this speed.
520
- ///
521
- /// This is chosen to equal the top speed that can be produced
522
- /// by a fling gesture in a Flutter [ScrollView] ,
523
- /// which in turn was chosen to equal the top speed of
524
- /// an (initial) fling gesture in a native Android scroll view.
525
- const double topSpeed = 8000 ;
526
-
527
- /// The desired duration of the animation when traveling short distances.
528
- ///
529
- /// The speed will be chosen so that traveling the distance
530
- /// will take this long, whenever that distance is short enough
531
- /// that that means a speed of at most [topSpeed] .
532
- const minDuration = Duration (milliseconds: 300 );
533
-
534
- final durationSecAtSpeedLimit = (target - pixels) / topSpeed;
535
- final durationSec = math.max (durationSecAtSpeedLimit,
536
- minDuration.inMilliseconds / 1000.0 );
537
- final duration = Duration (milliseconds: (durationSec * 1000.0 ).ceil ());
538
- beginActivity (ScrollToEndActivity (this , vsync: context.vsync,
539
- from: pixels, to: target, duration: duration, curve: Curves .linear));
588
+ beginActivity (ScrollToEndActivity (this ));
540
589
}
541
590
}
542
591
0 commit comments