Unverified Commit 0f1a95d1 authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

TwoDimensional scrolling foundation (#125437)

From the 2D scrolling proposal: [flutter.dev/go/2D-Foundation](https://flutter.dev/go/2D-Foundation) ✅  updated 4/25

Fixes https://github.com/flutter/flutter/issues/125505

Follow up issues:
- https://github.com/flutter/flutter/issues/126297
- https://github.com/flutter/flutter/issues/126298
- https://github.com/flutter/flutter/issues/126299
-  https://github.com/flutter/flutter/issues/122348

This adds a mostly abstract foundation for 2D scrolling in Flutter.

With these base classes, developers will be able to construct widgets that scroll in both dimensions and can lazily load their children for the best performance. This implementation is meant to be flexible in order to support different kinds of 2D compositions, from tables to scatter plots. 

The upcoming TableView, TreeView, etc widgets (coming soon in flutter/packages) are built on top of this foundation.
parent 15fa5b53
......@@ -21,6 +21,7 @@ import 'scroll_delegate.dart';
import 'scroll_notification.dart';
import 'scroll_physics.dart';
import 'scrollable.dart';
import 'scrollable_helpers.dart';
import 'sliver.dart';
import 'sliver_prototype_extent_list.dart';
import 'viewport.dart';
......@@ -39,7 +40,8 @@ enum ScrollViewKeyboardDismissBehavior {
onDrag,
}
/// A widget that scrolls.
/// A widget that combines a [Scrollable] and a [Viewport] to create an
/// interactive scrolling pane of content in one dimension.
///
/// Scrollable widgets consist of three pieces:
///
......@@ -71,6 +73,8 @@ enum ScrollViewKeyboardDismissBehavior {
/// effects using slivers.
/// * [ScrollNotification] and [NotificationListener], which can be used to watch
/// the scroll position without using a [ScrollController].
/// * [TwoDimensionalScrollView], which is a similar widget [ScrollView] that
/// scrolls in two dimensions.
abstract class ScrollView extends StatelessWidget {
/// Creates a widget that scrolls.
///
......@@ -1769,7 +1773,7 @@ class ListView extends BoxScrollView {
/// {@end-tool}
///
/// By default, [GridView] will automatically pad the limits of the
/// grids's scrollable to avoid partial obstructions indicated by
/// grid's scrollable to avoid partial obstructions indicated by
/// [MediaQuery]'s padding. To avoid this behavior, override with a
/// zero [padding] property.
///
......
......@@ -22,10 +22,8 @@ import 'scrollable.dart';
export 'package:flutter/physics.dart' show Tolerance;
/// Describes the aspects of a Scrollable widget to inform inherited widgets
/// like [ScrollBehavior] for decorating.
// TODO(Piinks): Fix doc with 2DScrollable change.
// or enumerate the properties of combined
// Scrollables, such as [TwoDimensionalScrollable].
/// like [ScrollBehavior] for decorating or enumerate the properties of combined
/// Scrollables, such as [TwoDimensionalScrollable].
///
/// Decorations like [GlowingOverscrollIndicator]s and [Scrollbar]s require
/// information about the Scrollable in order to be initialized.
......
// Copyright 2014 The Flutter 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/gestures.dart';
import 'package:flutter/rendering.dart';
import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart';
import 'notification_listener.dart';
import 'primary_scroll_controller.dart';
import 'scroll_controller.dart';
import 'scroll_delegate.dart';
import 'scroll_notification.dart';
import 'scroll_physics.dart';
import 'scroll_view.dart';
import 'scrollable.dart';
import 'scrollable_helpers.dart';
import 'two_dimensional_viewport.dart';
/// A widget that combines a [TwoDimensionalScrollable] and a
/// [TwoDimensionalViewport] to create an interactive scrolling pane of content
/// in both vertical and horizontal dimensions.
///
/// A two-way scrollable widget consist of three pieces:
///
/// 1. A [TwoDimensionalScrollable] widget, which listens for various user
/// gestures and implements the interaction design for scrolling.
/// 2. A [TwoDimensionalViewport] widget, which implements the visual design
/// for scrolling by displaying only a portion
/// of the widgets inside the scroll view.
/// 3. A [TwoDimensionalChildDelegate], which provides the children visible in
/// the scroll view.
///
/// [TwoDimensionalScrollView] helps orchestrate these pieces by creating the
/// [TwoDimensionalScrollable] and deferring to its subclass to implement
/// [buildViewport], which builds a subclass of [TwoDimensionalViewport]. The
/// [TwoDimensionalChildDelegate] is provided by the [delegate] parameter.
///
/// A [TwoDimensionalScrollView] has two different [ScrollPosition]s, one for
/// each [Axis]. This means that there are also two unique [ScrollController]s
/// for these positions. To provide a ScrollController to access the
/// ScrollPosition, use the [ScrollableDetails.controller] property of the
/// associated axis that is provided to this scroll view.
abstract class TwoDimensionalScrollView extends StatelessWidget {
/// Creates a widget that scrolls in both dimensions.
///
/// The [primary] argument is associated with the [mainAxis]. The main axis
/// [ScrollableDetails.controller] must be null if [primary] is configured for
/// that axis. If [primary] is true, the nearest [PrimaryScrollController]
/// surrounding the widget is attached to the scroll position of that axis.
const TwoDimensionalScrollView({
super.key,
this.primary,
this.mainAxis = Axis.vertical,
this.verticalDetails = const ScrollableDetails.vertical(),
this.horizontalDetails = const ScrollableDetails.horizontal(),
required this.delegate,
this.cacheExtent,
this.diagonalDragBehavior = DiagonalDragBehavior.none,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.clipBehavior = Clip.hardEdge,
});
/// A delegate that provides the children for the [TwoDimensionalScrollView].
final TwoDimensionalChildDelegate delegate;
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
final double? cacheExtent;
/// Whether scrolling gestures should lock to one axes, allow free movement
/// in both axes, or be evaluated on a weighted scale.
///
/// Defaults to [DiagonalDragBehavior.none], locking axes to receive input one
/// at a time.
final DiagonalDragBehavior diagonalDragBehavior;
/// {@macro flutter.widgets.scroll_view.primary}
final bool? primary;
/// The main axis of the two.
///
/// Used to determine how to apply [primary] when true.
///
/// This value should also be provided to the subclass of
/// [TwoDimensionalViewport], where it is used to determine paint order of
/// children.
final Axis mainAxis;
/// The configuration of the vertical Scrollable.
///
/// These [ScrollableDetails] can be used to set the [AxisDirection],
/// [ScrollController], [ScrollPhysics] and more for the vertical axis.
final ScrollableDetails verticalDetails;
/// The configuration of the horizontal Scrollable.
///
/// These [ScrollableDetails] can be used to set the [AxisDirection],
/// [ScrollController], [ScrollPhysics] and more for the horizontal axis.
final ScrollableDetails horizontalDetails;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior}
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
/// Build the two dimensional viewport.
///
/// Subclasses may override this method to change how the viewport is built,
/// likely a subclass of [TwoDimensionalViewport].
///
/// The `verticalOffset` and `horizontalOffset` arguments are the values
/// obtained from [TwoDimensionalScrollable.viewportBuilder].
Widget buildViewport(
BuildContext context,
ViewportOffset verticalOffset,
ViewportOffset horizontalOffset,
);
@override
Widget build(BuildContext context) {
assert(
axisDirectionToAxis(verticalDetails.direction) == Axis.vertical,
'TwoDimensionalScrollView.verticalDetails are not Axis.vertical.'
);
assert(
axisDirectionToAxis(horizontalDetails.direction) == Axis.horizontal,
'TwoDimensionalScrollView.horizontalDetails are not Axis.horizontal.'
);
ScrollableDetails mainAxisDetails = switch (mainAxis) {
Axis.vertical => verticalDetails,
Axis.horizontal => horizontalDetails,
};
final bool effectivePrimary = primary
?? mainAxisDetails.controller == null && PrimaryScrollController.shouldInherit(
context,
mainAxis,
);
if (effectivePrimary) {
// Using PrimaryScrollController for mainAxis.
assert(
mainAxisDetails.controller == null,
'TwoDimensionalScrollView.primary was explicitly set to true, but a '
'ScrollController was provided in the ScrollableDetails of the '
'TwoDimensionalScrollView.mainAxis.'
);
mainAxisDetails = mainAxisDetails.copyWith(
controller: PrimaryScrollController.of(context),
);
}
final TwoDimensionalScrollable scrollable = TwoDimensionalScrollable(
horizontalDetails : switch (mainAxis) {
Axis.horizontal => mainAxisDetails,
Axis.vertical => horizontalDetails,
},
verticalDetails: switch (mainAxis) {
Axis.vertical => mainAxisDetails,
Axis.horizontal => verticalDetails,
},
diagonalDragBehavior: diagonalDragBehavior,
viewportBuilder: buildViewport,
dragStartBehavior: dragStartBehavior,
);
final Widget scrollableResult = effectivePrimary
// Further descendant ScrollViews will not inherit the same PrimaryScrollController
? PrimaryScrollController.none(child: scrollable)
: scrollable;
if (keyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) {
return NotificationListener<ScrollUpdateNotification>(
child: scrollableResult,
onNotification: (ScrollUpdateNotification notification) {
final FocusScopeNode focusScope = FocusScope.of(context);
if (notification.dragDetails != null && focusScope.hasFocus) {
focusScope.unfocus();
}
return false;
},
);
}
return scrollableResult;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(EnumProperty<Axis>('mainAxis', mainAxis));
properties.add(EnumProperty<DiagonalDragBehavior>('diagonalDragBehavior', diagonalDragBehavior));
properties.add(FlagProperty('primary', value: primary, ifTrue: 'using primary controller', showName: true));
properties.add(DiagnosticsProperty<ScrollableDetails>('verticalDetails', verticalDetails, showName: false));
properties.add(DiagnosticsProperty<ScrollableDetails>('horizontalDetails', horizontalDetails, showName: false));
}
}
This diff is collapsed.
......@@ -13,7 +13,8 @@ export 'package:flutter/rendering.dart' show
AxisDirection,
GrowthDirection;
/// A widget that is bigger on the inside.
/// A widget through which a portion of larger content can be viewed, typically
/// in combination with a [Scrollable].
///
/// [Viewport] is the visual workhorse of the scrolling machinery. It displays a
/// subset of its children according to its own dimensions and the given
......
......@@ -149,6 +149,8 @@ export 'src/widgets/ticker_provider.dart';
export 'src/widgets/title.dart';
export 'src/widgets/transitions.dart';
export 'src/widgets/tween_animation_builder.dart';
export 'src/widgets/two_dimensional_scroll_view.dart';
export 'src/widgets/two_dimensional_viewport.dart';
export 'src/widgets/undo_history.dart';
export 'src/widgets/unique_widget.dart';
export 'src/widgets/value_listenable_builder.dart';
......
This diff is collapsed.
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