Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
7ce88694
Unverified
Commit
7ce88694
authored
Mar 10, 2023
by
Kate Lovett
Committed by
GitHub
Mar 10, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Clean up scrollable.dart for 2D (#122357)
Clean up scrollable.dart for 2D
parent
670f9d20
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
541 additions
and
443 deletions
+541
-443
app.dart
packages/flutter/lib/src/widgets/app.dart
+1
-1
default_text_editing_shortcuts.dart
...utter/lib/src/widgets/default_text_editing_shortcuts.dart
+1
-1
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+1
-0
reorderable_list.dart
packages/flutter/lib/src/widgets/reorderable_list.dart
+1
-0
scroll_configuration.dart
packages/flutter/lib/src/widgets/scroll_configuration.dart
+1
-0
scrollable.dart
packages/flutter/lib/src/widgets/scrollable.dart
+40
-441
scrollable_helpers.dart
packages/flutter/lib/src/widgets/scrollable_helpers.dart
+443
-0
scrollbar.dart
packages/flutter/lib/src/widgets/scrollbar.dart
+1
-0
widgets.dart
packages/flutter/lib/widgets.dart
+1
-0
scrollable_test.dart
packages/flutter/test/widgets/scrollable_test.dart
+51
-0
No files found.
packages/flutter/lib/src/widgets/app.dart
View file @
7ce88694
...
...
@@ -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'
;
...
...
packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart
View file @
7ce88694
...
...
@@ -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'
;
...
...
packages/flutter/lib/src/widgets/editable_text.dart
View file @
7ce88694
...
...
@@ -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'
;
...
...
packages/flutter/lib/src/widgets/reorderable_list.dart
View file @
7ce88694
...
...
@@ -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'
;
...
...
packages/flutter/lib/src/widgets/scroll_configuration.dart
View file @
7ce88694
...
...
@@ -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
);
...
...
packages/flutter/lib/src/widgets/scrollable.dart
View file @
7ce88694
...
...
@@ -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
?>
{
...
...
packages/flutter/lib/src/widgets/scrollable_helpers.dart
0 → 100644
View file @
7ce88694
// 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
,
);
}
}
packages/flutter/lib/src/widgets/scrollbar.dart
View file @
7ce88694
...
...
@@ -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
;
...
...
packages/flutter/lib/widgets.dart
View file @
7ce88694
...
...
@@ -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'
;
...
...
packages/flutter/test/widgets/scrollable_test.dart
View file @
7ce88694
...
...
@@ -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
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment