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> {
}
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()
));
......
// 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(
......
// 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 {
}
);
}
}
\ 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