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
a6702f6a
Unverified
Commit
a6702f6a
authored
Nov 04, 2021
by
Casey Rogers
Committed by
GitHub
Nov 04, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Allow Programmatic Control of Draggable Sheet (#92440)
parent
1f6c6e26
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
543 additions
and
61 deletions
+543
-61
draggable_scrollable_sheet.dart
...s/flutter/lib/src/widgets/draggable_scrollable_sheet.dart
+207
-37
draggable_scrollable_sheet_test.dart
...flutter/test/widgets/draggable_scrollable_sheet_test.dart
+336
-24
No files found.
packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart
View file @
a6702f6a
...
...
@@ -6,6 +6,7 @@ import 'dart:math' as math;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/physics.dart'
;
import
'basic.dart'
;
import
'binding.dart'
;
...
...
@@ -34,6 +35,138 @@ typedef ScrollableWidgetBuilder = Widget Function(
ScrollController
scrollController
,
);
/// Controls a [DraggableScrollableSheet].
///
/// Draggable scrollable controllers are typically stored as member variables in
/// [State] objects and are reused in each [State.build]. Controllers can only
/// be used to control one sheet at a time. A controller can be reused with a
/// new sheet if the previous sheet has been disposed.
///
/// The controller's methods cannot be used until after the controller has been
/// passed into a [DraggableScrollableSheet] and the sheet has run initState.
class
DraggableScrollableController
{
_DraggableScrollableSheetScrollController
?
_attachedController
;
/// Get the current size (as a fraction of the parent height) of the attached sheet.
double
get
size
{
_assertAttached
();
return
_attachedController
!.
extent
.
currentSize
;
}
/// Get the current pixel height of the attached sheet.
double
get
pixels
{
_assertAttached
();
return
_attachedController
!.
extent
.
currentPixels
;
}
/// Convert a sheet's size (fractional value of parent container height) to pixels.
double
sizeToPixels
(
double
size
)
{
_assertAttached
();
return
_attachedController
!.
extent
.
sizeToPixels
(
size
);
}
/// Convert a sheet's pixel height to size (fractional value of parent container height).
double
pixelsToSize
(
double
pixels
)
{
_assertAttached
();
return
_attachedController
!.
extent
.
pixelsToSize
(
pixels
);
}
/// Animates the attached sheet from its current size to [size] to the
/// provided new `size`, a fractional value of the parent container's height.
///
/// Any active sheet animation is canceled. If the sheet's internal scrollable
/// is currently animating (e.g. responding to a user fling), that animation is
/// canceled as well.
///
/// An animation will be interrupted whenever the user attempts to scroll
/// manually, whenever another activity is started, or when the sheet hits its
/// max or min size (e.g. if you animate to 1 but the max size is .8, the
/// animation will stop playing when it reaches .8).
///
/// The duration must not be zero. To jump to a particular value without an
/// animation, use [jumpTo].
///
/// When calling [animateTo] in widget tests, `await`ing the returned
/// [Future] may cause the test to hang and timeout. Instead, use
/// [WidgetTester.pumpAndSettle].
Future
<
void
>
animateTo
(
double
size
,
{
required
Duration
duration
,
required
Curve
curve
,
})
async
{
_assertAttached
();
assert
(
size
>=
0
&&
size
<=
1
);
assert
(
duration
!=
Duration
.
zero
);
final
AnimationController
animationController
=
AnimationController
.
unbounded
(
vsync:
_attachedController
!.
position
.
context
.
vsync
,
value:
_attachedController
!.
extent
.
currentSize
,
);
_attachedController
!.
position
.
goIdle
();
// This disables any snapping until the next user interaction with the sheet.
_attachedController
!.
extent
.
hasDragged
=
false
;
_attachedController
!.
extent
.
startActivity
(
onCanceled:
()
{
// Don't stop the controller if it's already finished and may have been disposed.
if
(
animationController
.
isAnimating
)
{
animationController
.
stop
();
}
});
CurvedAnimation
(
parent:
animationController
,
curve:
curve
).
addListener
(()
{
_attachedController
!.
extent
.
updateSize
(
animationController
.
value
,
_attachedController
!.
position
.
context
.
notificationContext
!,
);
if
(
animationController
.
value
>
_attachedController
!.
extent
.
maxSize
||
animationController
.
value
<
_attachedController
!.
extent
.
minSize
)
{
// Animation hit the max or min size, stop animating.
animationController
.
stop
(
canceled:
false
);
}
});
await
animationController
.
animateTo
(
size
,
duration:
duration
);
}
/// Jumps the attached sheet from its current size to the given [size], a
/// fractional value of the parent container's height.
///
/// If [size] is outside of a the attached sheet's min or max child size,
/// [jumpTo] will jump the sheet to the nearest valid size instead.
///
/// Any active sheet animation is canceled. If the sheet's inner scrollable
/// is currently animating (e.g. responding to a user fling), that animation is
/// canceled as well.
void
jumpTo
(
double
size
)
{
_assertAttached
();
assert
(
size
>=
0
&&
size
<=
1
);
// Call start activity to interrupt any other playing activities.
_attachedController
!.
extent
.
startActivity
(
onCanceled:
()
{});
_attachedController
!.
position
.
goIdle
();
_attachedController
!.
extent
.
hasDragged
=
false
;
_attachedController
!.
extent
.
updateSize
(
size
,
_attachedController
!.
position
.
context
.
notificationContext
!);
}
/// Reset the attached sheet to its initial size (see: [DraggableScrollableSheet.initialChildSize]).
void
reset
()
{
_assertAttached
();
_attachedController
!.
reset
();
}
void
_assertAttached
()
{
assert
(
_attachedController
!=
null
,
'DraggableScrollableController is not attached to a sheet. A DraggableScrollableController '
'must be used in a DraggableScrollableSheet before any of its methods are called.'
,
);
}
void
_attach
(
_DraggableScrollableSheetScrollController
scrollController
)
{
assert
(
_attachedController
==
null
,
'Draggable scrollable controller is already attached to a sheet.'
);
_attachedController
=
scrollController
;
}
void
_detach
()
{
_attachedController
=
null
;
}
}
/// A container for a [Scrollable] that responds to drag gestures by resizing
/// the scrollable until a limit is reached, and then scrolling.
///
...
...
@@ -118,6 +251,7 @@ class DraggableScrollableSheet extends StatefulWidget {
this
.
expand
=
true
,
this
.
snap
=
false
,
this
.
snapSizes
,
this
.
controller
,
required
this
.
builder
,
})
:
assert
(
initialChildSize
!=
null
),
assert
(
minChildSize
!=
null
),
...
...
@@ -194,6 +328,9 @@ class DraggableScrollableSheet extends StatefulWidget {
/// being built or since the last call to [DraggableScrollableActuator.reset].
final
List
<
double
>?
snapSizes
;
/// A controller that can be used to programmatically control this sheet.
final
DraggableScrollableController
?
controller
;
/// The builder that creates a child to display in this widget, which will
/// use the provided [ScrollController] to enable dragging and scrolling
/// of the contents.
...
...
@@ -293,7 +430,7 @@ class _DraggableSheetExtent {
required
this
.
initialSize
,
required
this
.
onSizeChanged
,
ValueNotifier
<
double
>?
currentSize
,
bool
?
has
Chan
ged
,
bool
?
has
Drag
ged
,
})
:
assert
(
minSize
!=
null
),
assert
(
maxSize
!=
null
),
assert
(
initialSize
!=
null
),
...
...
@@ -304,7 +441,9 @@ class _DraggableSheetExtent {
_currentSize
=
(
currentSize
??
ValueNotifier
<
double
>(
initialSize
))
..
addListener
(
onSizeChanged
),
availablePixels
=
double
.
infinity
,
hasChanged
=
hasChanged
??
false
;
hasDragged
=
hasDragged
??
false
;
VoidCallback
?
_cancelActivity
;
final
double
minSize
;
final
double
maxSize
;
...
...
@@ -315,19 +454,13 @@ class _DraggableSheetExtent {
final
VoidCallback
onSizeChanged
;
double
availablePixels
;
// Used to disable snapping until the user interacts with the sheet. We set
// this to false on initialization and after programmatic interaction with the
// sheet to prevent snapping away from the initial size and after animateTo/jumpTo.
bool
hasChanged
;
// Used to disable snapping until the user has dragged on the sheet. We do
// this because we don't want to snap away from an initial or programmatically set size.
bool
hasDragged
;
bool
get
isAtMin
=>
minSize
>=
_currentSize
.
value
;
bool
get
isAtMax
=>
maxSize
<=
_currentSize
.
value
;
set
currentSize
(
double
value
)
{
assert
(
value
!=
null
);
hasChanged
=
true
;
_currentSize
.
value
=
value
.
clamp
(
minSize
,
maxSize
);
}
double
get
currentSize
=>
_currentSize
.
value
;
double
get
currentPixels
=>
sizeToPixels
(
_currentSize
.
value
);
...
...
@@ -335,18 +468,43 @@ class _DraggableSheetExtent {
double
get
additionalMaxSize
=>
isAtMax
?
0.0
:
1.0
;
List
<
double
>
get
pixelSnapSizes
=>
snapSizes
.
map
(
sizeToPixels
).
toList
();
/// Start an activity that affects the sheet and register a cancel call back
/// that will be called if another activity starts.
///
/// Note that `onCanceled` will get called even if the subsequent activity
/// started after this one finished so `onCanceled` should be safe to call at
/// any time.
void
startActivity
({
required
VoidCallback
onCanceled
})
{
_cancelActivity
?.
call
();
_cancelActivity
=
onCanceled
;
}
/// The scroll position gets inputs in terms of pixels, but the size is
/// expected to be expressed as a number between 0..1.
///
/// This should only be called to respond to a user drag. To update the
/// size in response to a programmatic call, use [updateSize] directly.
void
addPixelDelta
(
double
delta
,
BuildContext
context
)
{
// Stop any playing sheet animations.
_cancelActivity
?.
call
();
_cancelActivity
=
null
;
// The user has interacted with the sheet, set `hasDragged` to true so that
// we'll snap if applicable.
hasDragged
=
true
;
if
(
availablePixels
==
0
)
{
return
;
}
updateSize
(
currentSize
+
pixelsToSize
(
delta
),
context
);
}
/// Set the size to the new value. [newSize] should be a number between 0..1.
/// Set the size to the new value. [newSize] should be a number between
/// [minSize] and [maxSize].
///
/// This can be triggered by a programmatic (e.g. controller triggered) change
/// or a user drag.
void
updateSize
(
double
newSize
,
BuildContext
context
)
{
currentSize
=
newSize
;
assert
(
newSize
!=
null
);
_currentSize
.
value
=
newSize
.
clamp
(
minSize
,
maxSize
);
DraggableScrollableNotification
(
minExtent:
minSize
,
maxExtent:
maxSize
,
...
...
@@ -360,8 +518,8 @@ class _DraggableSheetExtent {
return
pixels
/
availablePixels
*
maxSize
;
}
double
sizeToPixels
(
double
extent
)
{
return
extent
/
maxSize
*
availablePixels
;
double
sizeToPixels
(
double
size
)
{
return
size
/
maxSize
*
availablePixels
;
}
void
dispose
()
{
...
...
@@ -383,11 +541,11 @@ class _DraggableSheetExtent {
snapSizes:
snapSizes
,
initialSize:
initialSize
,
onSizeChanged:
onSizeChanged
,
// Use the possibly updated initial
Extent
if the user hasn't dragged yet.
currentSize:
ValueNotifier
<
double
>(
has
Chan
ged
// Use the possibly updated initial
Size
if the user hasn't dragged yet.
currentSize:
ValueNotifier
<
double
>(
has
Drag
ged
?
_currentSize
.
value
.
clamp
(
minSize
,
maxSize
)
:
initialSize
),
has
Changed:
hasChan
ged
,
has
Dragged:
hasDrag
ged
,
);
}
}
...
...
@@ -405,9 +563,10 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
snap:
widget
.
snap
,
snapSizes:
_impliedSnapSizes
(),
initialSize:
widget
.
initialChildSize
,
onSizeChanged:
_set
Size
,
onSizeChanged:
_set
Extent
,
);
_scrollController
=
_DraggableScrollableSheetScrollController
(
extent:
_extent
);
widget
.
controller
?.
_attach
(
_scrollController
);
}
List
<
double
>
_impliedSnapSizes
()
{
...
...
@@ -418,8 +577,6 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
assert
(
index
==
0
||
snapSize
>
widget
.
snapSizes
![
index
-
1
],
'
${_snapSizeErrorMessage(index)}
\n
Snap sizes must be in ascending order. '
);
}
widget
.
snapSizes
?.
asMap
().
forEach
((
int
index
,
double
snapSize
)
{
});
// Ensure the snap sizes start and end with the min and max child sizes.
if
(
widget
.
snapSizes
==
null
||
widget
.
snapSizes
!.
isEmpty
)
{
return
<
double
>[
...
...
@@ -444,22 +601,11 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
void
didChangeDependencies
()
{
super
.
didChangeDependencies
();
if
(
_InheritedResetNotifier
.
shouldReset
(
context
))
{
// jumpTo can result in trying to replace semantics during build.
// Just animate really fast.
// Avoid doing it at all if the offset is already 0.0.
if
(
_scrollController
.
offset
!=
0.0
)
{
_scrollController
.
animateTo
(
0.0
,
duration:
const
Duration
(
milliseconds:
1
),
curve:
Curves
.
linear
,
);
}
_extent
.
hasChanged
=
false
;
_extent
.
_currentSize
.
value
=
_extent
.
initialSize
;
_scrollController
.
reset
();
}
}
void
_set
Size
()
{
void
_set
Extent
()
{
setState
(()
{
// _extent has been updated when this is called.
});
...
...
@@ -482,6 +628,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
@override
void
dispose
()
{
widget
.
controller
?.
_detach
();
_scrollController
.
dispose
();
_extent
.
dispose
();
super
.
dispose
();
...
...
@@ -495,7 +642,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
snap:
widget
.
snap
,
snapSizes:
_impliedSnapSizes
(),
initialSize:
widget
.
initialChildSize
,
onSizeChanged:
_set
Size
,
onSizeChanged:
_set
Extent
,
);
// Modify the existing scroll controller instead of replacing it so that
// developers listening to the controller do not have to rebuild their listeners.
...
...
@@ -513,7 +660,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
String
_snapSizeErrorMessage
(
int
invalidIndex
)
{
final
List
<
String
>
snapSizesWithIndicator
=
widget
.
snapSizes
!.
asMap
().
keys
.
map
(
(
int
index
)
{
(
int
index
)
{
final
String
snapSizeString
=
widget
.
snapSizes
![
index
].
toString
();
if
(
index
==
invalidIndex
)
{
return
'>>>
$snapSizeString
<<<'
;
...
...
@@ -576,6 +723,22 @@ class _DraggableScrollableSheetScrollController extends ScrollController {
@override
_DraggableScrollableSheetScrollPosition
get
position
=>
super
.
position
as
_DraggableScrollableSheetScrollPosition
;
void
reset
()
{
extent
.
_cancelActivity
?.
call
();
extent
.
hasDragged
=
false
;
// jumpTo can result in trying to replace semantics during build.
// Just animate really fast.
// Avoid doing it at all if the offset is already 0.0.
if
(
offset
!=
0.0
)
{
animateTo
(
0.0
,
duration:
const
Duration
(
milliseconds:
1
),
curve:
Curves
.
linear
,
);
}
extent
.
updateSize
(
extent
.
initialSize
,
position
.
context
.
notificationContext
!);
}
}
/// A scroll position that manages scroll activities for
...
...
@@ -653,7 +816,7 @@ class _DraggableScrollableSheetScrollPosition
},
);
}
bool
get
_shouldSnap
=>
extent
.
snap
&&
extent
.
has
Chan
ged
&&
!
_isAtSnapSize
;
bool
get
_shouldSnap
=>
extent
.
snap
&&
extent
.
has
Drag
ged
&&
!
_isAtSnapSize
;
@override
void
dispose
()
{
...
...
@@ -742,6 +905,13 @@ class _DraggableScrollableSheetScrollPosition
/// the user has tapped back if the sheet has started to cover more of the body
/// than when at its initial position. This is important for users of assistive
/// technology, where dragging may be difficult to communicate.
///
/// This is just a wrapper on top of [DraggableScrollableController]. It is
/// primarily useful for controlling a sheet in a part of the widget tree that
/// the current code does not control (e.g. library code trying to affect a sheet
/// in library users' code). Generally, it's easier to control the sheet
/// directly by creating a controller and passing the controller to the sheet in
/// its constructor (see [DraggableScrollableSheet.controller]).
class
DraggableScrollableActuator
extends
StatelessWidget
{
/// Creates a widget that can notify descendent [DraggableScrollableSheet]s
/// to reset to their initial position.
...
...
packages/flutter/test/widgets/draggable_scrollable_sheet_test.dart
View file @
a6702f6a
...
...
@@ -8,6 +8,7 @@ import 'package:flutter_test/flutter_test.dart';
void
main
(
)
{
Widget
_boilerplate
(
VoidCallback
?
onButtonPressed
,
{
DraggableScrollableController
?
controller
,
int
itemCount
=
100
,
double
initialChildSize
=
.
5
,
double
maxChildSize
=
1.0
,
...
...
@@ -33,6 +34,7 @@ void main() {
),
DraggableScrollableActuator
(
child:
DraggableScrollableSheet
(
controller:
controller
,
maxChildSize:
maxChildSize
,
minChildSize:
minChildSize
,
initialChildSize:
initialChildSize
,
...
...
@@ -346,33 +348,42 @@ void main() {
));
},
variant:
TargetPlatformVariant
.
all
());
testWidgets
(
'Does not snap away from initial child on reset'
,
(
WidgetTester
tester
)
async
{
const
Key
containerKey
=
ValueKey
<
String
>(
'container'
);
const
Key
stackKey
=
ValueKey
<
String
>(
'stack'
);
await
tester
.
pumpWidget
(
_boilerplate
(
null
,
snap:
true
,
containerKey:
containerKey
,
stackKey:
stackKey
,
));
await
tester
.
pumpAndSettle
();
final
double
screenHeight
=
tester
.
getSize
(
find
.
byKey
(
stackKey
)).
height
;
for
(
final
bool
useActuator
in
<
bool
>[
false
,
true
])
{
testWidgets
(
'Does not snap away from initial child on
${useActuator ? 'actuator' : 'controller'}
.reset()'
,
(
WidgetTester
tester
)
async
{
const
Key
containerKey
=
ValueKey
<
String
>(
'container'
);
const
Key
stackKey
=
ValueKey
<
String
>(
'stack'
);
final
DraggableScrollableController
controller
=
DraggableScrollableController
();
await
tester
.
pumpWidget
(
_boilerplate
(
null
,
controller:
controller
,
snap:
true
,
containerKey:
containerKey
,
stackKey:
stackKey
,
));
await
tester
.
pumpAndSettle
();
final
double
screenHeight
=
tester
.
getSize
(
find
.
byKey
(
stackKey
)).
height
;
await
tester
.
drag
(
find
.
text
(
'Item 1'
),
Offset
(
0
,
-.
4
*
screenHeight
));
await
tester
.
pumpAndSettle
();
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(
1.0
,
precisionErrorTolerance
),
);
await
tester
.
drag
(
find
.
text
(
'Item 1'
),
Offset
(
0
,
-.
4
*
screenHeight
));
await
tester
.
pumpAndSettle
();
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(
1.0
,
precisionErrorTolerance
),
);
DraggableScrollableActuator
.
reset
(
tester
.
element
(
find
.
byKey
(
containerKey
)));
await
tester
.
pumpAndSettle
();
if
(
useActuator
)
{
DraggableScrollableActuator
.
reset
(
tester
.
element
(
find
.
byKey
(
containerKey
)));
}
else
{
controller
.
reset
();
}
await
tester
.
pumpAndSettle
();
// The sheet should have reset without snapping away from initial child.
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(.
5
,
precisionErrorTolerance
),
);
},
variant:
TargetPlatformVariant
.
all
());
// The sheet should have reset without snapping away from initial child.
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(.
5
,
precisionErrorTolerance
),
);
});
}
testWidgets
(
'Zero velocity drag snaps to nearest snap target'
,
(
WidgetTester
tester
)
async
{
const
Key
stackKey
=
ValueKey
<
String
>(
'stack'
);
...
...
@@ -695,4 +706,305 @@ void main() {
expect
(
tester
.
takeException
(),
isNull
);
});
for
(
final
bool
shouldAnimate
in
<
bool
>[
true
,
false
])
{
testWidgets
(
'Can
${shouldAnimate ? 'animate' : 'jump'}
to arbitrary positions'
,
(
WidgetTester
tester
)
async
{
const
Key
stackKey
=
ValueKey
<
String
>(
'stack'
);
const
Key
containerKey
=
ValueKey
<
String
>(
'container'
);
final
DraggableScrollableController
controller
=
DraggableScrollableController
();
await
tester
.
pumpWidget
(
_boilerplate
(
null
,
controller:
controller
,
stackKey:
stackKey
,
containerKey:
containerKey
,
));
await
tester
.
pumpAndSettle
();
final
double
screenHeight
=
tester
.
getSize
(
find
.
byKey
(
stackKey
)).
height
;
// Use a local helper to animate so we can share code across a jumpTo test
// and an animateTo test.
void
goTo
(
double
size
)
=>
shouldAnimate
?
controller
.
animateTo
(
size
,
duration:
const
Duration
(
milliseconds:
200
),
curve:
Curves
.
linear
)
:
controller
.
jumpTo
(
size
);
// If we're animating, pump will call four times, two of which are for the
// animation duration.
final
int
expectedPumpCount
=
shouldAnimate
?
4
:
2
;
goTo
(.
6
);
expect
(
await
tester
.
pumpAndSettle
(),
expectedPumpCount
);
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(.
6
,
precisionErrorTolerance
),
);
expect
(
find
.
text
(
'Item 1'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 20'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 70'
),
findsNothing
);
goTo
(.
4
);
expect
(
await
tester
.
pumpAndSettle
(),
expectedPumpCount
);
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(.
4
,
precisionErrorTolerance
),
);
expect
(
find
.
text
(
'Item 1'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 20'
),
findsNothing
);
expect
(
find
.
text
(
'Item 70'
),
findsNothing
);
await
tester
.
fling
(
find
.
text
(
'Item 1'
),
Offset
(
0
,
-
screenHeight
),
100
);
await
tester
.
pumpAndSettle
();
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(
1
,
precisionErrorTolerance
),
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 20'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 70'
),
findsNothing
);
// Programmatic control does not affect the inner scrollable's position.
goTo
(.
8
);
expect
(
await
tester
.
pumpAndSettle
(),
expectedPumpCount
);
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(.
8
,
precisionErrorTolerance
),
);
expect
(
find
.
text
(
'Item 1'
),
findsNothing
);
expect
(
find
.
text
(
'Item 20'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 70'
),
findsNothing
);
// Attempting to move to a size too big or too small instead moves to the
// min or max child size.
goTo
(.
5
);
await
tester
.
pumpAndSettle
();
goTo
(
0
);
// The animation was cut short by half, there should have been on less pumps
final
int
truncatedPumpCount
=
shouldAnimate
?
expectedPumpCount
-
1
:
expectedPumpCount
;
expect
(
await
tester
.
pumpAndSettle
(),
truncatedPumpCount
);
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(.
25
,
precisionErrorTolerance
),
);
});
}
testWidgets
(
'Can reuse a controller after the old controller is disposed'
,
(
WidgetTester
tester
)
async
{
const
Key
stackKey
=
ValueKey
<
String
>(
'stack'
);
const
Key
containerKey
=
ValueKey
<
String
>(
'container'
);
final
DraggableScrollableController
controller
=
DraggableScrollableController
();
await
tester
.
pumpWidget
(
_boilerplate
(
null
,
controller:
controller
,
stackKey:
stackKey
,
containerKey:
containerKey
,
));
await
tester
.
pumpAndSettle
();
// Pump a new sheet with the same controller. This will dispose of the old sheet first.
await
tester
.
pumpWidget
(
_boilerplate
(
null
,
controller:
controller
,
stackKey:
stackKey
,
containerKey:
containerKey
,
));
await
tester
.
pumpAndSettle
();
final
double
screenHeight
=
tester
.
getSize
(
find
.
byKey
(
stackKey
)).
height
;
controller
.
jumpTo
(.
6
);
await
tester
.
pumpAndSettle
();
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(.
6
,
precisionErrorTolerance
),
);
});
testWidgets
(
'animateTo interrupts other animations'
,
(
WidgetTester
tester
)
async
{
const
Key
stackKey
=
ValueKey
<
String
>(
'stack'
);
const
Key
containerKey
=
ValueKey
<
String
>(
'container'
);
final
DraggableScrollableController
controller
=
DraggableScrollableController
();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
_boilerplate
(
null
,
controller:
controller
,
stackKey:
stackKey
,
containerKey:
containerKey
,
),
));
await
tester
.
pumpAndSettle
();
final
double
screenHeight
=
tester
.
getSize
(
find
.
byKey
(
stackKey
)).
height
;
await
tester
.
flingFrom
(
Offset
(
0
,
.
5
*
screenHeight
),
Offset
(
0
,
-.
5
*
screenHeight
),
2000
);
// Wait until `flinFrom` finished dragging, but before the scrollable goes ballistic.
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(
1
,
precisionErrorTolerance
),
);
expect
(
find
.
text
(
'Item 1'
),
findsOneWidget
);
controller
.
animateTo
(.
9
,
duration:
const
Duration
(
milliseconds:
200
),
curve:
Curves
.
linear
);
await
tester
.
pumpAndSettle
();
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(.
9
,
precisionErrorTolerance
),
);
// The ballistic animation should have been canceled so item 1 should still be visible.
expect
(
find
.
text
(
'Item 1'
),
findsOneWidget
);
});
testWidgets
(
'Other animations interrupt animateTo'
,
(
WidgetTester
tester
)
async
{
const
Key
stackKey
=
ValueKey
<
String
>(
'stack'
);
const
Key
containerKey
=
ValueKey
<
String
>(
'container'
);
final
DraggableScrollableController
controller
=
DraggableScrollableController
();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
_boilerplate
(
null
,
controller:
controller
,
stackKey:
stackKey
,
containerKey:
containerKey
,
),
));
await
tester
.
pumpAndSettle
();
final
double
screenHeight
=
tester
.
getSize
(
find
.
byKey
(
stackKey
)).
height
;
controller
.
animateTo
(
1
,
duration:
const
Duration
(
milliseconds:
200
),
curve:
Curves
.
linear
);
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
100
));
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(.
75
,
precisionErrorTolerance
),
);
// Interrupt animation and drag downward.
await
tester
.
drag
(
find
.
text
(
'Item 1'
),
Offset
(
0
,
.
1
*
screenHeight
));
await
tester
.
pumpAndSettle
();
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(.
65
,
precisionErrorTolerance
),
);
});
testWidgets
(
'animateTo can be interrupted by other animateTo or jumpTo'
,
(
WidgetTester
tester
)
async
{
const
Key
stackKey
=
ValueKey
<
String
>(
'stack'
);
const
Key
containerKey
=
ValueKey
<
String
>(
'container'
);
final
DraggableScrollableController
controller
=
DraggableScrollableController
();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
_boilerplate
(
null
,
controller:
controller
,
stackKey:
stackKey
,
containerKey:
containerKey
,
),
));
await
tester
.
pumpAndSettle
();
final
double
screenHeight
=
tester
.
getSize
(
find
.
byKey
(
stackKey
)).
height
;
controller
.
animateTo
(
1
,
duration:
const
Duration
(
milliseconds:
200
),
curve:
Curves
.
linear
);
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
100
));
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(.
75
,
precisionErrorTolerance
),
);
// Interrupt animation with a new `animateTo`.
controller
.
animateTo
(.
25
,
duration:
const
Duration
(
milliseconds:
200
),
curve:
Curves
.
linear
);
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
100
));
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(.
5
,
precisionErrorTolerance
),
);
// Interrupt animation with a jump.
controller
.
jumpTo
(.
6
);
await
tester
.
pumpAndSettle
();
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(.
6
,
precisionErrorTolerance
),
);
});
testWidgets
(
'Can get size and pixels'
,
(
WidgetTester
tester
)
async
{
const
Key
stackKey
=
ValueKey
<
String
>(
'stack'
);
const
Key
containerKey
=
ValueKey
<
String
>(
'container'
);
final
DraggableScrollableController
controller
=
DraggableScrollableController
();
await
tester
.
pumpWidget
(
_boilerplate
(
null
,
controller:
controller
,
stackKey:
stackKey
,
containerKey:
containerKey
,
));
await
tester
.
pumpAndSettle
();
final
double
screenHeight
=
tester
.
getSize
(
find
.
byKey
(
stackKey
)).
height
;
expect
(
controller
.
sizeToPixels
(.
25
),
.
25
*
screenHeight
);
expect
(
controller
.
pixelsToSize
(.
25
*
screenHeight
),
.
25
);
controller
.
animateTo
(.
6
,
duration:
const
Duration
(
milliseconds:
200
),
curve:
Curves
.
linear
);
await
tester
.
pumpAndSettle
();
expect
(
tester
.
getSize
(
find
.
byKey
(
containerKey
)).
height
/
screenHeight
,
closeTo
(.
6
,
precisionErrorTolerance
),
);
expect
(
controller
.
size
,
closeTo
(.
6
,
precisionErrorTolerance
));
expect
(
controller
.
pixels
,
closeTo
(.
6
*
screenHeight
,
precisionErrorTolerance
));
await
tester
.
drag
(
find
.
text
(
'Item 5'
),
Offset
(
0
,
.
2
*
screenHeight
));
expect
(
controller
.
size
,
closeTo
(.
4
,
precisionErrorTolerance
));
expect
(
controller
.
pixels
,
closeTo
(.
4
*
screenHeight
,
precisionErrorTolerance
));
});
testWidgets
(
'Cannot attach a controller to multiple sheets'
,
(
WidgetTester
tester
)
async
{
final
DraggableScrollableController
controller
=
DraggableScrollableController
();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Stack
(
children:
<
Widget
>[
_boilerplate
(
null
,
controller:
controller
,
),
_boilerplate
(
null
,
controller:
controller
,
)
],
),
),
null
,
EnginePhase
.
build
);
expect
(
tester
.
takeException
(),
isAssertionError
);
});
testWidgets
(
'Invalid controller interactions throw assertion errors'
,
(
WidgetTester
tester
)
async
{
final
DraggableScrollableController
controller
=
DraggableScrollableController
();
// Can't use a controller before attaching it.
expect
(()
=>
controller
.
jumpTo
(.
1
),
throwsAssertionError
);
expect
(()
=>
controller
.
pixels
,
throwsAssertionError
);
expect
(()
=>
controller
.
size
,
throwsAssertionError
);
expect
(()
=>
controller
.
pixelsToSize
(
0
),
throwsAssertionError
);
expect
(()
=>
controller
.
sizeToPixels
(
0
),
throwsAssertionError
);
await
tester
.
pumpWidget
(
_boilerplate
(
null
,
controller:
controller
,
));
// Can't jump or animate to invalid sizes.
expect
(()
=>
controller
.
jumpTo
(-
1
),
throwsAssertionError
);
expect
(()
=>
controller
.
jumpTo
(
1.1
),
throwsAssertionError
);
expect
(
()
=>
controller
.
animateTo
(-
1
,
duration:
const
Duration
(
milliseconds:
1
),
curve:
Curves
.
linear
),
throwsAssertionError
,
);
expect
(
()
=>
controller
.
animateTo
(
1.1
,
duration:
const
Duration
(
milliseconds:
1
),
curve:
Curves
.
linear
),
throwsAssertionError
,
);
// Can't use animateTo with a zero duration.
expect
(()
=>
controller
.
animateTo
(.
5
,
duration:
Duration
.
zero
,
curve:
Curves
.
linear
),
throwsAssertionError
);
});
}
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