blob: b5a38bec6c6e9395e9714001c8a0fed43aa9c4a6 [file] [log] [blame] [edit]
// 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));
}
}