Commit c288c706 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Refactor scrolling code to prepare for nested scrolling (#9575)

This patch reworks some of the guts of scrolling to make it easier to
implement nested scrolling effects. The actually nested scrolling effect
will be included in a later patch.
parent 57648ba0
......@@ -411,7 +411,7 @@ class _AnimationDemoHomeState extends State<AnimationDemoHome> {
if (notification.depth == 0 && notification is ScrollUpdateNotification) {
selectedIndex.value = leader.page;
if (follower.page != leader.page)
follower.position.jumpTo(leader.position.pixels, settle: false);
follower.position.jumpToWithoutSettling(leader.position.pixels); // ignore: deprecated_member_use
}
return false;
}
......
......@@ -87,7 +87,7 @@ class ContactsDemo extends StatefulWidget {
ContactsDemoState createState() => new ContactsDemoState();
}
enum AppBarBehavior { normal, pinned, floating }
enum AppBarBehavior { normal, pinned, floating, snapping }
class ContactsDemoState extends State<ContactsDemo> {
static final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
......@@ -110,7 +110,8 @@ class ContactsDemoState extends State<ContactsDemo> {
new SliverAppBar(
expandedHeight: _appBarHeight,
pinned: _appBarBehavior == AppBarBehavior.pinned,
floating: _appBarBehavior == AppBarBehavior.floating,
floating: _appBarBehavior == AppBarBehavior.floating || _appBarBehavior == AppBarBehavior.snapping,
snap: _appBarBehavior == AppBarBehavior.snapping,
actions: <Widget>[
new IconButton(
icon: const Icon(Icons.create),
......@@ -140,6 +141,10 @@ class ContactsDemoState extends State<ContactsDemo> {
value: AppBarBehavior.floating,
child: const Text('App bar floats')
),
const PopupMenuItem<AppBarBehavior>(
value: AppBarBehavior.snapping,
child: const Text('App bar snaps')
),
],
),
],
......
......@@ -543,6 +543,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@required this.flexibleSpace,
@required this.bottom,
@required this.elevation,
@required this.forceElevated,
@required this.backgroundColor,
@required this.brightness,
@required this.iconTheme,
......@@ -565,6 +566,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final Widget flexibleSpace;
final PreferredSizeWidget bottom;
final int elevation;
final bool forceElevated;
final Color backgroundColor;
final Brightness brightness;
final IconThemeData iconTheme;
......@@ -604,7 +606,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
actions: actions,
flexibleSpace: flexibleSpace,
bottom: bottom,
elevation: overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent) ? elevation ?? 4 : 0,
elevation: forceElevated || overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent) ? elevation ?? 4 : 0,
backgroundColor: backgroundColor,
brightness: brightness,
iconTheme: iconTheme,
......@@ -685,6 +687,7 @@ class SliverAppBar extends StatefulWidget {
this.flexibleSpace,
this.bottom,
this.elevation,
this.forceElevated: false,
this.backgroundColor,
this.brightness,
this.iconTheme,
......@@ -695,11 +698,13 @@ class SliverAppBar extends StatefulWidget {
this.floating: false,
this.pinned: false,
this.snap: false,
}) : assert(primary != null),
}) : assert(forceElevated != null),
assert(primary != null),
assert(floating != null),
assert(pinned != null),
assert(pinned && floating ? bottom != null : true),
assert(!pinned || !floating || bottom != null, 'A pinned and floating app bar must have a bottom widget.'),
assert(snap != null),
assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
super(key: key);
/// A widget to display before the [title].
......@@ -765,17 +770,30 @@ class SliverAppBar extends StatefulWidget {
/// * [PreferredSize], which can be used to give an arbitrary widget a preferred size.
final PreferredSizeWidget bottom;
/// The z-coordinate at which to place this app bar.
/// The z-coordinate at which to place this app bar when it is above other
/// content.
///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
///
/// Defaults to 4, the appropriate elevation for app bars.
///
/// The elevation is ignored when the app bar has no content underneath it.
/// For example, if the app bar is [pinned] but no content is scrolled under
/// it, or if it scrolls with the content.
/// If [forceElevated] is false, the elevation is ignored when the app bar has
/// no content underneath it. For example, if the app bar is [pinned] but no
/// content is scrolled under it, or if it scrolls with the content, then no
/// shadow is drawn, regardless of the value of [elevation].
final int elevation;
/// Whether to show the shadow appropriate for the [elevation] even if the
/// content is not scrolled under the [AppBar].
///
/// Defaults to false, meaning that the [elevation] is only applied when the
/// [AppBar] is being displayed over content that is scrolled under it.
///
/// When set to true, the [elevation] is applied regardless.
///
/// Ignored when [elevation] is zero.
final bool forceElevated;
/// The color to use for the app bar's material. Typically this should be set
/// along with [brightness], [iconTheme], [textTheme].
///
......@@ -829,12 +847,9 @@ class SliverAppBar extends StatefulWidget {
/// Otherwise, the user will need to scroll near the top of the scroll view to
/// reveal the app bar.
///
/// See also:
///
/// * If [snap] is true then a scroll that exposes the app bar will trigger
/// an animation that slides the entire app bar into view. Similarly if
/// a scroll dismisses the app bar, the animation will slide it completely
/// out of view.
/// If [snap] is true then a scroll that exposes the app bar will trigger an
/// animation that slides the entire app bar into view. Similarly if a scroll
/// dismisses the app bar, the animation will slide it completely out of view.
final bool floating;
/// Whether the app bar should remain visible at the start of the scroll view.
......@@ -905,6 +920,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
flexibleSpace: widget.flexibleSpace,
bottom: widget.bottom,
elevation: widget.elevation,
forceElevated: widget.forceElevated,
backgroundColor: widget.backgroundColor,
brightness: widget.brightness,
iconTheme: widget.iconTheme,
......
......@@ -177,14 +177,14 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
if (notification.depth != 0)
return false;
if (notification is ScrollStartNotification && notification.metrics.extentBefore == 0.0 &&
_mode == null && _start(notification.axisDirection)) {
_mode == null && _start(notification.metrics.axisDirection)) {
setState(() {
_mode = _RefreshIndicatorMode.drag;
});
return false;
}
bool indicatorAtTopNow;
switch (notification.axisDirection) {
switch (notification.metrics.axisDirection) {
case AxisDirection.down:
indicatorAtTopNow = true;
break;
......
......@@ -55,7 +55,7 @@ class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
bool _handleScrollNotification(ScrollNotification notification) {
if (notification is ScrollUpdateNotification ||
notification is OverscrollNotification)
_controller.update(notification.metrics, notification.axisDirection);
_controller.update(notification.metrics, notification.metrics.axisDirection);
return false;
}
......
......@@ -338,15 +338,15 @@ class _DragAnimation extends Animation<double> with AnimationWithParentMixin<dou
// where a scrollable TabBar has a non-zero initialIndex. In that case we can
// only compute the scroll position's initial scroll offset (the "correct"
// pixels value) after the TabBar viewport width and scroll limits are known.
class _TabBarScrollPosition extends ScrollPosition {
class _TabBarScrollPosition extends ScrollPositionWithSingleContext {
_TabBarScrollPosition({
ScrollPhysics physics,
AbstractScrollState state,
ScrollContext context,
ScrollPosition oldPosition,
this.tabBar,
}) : super(
physics: physics,
state: state,
context: context,
initialPixels: null,
oldPosition: oldPosition,
);
......@@ -372,10 +372,10 @@ class _TabBarScrollController extends ScrollController {
final _TabBarState tabBar;
@override
ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) {
ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
return new _TabBarScrollPosition(
physics: physics,
state: state,
context: context,
oldPosition: oldPosition,
tabBar: tabBar,
);
......
......@@ -109,6 +109,24 @@ AxisDirection flipAxisDirection(AxisDirection axisDirection) {
return null;
}
/// Returns whether travelling along the given axis direction visits coordinates
/// along that axis in numerically decreasing order.
///
/// Specifically, returns true for [AxisDirection.up] and [AxisDirection.left]
/// and false for [AxisDirection.down] for [AxisDirection.right].
bool axisDirectionIsReversed(AxisDirection axisDirection) {
assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.up:
case AxisDirection.left:
return true;
case AxisDirection.down:
case AxisDirection.right:
return false;
}
return null;
}
/// Flips the [AxisDirection] if the [GrowthDirection] is [GrowthDirection.reverse].
///
/// Specifically, returns `axisDirection` if `growthDirection` is
......
......@@ -164,7 +164,7 @@ abstract class ViewportOffset extends ChangeNotifier {
String toString() {
final List<String> description = <String>[];
debugFillDescription(description);
return '$runtimeType(${description.join(", ")})';
return '$runtimeType#$hashCode(${description.join(", ")})';
}
/// Add additional information to the given description for use by [toString].
......
......@@ -1129,7 +1129,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
onPointerUp: _handlePointerUpOrCancel,
onPointerCancel: _handlePointerUpOrCancel,
child: new AbsorbPointer(
absorbing: false,
absorbing: false, // it's mutated directly by _cancelActivePointers above
child: new FocusScope(
node: focusScopeNode,
autofocus: true,
......
......@@ -152,7 +152,7 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
_accepted[isLeading] = confirmationNotification._accepted;
}
assert(controller != null);
assert(notification.axis == widget.axis);
assert(notification.metrics.axis == widget.axis);
if (_accepted[isLeading]) {
if (notification.velocity != 0.0) {
assert(notification.dragDetails == null);
......@@ -166,7 +166,7 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
assert(renderer.hasSize);
final Size size = renderer.size;
final Offset position = renderer.globalToLocal(notification.dragDetails.globalPosition);
switch (notification.axis) {
switch (notification.metrics.axis) {
case Axis.horizontal:
controller.pull(notification.overscroll.abs(), size.width, position.dy.clamp(0.0, size.height), size.height);
break;
......
......@@ -12,10 +12,13 @@ import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'framework.dart';
import 'notification_listener.dart';
import 'scroll_context.dart';
import 'scroll_controller.dart';
import 'scroll_metrics.dart';
import 'scroll_notification.dart';
import 'scroll_physics.dart';
import 'scroll_position.dart';
import 'scroll_position_with_single_context.dart';
import 'scroll_view.dart';
import 'scrollable.dart';
import 'sliver.dart';
......@@ -70,7 +73,11 @@ class PageController extends ScrollController {
@required Curve curve,
}) {
final _PagePosition position = this.position;
return position.animateTo(position.getPixelsFromPage(page.toDouble()), duration: duration, curve: curve);
return position.animateTo(
position.getPixelsFromPage(page.toDouble()),
duration: duration,
curve: curve,
);
}
/// Changes which page is displayed in the controlled [PageView].
......@@ -105,10 +112,10 @@ class PageController extends ScrollController {
}
@override
ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) {
ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
return new _PagePosition(
physics: physics,
state: state,
context: context,
initialPage: initialPage,
viewportFraction: viewportFraction,
oldPosition: oldPosition,
......@@ -127,7 +134,7 @@ class PageController extends ScrollController {
///
/// The metrics are available on [ScrollNotification]s generated from
/// [PageView]s.
class PageMetrics extends ScrollMetrics {
class PageMetrics extends FixedScrollMetrics {
/// Creates page metrics that add the given information to the `parent`
/// metrics.
PageMetrics({
......@@ -139,16 +146,16 @@ class PageMetrics extends ScrollMetrics {
final double page;
}
class _PagePosition extends ScrollPosition {
class _PagePosition extends ScrollPositionWithSingleContext {
_PagePosition({
ScrollPhysics physics,
AbstractScrollState state,
ScrollContext context,
this.initialPage: 0,
double viewportFraction: 1.0,
ScrollPosition oldPosition,
}) : _viewportFraction = viewportFraction, super(
physics: physics,
state: state,
context: context,
initialPixels: null,
oldPosition: oldPosition,
) {
......@@ -167,7 +174,7 @@ class _PagePosition extends ScrollPosition {
final double oldPage = page;
_viewportFraction = value;
if (oldPage != null)
correctPixels(getPixelsFromPage(oldPage));
forcePixels(getPixelsFromPage(oldPage));
}
double getPageFromPixels(double pixels, double viewportDimension) {
......@@ -195,9 +202,9 @@ class _PagePosition extends ScrollPosition {
}
@override
PageMetrics getMetrics() {
PageMetrics cloneMetrics() {
return new PageMetrics(
parent: super.getMetrics(),
parent: this,
page: page,
);
}
......@@ -235,7 +242,7 @@ class PageScrollPhysics extends ScrollPhysics {
}
@override
Simulation createBallisticSimulation(ScrollPosition position, double velocity) {
Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
// If we're out of range and not headed back in range, defer to the parent
// ballistics, which should put us back in range at a page boundary.
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
......@@ -243,7 +250,9 @@ class PageScrollPhysics extends ScrollPhysics {
return super.createBallisticSimulation(position, velocity);
final Tolerance tolerance = this.tolerance;
final double target = _getTargetPixels(position, tolerance, velocity);
return new ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance);
if (target != position.pixels)
return new ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance);
return null;
}
}
......@@ -421,10 +430,10 @@ class _PageViewState extends State<PageView> {
axisDirection: axisDirection,
controller: widget.controller,
physics: widget.physics == null ? _kPagePhysics : _kPagePhysics.applyTo(widget.physics),
viewportBuilder: (BuildContext context, ViewportOffset offset) {
viewportBuilder: (BuildContext context, ViewportOffset position) {
return new Viewport(
axisDirection: axisDirection,
offset: offset,
offset: position,
slivers: <Widget>[
new SliverFillViewport(
viewportFraction: widget.controller.viewportFraction,
......
......@@ -34,6 +34,6 @@ class PrimaryScrollController extends InheritedWidget {
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('${controller ?? 'no controller'}');
description.add('${controller ?? "no controller"}');
}
}
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'scroll_metrics.dart';
import 'scroll_notification.dart';
import 'ticker_provider.dart';
abstract class ScrollActivityDelegate {
AxisDirection get axisDirection;
double get pixels;
double setPixels(double pixels);
double applyUserOffset(double delta);
void goIdle();
void goBallistic(double velocity);
}
/// Base class for scrolling activities like dragging and flinging.
///
/// See also:
///
/// * [ScrollPositionWithSingleContext], which uses [ScrollActivity] objects to
/// manage the [ScrollPosition] of a [Scrollable].
abstract class ScrollActivity {
ScrollActivity(this._delegate);
ScrollActivityDelegate get delegate => _delegate;
ScrollActivityDelegate _delegate;
/// Updates the activity's link to the [ScrollActivityDelegate].
///
/// This should only be called when an activity is being moved from a defunct
/// (or about-to-be defunct) [ScrollActivityDelegate] object to a new one.
void updateDelegate(ScrollActivityDelegate value) {
assert(_delegate != value);
_delegate = value;
}
/// Called by the [ScrollActivityDelegate] when it has changed type (for
/// example, when changing from an Android-style scroll position to an
/// iOS-style scroll position). If this activity can differ between the two
/// modes, then it should tell the position to restart that activity
/// appropriately.
///
/// For example, [BallisticScrollActivity]'s implementation calls
/// [ScrollActivityDelegate.goBallistic].
void resetActivity() { }
void dispatchScrollStartNotification(ScrollMetrics metrics, BuildContext context) {
new ScrollStartNotification(metrics: metrics, context: context).dispatch(context);
}
void dispatchScrollUpdateNotification(ScrollMetrics metrics, BuildContext context, double scrollDelta) {
new ScrollUpdateNotification(metrics: metrics, context: context, scrollDelta: scrollDelta).dispatch(context);
}
void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {
new OverscrollNotification(metrics: metrics, context: context, overscroll: overscroll).dispatch(context);
}
void dispatchScrollEndNotification(ScrollMetrics metrics, BuildContext context) {
new ScrollEndNotification(metrics: metrics, context: context).dispatch(context);
}
void didTouch() { }
void applyNewDimensions() { }
bool get shouldIgnorePointer;
bool get isScrolling;
@mustCallSuper
void dispose() {
_delegate = null;
}
@override
String toString() => '$runtimeType';
}
class IdleScrollActivity extends ScrollActivity {
IdleScrollActivity(ScrollActivityDelegate delegate) : super(delegate);
@override
void applyNewDimensions() {
delegate.goBallistic(0.0);
}
@override
bool get shouldIgnorePointer => false;
@override
bool get isScrolling => false;
}
class DragScrollActivity extends ScrollActivity implements Drag {
DragScrollActivity(
ScrollActivityDelegate delegate,
DragStartDetails details,
this.onDragCanceled,
) : _lastDetails = details, super(delegate);
final VoidCallback onDragCanceled;
@override
void didTouch() {
assert(false);
}
bool get _reversed => axisDirectionIsReversed(delegate.axisDirection);
@override
void update(DragUpdateDetails details) {
assert(details.primaryDelta != null);
_lastDetails = details;
double offset = details.primaryDelta;
if (offset == 0.0)
return;
if (_reversed) // e.g. an AxisDirection.up scrollable
offset = -offset;
delegate.applyUserOffset(offset);
// We ignore any reported overscroll returned by setPixels,
// because it gets reported via the reportOverscroll path.
}
@override
void end(DragEndDetails details) {
assert(details.primaryVelocity != null);
double velocity = details.primaryVelocity;
if (_reversed) // e.g. an AxisDirection.up scrollable
velocity = -velocity;
_lastDetails = details;
// We negate the velocity here because if the touch is moving downwards,
// the scroll has to move upwards. It's the same reason that update()
// above negates the delta before applying it to the scroll offset.
delegate.goBallistic(-velocity);
}
@override
void cancel() {
delegate.goBallistic(0.0);
}
@override
void dispose() {
_lastDetails = null;
if (onDragCanceled != null)
onDragCanceled();
super.dispose();
}
dynamic _lastDetails;
@override
void dispatchScrollStartNotification(ScrollMetrics metrics, BuildContext context) {
assert(_lastDetails is DragStartDetails);
new ScrollStartNotification(metrics: metrics, context: context, dragDetails: _lastDetails).dispatch(context);
}
@override
void dispatchScrollUpdateNotification(ScrollMetrics metrics, BuildContext context, double scrollDelta) {
assert(_lastDetails is DragUpdateDetails);
new ScrollUpdateNotification(metrics: metrics, context: context, scrollDelta: scrollDelta, dragDetails: _lastDetails).dispatch(context);
}
@override
void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {
assert(_lastDetails is DragUpdateDetails);
new OverscrollNotification(metrics: metrics, context: context, overscroll: overscroll, dragDetails: _lastDetails).dispatch(context);
}
@override
void dispatchScrollEndNotification(ScrollMetrics metrics, BuildContext context) {
// We might not have DragEndDetails yet if we're being called from beginActivity.
new ScrollEndNotification(
metrics: metrics,
context: context,
dragDetails: _lastDetails is DragEndDetails ? _lastDetails : null
).dispatch(context);
}
@override
bool get shouldIgnorePointer => true;
@override
bool get isScrolling => true;
}
class BallisticScrollActivity extends ScrollActivity {
// ///
// /// The velocity should be in logical pixels per second.
BallisticScrollActivity(
ScrollActivityDelegate delegate,
Simulation simulation,
TickerProvider vsync,
) : super(delegate) {
_controller = new AnimationController.unbounded(
debugLabel: '$runtimeType',
vsync: vsync,
)
..addListener(_tick)
..animateWith(simulation)
.whenComplete(_end); // won't trigger if we dispose _controller first
}
double get velocity => _controller.velocity;
AnimationController _controller;
@override
void resetActivity() {
delegate.goBallistic(velocity);
}
@override
void didTouch() {
delegate.goIdle();
}
@override
void applyNewDimensions() {
delegate.goBallistic(velocity);
}
void _tick() {
if (!applyMoveTo(_controller.value))
delegate.goIdle();
}
/// Move the position to the given location.
///
/// If the new position was fully applied, return true.
/// If there was any overflow, return false.
///
/// The default implementation calls [ScrollActivityDelegate.setPixels]
/// and returns true if the overflow was zero.
@protected
bool applyMoveTo(double value) {
return delegate.setPixels(value) == 0.0;
}
void _end() {
delegate?.goBallistic(0.0);
}
@override
void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {
new OverscrollNotification(metrics: metrics, context: context, overscroll: overscroll, velocity: velocity).dispatch(context);
}
@override
bool get shouldIgnorePointer => true;
@override
bool get isScrolling => true;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
String toString() {
return '$runtimeType($_controller)';
}
}
class DrivenScrollActivity extends ScrollActivity {
DrivenScrollActivity(
ScrollActivityDelegate delegate, {
@required double from,
@required double to,
@required Duration duration,
@required Curve curve,
@required TickerProvider vsync,
}) : super(delegate) {
assert(from != null);
assert(to != null);
assert(duration != null);
assert(duration > Duration.ZERO);
assert(curve != null);
_completer = new Completer<Null>();
_controller = new AnimationController.unbounded(
value: from,
debugLabel: '$runtimeType',
vsync: vsync,
)
..addListener(_tick)
..animateTo(to, duration: duration, curve: curve)
.whenComplete(_end); // won't trigger if we dispose _controller first
}
Completer<Null> _completer;
AnimationController _controller;
Future<Null> get done => _completer.future;
double get velocity => _controller.velocity;
@override
void didTouch() {
delegate.goIdle();
}
void _tick() {
if (delegate.setPixels(_controller.value) != 0.0)
delegate.goIdle();
}
void _end() {
delegate?.goBallistic(velocity);
}
@override
void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {
new OverscrollNotification(metrics: metrics, context: context, overscroll: overscroll, velocity: velocity).dispatch(context);
}
@override
bool get shouldIgnorePointer => true;
@override
bool get isScrolling => true;
@override
void dispose() {
_completer.complete();
_controller.dispose();
super.dispose();
}
@override
String toString() {
return '$runtimeType($_controller)';
}
}
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/scheduler.dart';
import 'package:flutter/rendering.dart';
import 'framework.dart';
import 'ticker_provider.dart';
abstract class ScrollContext {
BuildContext get notificationContext;
TickerProvider get vsync;
AxisDirection get axisDirection;
void setIgnorePointer(bool value);
void setCanDrag(bool value);
}
......@@ -7,7 +7,10 @@ import 'dart:async';
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'scroll_context.dart';
import 'scroll_physics.dart';
import 'scroll_position.dart';
import 'scroll_position_with_single_context.dart';
class ScrollController extends ChangeNotifier {
ScrollController({
......@@ -22,6 +25,12 @@ class ScrollController extends ChangeNotifier {
/// controller will have their offset initialized to this value.
final double initialScrollOffset;
/// The currently attached positions.
///
/// This should not be mutated directly. [ScrollPosition] objects can be added
/// and removed using [attach] and [detach].
@protected
Iterable<ScrollPosition> get positions => _positions;
final List<ScrollPosition> _positions = <ScrollPosition>[];
/// Whether any [ScrollPosition] objects have attached themselves to the
......@@ -32,6 +41,10 @@ class ScrollController extends ChangeNotifier {
/// called.
bool get hasClients => _positions.isNotEmpty;
/// Returns the attached [ScrollPosition], from which the actual scroll offset
/// of the [ScrollView] can be obtained.
///
/// Calling this is only valid when only a single position is attached.
ScrollPosition get position {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.');
......@@ -71,7 +84,7 @@ class ScrollController extends ChangeNotifier {
}) {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
final List<Future<Null>> animations = new List<Future<Null>>(_positions.length);
for (int i = 0; i < _positions.length; i++)
for (int i = 0; i < _positions.length; i += 1)
animations[i] = _positions[i].animateTo(offset, duration: duration, curve: curve);
return Future.wait<Null>(animations).then((List<Null> _) => null);
}
......@@ -121,18 +134,22 @@ class ScrollController extends ChangeNotifier {
super.dispose();
}
static ScrollPosition createDefaultScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) {
return new ScrollPosition(
static ScrollPosition createDefaultScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
return new ScrollPositionWithSingleContext(
physics: physics,
state: state,
context: context,
oldPosition: oldPosition,
);
}
ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) {
return new ScrollPosition(
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition oldPosition,
) {
return new ScrollPositionWithSingleContext(
physics: physics,
state: state,
context: context,
initialPixels: initialScrollOffset,
oldPosition: oldPosition,
);
......@@ -140,18 +157,22 @@ class ScrollController extends ChangeNotifier {
@override
String toString() {
final StringBuffer result = new StringBuffer();
result.write('$runtimeType#$hashCode(');
final List<String> description = <String>[];
debugFillDescription(description);
return '$runtimeType#$hashCode(${description.join(", ")})';
}
@mustCallSuper
void debugFillDescription(List<String> description) {
if (initialScrollOffset != 0.0)
result.write('initialScrollOffset: ${initialScrollOffset.toStringAsFixed(1)}, ');
description.add('initialScrollOffset: ${initialScrollOffset.toStringAsFixed(1)}, ');
if (_positions.isEmpty) {
result.write('no clients');
description.add('no clients');
} else if (_positions.length == 1) {
result.write('one client, offset $offset');
// Don't actually list the client itself, since its toString may refer to us.
description.add('one client, offset ${offset.toStringAsFixed(1)}');
} else {
result.write('${_positions.length} clients');
description.add('${_positions.length} clients');
}
result.write(')');
return result.toString();
}
}
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
/// A description of a [Scrollable]'s contents, useful for modelling the state
/// of its viewport.
///
/// This class defines a current position, [pixels], and a range of values
/// considered "in bounds" for that position. The range has a minimum value at
/// [minScrollExtent] and a maximum value at [maxScrollExtent] (inclusive). The
/// viewport scrolls in the direction and axis described by [axisDirection]
/// and [axis].
///
/// The [outOfRange] getter will return true if [pixels] is outside this defined
/// range. The [atEdge] getter will return true if the [pixels] position equals
/// either the [minScrollExtent] or the [maxScrollExtent].
///
/// The dimensions of the viewport in the given [axis] are described by
/// [viewportDimension].
///
/// The above values are also exposed in terms of [extentBefore],
/// [extentInside], and [extentAfter], which may be more useful for use cases
/// such as scroll bars; for example, see [Scrollbar].
abstract class ScrollMetrics {
/// Creates a [ScrollMetrics] that has the same properties as this object.
///
/// This is useful if this object is mutable, but you want to get a snapshot
/// of the current state.
ScrollMetrics cloneMetrics() => new FixedScrollMetrics.clone(this);
double get minScrollExtent;
double get maxScrollExtent;
double get pixels;
double get viewportDimension;
AxisDirection get axisDirection;
Axis get axis => axisDirectionToAxis(axisDirection);
bool get outOfRange => pixels < minScrollExtent || pixels > maxScrollExtent;
bool get atEdge => pixels == minScrollExtent || pixels == maxScrollExtent;
/// The quantity of content conceptually "above" the currently visible content
/// of the viewport in the scrollable. This is the content above the content
/// described by [extentInside].
double get extentBefore => math.max(pixels - minScrollExtent, 0.0);
/// The quantity of visible content.
///
/// If [extentBefore] and [extentAfter] are non-zero, then this is typically
/// the height of the viewport. It could be less if there is less content
/// visible than the size of the viewport.
double get extentInside {
return math.min(pixels, maxScrollExtent) -
math.max(pixels, minScrollExtent) +
math.min(viewportDimension, maxScrollExtent - minScrollExtent);
}
/// The quantity of content conceptually "below" the currently visible content
/// of the viewport in the scrollable. This is the content below the content
/// described by [extentInside].
double get extentAfter => math.max(maxScrollExtent - pixels, 0.0);
@override
String toString() {
return '$runtimeType(${extentBefore.toStringAsFixed(1)}..[${extentInside.toStringAsFixed(1)}]..${extentAfter.toStringAsFixed(1)}})';
}
}
@immutable
class FixedScrollMetrics extends ScrollMetrics {
FixedScrollMetrics({
@required this.minScrollExtent,
@required this.maxScrollExtent,
@required this.pixels,
@required this.viewportDimension,
@required this.axisDirection,
});
FixedScrollMetrics.clone(ScrollMetrics parent) :
minScrollExtent = parent.minScrollExtent,
maxScrollExtent = parent.maxScrollExtent,
pixels = parent.pixels,
viewportDimension = parent.viewportDimension,
axisDirection = parent.axisDirection;
@override
final double minScrollExtent;
@override
final double maxScrollExtent;
@override
final double pixels;
@override
final double viewportDimension;
@override
final AxisDirection axisDirection;
}
\ No newline at end of file
......@@ -6,62 +6,9 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'framework.dart';
import 'notification_listener.dart';
import 'scrollable.dart' show Scrollable, ScrollableState;
/// A description of a [Scrollable]'s contents, useful for modelling the state
/// of the viewport, for example by a [Scrollbar].
///
/// The units used by the [extentBefore], [extentInside], and [extentAfter] are
/// not defined, but must be consistent. For example, they could be in pixels,
/// or in percentages, or in units of the [extentInside] (in the latter case,
/// [extentInside] would always be 1.0).
@immutable
class ScrollMetrics {
/// Create a description of the metrics of a [Scrollable]'s contents.
///
/// The three arguments must be present, non-null, finite, and non-negative.
const ScrollMetrics({
@required this.extentBefore,
@required this.extentInside,
@required this.extentAfter,
@required this.viewportDimension,
});
/// Creates a [ScrollMetrics] that has the same properties as the given
/// [ScrollMetrics].
ScrollMetrics.clone(ScrollMetrics other)
: extentBefore = other.extentBefore,
extentInside = other.extentInside,
extentAfter = other.extentAfter,
viewportDimension = other.viewportDimension;
/// The quantity of content conceptually "above" the currently visible content
/// of the viewport in the scrollable. This is the content above the content
/// described by [extentInside].
final double extentBefore;
/// The quantity of visible content.
///
/// If [extentBefore] and [extentAfter] are non-zero, then this is typically
/// the height of the viewport. It could be less if there is less content
/// visible than the size of the viewport.
final double extentInside;
/// The quantity of content conceptually "below" the currently visible content
/// of the viewport in the scrollable. This is the content below the content
/// described by [extentInside].
final double extentAfter;
final double viewportDimension;
@override
String toString() {
return '$runtimeType(${extentBefore.toStringAsFixed(1)}..[${extentInside.toStringAsFixed(1)}]..${extentAfter.toStringAsFixed(1)}})';
}
}
import 'scroll_metrics.dart';
/// Mixin for [Notification]s that track how many [RenderAbstractViewport] they
/// have bubbled through.
......@@ -95,19 +42,13 @@ abstract class ViewportNotificationMixin extends Notification {
abstract class ScrollNotification extends LayoutChangedNotification with ViewportNotificationMixin {
/// Creates a notification about scrolling.
ScrollNotification({
@required ScrollableState scrollable,
}) : axisDirection = scrollable.widget.axisDirection,
metrics = scrollable.position.getMetrics(),
context = scrollable.context;
/// The direction that positive scroll offsets indicate.
final AxisDirection axisDirection;
Axis get axis => axisDirectionToAxis(axisDirection);
@required this.metrics,
@required this.context,
});
final ScrollMetrics metrics;
/// The build context of the [Scrollable] that fired this notification.
/// The build context of the widget that fired this notification.
///
/// This can be used to find the scrollable's render objects to determine the
/// size of the viewport, for instance.
......@@ -116,16 +57,16 @@ abstract class ScrollNotification extends LayoutChangedNotification with Viewpor
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$axisDirection');
description.add('metrics: $metrics');
description.add('$metrics');
}
}
class ScrollStartNotification extends ScrollNotification {
ScrollStartNotification({
@required ScrollableState scrollable,
@required ScrollMetrics metrics,
@required BuildContext context,
this.dragDetails,
}) : super(scrollable: scrollable);
}) : super(metrics: metrics, context: context);
final DragStartDetails dragDetails;
......@@ -139,10 +80,11 @@ class ScrollStartNotification extends ScrollNotification {
class ScrollUpdateNotification extends ScrollNotification {
ScrollUpdateNotification({
@required ScrollableState scrollable,
@required ScrollMetrics metrics,
@required BuildContext context,
this.dragDetails,
this.scrollDelta,
}) : super(scrollable: scrollable);
}) : super(metrics: metrics, context: context);
final DragUpdateDetails dragDetails;
......@@ -160,11 +102,12 @@ class ScrollUpdateNotification extends ScrollNotification {
class OverscrollNotification extends ScrollNotification {
OverscrollNotification({
@required ScrollableState scrollable,
@required ScrollMetrics metrics,
@required BuildContext context,
this.dragDetails,
@required this.overscroll,
this.velocity: 0.0,
}) : super(scrollable: scrollable) {
}) : super(metrics: metrics, context: context) {
assert(overscroll != null);
assert(overscroll.isFinite);
assert(overscroll != 0.0);
......@@ -199,9 +142,10 @@ class OverscrollNotification extends ScrollNotification {
class ScrollEndNotification extends ScrollNotification {
ScrollEndNotification({
@required ScrollableState scrollable,
@required ScrollMetrics metrics,
@required BuildContext context,
this.dragDetails,
}) : super(scrollable: scrollable);
}) : super(metrics: metrics, context: context);
final DragEndDetails dragDetails;
......@@ -215,9 +159,10 @@ class ScrollEndNotification extends ScrollNotification {
class UserScrollNotification extends ScrollNotification {
UserScrollNotification({
@required ScrollableState scrollable,
@required ScrollMetrics metrics,
@required BuildContext context,
this.direction,
}) : super(scrollable: scrollable);
}) : super(metrics: metrics, context: context);
final ScrollDirection direction;
......
......@@ -10,7 +10,6 @@ import 'framework.dart';
import 'primary_scroll_controller.dart';
import 'scroll_controller.dart';
import 'scroll_physics.dart';
import 'scroll_position.dart';
import 'scrollable.dart';
import 'sliver.dart';
import 'viewport.dart';
......
......@@ -14,8 +14,9 @@ import 'framework.dart';
import 'gesture_detector.dart';
import 'notification_listener.dart';
import 'scroll_configuration.dart';
import 'scroll_context.dart';
import 'scroll_controller.dart';
import 'scroll_notification.dart';
import 'scroll_physics.dart';
import 'scroll_position.dart';
import 'ticker_provider.dart';
import 'viewport.dart';
......@@ -68,7 +69,8 @@ class Scrollable extends StatefulWidget {
return widget?.scrollable;
}
/// Scrolls the closest enclosing scrollable to make the given context visible.
/// Scrolls the scrollables that enclose the given context so as to make the
/// given context visible.
static Future<Null> ensureVisible(BuildContext context, {
double alignment: 0.0,
Duration duration: Duration.ZERO,
......@@ -91,7 +93,7 @@ class Scrollable extends StatefulWidget {
if (futures.isEmpty || duration == Duration.ZERO)
return new Future<Null>.value();
if (futures.length == 1)
return futures.first;
return futures.single;
return Future.wait<Null>(futures);
}
}
......@@ -128,16 +130,18 @@ class _ScrollableScope extends InheritedWidget {
/// This class is not intended to be subclassed. To specialize the behavior of a
/// [Scrollable], provide it with a [ScrollPhysics].
class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
implements AbstractScrollState {
/// The controller for this [Scrollable] widget's viewport position.
implements ScrollContext {
/// The manager for this [Scrollable] widget's viewport position.
///
/// To control what kind of [ScrollPosition] is created for a [Scrollable],
/// provide it with custom [ScrollPhysics] that creates the appropriate
/// [ScrollPosition] controller in its [ScrollPhysics.createScrollPosition]
/// method.
/// provide it with custom [ScrollController] that creates the appropriate
/// [ScrollPosition] in its [ScrollController.createScrollPosition] method.
ScrollPosition get position => _position;
ScrollPosition _position;
@override
AxisDirection get axisDirection => widget.axisDirection;
ScrollBehavior _configuration;
ScrollPhysics _physics;
......@@ -224,6 +228,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel
..minFlingDistance = _physics?.minFlingDistance
..minFlingVelocity = _physics?.minFlingVelocity
..maxFlingVelocity = _physics?.maxFlingVelocity;
......@@ -238,6 +243,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel
..minFlingDistance = _physics?.minFlingDistance
..minFlingVelocity = _physics?.minFlingVelocity
..maxFlingVelocity = _physics?.maxFlingVelocity;
......@@ -268,54 +274,41 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
}
@override
@protected
void dispatchNotification(Notification notification) {
assert(mounted);
notification.dispatch(_gestureDetectorKey.currentContext);
}
BuildContext get notificationContext => _gestureDetectorKey.currentContext;
// TOUCH HANDLERS
DragScrollActivity _drag;
bool get _reverseDirection {
assert(widget.axisDirection != null);
switch (widget.axisDirection) {
case AxisDirection.up:
case AxisDirection.left:
return true;
case AxisDirection.down:
case AxisDirection.right:
return false;
}
return null;
}
Drag _drag;
void _handleDragDown(DragDownDetails details) {
assert(_drag == null);
position.touched();
position.didTouch();
}
void _handleDragStart(DragStartDetails details) {
assert(_drag == null);
_drag = position.beginDragActivity(details);
_drag = position.drag(details, _disposeDrag);
assert(_drag != null);
}
void _handleDragUpdate(DragUpdateDetails details) {
// _drag might be null if the drag activity ended and called didEndDrag.
_drag?.update(details, reverse: _reverseDirection);
// _drag might be null if the drag activity ended and called _disposeDrag.
_drag?.update(details);
}
void _handleDragEnd(DragEndDetails details) {
// _drag might be null if the drag activity ended and called didEndDrag.
_drag?.end(details, reverse: _reverseDirection);
// _drag might be null if the drag activity ended and called _disposeDrag.
_drag?.end(details);
assert(_drag == null);
}
@override
@protected
void didEndDrag() {
void _handleDragCancel() {
// _drag might be null if the drag activity ended and called _disposeDrag.
_drag?.cancel();
assert(_drag == null);
}
void _disposeDrag() {
_drag = null;
}
......
......@@ -48,11 +48,15 @@ export 'src/widgets/preferred_size.dart';
export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/routes.dart';
export 'src/widgets/scroll_activity.dart';
export 'src/widgets/scroll_configuration.dart';
export 'src/widgets/scroll_context.dart';
export 'src/widgets/scroll_controller.dart';
export 'src/widgets/scroll_metrics.dart';
export 'src/widgets/scroll_notification.dart';
export 'src/widgets/scroll_physics.dart';
export 'src/widgets/scroll_position.dart';
export 'src/widgets/scroll_position_with_single_context.dart';
export 'src/widgets/scroll_simulation.dart';
export 'src/widgets/scroll_view.dart';
export 'src/widgets/scrollable.dart';
......
......@@ -25,7 +25,7 @@ void main() {
testWidgets('Inherited ScrollConfiguration changed', (WidgetTester tester) async {
final GlobalKey key = new GlobalKey(debugLabel: 'scrollable');
TestScrollBehavior behavior;
ScrollPosition position;
ScrollPositionWithSingleContext position;
final Widget scrollView = new SingleChildScrollView(
key: key,
......@@ -48,7 +48,7 @@ void main() {
expect(behavior, isNotNull);
expect(behavior.flag, isTrue);
expect(position.physics, const isInstanceOf<ClampingScrollPhysics>());
ScrollMetrics metrics = position.getMetrics();
ScrollMetrics metrics = position.cloneMetrics();
expect(metrics.extentAfter, equals(400.0));
expect(metrics.viewportDimension, equals(600.0));
......@@ -64,7 +64,7 @@ void main() {
expect(behavior.flag, isFalse);
expect(position.physics, const isInstanceOf<BouncingScrollPhysics>());
// Regression test for https://github.com/flutter/flutter/issues/5856
metrics = position.getMetrics();
metrics = position.cloneMetrics();
expect(metrics.extentAfter, equals(400.0));
expect(metrics.viewportDimension, equals(600.0));
});
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class TestScrollPosition extends ScrollPosition {
TestScrollPosition({
ScrollPhysics physics,
AbstractScrollState state,
ScrollPosition oldPosition,
}) : _pixels = 100.0, super(
physics: physics,
state: state,
oldPosition: oldPosition,
) {
assert(physics is TestScrollPhysics);
}
@override
TestScrollPhysics get physics => super.physics;
double _pixels;
@override
double get pixels => _pixels;
@override
double setPixels(double value) {
final double oldPixels = _pixels;
_pixels = value;
state.dispatchNotification(activity.createScrollUpdateNotification(state, _pixels - oldPixels));
return 0.0;
}
@override
void correctBy(double correction) {
_pixels += correction;
}
@override
ScrollMetrics getMetrics() {
final double insideExtent = viewportDimension;
final double beforeExtent = _pixels - minScrollExtent;
final double afterExtent = maxScrollExtent - _pixels;
if (insideExtent > 0.0) {
return new ScrollMetrics(
extentBefore: physics.extentMultiplier * beforeExtent / insideExtent,
extentInside: physics.extentMultiplier,
extentAfter: physics.extentMultiplier * afterExtent / insideExtent,
viewportDimension: viewportDimension,
);
} else {
return new ScrollMetrics(
extentBefore: 0.0,
extentInside: 0.0,
extentAfter: 0.0,
viewportDimension: viewportDimension,
);
}
}
@override
Future<Null> ensureVisible(RenderObject object, {
double alignment: 0.0,
Duration duration: Duration.ZERO,
Curve curve: Curves.ease,
}) {
return new Future<Null>.value();
}
}
class TestScrollPhysics extends ScrollPhysics {
const TestScrollPhysics({ this.extentMultiplier, ScrollPhysics parent }) : super(parent);
final double extentMultiplier;
@override
ScrollPhysics applyTo(ScrollPhysics parent) {
return new TestScrollPhysics(
extentMultiplier: extentMultiplier,
parent: parent,
);
}
}
class TestScrollController extends ScrollController {
@override
ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) {
return new TestScrollPosition(physics: physics, state: state, oldPosition: oldPosition);
}
}
class TestScrollBehavior extends ScrollBehavior {
const TestScrollBehavior(this.extentMultiplier);
final double extentMultiplier;
@override
ScrollPhysics getScrollPhysics(BuildContext context) {
return new TestScrollPhysics(
extentMultiplier: extentMultiplier
).applyTo(super.getScrollPhysics(context));
}
@override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) => child;
@override
bool shouldNotify(TestScrollBehavior oldDelegate) {
return extentMultiplier != oldDelegate.extentMultiplier;
}
}
void main() {
testWidgets('Changing the scroll behavior dynamically', (WidgetTester tester) async {
await tester.pumpWidget(new ScrollConfiguration(
behavior: const TestScrollBehavior(1.0),
child: new CustomScrollView(
controller: new TestScrollController(),
slivers: <Widget>[
const SliverToBoxAdapter(child: const SizedBox(height: 2000.0)),
],
),
));
final ScrollableState state = tester.state(find.byType(Scrollable));
expect(state.position.getMetrics().extentInside, 1.0);
await tester.pumpWidget(new ScrollConfiguration(
behavior: const TestScrollBehavior(2.0),
child: new CustomScrollView(
controller: new TestScrollController(),
slivers: <Widget>[
const SliverToBoxAdapter(child: const SizedBox(height: 2000.0)),
],
),
));
expect(state.position.getMetrics().extentInside, 2.0);
});
}
......@@ -5,15 +5,15 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
class TestScrollPosition extends ScrollPosition {
class TestScrollPosition extends ScrollPositionWithSingleContext {
TestScrollPosition({
ScrollPhysics physics,
AbstractScrollState state,
ScrollContext state,
double initialPixels: 0.0,
ScrollPosition oldPosition,
}) : super(
physics: physics,
state: state,
context: state,
initialPixels: initialPixels,
oldPosition: oldPosition,
);
......@@ -21,10 +21,10 @@ class TestScrollPosition extends ScrollPosition {
class TestScrollController extends ScrollController {
@override
ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) {
ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
return new TestScrollPosition(
physics: physics,
state: state,
state: context,
initialPixels: initialScrollOffset,
oldPosition: oldPosition,
);
......
......@@ -162,7 +162,7 @@ void main() {
],
),
);
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
final ScrollPositionWithSingleContext position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
verifyPaintPosition(key1, const Offset(0.0, 0.0), true);
verifyPaintPosition(key2, const Offset(0.0, 600.0), false);
......@@ -175,7 +175,7 @@ void main() {
verifyPaintPosition(key3, const Offset(0.0, 0.0), true);
position.animateTo(bigHeight + delegate.maxExtent * 1.9, curve: Curves.linear, duration: const Duration(minutes: 1));
position.updateUserScrollDirection(ScrollDirection.forward); // ignore: INVALID_USE_OF_PROTECTED_MEMBER, since this is using a protected method for testing purposes
position.updateUserScrollDirection(ScrollDirection.forward);
await tester.pumpAndSettle(const Duration(milliseconds: 1000));
verifyPaintPosition(key1, const Offset(0.0, 0.0), false);
verifyPaintPosition(key2, const Offset(0.0, 0.0), true);
......
......@@ -53,12 +53,12 @@ class TestScrollPhysics extends ClampingScrollPhysics {
Tolerance get tolerance => const Tolerance(velocity: 20.0, distance: 1.0);
}
class TestViewportScrollPosition extends ScrollPosition {
class TestViewportScrollPosition extends ScrollPositionWithSingleContext {
TestViewportScrollPosition({
ScrollPhysics physics,
AbstractScrollState state,
ScrollContext context,
ScrollPosition oldPosition,
}) : super(physics: physics, state: state, oldPosition: oldPosition);
}) : super(physics: physics, context: context, oldPosition: oldPosition);
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment