Commit 63160b3d authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Scrolling Refactor (#7420)

parent e52bda2c
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'arc.dart'; import 'arc.dart';
...@@ -212,6 +213,18 @@ class _ScrollLikeMountainViewDelegate extends ScrollConfigurationDelegate { ...@@ -212,6 +213,18 @@ class _ScrollLikeMountainViewDelegate extends ScrollConfigurationDelegate {
bool updateShouldNotify(ScrollConfigurationDelegate old) => false; bool updateShouldNotify(ScrollConfigurationDelegate old) => false;
} }
class _MaterialScrollBehavior extends ViewportScrollBehavior {
@override
TargetPlatform getPlatform(BuildContext context) {
return Theme.of(context).platform;
}
@override
Color getGlowColor(BuildContext context) {
return Theme.of(context).accentColor;
}
}
class _MaterialAppState extends State<MaterialApp> { class _MaterialAppState extends State<MaterialApp> {
HeroController _heroController; HeroController _heroController;
...@@ -288,9 +301,14 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -288,9 +301,14 @@ class _MaterialAppState extends State<MaterialApp> {
return true; return true;
}); });
return new ScrollConfiguration( result = new ScrollConfiguration(
delegate: _getScrollDelegate(theme.platform), delegate: _getScrollDelegate(theme.platform),
child: result child: result
); );
return new ScrollConfiguration2(
delegate: new _MaterialScrollBehavior(),
child: result
);
} }
} }
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// DELETE THIS FILE WHEN REMOVING LEGACY SCROLLING CODE
import 'dart:async' show Timer; import 'dart:async' show Timer;
import 'dart:math' as math; import 'dart:math' as math;
......
...@@ -547,5 +547,5 @@ class ThemeData { ...@@ -547,5 +547,5 @@ class ThemeData {
} }
@override @override
String toString() => '$runtimeType($brightness $primaryColor etc...)'; String toString() => '$runtimeType(${ platform != defaultTargetPlatform ? "$platform " : ''}$brightness $primaryColor etc...)';
} }
...@@ -2703,6 +2703,16 @@ class WidgetToRenderBoxAdapter extends LeafRenderObjectWidget { ...@@ -2703,6 +2703,16 @@ class WidgetToRenderBoxAdapter extends LeafRenderObjectWidget {
} }
} }
class SliverToBoxAdapter extends SingleChildRenderObjectWidget {
SliverToBoxAdapter({
Key key,
Widget child,
}) : super(key: key, child: child);
@override
RenderSliverToBoxAdapter createRenderObject(BuildContext context) => new RenderSliverToBoxAdapter();
}
// EVENT HANDLING // EVENT HANDLING
......
...@@ -12,9 +12,9 @@ import 'package:flutter/rendering.dart'; ...@@ -12,9 +12,9 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
export 'dart:ui' show hashValues, hashList; export 'dart:ui' show hashValues, hashList;
export 'package:flutter/foundation.dart' show FlutterError; export 'package:flutter/foundation.dart' show FlutterError, debugPrint, debugPrintStack;
export 'package:flutter/foundation.dart' show VoidCallback, ValueChanged, ValueGetter, ValueSetter; export 'package:flutter/foundation.dart' show VoidCallback, ValueChanged, ValueGetter, ValueSetter;
export 'package:flutter/rendering.dart' show RenderObject, RenderBox, debugPrint; export 'package:flutter/rendering.dart' show RenderObject, RenderBox, debugDumpRenderTree;
// KEYS // KEYS
...@@ -3864,6 +3864,13 @@ class MultiChildRenderObjectElement extends RenderObjectElement { ...@@ -3864,6 +3864,13 @@ class MultiChildRenderObjectElement extends RenderObjectElement {
@override @override
MultiChildRenderObjectWidget get widget => super.widget; MultiChildRenderObjectWidget get widget => super.widget;
/// The current list of children of this element.
///
/// This list is filtered to hide elements that have been forgotten (using
/// [forgetChild]).
@protected
Iterable<Element> get children => _children.where((Element child) => !_forgottenChildren.contains(child));
List<Element> _children; List<Element> _children;
// We keep a set of forgotten children to avoid O(n^2) work walking _children // We keep a set of forgotten children to avoid O(n^2) work walking _children
// repeatedly to remove children. // repeatedly to remove children.
......
...@@ -122,7 +122,7 @@ class NotificationListener<T extends Notification> extends StatelessWidget { ...@@ -122,7 +122,7 @@ class NotificationListener<T extends Notification> extends StatelessWidget {
/// Useful if, for instance, you're trying to align multiple descendants. /// Useful if, for instance, you're trying to align multiple descendants.
/// ///
/// In the widgets library, only the [SizeChangedLayoutNotifier] class and /// In the widgets library, only the [SizeChangedLayoutNotifier] class and
/// [Scrollable] classes dispatch this notification (specifically, they dispatch /// [Scrollable2] classes dispatch this notification (specifically, they dispatch
/// [SizeChangedLayoutNotification]s and [ScrollNotification]s respectively). /// [SizeChangedLayoutNotification]s and [ScrollNotification]s respectively).
/// Transitions, in particular, do not. Changing one's layout in one's build /// Transitions, in particular, do not. Changing one's layout in one's build
/// function does not cause this notification to be dispatched automatically. If /// function does not cause this notification to be dispatched automatically. If
......
This diff is collapsed.
This diff is collapsed.
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// DELETE THIS FILE WHEN REMOVING LEGACY SCROLLING CODE
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// DELETE THIS FILE WHEN REMOVING LEGACY SCROLLING CODE
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'framework.dart'; import 'framework.dart';
......
// 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 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'framework.dart';
import 'basic.dart';
import 'notification_listener.dart';
import 'scrollable.dart' show Scrollable2, Scrollable2State;
/// A description of a [Scrollable2]'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).
class ScrollableMetrics {
/// Create a description of the metrics of a [Scrollable2]'s contents.
///
/// The three arguments must be present, non-null, finite, and non-negative.
const ScrollableMetrics({
@required this.extentBefore,
@required this.extentInside,
@required this.extentAfter,
});
/// 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].
///
/// The units are in general arbitrary, and decided by the [ScrollPosition]
/// that generated the [ScrollableMetrics]. They will be the same units as for
/// [extentInside] and [extentAfter].
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.
///
/// The units are in general arbitrary, and decided by the [ScrollPosition]
/// that generated the [ScrollableMetrics]. They will be the same units as for
/// [extentBefore] and [extentAfter].
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].
///
/// The units are in general arbitrary, and decided by the [ScrollPosition]
/// that generated the [ScrollableMetrics]. They will be the same units as for
/// [extentBefore] and [extentInside].
final double extentAfter;
@override
String toString() {
return '$runtimeType(${extentBefore.toStringAsFixed(1)}..[${extentInside.toStringAsFixed(1)}]..${extentAfter.toStringAsFixed(1)}})';
}
}
abstract class ScrollNotification2 extends LayoutChangedNotification {
/// Creates a notification about scrolling.
ScrollNotification2({
@required Scrollable2State scrollable,
}) : axisDirection = scrollable.config.axisDirection,
metrics = scrollable.position.getMetrics(),
context = scrollable.context;
/// The direction that positive scroll offsets indicate.
final AxisDirection axisDirection;
Axis get axis => axisDirectionToAxis(axisDirection);
final ScrollableMetrics metrics;
/// The build context of the [Scrollable2] that fired this notification.
///
/// This can be used to find the scrollable's render objects to determine the
/// size of the viewport, for instance.
// TODO(ianh): Maybe just fold those into the ScrollableMetrics?
final BuildContext context;
/// The number of [Scrollable2] widgets that this notification has bubbled
/// through. Typically listeners only respond to notifications with a [depth]
/// of zero.
int get depth => _depth;
int _depth = 0;
@override
bool visitAncestor(Element element) {
if (element.widget is Scrollable2)
_depth += 1;
return super.visitAncestor(element);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$axisDirection');
description.add('metrics: $metrics');
description.add('depth: $depth');
}
}
class ScrollStartNotification extends ScrollNotification2 {
ScrollStartNotification({
@required Scrollable2State scrollable,
this.dragDetails,
}) : super(scrollable: scrollable);
final DragStartDetails dragDetails;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (dragDetails != null)
description.add('$dragDetails');
}
}
class ScrollUpdateNotification extends ScrollNotification2 {
ScrollUpdateNotification({
@required Scrollable2State scrollable,
this.dragDetails,
this.scrollDelta,
}) : super(scrollable: scrollable);
final DragUpdateDetails dragDetails;
/// The distance by which the [Scrollable2] was scrolled, in logical pixels.
final double scrollDelta;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('scrollDelta: $scrollDelta');
if (dragDetails != null)
description.add('$dragDetails');
}
}
class OverscrollNotification extends ScrollNotification2 {
OverscrollNotification({
@required Scrollable2State scrollable,
this.dragDetails,
@required this.overscroll,
this.velocity: 0.0,
}) : super(scrollable: scrollable) {
assert(overscroll != null);
assert(overscroll.isFinite);
assert(overscroll != 0.0);
assert(velocity != null);
}
final DragUpdateDetails dragDetails;
/// The number of logical pixels that the [Scrollable2] avoided scrolling.
///
/// This will be negative for overscroll on the "start" side and positive for
/// overscroll on the "end" side.
final double overscroll;
/// The velocity at which the [ScrollPosition] was changing when this
/// overscroll happened.
///
/// This will typically be 0.0 for touch-driven overscrolls, and positive
/// for overscrolls that happened from a [BallisticScrollActivity] or
/// [DrivenScrollActivity].
final double velocity;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('overscroll: ${overscroll.toStringAsFixed(1)}');
description.add('velocity: ${velocity.toStringAsFixed(1)}');
if (dragDetails != null)
description.add('$dragDetails');
}
}
class ScrollEndNotification extends ScrollNotification2 {
ScrollEndNotification({
@required Scrollable2State scrollable,
this.dragDetails,
}) : super(scrollable: scrollable);
final DragEndDetails dragDetails;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (dragDetails != null)
description.add('$dragDetails');
}
}
class UserScrollNotification extends ScrollNotification2 {
UserScrollNotification({
@required Scrollable2State scrollable,
this.direction,
}) : super(scrollable: scrollable);
final ScrollDirection direction;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('direction: $direction');
}
}
...@@ -7,17 +7,120 @@ import 'dart:math' as math; ...@@ -7,17 +7,120 @@ import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/physics.dart'; import 'package:flutter/physics.dart';
final SpringDescription _kScrollSpring = new SpringDescription.withDampingRatio(mass: 0.5, springConstant: 100.0, ratio: 1.1); /// An implementation of scroll physics that matches iOS.
final double _kDrag = 0.025; ///
/// See also:
///
/// * [ClampingScrollSimulation], which implements Android scroll physics.
class BouncingScrollSimulation extends SimulationGroup {
/// Creates a simulation group for scrolling on iOS, with the given
/// parameters.
///
/// The position and velocity arguments must use the same units as will be
/// expected from the [x] and [dx] methods respectively (typically logical
/// pixels and logical pixels per second respectively).
///
/// The leading and trailing extents must use the unit of length, the same
/// unit as used for the position argument and as expected from the [x]
/// method (typically logical pixels).
///
/// The units used with the provided [SpringDescription] must similarly be
/// consistent with the other arguments. A default set of constants is used
/// for the `spring` description if it is omitted; these defaults assume
/// that the unit of length is the logical pixel.
BouncingScrollSimulation({
@required double position,
@required double velocity,
@required double leadingExtent,
@required double trailingExtent,
SpringDescription spring,
}) : _leadingExtent = leadingExtent,
_trailingExtent = trailingExtent,
_spring = spring ?? _defaultScrollSpring {
assert(position != null);
assert(velocity != null);
assert(_leadingExtent != null);
assert(_trailingExtent != null);
assert(_leadingExtent <= _trailingExtent);
assert(_spring != null);
_chooseSimulation(position, velocity, 0.0);
}
// This class is based on Scroller.java from final double _leadingExtent;
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget final double _trailingExtent;
// The "See" comments refer to Scroller methods and values. Some simplifications final SpringDescription _spring;
// have been made.
class _MountainViewSimulation extends Simulation { static final SpringDescription _defaultScrollSpring = new SpringDescription.withDampingRatio(
_MountainViewSimulation({ mass: 0.5,
this.position, springConstant: 100.0,
this.velocity, ratio: 1.1,
);
bool _isSpringing = false;
Simulation _currentSimulation;
double _offset = 0.0;
// This simulation can only step forward.
@override
bool step(double time) => _chooseSimulation(
_currentSimulation.x(time - _offset),
_currentSimulation.dx(time - _offset),
time,
);
@override
Simulation get currentSimulation => _currentSimulation;
@override
double get currentIntervalOffset => _offset;
bool _chooseSimulation(double position, double velocity, double intervalOffset) {
if (!_isSpringing) {
if (position > _trailingExtent) {
_isSpringing = true;
_offset = intervalOffset;
_currentSimulation = new ScrollSpringSimulation(_spring, position, _trailingExtent, velocity);
return true;
} else if (position < _leadingExtent) {
_isSpringing = true;
_offset = intervalOffset;
_currentSimulation = new ScrollSpringSimulation(_spring, position, _leadingExtent, velocity);
return true;
} else if (_currentSimulation == null) {
_currentSimulation = new FrictionSimulation(0.135, position, velocity * 0.91);
return true;
}
}
return false;
}
@override
String toString() {
return '$runtimeType(leadingExtent: $_leadingExtent, trailingExtent: $_trailingExtent)';
}
}
/// An implementation of scroll physics that matches Android.
///
/// See also:
///
/// * [BouncingScrollSimulation], which implements iOS scroll physics.
//
// This class is based on Scroller.java from Android:
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget
//
// The "See..." comments below refer to Scroller methods and values. Some
// simplifications have been made.
class ClampingScrollSimulation extends Simulation {
/// Creates a scroll physics simulation that matches Android scrolling.
//
// TODO(ianh): The incoming `velocity` is used to determine the starting speed
// and duration, but does not represent the exact velocity of the simulation
// at t=0 as it should. This causes crazy scrolling irregularities when the
// scroll dimensions change during a fling.
ClampingScrollSimulation({
@required this.position,
@required this.velocity,
this.friction: 0.015, this.friction: 0.015,
}) { }) {
_scaledFriction = friction * _decelerationForFriction(0.84); // See mPhysicalCoeff _scaledFriction = friction * _decelerationForFriction(0.84); // See mPhysicalCoeff
...@@ -33,7 +136,7 @@ class _MountainViewSimulation extends Simulation { ...@@ -33,7 +136,7 @@ class _MountainViewSimulation extends Simulation {
double _duration; double _duration;
double _distance; double _distance;
// See DECELERATION_RATE // See DECELERATION_RATE.
static final double _decelerationRate = math.log(0.78) / math.log(0.9); static final double _decelerationRate = math.log(0.78) / math.log(0.9);
// See computeDeceleration(). // See computeDeceleration().
...@@ -41,24 +144,26 @@ class _MountainViewSimulation extends Simulation { ...@@ -41,24 +144,26 @@ class _MountainViewSimulation extends Simulation {
return friction * 61774.04968; return friction * 61774.04968;
} }
// See getSplineDeceleration() // See getSplineDeceleration().
double _flingDeceleration(double velocity) { double _flingDeceleration(double velocity) {
return math.log(0.35 * velocity.abs() / _scaledFriction); return math.log(0.35 * velocity.abs() / _scaledFriction);
} }
// See getSplineFlingDuration(). Returns a value in seconds. // See getSplineFlingDuration(). Returns a value in seconds.
double _flingDuration(double velocity) { double _flingDuration(double velocity) {
return math.exp(_flingDeceleration(velocity) / (_decelerationRate - 1.0)); return math.exp(_flingDeceleration(velocity) / (_decelerationRate - 1.0));
} }
// See getSplineFlingDistance() // See getSplineFlingDistance().
double _flingDistance(double velocity) { double _flingDistance(double velocity) {
final double rate = _decelerationRate / (_decelerationRate - 1.0) * _flingDeceleration(velocity); final double rate = _decelerationRate / (_decelerationRate - 1.0) * _flingDeceleration(velocity);
return _scaledFriction * math.exp(rate); return _scaledFriction * math.exp(rate);
} }
// Based on a cubic curve fit to the computeScrollOffset() values produced // Based on a cubic curve fit to the Scroller.computeScrollOffset() values
// for an initial velocity of 4000. The value of scroller.getDuration() // produced for an initial velocity of 4000. The value of Scroller.getDuration()
// and scroller.getFinalY() were 686ms and 961 pixels respectively. // and Scroller.getFinalY() were 686ms and 961 pixels respectively.
//
// Algebra courtesy of Wolfram Alpha. // Algebra courtesy of Wolfram Alpha.
// //
// f(x) = scrollOffset, x is time in millseconds // f(x) = scrollOffset, x is time in millseconds
...@@ -74,7 +179,7 @@ class _MountainViewSimulation extends Simulation { ...@@ -74,7 +179,7 @@ class _MountainViewSimulation extends Simulation {
return (1.2 * t * t * t) - (3.27 * t * t) + (3.065 * t); return (1.2 * t * t * t) - (3.27 * t * t) + (3.065 * t);
} }
// The deriviate of the _flingPenetration() function. // The derivative of the _flingDistancePenetration() function.
double _flingVelocityPenetration(double t) { double _flingVelocityPenetration(double t) {
return (3.63693 * t * t) - (6.5424 * t) + 3.06542; return (3.63693 * t * t) - (6.5424 * t) + 3.06542;
} }
...@@ -88,7 +193,7 @@ class _MountainViewSimulation extends Simulation { ...@@ -88,7 +193,7 @@ class _MountainViewSimulation extends Simulation {
@override @override
double dx(double time) { double dx(double time) {
final double t = (time / _duration).clamp(0.0, 1.0); final double t = (time / _duration).clamp(0.0, 1.0);
return velocity * _flingVelocityPenetration(t); return _distance * _flingVelocityPenetration(t) * velocity.sign;
} }
@override @override
...@@ -97,12 +202,29 @@ class _MountainViewSimulation extends Simulation { ...@@ -97,12 +202,29 @@ class _MountainViewSimulation extends Simulation {
} }
} }
// DELETE EVERYTHING BELOW THIS LINE WHEN REMOVING LEGACY SCROLLING CODE
final SpringDescription _kScrollSpring = new SpringDescription.withDampingRatio(mass: 0.5, springConstant: 100.0, ratio: 1.1);
final double _kDrag = 0.025;
class _CupertinoSimulation extends FrictionSimulation { class _CupertinoSimulation extends FrictionSimulation {
static const double drag = 0.135; static const double drag = 0.135;
_CupertinoSimulation({ double position, double velocity }) _CupertinoSimulation({ double position, double velocity })
: super(drag, position, velocity * 0.91); : super(drag, position, velocity * 0.91);
} }
class _MountainViewSimulation extends ClampingScrollSimulation {
_MountainViewSimulation({
double position,
double velocity,
double friction: 0.015,
}) : super(
position: position,
velocity: velocity,
friction: friction,
);
}
/// Composite simulation for scrollable interfaces. /// Composite simulation for scrollable interfaces.
/// ///
/// Simulates kinetic scrolling behavior between a leading and trailing /// Simulates kinetic scrolling behavior between a leading and trailing
...@@ -123,10 +245,10 @@ class ScrollSimulation extends SimulationGroup { ...@@ -123,10 +245,10 @@ class ScrollSimulation extends SimulationGroup {
/// ///
/// The final argument is the coefficient of friction, which is unitless. /// The final argument is the coefficient of friction, which is unitless.
ScrollSimulation({ ScrollSimulation({
double position, @required double position,
double velocity, @required double velocity,
double leadingExtent, @required double leadingExtent,
double trailingExtent, @required double trailingExtent,
SpringDescription spring, SpringDescription spring,
double drag, double drag,
TargetPlatform platform, TargetPlatform platform,
...@@ -135,6 +257,8 @@ class ScrollSimulation extends SimulationGroup { ...@@ -135,6 +257,8 @@ class ScrollSimulation extends SimulationGroup {
_spring = spring ?? _kScrollSpring, _spring = spring ?? _kScrollSpring,
_drag = drag ?? _kDrag, _drag = drag ?? _kDrag,
_platform = platform { _platform = platform {
assert(position != null);
assert(velocity != null);
assert(_leadingExtent != null); assert(_leadingExtent != null);
assert(_trailingExtent != null); assert(_trailingExtent != null);
assert(_spring != null); assert(_spring != null);
......
// 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/foundation.dart';
import 'package:flutter/rendering.dart';
import 'framework.dart';
export 'package:flutter/rendering.dart' show
AxisDirection,
GrowthDirection;
class Viewport2 extends MultiChildRenderObjectWidget {
Viewport2({
Key key,
this.axisDirection: AxisDirection.down,
this.anchor: 0.0,
this.offset,
this.center,
List<Widget> children: const <Widget>[],
}) : super(key: key, children: children) {
assert(center == null || children.where((Widget child) => child.key == center).length == 1);
}
final AxisDirection axisDirection;
final double anchor;
final ViewportOffset offset;
final Key center;
@override
RenderViewport2 createRenderObject(BuildContext context) {
return new RenderViewport2(
axisDirection: axisDirection,
anchor: anchor,
offset: offset,
);
}
@override
void updateRenderObject(BuildContext context, RenderViewport2 renderObject) {
renderObject.axisDirection = axisDirection;
renderObject.anchor = anchor;
renderObject.offset = offset;
}
@override
Viewport2Element createElement() => new Viewport2Element(this);
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$axisDirection');
description.add('anchor: $anchor');
description.add('offset: $offset');
if (center != null) {
description.add('center: $center');
} else if (children.isNotEmpty && children.first.key != null) {
description.add('center: ${children.first.key} (implicit)');
}
}
}
class Viewport2Element extends MultiChildRenderObjectElement {
/// Creates an element that uses the given widget as its configuration.
Viewport2Element(Viewport2 widget) : super(widget);
@override
Viewport2 get widget => super.widget;
@override
RenderViewport2 get renderObject => super.renderObject;
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
updateCenter();
}
@override
void update(MultiChildRenderObjectWidget newWidget) {
super.update(newWidget);
updateCenter();
}
@protected
void updateCenter() {
// TODO(ianh): cache the keys to make this faster
if (widget.center != null) {
renderObject.center = children.singleWhere(
(Element element) => element.widget.key == widget.center
).renderObject;
} else if (children.isNotEmpty) {
renderObject.center = children.first.renderObject;
} else {
renderObject.center = null;
}
}
}
...@@ -36,6 +36,7 @@ export 'src/widgets/navigator.dart'; ...@@ -36,6 +36,7 @@ export 'src/widgets/navigator.dart';
export 'src/widgets/notification_listener.dart'; export 'src/widgets/notification_listener.dart';
export 'src/widgets/orientation_builder.dart'; export 'src/widgets/orientation_builder.dart';
export 'src/widgets/overlay.dart'; export 'src/widgets/overlay.dart';
export 'src/widgets/overscroll_indicator.dart';
export 'src/widgets/page_storage.dart'; export 'src/widgets/page_storage.dart';
export 'src/widgets/pageable_list.dart'; export 'src/widgets/pageable_list.dart';
export 'src/widgets/pages.dart'; export 'src/widgets/pages.dart';
...@@ -43,8 +44,10 @@ export 'src/widgets/performance_overlay.dart'; ...@@ -43,8 +44,10 @@ export 'src/widgets/performance_overlay.dart';
export 'src/widgets/placeholder.dart'; export 'src/widgets/placeholder.dart';
export 'src/widgets/raw_keyboard_listener.dart'; export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/routes.dart'; export 'src/widgets/routes.dart';
export 'src/widgets/scroll_absolute.dart';
export 'src/widgets/scroll_behavior.dart'; export 'src/widgets/scroll_behavior.dart';
export 'src/widgets/scroll_configuration.dart'; export 'src/widgets/scroll_configuration.dart';
export 'src/widgets/scroll_notification.dart';
export 'src/widgets/scroll_simulation.dart'; export 'src/widgets/scroll_simulation.dart';
export 'src/widgets/scrollable.dart'; export 'src/widgets/scrollable.dart';
export 'src/widgets/scrollable_grid.dart'; export 'src/widgets/scrollable_grid.dart';
...@@ -59,6 +62,7 @@ export 'src/widgets/ticker_provider.dart'; ...@@ -59,6 +62,7 @@ export 'src/widgets/ticker_provider.dart';
export 'src/widgets/title.dart'; export 'src/widgets/title.dart';
export 'src/widgets/transitions.dart'; export 'src/widgets/transitions.dart';
export 'src/widgets/unique_widget.dart'; export 'src/widgets/unique_widget.dart';
export 'src/widgets/viewport.dart';
export 'src/widgets/virtual_viewport.dart'; export 'src/widgets/virtual_viewport.dart';
export 'package:vector_math/vector_math_64.dart' show Matrix4; export 'package:vector_math/vector_math_64.dart' show Matrix4;
...@@ -265,14 +265,14 @@ class _TestRecordingCanvas implements Canvas { ...@@ -265,14 +265,14 @@ class _TestRecordingCanvas implements Canvas {
@override @override
void save() { void save() {
_saveCount += 1; _saveCount += 1;
super.save(); // ends up in noSuchMethod _invocations.add(new _MethodCall(#save));
} }
@override @override
void restore() { void restore() {
_saveCount -= 1; _saveCount -= 1;
assert(_saveCount >= 0); assert(_saveCount >= 0);
super.restore(); // ends up in noSuchMethod _invocations.add(new _MethodCall(#restore));
} }
@override @override
...@@ -281,6 +281,25 @@ class _TestRecordingCanvas implements Canvas { ...@@ -281,6 +281,25 @@ class _TestRecordingCanvas implements Canvas {
} }
} }
class _MethodCall implements Invocation {
_MethodCall(this._name);
final Symbol _name;
@override
bool get isAccessor => false;
@override
bool get isGetter => false;
@override
bool get isMethod => true;
@override
bool get isSetter => false;
@override
Symbol get memberName => _name;
@override
Map<Symbol, dynamic> get namedArguments => <Symbol, dynamic>{};
@override
List<dynamic> get positionalArguments => <dynamic>[];
}
class _TestRecordingPaintingContext implements PaintingContext { class _TestRecordingPaintingContext implements PaintingContext {
_TestRecordingPaintingContext(this.canvas); _TestRecordingPaintingContext(this.canvas);
......
...@@ -8,6 +8,13 @@ import 'package:test/test.dart'; ...@@ -8,6 +8,13 @@ import 'package:test/test.dart';
import 'rendering_tester.dart'; import 'rendering_tester.dart';
void main() { void main() {
test('RenderViewport2 basic test - no children', () {
RenderViewport2 root = new RenderViewport2();
layout(root);
root.offset = new ViewportOffset.fixed(900.0);
pumpFrame();
});
test('RenderViewport2 basic test - down', () { test('RenderViewport2 basic test - down', () {
RenderBox a, b, c, d, e; RenderBox a, b, c, d, e;
RenderViewport2 root = new RenderViewport2( RenderViewport2 root = new RenderViewport2(
...@@ -199,8 +206,6 @@ void main() { ...@@ -199,8 +206,6 @@ void main() {
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(-300.0, 0.0)); expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(-300.0, 0.0));
}); });
// TODO(ianh): test positioning when the children are too big to fit in the main axis
// TODO(ianh): test shrinkWrap
// TODO(ianh): test anchor // TODO(ianh): test anchor
// TODO(ianh): test offset // TODO(ianh): test offset
// TODO(ianh): test center // TODO(ianh): test center
......
...@@ -98,6 +98,7 @@ void main() { ...@@ -98,6 +98,7 @@ void main() {
expect(didReceiveCallback, isTrue); expect(didReceiveCallback, isTrue);
}); });
testWidgets('Defunct setState throws exception', (WidgetTester tester) async { testWidgets('Defunct setState throws exception', (WidgetTester tester) async {
StateSetter setState; StateSetter setState;
...@@ -143,4 +144,23 @@ void main() { ...@@ -143,4 +144,23 @@ void main() {
expect(log[0], matches('Deactivated')); expect(log[0], matches('Deactivated'));
expect(log[1], matches('Discarding .+ from inactive elements list.')); expect(log[1], matches('Discarding .+ from inactive elements list.'));
}); });
testWidgets('MultiChildRenderObjectElement.children', (WidgetTester tester) async {
GlobalKey key0, key1, key2;
await tester.pumpWidget(new Column(
key: key0 = new GlobalKey(),
children: <Widget>[
new Container(),
new Container(key: key1 = new GlobalKey()),
new Container(child: new Container()),
new Container(key: key2 = new GlobalKey()),
new Container(),
],
));
MultiChildRenderObjectElement element = key0.currentContext;
expect(
element.children.map((Element element) => element.widget.key), // ignore: INVALID_USE_OF_PROTECTED_MEMBER
<Key>[null, key1, null, key2, null],
);
});
} }
// 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 'dart:math' as math;
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import '../rendering/mock_canvas.dart';
final Matcher doesNotOverscroll = isNot(paints..circle());
Future<Null> slowDrag(WidgetTester tester, Point start, Offset offset) async {
TestGesture gesture = await tester.startGesture(start);
for (int index = 0; index < 10; index += 1) {
await gesture.moveBy(offset);
await tester.pump(const Duration(milliseconds: 20));
}
await gesture.up();
}
void main() {
testWidgets('Overscroll indicator color', (WidgetTester tester) async {
await tester.pumpWidget(
new Scrollable2(
children: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
],
),
);
RenderObject painter = tester.renderObject(find.byType(CustomPaint));
expect(painter, doesNotOverscroll);
// the scroll gesture from tester.scroll happens in zero time, so nothing should appear:
await tester.scroll(find.byType(Scrollable2), const Offset(0.0, 100.0));
expect(painter, doesNotOverscroll);
await tester.pump(); // allow the ticker to register itself
expect(painter, doesNotOverscroll);
await tester.pump(const Duration(milliseconds: 100)); // animate
expect(painter, doesNotOverscroll);
TestGesture gesture = await tester.startGesture(const Point(200.0, 200.0));
await tester.pump(const Duration(milliseconds: 100)); // animate
expect(painter, doesNotOverscroll);
await gesture.up();
expect(painter, doesNotOverscroll);
await slowDrag(tester, const Point(200.0, 200.0), const Offset(0.0, 5.0));
expect(painter, paints..circle(color: const Color(0x0DFFFFFF)));
await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1));
expect(painter, doesNotOverscroll);
});
testWidgets('Overscroll indicator changes side when you drag on the other side', (WidgetTester tester) async {
await tester.pumpWidget(
new Scrollable2(
children: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
],
),
);
RenderObject painter = tester.renderObject(find.byType(CustomPaint));
await slowDrag(tester, const Point(400.0, 200.0), const Offset(0.0, 10.0));
expect(painter, paints..circle(x: 400.0));
await slowDrag(tester, const Point(100.0, 200.0), const Offset(0.0, 10.0));
expect(painter, paints..something((Symbol method, List<dynamic> arguments) {
if (method != #drawCircle)
return false;
final Point center = arguments[0];
if (center.x < 400.0)
return true;
throw 'Dragging on left hand side did not overscroll on left hand side.';
}));
await slowDrag(tester, const Point(700.0, 200.0), const Offset(0.0, 10.0));
expect(painter, paints..something((Symbol method, List<dynamic> arguments) {
if (method != #drawCircle)
return false;
final Point center = arguments[0];
if (center.x > 400.0)
return true;
throw 'Dragging on right hand side did not overscroll on right hand side.';
}));
await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1));
expect(painter, doesNotOverscroll);
});
testWidgets('Overscroll indicator changes side when you shift sides', (WidgetTester tester) async {
await tester.pumpWidget(
new Scrollable2(
children: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
],
),
);
RenderObject painter = tester.renderObject(find.byType(CustomPaint));
TestGesture gesture = await tester.startGesture(const Point(300.0, 200.0));
await gesture.moveBy(const Offset(0.0, 10.0));
await tester.pump(const Duration(milliseconds: 20));
double oldX = 0.0;
for (int index = 0; index < 10; index += 1) {
await gesture.moveBy(const Offset(50.0, 50.0));
await tester.pump(const Duration(milliseconds: 20));
expect(painter, paints..something((Symbol method, List<dynamic> arguments) {
if (method != #drawCircle)
return false;
final Point center = arguments[0];
if (center.x <= oldX)
throw 'Sliding to the right did not make the center of the radius slide to the right.';
oldX = center.x;
return true;
}));
}
await gesture.up();
await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1));
expect(painter, doesNotOverscroll);
});
group('Flipping direction of scrollable doesn\'t change overscroll behavior', () {
testWidgets('down', (WidgetTester tester) async {
await tester.pumpWidget(
new Scrollable2(
axisDirection: AxisDirection.down,
children: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 20.0)),
],
),
);
RenderObject painter = tester.renderObject(find.byType(CustomPaint));
await slowDrag(tester, const Point(200.0, 200.0), const Offset(0.0, 5.0));
expect(painter, paints..save()..circle()..restore()..save()..scale(y: -1.0)..restore()..restore());
await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1));
expect(painter, doesNotOverscroll);
});
testWidgets('up', (WidgetTester tester) async {
await tester.pumpWidget(
new Scrollable2(
axisDirection: AxisDirection.up,
children: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 20.0)),
],
),
);
RenderObject painter = tester.renderObject(find.byType(CustomPaint));
await slowDrag(tester, const Point(200.0, 200.0), const Offset(0.0, 5.0));
expect(painter, paints..save()..scale(y: -1.0)..restore()..save()..circle()..restore()..restore());
await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1));
expect(painter, doesNotOverscroll);
});
});
testWidgets('Overscroll in both directions', (WidgetTester tester) async {
await tester.pumpWidget(
new Scrollable2(
axisDirection: AxisDirection.down,
children: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 20.0)),
],
),
);
RenderObject painter = tester.renderObject(find.byType(CustomPaint));
await slowDrag(tester, const Point(200.0, 200.0), const Offset(0.0, 5.0));
expect(painter, paints..circle());
expect(painter, isNot(paints..circle()..circle()));
await slowDrag(tester, const Point(200.0, 200.0), const Offset(0.0, -5.0));
expect(painter, paints..circle()..circle());
await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1));
expect(painter, doesNotOverscroll);
});
testWidgets('Overscroll horizontally', (WidgetTester tester) async {
await tester.pumpWidget(
new Scrollable2(
axisDirection: AxisDirection.right,
children: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 20.0)),
],
),
);
RenderObject painter = tester.renderObject(find.byType(CustomPaint));
await slowDrag(tester, const Point(200.0, 200.0), const Offset(5.0, 0.0));
expect(painter, paints..rotate(angle: -math.PI / 2.0)..circle()..scale(y: -1.0));
expect(painter, isNot(paints..circle()..circle()));
await slowDrag(tester, const Point(200.0, 200.0), const Offset(-5.0, 0.0));
expect(painter, paints..rotate(angle: -math.PI / 2.0)..circle()
..rotate(angle: -math.PI / 2.0)..scale(y: -1.0)..circle());
await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1));
expect(painter, doesNotOverscroll);
});
testWidgets('Changing settings', (WidgetTester tester) async {
RenderObject painter;
await tester.pumpWidget(
new Scrollable2(
axisDirection: AxisDirection.left,
scrollBehavior: new TestScrollBehavior1(),
children: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 20.0)),
],
),
);
painter = tester.renderObject(find.byType(CustomPaint));
await slowDrag(tester, const Point(200.0, 200.0), const Offset(5.0, 0.0));
expect(painter, paints..scale(y: -1.0)..rotate(angle: -math.PI / 2.0)..circle(color: const Color(0x0A00FF00)));
expect(painter, isNot(paints..circle()..circle()));
await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1));
await tester.pumpWidget(
new Scrollable2(
axisDirection: AxisDirection.right,
scrollBehavior: new TestScrollBehavior2(),
children: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 20.0)),
],
),
);
painter = tester.renderObject(find.byType(CustomPaint));
await slowDrag(tester, const Point(200.0, 200.0), const Offset(5.0, 0.0));
expect(painter, paints..rotate(angle: -math.PI / 2.0)..circle(color: const Color(0x0A0000FF))..scale(y: -1.0));
expect(painter, isNot(paints..circle()..circle()));
});
}
class TestScrollBehavior1 extends ViewportScrollBehavior {
@override
Color getGlowColor(BuildContext context) {
return const Color(0xFF00FF00);
}
}
class TestScrollBehavior2 extends ViewportScrollBehavior {
@override
Color getGlowColor(BuildContext context) {
return const Color(0xFF0000FF);
}
}
// 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(
this.extentMultiplier,
Scrollable2State state,
Tolerance scrollTolerances,
ScrollPosition oldPosition,
) : _pixels = 100.0, super(state, scrollTolerances, oldPosition);
final double extentMultiplier;
double _min, _viewport, _max, _pixels;
@override
double get pixels => _pixels;
@override
double setPixels(double value) {
double oldPixels = _pixels;
_pixels = value;
dispatchNotification(activity.createScrollUpdateNotification(state, _pixels - oldPixels));
return 0.0;
}
@override
void correctBy(double correction) {
_pixels += correction;
}
@override
void applyViewportDimension(double viewportDimension) {
_viewport = viewportDimension;
super.applyViewportDimension(viewportDimension);
}
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
_min = minScrollExtent;
_max = maxScrollExtent;
return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
}
@override
ScrollableMetrics getMetrics() {
double insideExtent = _viewport;
double beforeExtent = _pixels - _min;
double afterExtent = _max - _pixels;
if (insideExtent > 0.0) {
return new ScrollableMetrics(
extentBefore: extentMultiplier * beforeExtent / insideExtent,
extentInside: extentMultiplier,
extentAfter: extentMultiplier * afterExtent / insideExtent,
);
} else {
return new ScrollableMetrics(
extentBefore: 0.0,
extentInside: 0.0,
extentAfter: 0.0,
);
}
}
}
class TestScrollBehavior extends ScrollBehavior2 {
TestScrollBehavior(this.extentMultiplier);
final double extentMultiplier;
@override
Widget wrap(BuildContext context, Widget child, AxisDirection axisDirection) => child;
@override
ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition) {
return new TestScrollPosition(extentMultiplier, state, ViewportScrollBehavior.defaultScrollTolerances, oldPosition);
}
@override
bool shouldNotify(TestScrollBehavior oldDelegate) {
return extentMultiplier != oldDelegate.extentMultiplier;
}
}
void main() {
testWidgets('Changing the scroll behavior dynamically', (WidgetTester tester) async {
GlobalKey<Scrollable2State> key = new GlobalKey<Scrollable2State>();
await tester.pumpWidget(new Scrollable2(
key: key,
scrollBehavior: new TestScrollBehavior(1.0),
children: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
],
));
expect(key.currentState.position.getMetrics().extentInside, 1.0);
await tester.pumpWidget(new Scrollable2(
key: key,
scrollBehavior: new TestScrollBehavior(2.0),
children: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
],
));
expect(key.currentState.position.getMetrics().extentInside, 2.0);
});
}
\ No newline at end of file
// 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';
Future<Null> pumpTest(WidgetTester tester, TargetPlatform platform) async {
await tester.pumpWidget(new MaterialApp(
theme: new ThemeData(
platform: platform,
),
home: new Scrollable2(
children: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
],
),
));
await tester.pump(const Duration(seconds: 5)); // to let the theme animate
return null;
}
const double dragOffset = 200.0;
double getScrollOffset(WidgetTester tester) {
RenderViewport2 viewport = tester.renderObject(find.byType(Viewport2));
return viewport.offset.pixels;
}
void resetScrollOffset(WidgetTester tester) {
RenderViewport2 viewport = tester.renderObject(find.byType(Viewport2));
AbsoluteScrollPosition position = viewport.offset;
position.jumpTo(0.0);
}
void main() {
testWidgets('Flings on different platforms', (WidgetTester tester) async {
await pumpTest(tester, TargetPlatform.android);
await tester.fling(find.byType(Viewport2), const Offset(0.0, -dragOffset), 1000.0);
expect(getScrollOffset(tester), dragOffset);
await tester.pump(); // trigger fling
expect(getScrollOffset(tester), dragOffset);
await tester.pump(const Duration(seconds: 5));
final double result1 = getScrollOffset(tester);
resetScrollOffset(tester);
await pumpTest(tester, TargetPlatform.iOS);
await tester.fling(find.byType(Viewport2), const Offset(0.0, -dragOffset), 1000.0);
expect(getScrollOffset(tester), dragOffset);
await tester.pump(); // trigger fling
expect(getScrollOffset(tester), dragOffset);
await tester.pump(const Duration(seconds: 5));
final double result2 = getScrollOffset(tester);
expect(result1, lessThan(result2)); // iOS (result2) is slipperier than Android (result1)
});
testWidgets('Flings on different platforms', (WidgetTester tester) async {
await pumpTest(tester, TargetPlatform.iOS);
await tester.fling(find.byType(Viewport2), const Offset(0.0, -dragOffset), 1000.0);
expect(getScrollOffset(tester), dragOffset);
await tester.pump(); // trigger fling
expect(getScrollOffset(tester), dragOffset);
await tester.pump(const Duration(seconds: 5));
final double result1 = getScrollOffset(tester);
resetScrollOffset(tester);
await pumpTest(tester, TargetPlatform.android);
await tester.fling(find.byType(Viewport2), const Offset(0.0, -dragOffset), 1000.0);
expect(getScrollOffset(tester), dragOffset);
await tester.pump(); // trigger fling
expect(getScrollOffset(tester), dragOffset);
await tester.pump(const Duration(seconds: 5));
final double result2 = getScrollOffset(tester);
expect(result1, greaterThan(result2)); // iOS (result1) is slipperier than Android (result2)
});
}
\ No newline at end of file
// 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_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
void verifyPaintPosition(GlobalKey key, Offset ideal) {
RenderObject target = key.currentContext.findRenderObject();
expect(target.parent, new isInstanceOf<RenderViewport2>());
SliverPhysicalParentData parentData = target.parentData;
Offset actual = parentData.paintOffset;
expect(actual, ideal);
}
void main() {
testWidgets('Sliver protocol', (WidgetTester tester) async {
final GlobalKey<Scrollable2State> scrollableKey = new GlobalKey<Scrollable2State>();
GlobalKey key1, key2, key3, key4, key5;
await tester.pumpWidget(
new Scrollable2(
key: scrollableKey,
axisDirection: AxisDirection.down,
children: <Widget>[
new BigSliver(key: key1 = new GlobalKey()),
new OverlappingSliver(key: key2 = new GlobalKey()),
new OverlappingSliver(key: key3 = new GlobalKey()),
new BigSliver(key: key4 = new GlobalKey()),
new BigSliver(key: key5 = new GlobalKey()),
],
),
);
AbsoluteScrollPosition position = scrollableKey.currentState.position;
final double max = RenderBigSliver.height * 3.0 + (RenderOverlappingSliver.totalHeight) * 2.0 - 600.0; // 600 is the height of the test viewport
assert(max < 10000.0);
expect(max, 1450.0);
expect(position.pixels, 0.0);
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, max);
position.animate(to: 10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 10));
expect(position.pixels, max);
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, max);
verifyPaintPosition(key1, new Offset(0.0, 0.0));
verifyPaintPosition(key2, new Offset(0.0, 0.0));
verifyPaintPosition(key3, new Offset(0.0, 0.0));
verifyPaintPosition(key4, new Offset(0.0, 0.0));
verifyPaintPosition(key5, new Offset(0.0, 50.0));
});
}
class RenderBigSliver extends RenderSliver {
static const double height = 550.0;
double get paintExtent => (height - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent);
@override
void performLayout() {
geometry = new SliverGeometry(
scrollExtent: height,
paintExtent: paintExtent,
maxPaintExtent: height,
);
}
}
class BigSliver extends LeafRenderObjectWidget {
BigSliver({ Key key }) : super(key: key);
@override
RenderBigSliver createRenderObject(BuildContext context) {
return new RenderBigSliver();
}
}
class RenderOverlappingSliver extends RenderSliver {
static const double totalHeight = 200.0;
static const double fixedHeight = 100.0;
double get paintExtent {
return math.min(
math.max(
fixedHeight,
totalHeight - constraints.scrollOffset,
),
constraints.remainingPaintExtent,
);
}
double get layoutExtent {
return (totalHeight - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent);
}
@override
void performLayout() {
geometry = new SliverGeometry(
scrollExtent: totalHeight,
paintExtent: paintExtent,
layoutExtent: layoutExtent,
maxPaintExtent: totalHeight,
);
}
}
class OverlappingSliver extends LeafRenderObjectWidget {
OverlappingSliver({ Key key }) : super(key: key);
@override
RenderOverlappingSliver createRenderObject(BuildContext context) {
return new RenderOverlappingSliver();
}
}
// 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 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
Future<Null> test(WidgetTester tester, double offset, { double anchor: 0.0 }) {
return tester.pumpWidget(new Viewport2(
anchor: anchor / 600.0,
offset: new ViewportOffset.fixed(offset),
children: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 400.0)),
new SliverToBoxAdapter(child: new SizedBox(height: 400.0)),
new SliverToBoxAdapter(child: new SizedBox(height: 400.0)),
new SliverToBoxAdapter(child: new SizedBox(height: 400.0)),
new SliverToBoxAdapter(child: new SizedBox(height: 400.0)),
],
));
}
void verify(WidgetTester tester, List<Point> idealPositions, List<bool> idealVisibles) {
List<Point> actualPositions = tester.renderObjectList/*<RenderBox>*/(find.byType(SizedBox)).map/*<Point>*/(
(RenderBox target) => target.localToGlobal(const Point(0.0, 0.0))
).toList();
List<bool> actualVisibles = tester.renderObjectList/*<RenderSliverToBoxAdapter>*/(find.byType(SliverToBoxAdapter)).map/*<bool>*/(
(RenderSliverToBoxAdapter target) => target.geometry.visible
).toList();
expect(actualPositions, equals(idealPositions));
expect(actualVisibles, equals(idealVisibles));
}
void main() {
testWidgets('Viewport2 basic test', (WidgetTester tester) async {
await test(tester, 0.0);
expect(tester.renderObject/*<RenderBox>*/(find.byType(Viewport2)).size, equals(const Size(800.0, 600.0)));
verify(tester, <Point>[
const Point(0.0, 0.0),
const Point(0.0, 400.0),
const Point(0.0, 600.0),
const Point(0.0, 600.0),
const Point(0.0, 600.0),
], <bool>[true, true, false, false, false]);
await test(tester, 200.0);
verify(tester, <Point>[
const Point(0.0, -200.0),
const Point(0.0, 200.0),
const Point(0.0, 600.0),
const Point(0.0, 600.0),
const Point(0.0, 600.0),
], <bool>[true, true, false, false, false]);
await test(tester, 600.0);
verify(tester, <Point>[
const Point(0.0, -600.0),
const Point(0.0, -200.0),
const Point(0.0, 200.0),
const Point(0.0, 600.0),
const Point(0.0, 600.0),
], <bool>[false, true, true, false, false]);
await test(tester, 900.0);
verify(tester, <Point>[
const Point(0.0, -900.0),
const Point(0.0, -500.0),
const Point(0.0, -100.0),
const Point(0.0, 300.0),
const Point(0.0, 600.0),
], <bool>[false, false, true, true, false]);
});
testWidgets('Viewport2 anchor test', (WidgetTester tester) async {
await test(tester, 0.0, anchor: 100.0);
expect(tester.renderObject/*<RenderBox>*/(find.byType(Viewport2)).size, equals(const Size(800.0, 600.0)));
verify(tester, <Point>[
const Point(0.0, 100.0),
const Point(0.0, 500.0),
const Point(0.0, 600.0),
const Point(0.0, 600.0),
const Point(0.0, 600.0),
], <bool>[true, true, false, false, false]);
await test(tester, 200.0, anchor: 100.0);
verify(tester, <Point>[
const Point(0.0, -100.0),
const Point(0.0, 300.0),
const Point(0.0, 600.0),
const Point(0.0, 600.0),
const Point(0.0, 600.0),
], <bool>[true, true, false, false, false]);
await test(tester, 600.0, anchor: 100.0);
verify(tester, <Point>[
const Point(0.0, -500.0),
const Point(0.0, -100.0),
const Point(0.0, 300.0),
const Point(0.0, 600.0),
const Point(0.0, 600.0),
], <bool>[false, true, true, false, false]);
await test(tester, 900.0, anchor: 100.0);
verify(tester, <Point>[
const Point(0.0, -800.0),
const Point(0.0, -400.0),
const Point(0.0, 0.0),
const Point(0.0, 400.0),
const Point(0.0, 600.0),
], <bool>[false, false, true, true, false]);
});
}
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