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

Add PageView (#7809)

This widget is a start towards replacing PageableList. There are still a number
of features that we'll need to add before this widget can replace PageableList.
parent 3831e0b0
...@@ -124,9 +124,9 @@ class PageableListAppState extends State<PageableListApp> { ...@@ -124,9 +124,9 @@ class PageableListAppState extends State<PageableListApp> {
} }
Widget _buildBody(BuildContext context) { Widget _buildBody(BuildContext context) {
return new PageableList( return new PageView(
children: cardModels.map(buildCard), children: cardModels.map(buildCard).toList(),
itemsWrap: itemsWrap, // TODO(abarth): itemsWrap: itemsWrap,
scrollDirection: scrollDirection scrollDirection: scrollDirection
); );
} }
...@@ -150,7 +150,7 @@ void main() { ...@@ -150,7 +150,7 @@ void main() {
theme: new ThemeData( theme: new ThemeData(
brightness: Brightness.light, brightness: Brightness.light,
primarySwatch: Colors.blue, primarySwatch: Colors.blue,
accentColor: Colors.redAccent[200] accentColor: Colors.redAccent[200],
), ),
home: new PageableListApp() home: new PageableListApp()
)); ));
......
// 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/physics.dart';
import 'scroll_absolute.dart';
class PageScrollPhysics extends ScrollPhysicsProxy {
const PageScrollPhysics({
ScrollPhysics parent,
this.springDescription,
}) : super(parent);
final SpringDescription springDescription;
@override
PageScrollPhysics applyTo(ScrollPhysics parent) {
return new PageScrollPhysics(
parent: parent,
springDescription: springDescription,
);
}
double _roundToPage(AbsoluteScrollPosition position, double pixels, double pageSize) {
final int index = (pixels + pageSize / 2.0) ~/ pageSize;
return (pageSize * index).clamp(position.minScrollExtent, position.maxScrollExtent);
}
double _getTargetPixels(AbsoluteScrollPosition position, double velocity) {
final double pageSize = position.viewportDimension;
if (velocity < -position.scrollTolerances.velocity)
return _roundToPage(position, position.pixels - pageSize / 2.0, pageSize);
if (velocity > position.scrollTolerances.velocity)
return _roundToPage(position, position.pixels + pageSize / 2.0, pageSize);
return _roundToPage(position, position.pixels, pageSize);
}
@override
Simulation createBallisticSimulation(AbsoluteScrollPosition 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) ||
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent))
return super.createBallisticSimulation(position, velocity);
final double target = _getTargetPixels(position, velocity);
return new ScrollSpringSimulation(scrollSpring, position.pixels, target, velocity);
}
}
...@@ -94,9 +94,16 @@ class ViewportScrollBehavior extends ScrollBehavior2 { ...@@ -94,9 +94,16 @@ class ViewportScrollBehavior extends ScrollBehavior2 {
return null; return null;
} }
ScrollPhysics _getEffectiveScrollPhysics(BuildContext context, ScrollPhysics physics) {
final ScrollPhysics defaultPhysics = getScrollPhysics(getPlatform(context));
if (physics != null)
return physics.applyTo(defaultPhysics);
return defaultPhysics;
}
@override @override
ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition) { ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition, ScrollPhysics physics) {
return new AbsoluteScrollPosition(state, scrollTolerances, oldPosition, getScrollPhysics(getPlatform(context))); return new AbsoluteScrollPosition(state, scrollTolerances, oldPosition, _getEffectiveScrollPhysics(context, physics));
} }
@override @override
...@@ -108,6 +115,8 @@ class ViewportScrollBehavior extends ScrollBehavior2 { ...@@ -108,6 +115,8 @@ class ViewportScrollBehavior extends ScrollBehavior2 {
abstract class ScrollPhysics { abstract class ScrollPhysics {
const ScrollPhysics(); const ScrollPhysics();
ScrollPhysicsProxy applyTo(ScrollPhysics parent) => this;
/// Used by [AbsoluteDragScrollActivity] and other user-driven activities to /// Used by [AbsoluteDragScrollActivity] and other user-driven activities to
/// convert an offset in logical pixels as provided by the [DragUpdateDetails] /// convert an offset in logical pixels as provided by the [DragUpdateDetails]
/// into a delta to apply using [setPixels]. /// into a delta to apply using [setPixels].
...@@ -133,6 +142,58 @@ abstract class ScrollPhysics { ...@@ -133,6 +142,58 @@ abstract class ScrollPhysics {
/// [AbsoluteBallisticScrollActivity] with the returned value. Otherwise, the /// [AbsoluteBallisticScrollActivity] with the returned value. Otherwise, the
/// [ScrollPosition] will begin an idle activity instead. /// [ScrollPosition] will begin an idle activity instead.
Simulation createBallisticSimulation(AbsoluteScrollPosition position, double velocity) => null; Simulation createBallisticSimulation(AbsoluteScrollPosition position, double velocity) => null;
static final SpringDescription _kDefaultScrollSpring = new SpringDescription.withDampingRatio(
mass: 0.5,
springConstant: 100.0,
ratio: 1.1,
);
SpringDescription get scrollSpring => _kDefaultScrollSpring;
}
abstract class ScrollPhysicsProxy extends ScrollPhysics {
const ScrollPhysicsProxy(this.parent);
final ScrollPhysics parent;
@override
ScrollPhysicsProxy applyTo(ScrollPhysics parent) {
throw new FlutterError(
'$runtimeType must override applyTo.\n'
'The default implementation of applyTo is not appropriate for subclasses '
'of ScrollPhysicsProxy because they should return an instance of themselves '
'with their parent property replaced with the given ScrollPhysics instance.'
);
}
@override
double applyPhysicsToUserOffset(AbsoluteScrollPosition position, double offset) {
if (parent == null)
return super.applyPhysicsToUserOffset(position, offset);
return parent.applyPhysicsToUserOffset(position, offset);
}
@override
double applyBoundaryConditions(AbsoluteScrollPosition position, double value) {
if (parent == null)
return super.applyBoundaryConditions(position, value);
return parent.applyBoundaryConditions(position, value);
}
@override
Simulation createBallisticSimulation(AbsoluteScrollPosition position, double velocity) {
if (parent == null)
return super.createBallisticSimulation(position, velocity);
return parent.createBallisticSimulation(position, velocity);
}
@override
SpringDescription get scrollSpring {
if (parent == null)
return super.scrollSpring;
return parent.scrollSpring;
}
} }
class AbsoluteScrollPosition extends ScrollPosition { class AbsoluteScrollPosition extends ScrollPosition {
...@@ -405,6 +466,7 @@ class BouncingScrollPhysics extends ScrollPhysics { ...@@ -405,6 +466,7 @@ class BouncingScrollPhysics extends ScrollPhysics {
Simulation createBallisticSimulation(AbsoluteScrollPosition position, double velocity) { Simulation createBallisticSimulation(AbsoluteScrollPosition position, double velocity) {
if (velocity.abs() >= position.scrollTolerances.velocity || position.outOfRange) { if (velocity.abs() >= position.scrollTolerances.velocity || position.outOfRange) {
return new BouncingScrollSimulation( return new BouncingScrollSimulation(
spring: scrollSpring,
position: position.pixels, position: position.pixels,
velocity: velocity, velocity: velocity,
leadingExtent: position.minScrollExtent, leadingExtent: position.minScrollExtent,
...@@ -446,19 +508,13 @@ class ClampingScrollPhysics extends ScrollPhysics { ...@@ -446,19 +508,13 @@ class ClampingScrollPhysics extends ScrollPhysics {
return 0.0; return 0.0;
} }
static final SpringDescription _defaultScrollSpring = new SpringDescription.withDampingRatio(
mass: 0.5,
springConstant: 100.0,
ratio: 1.1,
);
@override @override
Simulation createBallisticSimulation(AbsoluteScrollPosition position, double velocity) { Simulation createBallisticSimulation(AbsoluteScrollPosition position, double velocity) {
if (position.outOfRange) { if (position.outOfRange) {
if (position.pixels > position.maxScrollExtent) if (position.pixels > position.maxScrollExtent)
return new ScrollSpringSimulation(_defaultScrollSpring, position.pixels, position.maxScrollExtent, math.min(0.0, velocity)); return new ScrollSpringSimulation(scrollSpring, position.pixels, position.maxScrollExtent, math.min(0.0, velocity));
if (position.pixels < position.minScrollExtent) if (position.pixels < position.minScrollExtent)
return new ScrollSpringSimulation(_defaultScrollSpring, position.pixels, position.minScrollExtent, math.max(0.0, velocity)); return new ScrollSpringSimulation(scrollSpring, position.pixels, position.minScrollExtent, math.max(0.0, velocity));
assert(false); assert(false);
} }
if (!position.atEdge && velocity.abs() >= position.scrollTolerances.velocity) { if (!position.atEdge && velocity.abs() >= position.scrollTolerances.velocity) {
......
...@@ -33,10 +33,10 @@ class BouncingScrollSimulation extends SimulationGroup { ...@@ -33,10 +33,10 @@ class BouncingScrollSimulation extends SimulationGroup {
@required double velocity, @required double velocity,
@required double leadingExtent, @required double leadingExtent,
@required double trailingExtent, @required double trailingExtent,
SpringDescription spring, @required SpringDescription spring,
}) : _leadingExtent = leadingExtent, }) : _leadingExtent = leadingExtent,
_trailingExtent = trailingExtent, _trailingExtent = trailingExtent,
_spring = spring ?? _defaultScrollSpring { _spring = spring {
assert(position != null); assert(position != null);
assert(velocity != null); assert(velocity != null);
assert(_leadingExtent != null); assert(_leadingExtent != null);
...@@ -50,12 +50,6 @@ class BouncingScrollSimulation extends SimulationGroup { ...@@ -50,12 +50,6 @@ class BouncingScrollSimulation extends SimulationGroup {
final double _trailingExtent; final double _trailingExtent;
final SpringDescription _spring; final SpringDescription _spring;
static final SpringDescription _defaultScrollSpring = new SpringDescription.withDampingRatio(
mass: 0.5,
springConstant: 100.0,
ratio: 1.1,
);
bool _isSpringing = false; bool _isSpringing = false;
Simulation _currentSimulation; Simulation _currentSimulation;
double _offset = 0.0; double _offset = 0.0;
......
...@@ -7,6 +7,8 @@ import 'package:meta/meta.dart'; ...@@ -7,6 +7,8 @@ import 'package:meta/meta.dart';
import 'framework.dart'; import 'framework.dart';
import 'basic.dart'; import 'basic.dart';
import 'page_scroll_physics.dart';
import 'scroll_absolute.dart';
import 'scrollable.dart'; import 'scrollable.dart';
import 'sliver.dart'; import 'sliver.dart';
import 'viewport.dart'; import 'viewport.dart';
...@@ -20,6 +22,7 @@ class ScrollView extends StatelessWidget { ...@@ -20,6 +22,7 @@ class ScrollView extends StatelessWidget {
this.padding, this.padding,
this.initialScrollOffset: 0.0, this.initialScrollOffset: 0.0,
this.itemExtent, this.itemExtent,
this.physics,
this.shrinkWrap: false, this.shrinkWrap: false,
this.children: const <Widget>[], this.children: const <Widget>[],
}) : super(key: key) { }) : super(key: key) {
...@@ -38,6 +41,8 @@ class ScrollView extends StatelessWidget { ...@@ -38,6 +41,8 @@ class ScrollView extends StatelessWidget {
final double itemExtent; final double itemExtent;
final ScrollPhysics physics;
final bool shrinkWrap; final bool shrinkWrap;
final List<Widget> children; final List<Widget> children;
...@@ -76,6 +81,7 @@ class ScrollView extends StatelessWidget { ...@@ -76,6 +81,7 @@ class ScrollView extends StatelessWidget {
return new Scrollable2( return new Scrollable2(
axisDirection: axisDirection, axisDirection: axisDirection,
initialScrollOffset: initialScrollOffset, initialScrollOffset: initialScrollOffset,
physics: physics,
viewportBuilder: (BuildContext context, ViewportOffset offset) { viewportBuilder: (BuildContext context, ViewportOffset offset) {
if (shrinkWrap) { if (shrinkWrap) {
return new ShrinkWrappingViewport( return new ShrinkWrappingViewport(
...@@ -166,3 +172,21 @@ class ScrollGrid extends ScrollView { ...@@ -166,3 +172,21 @@ class ScrollGrid extends ScrollView {
); );
} }
} }
class PageView extends ScrollView {
PageView({
Key key,
Axis scrollDirection: Axis.horizontal,
List<Widget> children: const <Widget>[],
}) : super(
key: key,
scrollDirection: scrollDirection,
physics: const PageScrollPhysics(),
children: children,
);
@override
Widget buildChildLayout(BuildContext context) {
return new SliverFill(delegate: childrenDelegate);
}
}
...@@ -20,13 +20,15 @@ import 'framework.dart'; ...@@ -20,13 +20,15 @@ import 'framework.dart';
import 'gesture_detector.dart'; import 'gesture_detector.dart';
import 'notification_listener.dart'; import 'notification_listener.dart';
import 'page_storage.dart'; import 'page_storage.dart';
import 'scroll_absolute.dart' show ViewportScrollBehavior;
import 'scroll_behavior.dart'; import 'scroll_behavior.dart';
import 'scroll_configuration.dart'; import 'scroll_configuration.dart';
import 'scroll_notification.dart'; import 'scroll_notification.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
import 'viewport.dart'; import 'viewport.dart';
// TODO(abarth): Merge AbsoluteScrollPosition and ScrollPosition.
import 'scroll_absolute.dart' show ViewportScrollBehavior, ScrollPhysics;
export 'package:flutter/physics.dart' show Tolerance; export 'package:flutter/physics.dart' show Tolerance;
// This file defines an unopinionated scrolling mechanism. // This file defines an unopinionated scrolling mechanism.
...@@ -360,7 +362,7 @@ abstract class ScrollBehavior2 { ...@@ -360,7 +362,7 @@ abstract class ScrollBehavior2 {
/// object must be disposed (via [ScrollPosition.oldPosition]) in the same /// object must be disposed (via [ScrollPosition.oldPosition]) in the same
/// call stack. Passing a non-null `oldPosition` is a destructive operation /// call stack. Passing a non-null `oldPosition` is a destructive operation
/// for that [ScrollPosition]. /// for that [ScrollPosition].
ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition); ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition, ScrollPhysics physics);
/// Whether this delegate is different than the old delegate, or would now /// Whether this delegate is different than the old delegate, or would now
/// return meaningfully different widgets from [wrap] or a meaningfully /// return meaningfully different widgets from [wrap] or a meaningfully
...@@ -405,6 +407,7 @@ class Scrollable2 extends StatefulWidget { ...@@ -405,6 +407,7 @@ class Scrollable2 extends StatefulWidget {
Key key, Key key,
this.initialScrollOffset: 0.0, this.initialScrollOffset: 0.0,
this.axisDirection: AxisDirection.down, this.axisDirection: AxisDirection.down,
this.physics,
this.scrollBehavior, this.scrollBehavior,
@required this.viewportBuilder, @required this.viewportBuilder,
}) : super (key: key) { }) : super (key: key) {
...@@ -417,6 +420,8 @@ class Scrollable2 extends StatefulWidget { ...@@ -417,6 +420,8 @@ class Scrollable2 extends StatefulWidget {
final AxisDirection axisDirection; final AxisDirection axisDirection;
final ScrollPhysics physics;
/// The delegate that creates the [ScrollPosition] and wraps the viewport /// The delegate that creates the [ScrollPosition] and wraps the viewport
/// in extra widgets (e.g. for overscroll effects). /// in extra widgets (e.g. for overscroll effects).
/// ///
...@@ -484,7 +489,7 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin ...@@ -484,7 +489,7 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
void _updatePosition() { void _updatePosition() {
_scrollBehavior = config.scrollBehavior ?? Scrollable2.getScrollBehavior(context); _scrollBehavior = config.scrollBehavior ?? Scrollable2.getScrollBehavior(context);
final ScrollPosition oldPosition = position; final ScrollPosition oldPosition = position;
_position = _scrollBehavior.createScrollPosition(context, this, oldPosition); _position = _scrollBehavior.createScrollPosition(context, this, oldPosition, config.physics);
assert(position != null); assert(position != null);
if (oldPosition != null) { if (oldPosition != null) {
// It's important that we not do this until after the viewport has had a // It's important that we not do this until after the viewport has had a
......
...@@ -54,8 +54,6 @@ abstract class SliverChildDelegate { ...@@ -54,8 +54,6 @@ abstract class SliverChildDelegate {
// /// demand). For example, the body of a dialog box might fit both of these // /// demand). For example, the body of a dialog box might fit both of these
// /// conditions. // /// conditions.
class SliverChildListDelegate extends SliverChildDelegate { class SliverChildListDelegate extends SliverChildDelegate {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const SliverChildListDelegate(this.children); const SliverChildListDelegate(this.children);
final List<Widget> children; final List<Widget> children;
......
...@@ -313,9 +313,7 @@ void main() { ...@@ -313,9 +313,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 100)); await tester.pump(const Duration(milliseconds: 100));
expect(scrollableState.position.pixels, greaterThan(0.0)); expect(scrollableState.position.pixels, greaterThan(0.0));
}, skip: Scrollable == Scrollable && }, skip: Scrollable != Scrollable2); // TODO(abarth): re-enable when ensureVisible is implemented
ScrollableViewport == ScrollableViewport &&
Block == Block); // TODO(abarth): re-enable when ensureVisible is implemented
testWidgets('Stepper index test', (WidgetTester tester) async { testWidgets('Stepper index test', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
......
// 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/widgets.dart';
import 'states.dart';
const Duration _frameDuration = const Duration(milliseconds: 100);
void main() {
testWidgets('PageView control test', (WidgetTester tester) async {
List<String> log = <String>[];
await tester.pumpWidget(new PageView(
children: kStates.map<Widget>((String state) {
return new GestureDetector(
onTap: () {
log.add(state);
},
child: new Container(
height: 200.0,
decoration: const BoxDecoration(
backgroundColor: const Color(0xFF0000FF),
),
child: new Text(state),
),
);
}).toList()
));
await tester.tap(find.text('Alabama'));
expect(log, equals(<String>['Alabama']));
log.clear();
expect(find.text('Alaska'), findsNothing);
await tester.scroll(find.byType(PageView), const Offset(-10.0, 0.0));
await tester.pump();
expect(find.text('Alabama'), findsOneWidget);
expect(find.text('Alaska'), findsOneWidget);
expect(find.text('Arizona'), findsNothing);
await tester.pumpUntilNoTransientCallbacks(_frameDuration);
expect(find.text('Alabama'), findsOneWidget);
expect(find.text('Alaska'), findsNothing);
await tester.scroll(find.byType(PageView), const Offset(-401.0, 0.0));
await tester.pumpUntilNoTransientCallbacks(_frameDuration);
expect(find.text('Alabama'), findsNothing);
expect(find.text('Alaska'), findsOneWidget);
expect(find.text('Arizona'), findsNothing);
await tester.tap(find.text('Alaska'));
expect(log, equals(<String>['Alaska']));
log.clear();
await tester.fling(find.byType(PageView), const Offset(-200.0, 0.0), 1000.0);
await tester.pumpUntilNoTransientCallbacks(_frameDuration);
expect(find.text('Alabama'), findsNothing);
expect(find.text('Alaska'), findsNothing);
expect(find.text('Arizona'), findsOneWidget);
await tester.fling(find.byType(PageView), const Offset(200.0, 0.0), 1000.0);
await tester.pumpUntilNoTransientCallbacks(_frameDuration);
expect(find.text('Alabama'), findsNothing);
expect(find.text('Alaska'), findsOneWidget);
expect(find.text('Arizona'), findsNothing);
});
}
...@@ -78,7 +78,7 @@ class TestScrollBehavior extends ScrollBehavior2 { ...@@ -78,7 +78,7 @@ class TestScrollBehavior extends ScrollBehavior2 {
Widget wrap(BuildContext context, Widget child, AxisDirection axisDirection) => child; Widget wrap(BuildContext context, Widget child, AxisDirection axisDirection) => child;
@override @override
ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition) { ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition, ScrollPhysics physics) {
return new TestScrollPosition(extentMultiplier, state, ViewportScrollBehavior.defaultScrollTolerances, oldPosition); return new TestScrollPosition(extentMultiplier, state, ViewportScrollBehavior.defaultScrollTolerances, oldPosition);
} }
......
...@@ -42,12 +42,12 @@ class TestBehavior extends ScrollBehavior2 { ...@@ -42,12 +42,12 @@ class TestBehavior extends ScrollBehavior2 {
} }
@override @override
ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition) { ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition, ScrollPhysics physics) {
return new TestViewportScrollPosition( return new TestViewportScrollPosition(
state, state,
new Tolerance(velocity: 20.0, distance: 1.0), new Tolerance(velocity: 20.0, distance: 1.0),
oldPosition, oldPosition,
const ClampingScrollPhysics(), physics,
); );
} }
...@@ -80,6 +80,7 @@ void main() { ...@@ -80,6 +80,7 @@ void main() {
axisDirection: AxisDirection.down, axisDirection: AxisDirection.down,
center: centerKey, center: centerKey,
anchor: 0.25, anchor: 0.25,
physics: const ClampingScrollPhysics(),
scrollBehavior: new TestBehavior(), scrollBehavior: new TestBehavior(),
slivers: <Widget>[ slivers: <Widget>[
new SliverToBoxAdapter(child: new Container(height: 5.0)), new SliverToBoxAdapter(child: new Container(height: 5.0)),
......
...@@ -63,6 +63,7 @@ class TestScrollable extends StatelessWidget { ...@@ -63,6 +63,7 @@ class TestScrollable extends StatelessWidget {
Key key, Key key,
this.initialScrollOffset: 0.0, this.initialScrollOffset: 0.0,
this.axisDirection: AxisDirection.down, this.axisDirection: AxisDirection.down,
this.physics,
this.anchor: 0.0, this.anchor: 0.0,
this.center, this.center,
this.scrollBehavior, this.scrollBehavior,
...@@ -75,6 +76,8 @@ class TestScrollable extends StatelessWidget { ...@@ -75,6 +76,8 @@ class TestScrollable extends StatelessWidget {
final AxisDirection axisDirection; final AxisDirection axisDirection;
final ScrollPhysics physics;
final double anchor; final double anchor;
final Key center; final Key center;
...@@ -90,6 +93,7 @@ class TestScrollable extends StatelessWidget { ...@@ -90,6 +93,7 @@ class TestScrollable extends StatelessWidget {
return new Scrollable2( return new Scrollable2(
initialScrollOffset: initialScrollOffset, initialScrollOffset: initialScrollOffset,
axisDirection: axisDirection, axisDirection: axisDirection,
physics: physics,
scrollBehavior: scrollBehavior, scrollBehavior: scrollBehavior,
viewportBuilder: (BuildContext context, ViewportOffset offset) { viewportBuilder: (BuildContext context, ViewportOffset offset) {
return new Viewport2( return new Viewport2(
...@@ -102,4 +106,4 @@ class TestScrollable extends StatelessWidget { ...@@ -102,4 +106,4 @@ class TestScrollable extends StatelessWidget {
} }
); );
} }
} }
\ No newline at end of file
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