Skip to content

Commit

Permalink
Implement PageView using SliverLayoutBuilder, Deprecate RenderSliverF…
Browse files Browse the repository at this point in the history
…illViewport (flutter#37024)
  • Loading branch information
LongCatIsLooong committed Aug 21, 2019
1 parent bf097ee commit 9aea03f
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 83 deletions.
3 changes: 1 addition & 2 deletions packages/flutter/lib/src/rendering/sliver_fill.dart
Expand Up @@ -27,6 +27,7 @@ import 'sliver_multi_box_adaptor.dart';
/// * [RenderSliverFixedExtentList], which has a configurable [itemExtent].
/// * [RenderSliverList], which does not require its children to have the same
/// extent in the main axis.
@Deprecated('Use SliverLayoutBuilder instead.')
class RenderSliverFillViewport extends RenderSliverFixedExtentBoxAdaptor {
/// Creates a sliver that contains multiple box children that each fill the
/// viewport.
Expand Down Expand Up @@ -105,8 +106,6 @@ class RenderSliverFillViewport extends RenderSliverFixedExtentBoxAdaptor {
///
/// See also:
///
/// * [RenderSliverFillViewport], which sizes its children based on the
/// size of the viewport, regardless of what else is in the scroll view.
/// * [RenderSliverList], which shows a list of variable-sized children in a
/// viewport.
class RenderSliverFillRemaining extends RenderSliverSingleBoxAdapter {
Expand Down
19 changes: 10 additions & 9 deletions packages/flutter/lib/src/widgets/gesture_detector.dart
Expand Up @@ -930,8 +930,8 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
}
}

/// This method can be called outside of the build phase to filter the list of
/// available semantic actions.
/// This method can be called to filter the list of available semantic actions,
/// after the render object was created.
///
/// The actual filtering is happening in the next frame and a frame will be
/// scheduled if non is pending.
Expand All @@ -942,20 +942,21 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
/// If this is never called, then the actions are not filtered. If the list of
/// actions to filter changes, it must be called again.
void replaceSemanticsActions(Set<SemanticsAction> actions) {
if (widget.excludeFromSemantics)
return;

final RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject();
assert(() {
final Element element = context;
if (element.owner.debugBuilding) {
if (semanticsGestureHandler == null) {
throw FlutterError(
'Unexpected call to replaceSemanticsActions() method of RawGestureDetectorState.\n'
'The replaceSemanticsActions() method can only be called outside of the build phase.'
'The replaceSemanticsActions() method can only be called after the RenderSemanticsGestureHandler has been created.'
);
}
return true;
}());
if (!widget.excludeFromSemantics) {
final RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject();
semanticsGestureHandler.validActions = actions; // will call _markNeedsSemanticsUpdate(), if required.
}

semanticsGestureHandler.validActions = actions; // will call _markNeedsSemanticsUpdate(), if required.
}

@override
Expand Down
21 changes: 19 additions & 2 deletions packages/flutter/lib/src/widgets/page_view.dart
Expand Up @@ -368,8 +368,16 @@ class _PagePosition extends ScrollPositionWithSingleContext implements PageMetri
forcePixels(getPixelsFromPage(oldPage));
}

// The amount of offset that will be added to [minScrollExtent] and subtracted
// from [maxScrollExtent], such that every page will properly snap to the center
// of the viewport when viewportFraction is greater than 1.
//
// The value is 0 if viewportFraction is less than or equal to 1, larger than 0
// otherwise.
double get _initialPageOffset => math.max(0, viewportDimension * (viewportFraction - 1) / 2);

double getPageFromPixels(double pixels, double viewportDimension) {
final double actual = math.max(0.0, pixels) / math.max(1.0, viewportDimension * viewportFraction);
final double actual = math.max(0, pixels - _initialPageOffset) / math.max(1.0, viewportDimension * viewportFraction);
final double round = actual.roundToDouble();
if ((actual - round).abs() < precisionErrorTolerance) {
return round;
Expand All @@ -378,7 +386,7 @@ class _PagePosition extends ScrollPositionWithSingleContext implements PageMetri
}

double getPixelsFromPage(double page) {
return page * viewportDimension * viewportFraction;
return page * viewportDimension * viewportFraction + _initialPageOffset;
}

@override
Expand Down Expand Up @@ -420,6 +428,15 @@ class _PagePosition extends ScrollPositionWithSingleContext implements PageMetri
return result;
}

@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
final double newMinScrollExtent = minScrollExtent + _initialPageOffset;
return super.applyContentDimensions(
newMinScrollExtent,
math.max(newMinScrollExtent, maxScrollExtent - _initialPageOffset),
);
}

@override
PageMetrics copyWith({
double minScrollExtent,
Expand Down
44 changes: 34 additions & 10 deletions packages/flutter/lib/src/widgets/sliver.dart
Expand Up @@ -3,13 +3,15 @@
// found in the LICENSE file.

import 'dart:collection' show SplayTreeMap, HashMap;
import 'dart:math' as math show max;

import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';

import 'automatic_keep_alive.dart';
import 'basic.dart';
import 'framework.dart';
import 'sliver_layout_builder.dart';

export 'package:flutter/rendering.dart' show
SliverGridDelegate,
Expand Down Expand Up @@ -717,6 +719,7 @@ abstract class SliverMultiBoxAdaptorWidget extends SliverWithKeepAliveWidget {
}) : assert(delegate != null),
super(key: key);

/// {@template flutter.widgets.sliverChildDelegate}
/// The delegate that provides the children for this widget.
///
/// The children are constructed lazily using this widget to avoid creating
Expand All @@ -727,6 +730,7 @@ abstract class SliverMultiBoxAdaptorWidget extends SliverWithKeepAliveWidget {
/// * [SliverChildBuilderDelegate] and [SliverChildListDelegate], which are
/// commonly used subclasses of [SliverChildDelegate] that use a builder
/// callback and an explicit child list, respectively.
/// {@endtemplate}
final SliverChildDelegate delegate;

@override
Expand Down Expand Up @@ -1030,15 +1034,16 @@ class SliverGrid extends SliverMultiBoxAdaptorWidget {
/// the main axis extent of each item.
/// * [SliverList], which does not require its children to have the same
/// extent in the main axis.
class SliverFillViewport extends SliverMultiBoxAdaptorWidget {
class SliverFillViewport extends StatelessWidget {
/// Creates a sliver whose box children that each fill the viewport.
const SliverFillViewport({
Key key,
@required SliverChildDelegate delegate,
@required this.delegate,
this.viewportFraction = 1.0,
}) : assert(viewportFraction != null),
assert(viewportFraction > 0.0),
super(key: key, delegate: delegate);
assert(delegate != null),
super(key: key);

/// The fraction of the viewport that each child should fill in the main axis.
///
Expand All @@ -1047,15 +1052,34 @@ class SliverFillViewport extends SliverMultiBoxAdaptorWidget {
/// the viewport in the main axis.
final double viewportFraction;

@override
RenderSliverFillViewport createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element = context;
return RenderSliverFillViewport(childManager: element, viewportFraction: viewportFraction);
}
/// {@macro flutter.widgets.sliverChildDelegate}
final SliverChildDelegate delegate;

@override
void updateRenderObject(BuildContext context, RenderSliverFillViewport renderObject) {
renderObject.viewportFraction = viewportFraction;
Widget build(BuildContext context) {
return SliverLayoutBuilder(
builder: (BuildContext context, SliverConstraints constraints) {
final double fixedExtent = constraints.viewportMainAxisExtent * viewportFraction;
final double padding = math.max(0, constraints.viewportMainAxisExtent - fixedExtent) / 2;

EdgeInsets sliverPaddingValue;
switch (constraints.axis) {
case Axis.horizontal:
sliverPaddingValue = EdgeInsets.symmetric(horizontal: padding);
break;
case Axis.vertical:
sliverPaddingValue = EdgeInsets.symmetric(vertical: padding);
}

return SliverPadding(
padding: sliverPaddingValue,
sliver: SliverFixedExtentList(
delegate: delegate,
itemExtent: fixedExtent,
),
);
}
);
}
}

Expand Down
Expand Up @@ -24,7 +24,7 @@ void main() {
offset: ViewportOffset.zero(),
cacheExtent: 0,
children: <RenderSliver>[
childManager.createRenderSliverFillViewport(),
childManager.createRenderSliverFixedExtentList(),
],
);
layout(root);
Expand Down Expand Up @@ -52,10 +52,11 @@ class TestRenderSliverBoxChildManager extends RenderSliverBoxChildManager {
RenderSliverMultiBoxAdaptor _renderObject;
List<RenderBox> children;

RenderSliverFillViewport createRenderSliverFillViewport() {
RenderSliverFixedExtentList createRenderSliverFixedExtentList() {
assert(_renderObject == null);
_renderObject = RenderSliverFillViewport(
_renderObject = RenderSliverFixedExtentList(
childManager: this,
itemExtent: 600,
);
return _renderObject;
}
Expand Down
94 changes: 88 additions & 6 deletions packages/flutter/test/widgets/page_view_test.dart
Expand Up @@ -394,8 +394,8 @@ void main() {
return Container(
height: 200.0,
color: index % 2 == 0
? const Color(0xFF0000FF)
: const Color(0xFF00FF00),
? const Color(0xFF0000FF)
: const Color(0xFF00FF00),
child: Text(kStates[index]),
);
},
Expand Down Expand Up @@ -500,8 +500,8 @@ void main() {
return Container(
height: 200.0,
color: index % 2 == 0
? const Color(0xFF0000FF)
: const Color(0xFF00FF00),
? const Color(0xFF0000FF)
: const Color(0xFF00FF00),
child: Text(kStates[index]),
);
},
Expand Down Expand Up @@ -545,8 +545,8 @@ void main() {
return Container(
height: 200.0,
color: index % 2 == 0
? const Color(0xFF0000FF)
: const Color(0xFF00FF00),
? const Color(0xFF0000FF)
: const Color(0xFF00FF00),
child: Text(kStates[index]),
);
},
Expand All @@ -565,6 +565,88 @@ void main() {
expect(tester.getTopLeft(find.text('Hawaii')), const Offset(-100.0, 0.0));
});

testWidgets(
'Updating PageView large viewportFraction',
(WidgetTester tester) async {
Widget build(PageController controller) {
return Directionality(
textDirection: TextDirection.ltr,
child: PageView.builder(
controller: controller,
itemCount: kStates.length,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 200.0,
color: index % 2 == 0
? const Color(0xFF0000FF)
: const Color(0xFF00FF00),
child: Text(kStates[index]),
);
},
),
);
}

final PageController oldController = PageController(viewportFraction: 5/4);
await tester.pumpWidget(build(oldController));

expect(tester.getTopLeft(find.text('Alabama')), const Offset(-100, 0));
expect(tester.getBottomRight(find.text('Alabama')), const Offset(900.0, 600.0));

final PageController newController = PageController(viewportFraction: 4);
await tester.pumpWidget(build(newController));
newController.jumpToPage(10);
await tester.pump();

expect(tester.getTopLeft(find.text('Hawaii')), const Offset(-(4 - 1) * 800 / 2, 0));
});

testWidgets(
'All visible pages are able to receive touch events',
(WidgetTester tester) async {
final PageController controller = PageController(viewportFraction: 1/4, initialPage: 0);
int tappedIndex;

Widget build() {
return Directionality(
textDirection: TextDirection.ltr,
child: PageView.builder(
controller: controller,
itemCount: 20,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () => tappedIndex = index,
child: SizedBox.expand(child: Text('$index')),
);
},
),
);
}

Iterable<int> visiblePages = const <int> [0, 1, 2];
await tester.pumpWidget(build());

// The first 3 items should be visible and tappable.
for (int index in visiblePages) {
expect(find.text(index.toString()), findsOneWidget);
// The center of page 2's x-coordinate is 800, so we have to manually
// offset it a bit to make sure the tap lands within the screen.
final Offset center = tester.getCenter(find.text('$index')) - const Offset(3, 0);
await tester.tapAt(center);
expect(tappedIndex, index);
}

controller.jumpToPage(19);
await tester.pump();
// The last 3 items should be visible and tappable.
visiblePages = const <int> [17, 18, 19];
for (int index in visiblePages) {
expect(find.text('$index'), findsOneWidget);
await tester.tap(find.text('$index'));
expect(tappedIndex, index);
}
});

testWidgets('PageView does not report page changed on overscroll', (WidgetTester tester) async {
final PageController controller = PageController(
initialPage: kStates.length - 1,
Expand Down

0 comments on commit 9aea03f

Please sign in to comment.