Clean up scrollable.dart for 2D (#122357)
Clean up scrollable.dart for 2D
This commit is contained in:
@@ -23,7 +23,7 @@ import 'pages.dart';
|
||||
import 'performance_overlay.dart';
|
||||
import 'restoration.dart';
|
||||
import 'router.dart';
|
||||
import 'scrollable.dart';
|
||||
import 'scrollable_helpers.dart';
|
||||
import 'semantics_debugger.dart';
|
||||
import 'shared_app_data.dart';
|
||||
import 'shortcuts.dart';
|
||||
|
||||
@@ -9,7 +9,7 @@ import 'package:flutter/services.dart';
|
||||
import 'actions.dart';
|
||||
import 'focus_traversal.dart';
|
||||
import 'framework.dart';
|
||||
import 'scrollable.dart';
|
||||
import 'scrollable_helpers.dart';
|
||||
import 'shortcuts.dart';
|
||||
import 'text_editing_intents.dart';
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import 'scroll_controller.dart';
|
||||
import 'scroll_physics.dart';
|
||||
import 'scroll_position.dart';
|
||||
import 'scrollable.dart';
|
||||
import 'scrollable_helpers.dart';
|
||||
import 'shortcuts.dart';
|
||||
import 'spell_check.dart';
|
||||
import 'tap_region.dart';
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'scroll_controller.dart';
|
||||
import 'scroll_physics.dart';
|
||||
import 'scroll_view.dart';
|
||||
import 'scrollable.dart';
|
||||
import 'scrollable_helpers.dart';
|
||||
import 'sliver.dart';
|
||||
import 'sliver_prototype_extent_list.dart';
|
||||
import 'ticker_provider.dart';
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'framework.dart';
|
||||
import 'overscroll_indicator.dart';
|
||||
import 'scroll_physics.dart';
|
||||
import 'scrollable.dart';
|
||||
import 'scrollable_helpers.dart';
|
||||
import 'scrollbar.dart';
|
||||
|
||||
const Color _kDefaultGlowColor = Color(0xFFFFFFFF);
|
||||
|
||||
@@ -11,23 +11,20 @@ import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'actions.dart';
|
||||
import 'basic.dart';
|
||||
import 'focus_manager.dart';
|
||||
import 'framework.dart';
|
||||
import 'gesture_detector.dart';
|
||||
import 'media_query.dart';
|
||||
import 'notification_listener.dart';
|
||||
import 'primary_scroll_controller.dart';
|
||||
import 'restoration.dart';
|
||||
import 'restoration_properties.dart';
|
||||
import 'scroll_activity.dart';
|
||||
import 'scroll_configuration.dart';
|
||||
import 'scroll_context.dart';
|
||||
import 'scroll_controller.dart';
|
||||
import 'scroll_metrics.dart';
|
||||
import 'scroll_physics.dart';
|
||||
import 'scroll_position.dart';
|
||||
import 'scrollable_helpers.dart';
|
||||
import 'selectable_region.dart';
|
||||
import 'selection_container.dart';
|
||||
import 'ticker_provider.dart';
|
||||
@@ -440,6 +437,9 @@ class _ScrollableScope extends InheritedWidget {
|
||||
/// [Scrollable], provide it with a [ScrollPhysics].
|
||||
class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, RestorationMixin
|
||||
implements ScrollContext {
|
||||
|
||||
// GETTERS
|
||||
|
||||
/// The manager for this [Scrollable] widget's viewport position.
|
||||
///
|
||||
/// To control what kind of [ScrollPosition] is created for a [Scrollable],
|
||||
@@ -448,18 +448,50 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
|
||||
ScrollPosition get position => _position!;
|
||||
ScrollPosition? _position;
|
||||
|
||||
final _RestorableScrollOffset _persistedScrollOffset = _RestorableScrollOffset();
|
||||
/// The resolved [ScrollPhysics] of the [ScrollableState].
|
||||
ScrollPhysics? get resolvedPhysics => _physics;
|
||||
ScrollPhysics? _physics;
|
||||
|
||||
/// An [Offset] that represents the absolute distance from the origin, or 0,
|
||||
/// of the [ScrollPosition] expressed in the associated [Axis].
|
||||
///
|
||||
/// Used by [EdgeDraggingAutoScroller] to progress the position forward when a
|
||||
/// drag gesture reaches the edge of the [Viewport].
|
||||
Offset get deltaToScrollOrigin {
|
||||
switch (axisDirection) {
|
||||
case AxisDirection.down:
|
||||
return Offset(0, position.pixels);
|
||||
case AxisDirection.up:
|
||||
return Offset(0, -position.pixels);
|
||||
case AxisDirection.left:
|
||||
return Offset(-position.pixels, 0);
|
||||
case AxisDirection.right:
|
||||
return Offset(position.pixels, 0);
|
||||
}
|
||||
}
|
||||
|
||||
ScrollController get _effectiveScrollController => widget.controller ?? _fallbackScrollController!;
|
||||
|
||||
@override
|
||||
AxisDirection get axisDirection => widget.axisDirection;
|
||||
|
||||
@override
|
||||
TickerProvider get vsync => this;
|
||||
|
||||
@override
|
||||
BuildContext? get notificationContext => _gestureDetectorKey.currentContext;
|
||||
|
||||
@override
|
||||
BuildContext get storageContext => context;
|
||||
|
||||
@override
|
||||
String? get restorationId => widget.restorationId;
|
||||
final _RestorableScrollOffset _persistedScrollOffset = _RestorableScrollOffset();
|
||||
|
||||
late ScrollBehavior _configuration;
|
||||
ScrollPhysics? _physics;
|
||||
ScrollController? _fallbackScrollController;
|
||||
DeviceGestureSettings? _mediaQueryGestureSettings;
|
||||
|
||||
ScrollController get _effectiveScrollController => widget.controller ?? _fallbackScrollController!;
|
||||
|
||||
// Only call this from places that will definitely trigger a rebuild.
|
||||
void _updatePosition() {
|
||||
_configuration = widget.scrollBehavior ?? ScrollConfiguration.of(context);
|
||||
@@ -575,7 +607,6 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
// SEMANTICS
|
||||
|
||||
final GlobalKey _scrollSemanticsKey = GlobalKey();
|
||||
@@ -667,9 +698,6 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
TickerProvider get vsync => this;
|
||||
|
||||
@override
|
||||
@protected
|
||||
void setIgnorePointer(bool value) {
|
||||
@@ -683,12 +711,6 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
BuildContext? get notificationContext => _gestureDetectorKey.currentContext;
|
||||
|
||||
@override
|
||||
BuildContext get storageContext => context;
|
||||
|
||||
// TOUCH HANDLERS
|
||||
|
||||
Drag? _drag;
|
||||
@@ -909,9 +931,6 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
|
||||
properties.add(DiagnosticsProperty<ScrollPosition>('position', position));
|
||||
properties.add(DiagnosticsProperty<ScrollPhysics>('effective physics', _physics));
|
||||
}
|
||||
|
||||
@override
|
||||
String? get restorationId => widget.restorationId;
|
||||
}
|
||||
|
||||
/// A widget to handle selection for a scrollable.
|
||||
@@ -971,151 +990,6 @@ class _ScrollableSelectionHandlerState extends State<_ScrollableSelectionHandler
|
||||
}
|
||||
}
|
||||
|
||||
/// An auto scroller that scrolls the [scrollable] if a drag gesture drags close
|
||||
/// to its edge.
|
||||
///
|
||||
/// The scroll velocity is controlled by the [velocityScalar]:
|
||||
///
|
||||
/// velocity = <distance of overscroll> * [velocityScalar].
|
||||
class EdgeDraggingAutoScroller {
|
||||
/// Creates a auto scroller that scrolls the [scrollable].
|
||||
EdgeDraggingAutoScroller(this.scrollable, {this.onScrollViewScrolled, this.velocityScalar = _kDefaultAutoScrollVelocityScalar});
|
||||
|
||||
// An eyeballed value for a smooth scrolling experience.
|
||||
static const double _kDefaultAutoScrollVelocityScalar = 7;
|
||||
|
||||
/// The [Scrollable] this auto scroller is scrolling.
|
||||
final ScrollableState scrollable;
|
||||
|
||||
/// Called when a scroll view is scrolled.
|
||||
///
|
||||
/// The scroll view may be scrolled multiple times in a row until the drag
|
||||
/// target no longer triggers the auto scroll. This callback will be called
|
||||
/// in between each scroll.
|
||||
final VoidCallback? onScrollViewScrolled;
|
||||
|
||||
/// The velocity scalar per pixel over scroll.
|
||||
///
|
||||
/// It represents how the velocity scale with the over scroll distance. The
|
||||
/// auto-scroll velocity = <distance of overscroll> * velocityScalar.
|
||||
final double velocityScalar;
|
||||
|
||||
late Rect _dragTargetRelatedToScrollOrigin;
|
||||
|
||||
/// Whether the auto scroll is in progress.
|
||||
bool get scrolling => _scrolling;
|
||||
bool _scrolling = false;
|
||||
|
||||
double _offsetExtent(Offset offset, Axis scrollDirection) {
|
||||
switch (scrollDirection) {
|
||||
case Axis.horizontal:
|
||||
return offset.dx;
|
||||
case Axis.vertical:
|
||||
return offset.dy;
|
||||
}
|
||||
}
|
||||
|
||||
double _sizeExtent(Size size, Axis scrollDirection) {
|
||||
switch (scrollDirection) {
|
||||
case Axis.horizontal:
|
||||
return size.width;
|
||||
case Axis.vertical:
|
||||
return size.height;
|
||||
}
|
||||
}
|
||||
|
||||
AxisDirection get _axisDirection => scrollable.axisDirection;
|
||||
Axis get _scrollDirection => axisDirectionToAxis(_axisDirection);
|
||||
|
||||
/// Starts the auto scroll if the [dragTarget] is close to the edge.
|
||||
///
|
||||
/// The scroll starts to scroll the [scrollable] if the target rect is close
|
||||
/// to the edge of the [scrollable]; otherwise, it remains stationary.
|
||||
///
|
||||
/// If the scrollable is already scrolling, calling this method updates the
|
||||
/// previous dragTarget to the new value and continues scrolling if necessary.
|
||||
void startAutoScrollIfNecessary(Rect dragTarget) {
|
||||
final Offset deltaToOrigin = _getDeltaToScrollOrigin(scrollable);
|
||||
_dragTargetRelatedToScrollOrigin = dragTarget.translate(deltaToOrigin.dx, deltaToOrigin.dy);
|
||||
if (_scrolling) {
|
||||
// The change will be picked up in the next scroll.
|
||||
return;
|
||||
}
|
||||
if (!_scrolling) {
|
||||
_scroll();
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop any ongoing auto scrolling.
|
||||
void stopAutoScroll() {
|
||||
_scrolling = false;
|
||||
}
|
||||
|
||||
Future<void> _scroll() async {
|
||||
final RenderBox scrollRenderBox = scrollable.context.findRenderObject()! as RenderBox;
|
||||
final Rect globalRect = MatrixUtils.transformRect(
|
||||
scrollRenderBox.getTransformTo(null),
|
||||
Rect.fromLTWH(0, 0, scrollRenderBox.size.width, scrollRenderBox.size.height)
|
||||
);
|
||||
assert(
|
||||
globalRect.size.width >= _dragTargetRelatedToScrollOrigin.size.width &&
|
||||
globalRect.size.height >= _dragTargetRelatedToScrollOrigin.size.height,
|
||||
'Drag target size is larger than scrollable size, which may cause bouncing',
|
||||
);
|
||||
_scrolling = true;
|
||||
double? newOffset;
|
||||
const double overDragMax = 20.0;
|
||||
|
||||
final Offset deltaToOrigin = _getDeltaToScrollOrigin(scrollable);
|
||||
final Offset viewportOrigin = globalRect.topLeft.translate(deltaToOrigin.dx, deltaToOrigin.dy);
|
||||
final double viewportStart = _offsetExtent(viewportOrigin, _scrollDirection);
|
||||
final double viewportEnd = viewportStart + _sizeExtent(globalRect.size, _scrollDirection);
|
||||
|
||||
final double proxyStart = _offsetExtent(_dragTargetRelatedToScrollOrigin.topLeft, _scrollDirection);
|
||||
final double proxyEnd = _offsetExtent(_dragTargetRelatedToScrollOrigin.bottomRight, _scrollDirection);
|
||||
switch (_axisDirection) {
|
||||
case AxisDirection.up:
|
||||
case AxisDirection.left:
|
||||
if (proxyEnd > viewportEnd && scrollable.position.pixels > scrollable.position.minScrollExtent) {
|
||||
final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax);
|
||||
newOffset = math.max(scrollable.position.minScrollExtent, scrollable.position.pixels - overDrag);
|
||||
} else if (proxyStart < viewportStart && scrollable.position.pixels < scrollable.position.maxScrollExtent) {
|
||||
final double overDrag = math.min(viewportStart - proxyStart, overDragMax);
|
||||
newOffset = math.min(scrollable.position.maxScrollExtent, scrollable.position.pixels + overDrag);
|
||||
}
|
||||
break;
|
||||
case AxisDirection.right:
|
||||
case AxisDirection.down:
|
||||
if (proxyStart < viewportStart && scrollable.position.pixels > scrollable.position.minScrollExtent) {
|
||||
final double overDrag = math.min(viewportStart - proxyStart, overDragMax);
|
||||
newOffset = math.max(scrollable.position.minScrollExtent, scrollable.position.pixels - overDrag);
|
||||
} else if (proxyEnd > viewportEnd && scrollable.position.pixels < scrollable.position.maxScrollExtent) {
|
||||
final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax);
|
||||
newOffset = math.min(scrollable.position.maxScrollExtent, scrollable.position.pixels + overDrag);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (newOffset == null || (newOffset - scrollable.position.pixels).abs() < 1.0) {
|
||||
// Drag should not trigger scroll.
|
||||
_scrolling = false;
|
||||
return;
|
||||
}
|
||||
final Duration duration = Duration(milliseconds: (1000 / velocityScalar).round());
|
||||
await scrollable.position.animateTo(
|
||||
newOffset,
|
||||
duration: duration,
|
||||
curve: Curves.linear,
|
||||
);
|
||||
if (onScrollViewScrolled != null) {
|
||||
onScrollViewScrolled!();
|
||||
}
|
||||
if (_scrolling) {
|
||||
await _scroll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This updater handles the case where the selectables change frequently, and
|
||||
/// it optimizes toward scrolling updates.
|
||||
///
|
||||
@@ -1513,41 +1387,6 @@ Offset _getDeltaToScrollOrigin(ScrollableState scrollableState) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes the aspects of a Scrollable widget to inform inherited widgets
|
||||
/// like [ScrollBehavior] for decorating.
|
||||
///
|
||||
/// Decorations like [GlowingOverscrollIndicator]s and [Scrollbar]s require
|
||||
/// information about the Scrollable in order to be initialized.
|
||||
@immutable
|
||||
class ScrollableDetails {
|
||||
/// Creates a set of details describing the [Scrollable]. The [direction]
|
||||
/// cannot be null.
|
||||
const ScrollableDetails({
|
||||
required this.direction,
|
||||
required this.controller,
|
||||
this.clipBehavior,
|
||||
});
|
||||
|
||||
/// The direction in which this widget scrolls.
|
||||
///
|
||||
/// Cannot be null.
|
||||
final AxisDirection direction;
|
||||
|
||||
/// A [ScrollController] that can be used to control the position of the
|
||||
/// [Scrollable] widget.
|
||||
///
|
||||
/// This can be used by [ScrollBehavior] to apply a [Scrollbar] to the associated
|
||||
/// [Scrollable].
|
||||
final ScrollController controller;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// This can be used by [MaterialScrollBehavior] to clip [StretchingOverscrollIndicator].
|
||||
///
|
||||
/// Defaults to null.
|
||||
final Clip? clipBehavior;
|
||||
}
|
||||
|
||||
/// With [_ScrollSemantics] certain child [SemanticsNode]s can be
|
||||
/// excluded from the scrollable area for semantics purposes.
|
||||
///
|
||||
@@ -1695,246 +1534,6 @@ class _RenderScrollSemantics extends RenderProxyBox {
|
||||
}
|
||||
}
|
||||
|
||||
/// A typedef for a function that can calculate the offset for a type of scroll
|
||||
/// increment given a [ScrollIncrementDetails].
|
||||
///
|
||||
/// This function is used as the type for [Scrollable.incrementCalculator],
|
||||
/// which is called from a [ScrollAction].
|
||||
typedef ScrollIncrementCalculator = double Function(ScrollIncrementDetails details);
|
||||
|
||||
/// Describes the type of scroll increment that will be performed by a
|
||||
/// [ScrollAction] on a [Scrollable].
|
||||
///
|
||||
/// This is used to configure a [ScrollIncrementDetails] object to pass to a
|
||||
/// [ScrollIncrementCalculator] function on a [Scrollable].
|
||||
///
|
||||
/// {@template flutter.widgets.ScrollIncrementType.intent}
|
||||
/// This indicates the *intent* of the scroll, not necessarily the size. Not all
|
||||
/// scrollable areas will have the concept of a "line" or "page", but they can
|
||||
/// respond to the different standard key bindings that cause scrolling, which
|
||||
/// are bound to keys that people use to indicate a "line" scroll (e.g.
|
||||
/// control-arrowDown keys) or a "page" scroll (e.g. pageDown key). It is
|
||||
/// recommended that at least the relative magnitudes of the scrolls match
|
||||
/// expectations.
|
||||
/// {@endtemplate}
|
||||
enum ScrollIncrementType {
|
||||
/// Indicates that the [ScrollIncrementCalculator] should return the scroll
|
||||
/// distance it should move when the user requests to scroll by a "line".
|
||||
///
|
||||
/// The distance a "line" scrolls refers to what should happen when the key
|
||||
/// binding for "scroll down/up by a line" is triggered. It's up to the
|
||||
/// [ScrollIncrementCalculator] function to decide what that means for a
|
||||
/// particular scrollable.
|
||||
line,
|
||||
|
||||
/// Indicates that the [ScrollIncrementCalculator] should return the scroll
|
||||
/// distance it should move when the user requests to scroll by a "page".
|
||||
///
|
||||
/// The distance a "page" scrolls refers to what should happen when the key
|
||||
/// binding for "scroll down/up by a page" is triggered. It's up to the
|
||||
/// [ScrollIncrementCalculator] function to decide what that means for a
|
||||
/// particular scrollable.
|
||||
page,
|
||||
}
|
||||
|
||||
/// A details object that describes the type of scroll increment being requested
|
||||
/// of a [ScrollIncrementCalculator] function, as well as the current metrics
|
||||
/// for the scrollable.
|
||||
class ScrollIncrementDetails {
|
||||
/// A const constructor for a [ScrollIncrementDetails].
|
||||
///
|
||||
/// All of the arguments must not be null, and are required.
|
||||
const ScrollIncrementDetails({
|
||||
required this.type,
|
||||
required this.metrics,
|
||||
});
|
||||
|
||||
/// The type of scroll this is (e.g. line, page, etc.).
|
||||
///
|
||||
/// {@macro flutter.widgets.ScrollIncrementType.intent}
|
||||
final ScrollIncrementType type;
|
||||
|
||||
/// The current metrics of the scrollable that is being scrolled.
|
||||
final ScrollMetrics metrics;
|
||||
}
|
||||
|
||||
/// An [Intent] that represents scrolling the nearest scrollable by an amount
|
||||
/// appropriate for the [type] specified.
|
||||
///
|
||||
/// The actual amount of the scroll is determined by the
|
||||
/// [Scrollable.incrementCalculator], or by its defaults if that is not
|
||||
/// specified.
|
||||
class ScrollIntent extends Intent {
|
||||
/// Creates a const [ScrollIntent] that requests scrolling in the given
|
||||
/// [direction], with the given [type].
|
||||
const ScrollIntent({
|
||||
required this.direction,
|
||||
this.type = ScrollIncrementType.line,
|
||||
});
|
||||
|
||||
/// The direction in which to scroll the scrollable containing the focused
|
||||
/// widget.
|
||||
final AxisDirection direction;
|
||||
|
||||
/// The type of scrolling that is intended.
|
||||
final ScrollIncrementType type;
|
||||
}
|
||||
|
||||
/// An [Action] that scrolls the [Scrollable] that encloses the current
|
||||
/// [primaryFocus] by the amount configured in the [ScrollIntent] given to it.
|
||||
///
|
||||
/// If a Scrollable cannot be found above the current [primaryFocus], the
|
||||
/// [PrimaryScrollController] will be considered for default handling of
|
||||
/// [ScrollAction]s.
|
||||
///
|
||||
/// If [Scrollable.incrementCalculator] is null for the scrollable, the default
|
||||
/// for a [ScrollIntent.type] set to [ScrollIncrementType.page] is 80% of the
|
||||
/// size of the scroll window, and for [ScrollIncrementType.line], 50 logical
|
||||
/// pixels.
|
||||
class ScrollAction extends Action<ScrollIntent> {
|
||||
@override
|
||||
bool isEnabled(ScrollIntent intent) {
|
||||
final FocusNode? focus = primaryFocus;
|
||||
final bool contextIsValid = focus != null && focus.context != null;
|
||||
if (contextIsValid) {
|
||||
// Check for primary scrollable within the current context
|
||||
if (Scrollable.maybeOf(focus.context!) != null) {
|
||||
return true;
|
||||
}
|
||||
// Check for fallback scrollable with context from PrimaryScrollController
|
||||
final ScrollController? primaryScrollController = PrimaryScrollController.maybeOf(focus.context!);
|
||||
return primaryScrollController != null && primaryScrollController.hasClients;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns the scroll increment for a single scroll request, for use when
|
||||
/// scrolling using a hardware keyboard.
|
||||
///
|
||||
/// Must not be called when the position is null, or when any of the position
|
||||
/// metrics (pixels, viewportDimension, maxScrollExtent, minScrollExtent) are
|
||||
/// null. The type and state arguments must not be null, and the widget must
|
||||
/// have already been laid out so that the position fields are valid.
|
||||
static double _calculateScrollIncrement(ScrollableState state, { ScrollIncrementType type = ScrollIncrementType.line }) {
|
||||
assert(state.position.hasPixels);
|
||||
assert(state._physics == null || state._physics!.shouldAcceptUserOffset(state.position));
|
||||
if (state.widget.incrementCalculator != null) {
|
||||
return state.widget.incrementCalculator!(
|
||||
ScrollIncrementDetails(
|
||||
type: type,
|
||||
metrics: state.position,
|
||||
),
|
||||
);
|
||||
}
|
||||
switch (type) {
|
||||
case ScrollIncrementType.line:
|
||||
return 50.0;
|
||||
case ScrollIncrementType.page:
|
||||
return 0.8 * state.position.viewportDimension;
|
||||
}
|
||||
}
|
||||
|
||||
/// Find out how much of an increment to move by, taking the different
|
||||
/// directions into account.
|
||||
static double getDirectionalIncrement(ScrollableState state, ScrollIntent intent) {
|
||||
final double increment = _calculateScrollIncrement(state, type: intent.type);
|
||||
switch (intent.direction) {
|
||||
case AxisDirection.down:
|
||||
switch (state.axisDirection) {
|
||||
case AxisDirection.up:
|
||||
return -increment;
|
||||
case AxisDirection.down:
|
||||
return increment;
|
||||
case AxisDirection.right:
|
||||
case AxisDirection.left:
|
||||
return 0.0;
|
||||
}
|
||||
case AxisDirection.up:
|
||||
switch (state.axisDirection) {
|
||||
case AxisDirection.up:
|
||||
return increment;
|
||||
case AxisDirection.down:
|
||||
return -increment;
|
||||
case AxisDirection.right:
|
||||
case AxisDirection.left:
|
||||
return 0.0;
|
||||
}
|
||||
case AxisDirection.left:
|
||||
switch (state.axisDirection) {
|
||||
case AxisDirection.right:
|
||||
return -increment;
|
||||
case AxisDirection.left:
|
||||
return increment;
|
||||
case AxisDirection.up:
|
||||
case AxisDirection.down:
|
||||
return 0.0;
|
||||
}
|
||||
case AxisDirection.right:
|
||||
switch (state.axisDirection) {
|
||||
case AxisDirection.right:
|
||||
return increment;
|
||||
case AxisDirection.left:
|
||||
return -increment;
|
||||
case AxisDirection.up:
|
||||
case AxisDirection.down:
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void invoke(ScrollIntent intent) {
|
||||
ScrollableState? state = Scrollable.maybeOf(primaryFocus!.context!);
|
||||
if (state == null) {
|
||||
final ScrollController primaryScrollController = PrimaryScrollController.of(primaryFocus!.context!);
|
||||
assert (() {
|
||||
if (primaryScrollController.positions.length != 1) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary(
|
||||
'A ScrollAction was invoked with the PrimaryScrollController, but '
|
||||
'more than one ScrollPosition is attached.',
|
||||
),
|
||||
ErrorDescription(
|
||||
'Only one ScrollPosition can be manipulated by a ScrollAction at '
|
||||
'a time.',
|
||||
),
|
||||
ErrorHint(
|
||||
'The PrimaryScrollController can be inherited automatically by '
|
||||
'descendant ScrollViews based on the TargetPlatform and scroll '
|
||||
'direction. By default, the PrimaryScrollController is '
|
||||
'automatically inherited on mobile platforms for vertical '
|
||||
'ScrollViews. ScrollView.primary can also override this behavior.',
|
||||
),
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
||||
if (primaryScrollController.position.context.notificationContext == null
|
||||
&& Scrollable.maybeOf(primaryScrollController.position.context.notificationContext!) == null) {
|
||||
return;
|
||||
}
|
||||
state = Scrollable.maybeOf(primaryScrollController.position.context.notificationContext!);
|
||||
}
|
||||
assert(state != null, '$ScrollAction was invoked on a context that has no scrollable parent');
|
||||
assert(state!.position.hasPixels, 'Scrollable must be laid out before it can be scrolled via a ScrollAction');
|
||||
|
||||
// Don't do anything if the user isn't allowed to scroll.
|
||||
if (state!._physics != null && !state._physics!.shouldAcceptUserOffset(state.position)) {
|
||||
return;
|
||||
}
|
||||
final double increment = getDirectionalIncrement(state, intent);
|
||||
if (increment == 0.0) {
|
||||
return;
|
||||
}
|
||||
state.position.moveTo(
|
||||
state.position.pixels + increment,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Not using a RestorableDouble because we want to allow null values and override
|
||||
// [enabled].
|
||||
class _RestorableScrollOffset extends RestorableValue<double?> {
|
||||
|
||||
443
packages/flutter/lib/src/widgets/scrollable_helpers.dart
Normal file
443
packages/flutter/lib/src/widgets/scrollable_helpers.dart
Normal file
@@ -0,0 +1,443 @@
|
||||
// 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 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'actions.dart';
|
||||
import 'basic.dart';
|
||||
import 'focus_manager.dart';
|
||||
import 'framework.dart';
|
||||
import 'primary_scroll_controller.dart';
|
||||
import 'scroll_configuration.dart';
|
||||
import 'scroll_controller.dart';
|
||||
import 'scroll_metrics.dart';
|
||||
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.
|
||||
///
|
||||
/// Decorations like [GlowingOverscrollIndicator]s and [Scrollbar]s require
|
||||
/// information about the Scrollable in order to be initialized.
|
||||
@immutable
|
||||
class ScrollableDetails {
|
||||
/// Creates a set of details describing the [Scrollable]. The [direction]
|
||||
/// cannot be null.
|
||||
const ScrollableDetails({
|
||||
required this.direction,
|
||||
required this.controller,
|
||||
this.clipBehavior,
|
||||
});
|
||||
|
||||
/// The direction in which this widget scrolls.
|
||||
///
|
||||
/// Cannot be null.
|
||||
final AxisDirection direction;
|
||||
|
||||
/// A [ScrollController] that can be used to control the position of the
|
||||
/// [Scrollable] widget.
|
||||
///
|
||||
/// This can be used by [ScrollBehavior] to apply a [Scrollbar] to the associated
|
||||
/// [Scrollable].
|
||||
final ScrollController controller;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// This can be used by [MaterialScrollBehavior] to clip [StretchingOverscrollIndicator].
|
||||
///
|
||||
/// Defaults to null.
|
||||
final Clip? clipBehavior;
|
||||
}
|
||||
|
||||
/// An auto scroller that scrolls the [scrollable] if a drag gesture drags close
|
||||
/// to its edge.
|
||||
///
|
||||
/// The scroll velocity is controlled by the [velocityScalar]:
|
||||
///
|
||||
/// velocity = <distance of overscroll> * [velocityScalar].
|
||||
class EdgeDraggingAutoScroller {
|
||||
/// Creates a auto scroller that scrolls the [scrollable].
|
||||
EdgeDraggingAutoScroller(
|
||||
this.scrollable, {
|
||||
this.onScrollViewScrolled,
|
||||
this.velocityScalar = _kDefaultAutoScrollVelocityScalar,
|
||||
});
|
||||
|
||||
// An eyeballed value for a smooth scrolling experience.
|
||||
static const double _kDefaultAutoScrollVelocityScalar = 7;
|
||||
|
||||
/// The [Scrollable] this auto scroller is scrolling.
|
||||
final ScrollableState scrollable;
|
||||
|
||||
/// Called when a scroll view is scrolled.
|
||||
///
|
||||
/// The scroll view may be scrolled multiple times in a row until the drag
|
||||
/// target no longer triggers the auto scroll. This callback will be called
|
||||
/// in between each scroll.
|
||||
final VoidCallback? onScrollViewScrolled;
|
||||
|
||||
/// The velocity scalar per pixel over scroll.
|
||||
///
|
||||
/// It represents how the velocity scale with the over scroll distance. The
|
||||
/// auto-scroll velocity = <distance of overscroll> * velocityScalar.
|
||||
final double velocityScalar;
|
||||
|
||||
late Rect _dragTargetRelatedToScrollOrigin;
|
||||
|
||||
/// Whether the auto scroll is in progress.
|
||||
bool get scrolling => _scrolling;
|
||||
bool _scrolling = false;
|
||||
|
||||
double _offsetExtent(Offset offset, Axis scrollDirection) {
|
||||
switch (scrollDirection) {
|
||||
case Axis.horizontal:
|
||||
return offset.dx;
|
||||
case Axis.vertical:
|
||||
return offset.dy;
|
||||
}
|
||||
}
|
||||
|
||||
double _sizeExtent(Size size, Axis scrollDirection) {
|
||||
switch (scrollDirection) {
|
||||
case Axis.horizontal:
|
||||
return size.width;
|
||||
case Axis.vertical:
|
||||
return size.height;
|
||||
}
|
||||
}
|
||||
|
||||
AxisDirection get _axisDirection => scrollable.axisDirection;
|
||||
Axis get _scrollDirection => axisDirectionToAxis(_axisDirection);
|
||||
|
||||
/// Starts the auto scroll if the [dragTarget] is close to the edge.
|
||||
///
|
||||
/// The scroll starts to scroll the [scrollable] if the target rect is close
|
||||
/// to the edge of the [scrollable]; otherwise, it remains stationary.
|
||||
///
|
||||
/// If the scrollable is already scrolling, calling this method updates the
|
||||
/// previous dragTarget to the new value and continues scrolling if necessary.
|
||||
void startAutoScrollIfNecessary(Rect dragTarget) {
|
||||
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
|
||||
_dragTargetRelatedToScrollOrigin = dragTarget.translate(deltaToOrigin.dx, deltaToOrigin.dy);
|
||||
if (_scrolling) {
|
||||
// The change will be picked up in the next scroll.
|
||||
return;
|
||||
}
|
||||
assert(!_scrolling);
|
||||
_scroll();
|
||||
}
|
||||
|
||||
/// Stop any ongoing auto scrolling.
|
||||
void stopAutoScroll() {
|
||||
_scrolling = false;
|
||||
}
|
||||
|
||||
Future<void> _scroll() async {
|
||||
final RenderBox scrollRenderBox = scrollable.context.findRenderObject()! as RenderBox;
|
||||
final Rect globalRect = MatrixUtils.transformRect(
|
||||
scrollRenderBox.getTransformTo(null),
|
||||
Rect.fromLTWH(0, 0, scrollRenderBox.size.width, scrollRenderBox.size.height),
|
||||
);
|
||||
assert(
|
||||
globalRect.size.width >= _dragTargetRelatedToScrollOrigin.size.width &&
|
||||
globalRect.size.height >= _dragTargetRelatedToScrollOrigin.size.height,
|
||||
'Drag target size is larger than scrollable size, which may cause bouncing',
|
||||
);
|
||||
_scrolling = true;
|
||||
double? newOffset;
|
||||
const double overDragMax = 20.0;
|
||||
|
||||
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
|
||||
final Offset viewportOrigin = globalRect.topLeft.translate(deltaToOrigin.dx, deltaToOrigin.dy);
|
||||
final double viewportStart = _offsetExtent(viewportOrigin, _scrollDirection);
|
||||
final double viewportEnd = viewportStart + _sizeExtent(globalRect.size, _scrollDirection);
|
||||
|
||||
final double proxyStart = _offsetExtent(_dragTargetRelatedToScrollOrigin.topLeft, _scrollDirection);
|
||||
final double proxyEnd = _offsetExtent(_dragTargetRelatedToScrollOrigin.bottomRight, _scrollDirection);
|
||||
switch (_axisDirection) {
|
||||
case AxisDirection.up:
|
||||
case AxisDirection.left:
|
||||
if (proxyEnd > viewportEnd && scrollable.position.pixels > scrollable.position.minScrollExtent) {
|
||||
final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax);
|
||||
newOffset = math.max(scrollable.position.minScrollExtent, scrollable.position.pixels - overDrag);
|
||||
} else if (proxyStart < viewportStart && scrollable.position.pixels < scrollable.position.maxScrollExtent) {
|
||||
final double overDrag = math.min(viewportStart - proxyStart, overDragMax);
|
||||
newOffset = math.min(scrollable.position.maxScrollExtent, scrollable.position.pixels + overDrag);
|
||||
}
|
||||
break;
|
||||
case AxisDirection.right:
|
||||
case AxisDirection.down:
|
||||
if (proxyStart < viewportStart && scrollable.position.pixels > scrollable.position.minScrollExtent) {
|
||||
final double overDrag = math.min(viewportStart - proxyStart, overDragMax);
|
||||
newOffset = math.max(scrollable.position.minScrollExtent, scrollable.position.pixels - overDrag);
|
||||
} else if (proxyEnd > viewportEnd && scrollable.position.pixels < scrollable.position.maxScrollExtent) {
|
||||
final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax);
|
||||
newOffset = math.min(scrollable.position.maxScrollExtent, scrollable.position.pixels + overDrag);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (newOffset == null || (newOffset - scrollable.position.pixels).abs() < 1.0) {
|
||||
// Drag should not trigger scroll.
|
||||
_scrolling = false;
|
||||
return;
|
||||
}
|
||||
final Duration duration = Duration(milliseconds: (1000 / velocityScalar).round());
|
||||
await scrollable.position.animateTo(
|
||||
newOffset,
|
||||
duration: duration,
|
||||
curve: Curves.linear,
|
||||
);
|
||||
if (onScrollViewScrolled != null) {
|
||||
onScrollViewScrolled!();
|
||||
}
|
||||
if (_scrolling) {
|
||||
await _scroll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A typedef for a function that can calculate the offset for a type of scroll
|
||||
/// increment given a [ScrollIncrementDetails].
|
||||
///
|
||||
/// This function is used as the type for [Scrollable.incrementCalculator],
|
||||
/// which is called from a [ScrollAction].
|
||||
typedef ScrollIncrementCalculator = double Function(ScrollIncrementDetails details);
|
||||
|
||||
/// Describes the type of scroll increment that will be performed by a
|
||||
/// [ScrollAction] on a [Scrollable].
|
||||
///
|
||||
/// This is used to configure a [ScrollIncrementDetails] object to pass to a
|
||||
/// [ScrollIncrementCalculator] function on a [Scrollable].
|
||||
///
|
||||
/// {@template flutter.widgets.ScrollIncrementType.intent}
|
||||
/// This indicates the *intent* of the scroll, not necessarily the size. Not all
|
||||
/// scrollable areas will have the concept of a "line" or "page", but they can
|
||||
/// respond to the different standard key bindings that cause scrolling, which
|
||||
/// are bound to keys that people use to indicate a "line" scroll (e.g.
|
||||
/// control-arrowDown keys) or a "page" scroll (e.g. pageDown key). It is
|
||||
/// recommended that at least the relative magnitudes of the scrolls match
|
||||
/// expectations.
|
||||
/// {@endtemplate}
|
||||
enum ScrollIncrementType {
|
||||
/// Indicates that the [ScrollIncrementCalculator] should return the scroll
|
||||
/// distance it should move when the user requests to scroll by a "line".
|
||||
///
|
||||
/// The distance a "line" scrolls refers to what should happen when the key
|
||||
/// binding for "scroll down/up by a line" is triggered. It's up to the
|
||||
/// [ScrollIncrementCalculator] function to decide what that means for a
|
||||
/// particular scrollable.
|
||||
line,
|
||||
|
||||
/// Indicates that the [ScrollIncrementCalculator] should return the scroll
|
||||
/// distance it should move when the user requests to scroll by a "page".
|
||||
///
|
||||
/// The distance a "page" scrolls refers to what should happen when the key
|
||||
/// binding for "scroll down/up by a page" is triggered. It's up to the
|
||||
/// [ScrollIncrementCalculator] function to decide what that means for a
|
||||
/// particular scrollable.
|
||||
page,
|
||||
}
|
||||
|
||||
/// A details object that describes the type of scroll increment being requested
|
||||
/// of a [ScrollIncrementCalculator] function, as well as the current metrics
|
||||
/// for the scrollable.
|
||||
class ScrollIncrementDetails {
|
||||
/// A const constructor for a [ScrollIncrementDetails].
|
||||
///
|
||||
/// All of the arguments must not be null, and are required.
|
||||
const ScrollIncrementDetails({
|
||||
required this.type,
|
||||
required this.metrics,
|
||||
});
|
||||
|
||||
/// The type of scroll this is (e.g. line, page, etc.).
|
||||
///
|
||||
/// {@macro flutter.widgets.ScrollIncrementType.intent}
|
||||
final ScrollIncrementType type;
|
||||
|
||||
/// The current metrics of the scrollable that is being scrolled.
|
||||
final ScrollMetrics metrics;
|
||||
}
|
||||
|
||||
/// An [Intent] that represents scrolling the nearest scrollable by an amount
|
||||
/// appropriate for the [type] specified.
|
||||
///
|
||||
/// The actual amount of the scroll is determined by the
|
||||
/// [Scrollable.incrementCalculator], or by its defaults if that is not
|
||||
/// specified.
|
||||
class ScrollIntent extends Intent {
|
||||
/// Creates a const [ScrollIntent] that requests scrolling in the given
|
||||
/// [direction], with the given [type].
|
||||
const ScrollIntent({
|
||||
required this.direction,
|
||||
this.type = ScrollIncrementType.line,
|
||||
});
|
||||
|
||||
/// The direction in which to scroll the scrollable containing the focused
|
||||
/// widget.
|
||||
final AxisDirection direction;
|
||||
|
||||
/// The type of scrolling that is intended.
|
||||
final ScrollIncrementType type;
|
||||
}
|
||||
|
||||
/// An [Action] that scrolls the [Scrollable] that encloses the current
|
||||
/// [primaryFocus] by the amount configured in the [ScrollIntent] given to it.
|
||||
///
|
||||
/// If a Scrollable cannot be found above the current [primaryFocus], the
|
||||
/// [PrimaryScrollController] will be considered for default handling of
|
||||
/// [ScrollAction]s.
|
||||
///
|
||||
/// If [Scrollable.incrementCalculator] is null for the scrollable, the default
|
||||
/// for a [ScrollIntent.type] set to [ScrollIncrementType.page] is 80% of the
|
||||
/// size of the scroll window, and for [ScrollIncrementType.line], 50 logical
|
||||
/// pixels.
|
||||
class ScrollAction extends Action<ScrollIntent> {
|
||||
@override
|
||||
bool isEnabled(ScrollIntent intent) {
|
||||
final FocusNode? focus = primaryFocus;
|
||||
final bool contextIsValid = focus != null && focus.context != null;
|
||||
if (contextIsValid) {
|
||||
// Check for primary scrollable within the current context
|
||||
if (Scrollable.maybeOf(focus.context!) != null) {
|
||||
return true;
|
||||
}
|
||||
// Check for fallback scrollable with context from PrimaryScrollController
|
||||
final ScrollController? primaryScrollController = PrimaryScrollController.maybeOf(focus.context!);
|
||||
return primaryScrollController != null && primaryScrollController.hasClients;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns the scroll increment for a single scroll request, for use when
|
||||
/// scrolling using a hardware keyboard.
|
||||
///
|
||||
/// Must not be called when the position is null, or when any of the position
|
||||
/// metrics (pixels, viewportDimension, maxScrollExtent, minScrollExtent) are
|
||||
/// null. The type and state arguments must not be null, and the widget must
|
||||
/// have already been laid out so that the position fields are valid.
|
||||
static double _calculateScrollIncrement(ScrollableState state, { ScrollIncrementType type = ScrollIncrementType.line }) {
|
||||
assert(state.position.hasPixels);
|
||||
assert(state.resolvedPhysics == null || state.resolvedPhysics!.shouldAcceptUserOffset(state.position));
|
||||
if (state.widget.incrementCalculator != null) {
|
||||
return state.widget.incrementCalculator!(
|
||||
ScrollIncrementDetails(
|
||||
type: type,
|
||||
metrics: state.position,
|
||||
),
|
||||
);
|
||||
}
|
||||
switch (type) {
|
||||
case ScrollIncrementType.line:
|
||||
return 50.0;
|
||||
case ScrollIncrementType.page:
|
||||
return 0.8 * state.position.viewportDimension;
|
||||
}
|
||||
}
|
||||
|
||||
/// Find out how much of an increment to move by, taking the different
|
||||
/// directions into account.
|
||||
static double getDirectionalIncrement(ScrollableState state, ScrollIntent intent) {
|
||||
final double increment = _calculateScrollIncrement(state, type: intent.type);
|
||||
switch (intent.direction) {
|
||||
case AxisDirection.down:
|
||||
switch (state.axisDirection) {
|
||||
case AxisDirection.up:
|
||||
return -increment;
|
||||
case AxisDirection.down:
|
||||
return increment;
|
||||
case AxisDirection.right:
|
||||
case AxisDirection.left:
|
||||
return 0.0;
|
||||
}
|
||||
case AxisDirection.up:
|
||||
switch (state.axisDirection) {
|
||||
case AxisDirection.up:
|
||||
return increment;
|
||||
case AxisDirection.down:
|
||||
return -increment;
|
||||
case AxisDirection.right:
|
||||
case AxisDirection.left:
|
||||
return 0.0;
|
||||
}
|
||||
case AxisDirection.left:
|
||||
switch (state.axisDirection) {
|
||||
case AxisDirection.right:
|
||||
return -increment;
|
||||
case AxisDirection.left:
|
||||
return increment;
|
||||
case AxisDirection.up:
|
||||
case AxisDirection.down:
|
||||
return 0.0;
|
||||
}
|
||||
case AxisDirection.right:
|
||||
switch (state.axisDirection) {
|
||||
case AxisDirection.right:
|
||||
return increment;
|
||||
case AxisDirection.left:
|
||||
return -increment;
|
||||
case AxisDirection.up:
|
||||
case AxisDirection.down:
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void invoke(ScrollIntent intent) {
|
||||
ScrollableState? state = Scrollable.maybeOf(primaryFocus!.context!);
|
||||
if (state == null) {
|
||||
final ScrollController primaryScrollController = PrimaryScrollController.of(primaryFocus!.context!);
|
||||
assert (() {
|
||||
if (primaryScrollController.positions.length != 1) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary(
|
||||
'A ScrollAction was invoked with the PrimaryScrollController, but '
|
||||
'more than one ScrollPosition is attached.',
|
||||
),
|
||||
ErrorDescription(
|
||||
'Only one ScrollPosition can be manipulated by a ScrollAction at '
|
||||
'a time.',
|
||||
),
|
||||
ErrorHint(
|
||||
'The PrimaryScrollController can be inherited automatically by '
|
||||
'descendant ScrollViews based on the TargetPlatform and scroll '
|
||||
'direction. By default, the PrimaryScrollController is '
|
||||
'automatically inherited on mobile platforms for vertical '
|
||||
'ScrollViews. ScrollView.primary can also override this behavior.',
|
||||
),
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
||||
if (primaryScrollController.position.context.notificationContext == null
|
||||
&& Scrollable.maybeOf(primaryScrollController.position.context.notificationContext!) == null) {
|
||||
return;
|
||||
}
|
||||
state = Scrollable.maybeOf(primaryScrollController.position.context.notificationContext!);
|
||||
}
|
||||
assert(state != null, '$ScrollAction was invoked on a context that has no scrollable parent');
|
||||
assert(state!.position.hasPixels, 'Scrollable must be laid out before it can be scrolled via a ScrollAction');
|
||||
|
||||
// Don't do anything if the user isn't allowed to scroll.
|
||||
if (state!.resolvedPhysics != null && !state.resolvedPhysics!.shouldAcceptUserOffset(state.position)) {
|
||||
return;
|
||||
}
|
||||
final double increment = getDirectionalIncrement(state, intent);
|
||||
if (increment == 0.0) {
|
||||
return;
|
||||
}
|
||||
state.position.moveTo(
|
||||
state.position.pixels + increment,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import 'scroll_metrics.dart';
|
||||
import 'scroll_notification.dart';
|
||||
import 'scroll_position.dart';
|
||||
import 'scrollable.dart';
|
||||
import 'scrollable_helpers.dart';
|
||||
import 'ticker_provider.dart';
|
||||
|
||||
const double _kMinThumbExtent = 18.0;
|
||||
|
||||
@@ -115,6 +115,7 @@ export 'src/widgets/scroll_position_with_single_context.dart';
|
||||
export 'src/widgets/scroll_simulation.dart';
|
||||
export 'src/widgets/scroll_view.dart';
|
||||
export 'src/widgets/scrollable.dart';
|
||||
export 'src/widgets/scrollable_helpers.dart';
|
||||
export 'src/widgets/scrollbar.dart';
|
||||
export 'src/widgets/selectable_region.dart';
|
||||
export 'src/widgets/selection_container.dart';
|
||||
|
||||
@@ -1690,6 +1690,57 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('deltaToScrollOrigin getter', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true), kind: ui.PointerDeviceKind.unknown);
|
||||
expect(getScrollOffset(tester), 0.0);
|
||||
await gesture.moveBy(const Offset(0.0, -200));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(getScrollOffset(tester), 200);
|
||||
final ScrollableState scrollable = tester.state(find.byType(Scrollable));
|
||||
expect(scrollable.deltaToScrollOrigin, const Offset(0.0, 200));
|
||||
});
|
||||
|
||||
testWidgets('resolvedPhysics getter', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData.light().copyWith(
|
||||
platform: TargetPlatform.android,
|
||||
),
|
||||
home: const CustomScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
slivers: <Widget>[
|
||||
SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true), kind: ui.PointerDeviceKind.unknown);
|
||||
expect(getScrollOffset(tester), 0.0);
|
||||
await gesture.moveBy(const Offset(0.0, -200));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(getScrollOffset(tester), 200);
|
||||
final ScrollableState scrollable = tester.state(find.byType(Scrollable));
|
||||
String types(ScrollPhysics? value) => value!.parent == null ? '${value.runtimeType}' : '${value.runtimeType} ${types(value.parent)}';
|
||||
|
||||
expect(
|
||||
types(scrollable.resolvedPhysics),
|
||||
'AlwaysScrollableScrollPhysics ClampingScrollPhysics RangeMaintainingScrollPhysics',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ignore: must_be_immutable
|
||||
|
||||
Reference in New Issue
Block a user