@@ -321,19 +321,91 @@ class SimulationDrivenScrollActivity extends ScrollActivity {
321321 }
322322}
323323
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+
324397/// An activity that animates a scroll view smoothly to its end.
325398///
326399/// In particular this drives the "scroll to bottom" button
327400/// 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 ));
337409
338410 ScrollPosition get _position => delegate as ScrollPosition ;
339411
@@ -488,20 +560,20 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext {
488560
489561 /// Scroll the position smoothly to the end of the scrollable content.
490562 ///
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] .
498572 void scrollToEnd () {
499- final target = maxScrollExtent;
500-
501573 final tolerance = physics.toleranceFor (this );
502- if (nearEqual (pixels, target , tolerance.distance)) {
574+ if (nearEqual (pixels, maxScrollExtent , tolerance.distance)) {
503575 // Skip the animation; jump right to the target, which is already close.
504- jumpTo (target );
576+ jumpTo (maxScrollExtent );
505577 return ;
506578 }
507579
@@ -513,30 +585,7 @@ class MessageListScrollPosition extends ScrollPositionWithSingleContext {
513585 return ;
514586 }
515587
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 ));
540589 }
541590}
542591
0 commit comments