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.
This commit is contained in:
parent
3831e0b06d
commit
3231465769
@ -124,9 +124,9 @@ class PageableListAppState extends State<PageableListApp> {
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext context) {
|
||||
return new PageableList(
|
||||
children: cardModels.map(buildCard),
|
||||
itemsWrap: itemsWrap,
|
||||
return new PageView(
|
||||
children: cardModels.map(buildCard).toList(),
|
||||
// TODO(abarth): itemsWrap: itemsWrap,
|
||||
scrollDirection: scrollDirection
|
||||
);
|
||||
}
|
||||
@ -150,7 +150,7 @@ void main() {
|
||||
theme: new ThemeData(
|
||||
brightness: Brightness.light,
|
||||
primarySwatch: Colors.blue,
|
||||
accentColor: Colors.redAccent[200]
|
||||
accentColor: Colors.redAccent[200],
|
||||
),
|
||||
home: new PageableListApp()
|
||||
));
|
||||
|
49
packages/flutter/lib/src/widgets/page_scroll_physics.dart
Normal file
49
packages/flutter/lib/src/widgets/page_scroll_physics.dart
Normal file
@ -0,0 +1,49 @@
|
||||
// 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 {
|
||||
return null;
|
||||
}
|
||||
|
||||
ScrollPhysics _getEffectiveScrollPhysics(BuildContext context, ScrollPhysics physics) {
|
||||
final ScrollPhysics defaultPhysics = getScrollPhysics(getPlatform(context));
|
||||
if (physics != null)
|
||||
return physics.applyTo(defaultPhysics);
|
||||
return defaultPhysics;
|
||||
}
|
||||
|
||||
@override
|
||||
ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition) {
|
||||
return new AbsoluteScrollPosition(state, scrollTolerances, oldPosition, getScrollPhysics(getPlatform(context)));
|
||||
ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition, ScrollPhysics physics) {
|
||||
return new AbsoluteScrollPosition(state, scrollTolerances, oldPosition, _getEffectiveScrollPhysics(context, physics));
|
||||
}
|
||||
|
||||
@override
|
||||
@ -108,6 +115,8 @@ class ViewportScrollBehavior extends ScrollBehavior2 {
|
||||
abstract class ScrollPhysics {
|
||||
const ScrollPhysics();
|
||||
|
||||
ScrollPhysicsProxy applyTo(ScrollPhysics parent) => this;
|
||||
|
||||
/// Used by [AbsoluteDragScrollActivity] and other user-driven activities to
|
||||
/// convert an offset in logical pixels as provided by the [DragUpdateDetails]
|
||||
/// into a delta to apply using [setPixels].
|
||||
@ -133,6 +142,58 @@ abstract class ScrollPhysics {
|
||||
/// [AbsoluteBallisticScrollActivity] with the returned value. Otherwise, the
|
||||
/// [ScrollPosition] will begin an idle activity instead.
|
||||
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 {
|
||||
@ -405,6 +466,7 @@ class BouncingScrollPhysics extends ScrollPhysics {
|
||||
Simulation createBallisticSimulation(AbsoluteScrollPosition position, double velocity) {
|
||||
if (velocity.abs() >= position.scrollTolerances.velocity || position.outOfRange) {
|
||||
return new BouncingScrollSimulation(
|
||||
spring: scrollSpring,
|
||||
position: position.pixels,
|
||||
velocity: velocity,
|
||||
leadingExtent: position.minScrollExtent,
|
||||
@ -446,19 +508,13 @@ class ClampingScrollPhysics extends ScrollPhysics {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
static final SpringDescription _defaultScrollSpring = new SpringDescription.withDampingRatio(
|
||||
mass: 0.5,
|
||||
springConstant: 100.0,
|
||||
ratio: 1.1,
|
||||
);
|
||||
|
||||
@override
|
||||
Simulation createBallisticSimulation(AbsoluteScrollPosition position, double velocity) {
|
||||
if (position.outOfRange) {
|
||||
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)
|
||||
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);
|
||||
}
|
||||
if (!position.atEdge && velocity.abs() >= position.scrollTolerances.velocity) {
|
||||
|
@ -33,10 +33,10 @@ class BouncingScrollSimulation extends SimulationGroup {
|
||||
@required double velocity,
|
||||
@required double leadingExtent,
|
||||
@required double trailingExtent,
|
||||
SpringDescription spring,
|
||||
@required SpringDescription spring,
|
||||
}) : _leadingExtent = leadingExtent,
|
||||
_trailingExtent = trailingExtent,
|
||||
_spring = spring ?? _defaultScrollSpring {
|
||||
_spring = spring {
|
||||
assert(position != null);
|
||||
assert(velocity != null);
|
||||
assert(_leadingExtent != null);
|
||||
@ -50,12 +50,6 @@ class BouncingScrollSimulation extends SimulationGroup {
|
||||
final double _trailingExtent;
|
||||
final SpringDescription _spring;
|
||||
|
||||
static final SpringDescription _defaultScrollSpring = new SpringDescription.withDampingRatio(
|
||||
mass: 0.5,
|
||||
springConstant: 100.0,
|
||||
ratio: 1.1,
|
||||
);
|
||||
|
||||
bool _isSpringing = false;
|
||||
Simulation _currentSimulation;
|
||||
double _offset = 0.0;
|
||||
|
@ -7,6 +7,8 @@ import 'package:meta/meta.dart';
|
||||
|
||||
import 'framework.dart';
|
||||
import 'basic.dart';
|
||||
import 'page_scroll_physics.dart';
|
||||
import 'scroll_absolute.dart';
|
||||
import 'scrollable.dart';
|
||||
import 'sliver.dart';
|
||||
import 'viewport.dart';
|
||||
@ -20,6 +22,7 @@ class ScrollView extends StatelessWidget {
|
||||
this.padding,
|
||||
this.initialScrollOffset: 0.0,
|
||||
this.itemExtent,
|
||||
this.physics,
|
||||
this.shrinkWrap: false,
|
||||
this.children: const <Widget>[],
|
||||
}) : super(key: key) {
|
||||
@ -38,6 +41,8 @@ class ScrollView extends StatelessWidget {
|
||||
|
||||
final double itemExtent;
|
||||
|
||||
final ScrollPhysics physics;
|
||||
|
||||
final bool shrinkWrap;
|
||||
|
||||
final List<Widget> children;
|
||||
@ -76,6 +81,7 @@ class ScrollView extends StatelessWidget {
|
||||
return new Scrollable2(
|
||||
axisDirection: axisDirection,
|
||||
initialScrollOffset: initialScrollOffset,
|
||||
physics: physics,
|
||||
viewportBuilder: (BuildContext context, ViewportOffset offset) {
|
||||
if (shrinkWrap) {
|
||||
return new ShrinkWrappingViewport(
|
||||
@ -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';
|
||||
import 'gesture_detector.dart';
|
||||
import 'notification_listener.dart';
|
||||
import 'page_storage.dart';
|
||||
import 'scroll_absolute.dart' show ViewportScrollBehavior;
|
||||
import 'scroll_behavior.dart';
|
||||
import 'scroll_configuration.dart';
|
||||
import 'scroll_notification.dart';
|
||||
import 'ticker_provider.dart';
|
||||
import 'viewport.dart';
|
||||
|
||||
// TODO(abarth): Merge AbsoluteScrollPosition and ScrollPosition.
|
||||
import 'scroll_absolute.dart' show ViewportScrollBehavior, ScrollPhysics;
|
||||
|
||||
export 'package:flutter/physics.dart' show Tolerance;
|
||||
|
||||
// This file defines an unopinionated scrolling mechanism.
|
||||
@ -360,7 +362,7 @@ abstract class ScrollBehavior2 {
|
||||
/// object must be disposed (via [ScrollPosition.oldPosition]) in the same
|
||||
/// call stack. Passing a non-null `oldPosition` is a destructive operation
|
||||
/// 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
|
||||
/// return meaningfully different widgets from [wrap] or a meaningfully
|
||||
@ -405,6 +407,7 @@ class Scrollable2 extends StatefulWidget {
|
||||
Key key,
|
||||
this.initialScrollOffset: 0.0,
|
||||
this.axisDirection: AxisDirection.down,
|
||||
this.physics,
|
||||
this.scrollBehavior,
|
||||
@required this.viewportBuilder,
|
||||
}) : super (key: key) {
|
||||
@ -417,6 +420,8 @@ class Scrollable2 extends StatefulWidget {
|
||||
|
||||
final AxisDirection axisDirection;
|
||||
|
||||
final ScrollPhysics physics;
|
||||
|
||||
/// The delegate that creates the [ScrollPosition] and wraps the viewport
|
||||
/// in extra widgets (e.g. for overscroll effects).
|
||||
///
|
||||
@ -484,7 +489,7 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
|
||||
void _updatePosition() {
|
||||
_scrollBehavior = config.scrollBehavior ?? Scrollable2.getScrollBehavior(context);
|
||||
final ScrollPosition oldPosition = position;
|
||||
_position = _scrollBehavior.createScrollPosition(context, this, oldPosition);
|
||||
_position = _scrollBehavior.createScrollPosition(context, this, oldPosition, config.physics);
|
||||
assert(position != null);
|
||||
if (oldPosition != null) {
|
||||
// It's important that we not do this until after the viewport has had a
|
||||
|
@ -54,8 +54,6 @@ abstract class SliverChildDelegate {
|
||||
// /// demand). For example, the body of a dialog box might fit both of these
|
||||
// /// conditions.
|
||||
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);
|
||||
|
||||
final List<Widget> children;
|
||||
|
@ -313,9 +313,7 @@ void main() {
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
expect(scrollableState.position.pixels, greaterThan(0.0));
|
||||
}, skip: Scrollable == Scrollable &&
|
||||
ScrollableViewport == ScrollableViewport &&
|
||||
Block == Block); // TODO(abarth): re-enable when ensureVisible is implemented
|
||||
}, skip: Scrollable != Scrollable2); // TODO(abarth): re-enable when ensureVisible is implemented
|
||||
|
||||
testWidgets('Stepper index test', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
|
76
packages/flutter/test/widgets/page_view_test.dart
Normal file
76
packages/flutter/test/widgets/page_view_test.dart
Normal file
@ -0,0 +1,76 @@
|
||||
// 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 {
|
||||
Widget wrap(BuildContext context, Widget child, AxisDirection axisDirection) => child;
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
|
@ -42,12 +42,12 @@ class TestBehavior extends ScrollBehavior2 {
|
||||
}
|
||||
|
||||
@override
|
||||
ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition) {
|
||||
ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition, ScrollPhysics physics) {
|
||||
return new TestViewportScrollPosition(
|
||||
state,
|
||||
new Tolerance(velocity: 20.0, distance: 1.0),
|
||||
oldPosition,
|
||||
const ClampingScrollPhysics(),
|
||||
physics,
|
||||
);
|
||||
}
|
||||
|
||||
@ -80,6 +80,7 @@ void main() {
|
||||
axisDirection: AxisDirection.down,
|
||||
center: centerKey,
|
||||
anchor: 0.25,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
scrollBehavior: new TestBehavior(),
|
||||
slivers: <Widget>[
|
||||
new SliverToBoxAdapter(child: new Container(height: 5.0)),
|
||||
|
@ -63,6 +63,7 @@ class TestScrollable extends StatelessWidget {
|
||||
Key key,
|
||||
this.initialScrollOffset: 0.0,
|
||||
this.axisDirection: AxisDirection.down,
|
||||
this.physics,
|
||||
this.anchor: 0.0,
|
||||
this.center,
|
||||
this.scrollBehavior,
|
||||
@ -75,6 +76,8 @@ class TestScrollable extends StatelessWidget {
|
||||
|
||||
final AxisDirection axisDirection;
|
||||
|
||||
final ScrollPhysics physics;
|
||||
|
||||
final double anchor;
|
||||
|
||||
final Key center;
|
||||
@ -90,6 +93,7 @@ class TestScrollable extends StatelessWidget {
|
||||
return new Scrollable2(
|
||||
initialScrollOffset: initialScrollOffset,
|
||||
axisDirection: axisDirection,
|
||||
physics: physics,
|
||||
scrollBehavior: scrollBehavior,
|
||||
viewportBuilder: (BuildContext context, ViewportOffset offset) {
|
||||
return new Viewport2(
|
||||
@ -102,4 +106,4 @@ class TestScrollable extends StatelessWidget {
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user