Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement PageView using SliverLayoutBuilder, Deprecate RenderSliverFillViewport #37024

Merged

Conversation

LongCatIsLooong
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong commented Jul 26, 2019

Description

  • Deprecate RenderSliverFillViewport
  • Change PageView's build method to use SliverLayoutBuilder instead.

Breaking Changes

  • RenderSliverFillViewport is now marked as deprecated.

Related Issues

Closes #23873
Closes #27744

Tests

I added the following tests:

All visible pages are able to receive touch events.

Checklist

Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes ([x]). This will ensure a smooth and quick review process.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I signed the CLA.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I updated/added relevant documentation (doc comments with ///).
  • All existing and new tests are passing.
  • The analyzer (flutter analyze --flutter-repo) does not report any problems on my PR.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Does your PR require Flutter developers to manually update their apps to accommodate your change?

  • Yes, this is a breaking change (Please read Handling breaking changes). Replace this with a link to the e-mail where you asked for input on this proposed change.

@LongCatIsLooong LongCatIsLooong added framework flutter/packages/flutter repository. See also f: labels. f: scrolling Viewports, list views, slivers, etc. labels Jul 26, 2019
@LongCatIsLooong LongCatIsLooong added the c: API break Backwards-incompatible API changes label Jul 26, 2019
@@ -1030,6 +1028,7 @@ 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.
@Deprecated('Use SliverLayoutBuilder instead.')
class SliverFillViewport extends SliverMultiBoxAdaptorWidget {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep this widget and just implement it in terms of SliverLayoutBuilder and SliverFixedExtendList? That would limit the scope of the API breakage and we'd only remove the RenderSliverFillViewport, I think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aye

@LongCatIsLooong LongCatIsLooong changed the title Implement PageView using SliverLayoutBuilder, Deprecate SliverFillViewport Implement PageView using SliverLayoutBuilder, Deprecate RenderSliverFillViewport Jul 29, 2019
Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall looks good, but one concern about the behavior when viewportFraction > 1.0

@@ -556,13 +556,59 @@ void main() {

await tester.pumpWidget(build(controller));

expect(tester.getTopLeft(find.text('Alabama')), const Offset(-100.0, 0.0));
expect(tester.getBottomRight(find.text('Alabama')), const Offset(900.0, 600.0));
expect(tester.getTopLeft(find.text('Alabama')), Offset.zero);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this change?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like we changed the behavior when viewportFraction > 1
The page start at the very left edge of the page instead of after extra space.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it does change the behavior when viewportFraction > 1 and breaks the API (although I doubt it would really affect anyone, and starting from 0 makes more sense to me) . Context: #8539

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the motivation for changing this?

Please add a description of the changed behavior to the PR description.

Copy link
Contributor

@chunhtai chunhtai Jul 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the original behavior make more sense to me. I would expect the viewport is center align with the page, that is why you have the padding when viewportfraction<1. If the same thing apply when viewportfraction>1, the original behavior is expected. and i am not quite sure how #8539 related to this behavior. Can you elaborate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I thought a weird viewportFraction like 1.3 would mess up the snapping behavior but that's not true. Thanks for pointing out. But SliverPaddings can't go negative. I'll find an alternative for it.

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you update the PR's description? Please also include all changes in behavior there.

You'll also have to follow https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes for both of the breaking changes.

final double fixedExtent = constraints.viewportMainAxisExtent * viewportFraction;
final double padding = math.max(0, constraints.viewportMainAxisExtent - fixedExtent) / 2;

final bool isHorizontal = constraints.axisDirection == AxisDirection.left
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you just use constraints.axis?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, with enums instead of checking individual values use a switch/case. That way the analyzer tells us that we need to update this code if we ever add new enum values.

@@ -556,13 +556,59 @@ void main() {

await tester.pumpWidget(build(controller));

expect(tester.getTopLeft(find.text('Alabama')), const Offset(-100.0, 0.0));
expect(tester.getBottomRight(find.text('Alabama')), const Offset(900.0, 600.0));
expect(tester.getTopLeft(find.text('Alabama')), Offset.zero);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the motivation for changing this?

Please add a description of the changed behavior to the PR description.

});

testWidgets('All visible pages are able to receive touch events',
(WidgetTester tester) async {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: either place this at the end of the previous line (preferred) or indent this more.

itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () => tappedIndex = index,
child: SizedBox.expand(child: Text(index.toString())),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
child: SizedBox.expand(child: Text(index.toString())),
child: SizedBox.expand(child: Text('$index')),

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also below - I think that improves readability.

(WidgetTester tester) async {
final PageController controller = PageController(viewportFraction: 1/4, initialPage: 0);
int tappedIndex;
Iterable<int> visiblePages;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declare this were the first value is assigned to it?

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.toString())).translate(-3, 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also just subtract a small offset, which may make the intend here clearer:

Suggested change
final Offset center = tester.getCenter(find.text(index.toString())).translate(-3, 0);
final Offset center = tester.getCenter(find.text('$index')) - const Offset(3, 0);

@LongCatIsLooong LongCatIsLooong changed the title Implement PageView using SliverLayoutBuilder, Deprecate RenderSliverFillViewport WIP Implement PageView using SliverLayoutBuilder, Deprecate RenderSliverFillViewport Jul 30, 2019
@LongCatIsLooong LongCatIsLooong force-pushed the deprecate-sliverfillviewport branch 2 times, most recently from 56c16e2 to 9662143 Compare July 31, 2019 22:18
@Piinks Piinks added the f: gestures flutter/packages/flutter/gestures repository. label Jul 31, 2019
@LongCatIsLooong LongCatIsLooong force-pushed the deprecate-sliverfillviewport branch 2 times, most recently from bffde64 to 8cc50bf Compare July 31, 2019 22:20
@LongCatIsLooong LongCatIsLooong changed the title WIP Implement PageView using SliverLayoutBuilder, Deprecate RenderSliverFillViewport Implement PageView using SliverLayoutBuilder, Deprecate RenderSliverFillViewport Jul 31, 2019
if (oldPage != null)
forcePixels(getPixelsFromPage(oldPage));
// This part should only be reacheable when it will definitely trigger a rebuild.
correctPixels(getPixelsFromPage(oldPage));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feel dangerous to me. That is true when you change this property in didupdatewidget. If you are certain that is the case for the future as well, you should add an assert to verify that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if this is better but how about

if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
     correctPixels(getPixelsFromPage(oldPage));
    } else {
      forcePixels(getPixelsFromPage(oldPage));
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class itself is private, and the setter is only used in PageController.attach. I'm not sure if we should call notifyListeners in ScrollController.attach, I think it should be up to the caller?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am more worry about we change the scroll offset but not markneedbuild, and yes this should be fine because this is private class and as of now it only called during the build stage. However, it is possible this might change. If you think that is unlikely, we should still add an assert here to guard it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah let me get rid of the setter altogether. The implementation should probably live in PageController.attach which has more context.

@@ -364,22 +364,24 @@ class _PagePosition extends ScrollPositionWithSingleContext implements PageMetri
return;
final double oldPage = page;
_viewportFraction = value;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, extra line

@@ -556,13 +556,95 @@ void main() {

await tester.pumpWidget(build(controller));

expect(tester.getTopLeft(find.text('Alabama')), const Offset(-100.0, 0.0));
expect(tester.getTopLeft(find.text('Alabama')), const Offset(-100, 0));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's revert this change

@LongCatIsLooong
Copy link
Contributor Author

@goderbauer It no longer changes the behavior when viewFraction > 1 I believe. Should I post the breakage to flutter-announcement now or there are further changes that need to be made?

@@ -252,7 +252,14 @@ class PageController extends ScrollController {
void attach(ScrollPosition position) {
super.attach(position);
final _PagePosition pagePosition = position;
pagePosition.viewportFraction = viewportFraction;

if (pagePosition.viewportFraction == viewportFraction)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why move this out of the setter for viewportFraction?

forcePixel (in this new version) and correctPixel also have slightly different semantics regarding notifications...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from ScrollPosition.forcePixel documentation:

/// This should not be called during layout (e.g. when setting the initial
/// scroll offset). Consider [correctPixels] if you find you need to adjust
/// the position during layout.

Also it doesn't sound right to notify the listeners in ScrollController.attach.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why move it out of the setter though?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO since using either forcePixel or correctPixel needs to be decided on a case-by-case basis, it feels a bit error-prone to have a viewportFraction setter which sounds like a silver bullet for changing viewportFraction.
Also attach is the only caller of the setter right now, so I got rid of the setter and moved the implementation to attach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is build, not layout. should be fine to call during build.

forcePixels(getPixelsFromPage(oldPage));
}

double get _initialPageOffset => math.max(0, viewportDimension * (viewportFraction - 1) / 2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be worthwhile to document what the "initial page offset" is.


testWidgets(
'All visible pages are able to receive touch events',
(WidgetTester tester) async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same nit as above here regarding formatting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I double checked the indentation, line 607 does have 2 more spaces than 606, it's just ( takes much less space than a regular character.

final double oldPage = pagePosition.page;
pagePosition._viewportFraction = viewportFraction;
if (oldPage != null) {
pagePosition.correctPixels(pagePosition.getPixelsFromPage(oldPage));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

according to documentation of correctPixels, this function should only be called during layout phase
can you add an assert something like
assert(SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks, 'attach can only be called during SchedulerPhase.persistentCallbacks')

If this assumption no longer held, would be better to update the documentation of correctPixels to include this case

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should really only be called when debugDoingLayout is true for the viewport in question

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@@ -930,8 +930,7 @@ 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should document when it is valid to call this method (e.g. what the assert is asserting).

Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, with some nit

double getPixelsFromPage(double page) {
return page * viewportDimension * viewportFraction;
}
double getPixelsFromPage(double page) => page * viewportDimension * viewportFraction + _initialPageOffset;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it has to be a good old function if the line is too long

@LongCatIsLooong LongCatIsLooong merged commit 9aea03f into flutter:master Aug 21, 2019
@LongCatIsLooong LongCatIsLooong deleted the deprecate-sliverfillviewport branch August 21, 2019 22:02
@tvolkert
Copy link
Contributor

It looks like this regressed the "stock_build_iteration" microbenchmark by about 2%.

Screen Shot 2019-08-22 at 8 51 51 AM

@LongCatIsLooong can you investigate? #39060

@@ -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.')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking into this further, I'm not sure this deprecation makes sense. If someone is using RenderSliverFillViewport directly, then they're not using widgets, which means that using SliverLayoutBuilder isn't an option.

We should probably remove this render object if we're not supporting it any more (which I presume we're not, since we didn't try to fix the hit test bug in this render object).

cc @goderbauer @Piinks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we move the padding from rendering layer to widget layer. I can't think of a way to achieve the same behavior by using our current rendering library. I agree we should remove this given we have deprecated warning for a while.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is no longer used and should just be removed: #44612

LongCatIsLooong added a commit to LongCatIsLooong/flutter that referenced this pull request Nov 13, 2019
LongCatIsLooong added a commit that referenced this pull request Nov 15, 2019
…rSliverFillViewport (#37024)" (#44778)

* Revert "Implement PageView using SliverLayoutBuilder, Deprecate RenderSliverFillViewport (#37024)"

This reverts commit 9aea03f.
LongCatIsLooong added a commit to LongCatIsLooong/flutter that referenced this pull request Nov 15, 2019
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 3, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
c: API break Backwards-incompatible API changes f: gestures flutter/packages/flutter/gestures repository. f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
7 participants