From 496ddc580c9c93496773240d68f124261e059fe5 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Sat, 2 Feb 2019 16:23:39 -0800 Subject: [PATCH] [H] Expose "center" on CustomScrollView (#27424) * Expose "center" on CustomScrollView * Also support anchor --- .../flutter/lib/src/widgets/scroll_view.dart | 56 +++++++++++- .../test/widgets/custom_scroll_view_test.dart | 91 +++++++++++++++++++ 2 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 packages/flutter/test/widgets/custom_scroll_view_test.dart diff --git a/packages/flutter/lib/src/widgets/scroll_view.dart b/packages/flutter/lib/src/widgets/scroll_view.dart index 7424562f7d3..80690f08ab5 100644 --- a/packages/flutter/lib/src/widgets/scroll_view.dart +++ b/packages/flutter/lib/src/widgets/scroll_view.dart @@ -51,6 +51,12 @@ abstract class ScrollView extends StatelessWidget { /// Creates a widget that scrolls. /// /// If the [primary] argument is true, the [controller] must be null. + /// + /// If the [shrinkWrap] argument is true, the [center] argument must be null. + /// + /// The [scrollDirection], [reverse], and [shrinkWrap] arguments must not be null. + /// + /// The [anchor] argument must be non-null and in the range 0.0 to 1.0. const ScrollView({ Key key, this.scrollDirection = Axis.vertical, @@ -59,16 +65,22 @@ abstract class ScrollView extends StatelessWidget { bool primary, ScrollPhysics physics, this.shrinkWrap = false, + this.center, + this.anchor = 0.0, this.cacheExtent, this.semanticChildCount, this.dragStartBehavior = DragStartBehavior.down, - }) : assert(reverse != null), + }) : assert(scrollDirection != null), + assert(reverse != null), assert(shrinkWrap != null), assert(dragStartBehavior != null), assert(!(controller != null && primary == true), 'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. ' 'You cannot both set primary to true and pass an explicit controller.' ), + assert(!shrinkWrap || center == null), + assert(anchor != null), + assert(anchor >= 0.0 && anchor <= 1.0), primary = primary ?? controller == null && identical(scrollDirection, Axis.vertical), physics = physics ?? (primary == true || (primary == null && controller == null && identical(scrollDirection, Axis.vertical)) ? const AlwaysScrollableScrollPhysics() : null), super(key: key); @@ -172,6 +184,32 @@ abstract class ScrollView extends StatelessWidget { /// Defaults to false. final bool shrinkWrap; + /// The first child in the [GrowthDirection.forward] growth direction. + /// + /// Children after [center] will be placed in the [axisDirection] relative to + /// the [center]. Children before [center] will be placed in the opposite of + /// the [axisDirection] relative to the [center]. + /// + /// The [center] must be the key of one of the slivers built by [buildSlivers]. + /// + /// Of the built-in subclasses of [ScrollView], only [CustomScrollView] + /// supports [center]; for that class, the given key must be the key of one of + /// the slivers in the [CustomScrollView.slivers] list. + /// + /// See also: + /// + /// * [anchor], which controls where the [center] as aligned in the viewport. + final Key center; + + /// The relative position of the zero scroll offset. + /// + /// For example, if [anchor] is 0.5 and the [axisDirection] is + /// [AxisDirection.down] or [AxisDirection.up], then the zero scroll offset is + /// vertically centered within the viewport. If the [anchor] is 1.0, and the + /// [axisDirection] is [AxisDirection.right], then the zero scroll offset is + /// on the left edge of the viewport. + final double anchor; + /// {@macro flutter.rendering.viewport.cacheExtent} final double cacheExtent; @@ -221,6 +259,14 @@ abstract class ScrollView extends StatelessWidget { /// Subclasses may override this method to change how the viewport is built. /// The default implementation uses a [ShrinkWrappingViewport] if [shrinkWrap] /// is true, and a regular [Viewport] otherwise. + /// + /// The `offset` argument is the value obtained from + /// [Scrollable.viewportBuilder]. + /// + /// The `axisDirection` argument is the value obtained from [getDirection], + /// which by default uses [scrollDirection] and [reverse]. + /// + /// The `slivers` argument is the value obtained from [buildSlivers]. @protected Widget buildViewport( BuildContext context, @@ -240,6 +286,8 @@ abstract class ScrollView extends StatelessWidget { offset: offset, slivers: slivers, cacheExtent: cacheExtent, + center: center, + anchor: anchor, ); } @@ -392,7 +440,7 @@ abstract class ScrollView extends StatelessWidget { class CustomScrollView extends ScrollView { /// Creates a [ScrollView] that creates custom scroll effects using slivers. /// - /// If the [primary] argument is true, the [controller] must be null. + /// See the [new ScrollView] constructor for more details on these arguments. const CustomScrollView({ Key key, Axis scrollDirection = Axis.vertical, @@ -401,6 +449,8 @@ class CustomScrollView extends ScrollView { bool primary, ScrollPhysics physics, bool shrinkWrap = false, + Key center, + double anchor = 0.0, double cacheExtent, this.slivers = const [], int semanticChildCount, @@ -413,6 +463,8 @@ class CustomScrollView extends ScrollView { primary: primary, physics: physics, shrinkWrap: shrinkWrap, + center: center, + anchor: anchor, cacheExtent: cacheExtent, semanticChildCount: semanticChildCount, dragStartBehavior: dragStartBehavior, diff --git a/packages/flutter/test/widgets/custom_scroll_view_test.dart b/packages/flutter/test/widgets/custom_scroll_view_test.dart new file mode 100644 index 00000000000..9513de051bd --- /dev/null +++ b/packages/flutter/test/widgets/custom_scroll_view_test.dart @@ -0,0 +1,91 @@ +// Copyright 2018 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'; + +void main() { + testWidgets('CustomScrollView.center', (WidgetTester tester) async { + await tester.pumpWidget(const Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter(key: Key('a'), child: SizedBox(height: 100.0)), + SliverToBoxAdapter(key: Key('b'), child: SizedBox(height: 100.0)), + ], + center: Key('a'), + ), + )); + await tester.pumpAndSettle(); + expect(tester.getRect(find.descendant(of: find.byKey(const Key('a')), matching: find.byType(SizedBox))), + Rect.fromLTRB(0.0, 0.0, 800.0, 100.0)); + expect(tester.getRect(find.descendant(of: find.byKey(const Key('b')), matching: find.byType(SizedBox))), + Rect.fromLTRB(0.0, 100.0, 800.0, 200.0)); + }); + + testWidgets('CustomScrollView.center', (WidgetTester tester) async { + await tester.pumpWidget(const Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter(key: Key('a'), child: SizedBox(height: 100.0)), + SliverToBoxAdapter(key: Key('b'), child: SizedBox(height: 100.0)), + ], + center: Key('b'), + ), + )); + await tester.pumpAndSettle(); + expect( + tester.getRect( + find.descendant( + of: find.byKey(const Key('a'), skipOffstage: false), + matching: find.byType(SizedBox, skipOffstage: false), + ), + ), + Rect.fromLTRB(0.0, -100.0, 800.0, 0.0), + ); + expect( + tester.getRect( + find.descendant( + of: find.byKey(const Key('b')), + matching: find.byType(SizedBox), + ), + ), + Rect.fromLTRB(0.0, 0.0, 800.0, 100.0), + ); + }); + + testWidgets('CustomScrollView.anchor', (WidgetTester tester) async { + await tester.pumpWidget(const Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter(key: Key('a'), child: SizedBox(height: 100.0)), + SliverToBoxAdapter(key: Key('b'), child: SizedBox(height: 100.0)), + ], + center: Key('b'), + anchor: 1.0, + ), + )); + await tester.pumpAndSettle(); + expect( + tester.getRect( + find.descendant( + of: find.byKey(const Key('a')), + matching: find.byType(SizedBox), + ), + ), + Rect.fromLTRB(0.0, 500.0, 800.0, 600.0), + ); + expect( + tester.getRect( + find.descendant( + of: find.byKey(const Key('b'), skipOffstage: false), + matching: find.byType(SizedBox, skipOffstage: false), + ), + ), + Rect.fromLTRB(0.0, 600.0, 800.0, 700.0), + ); + }); +}