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
6a48e663
Unverified
Commit
6a48e663
authored
Apr 26, 2019
by
Dan Field
Committed by
GitHub
Apr 26, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Bottom sheet scrolling (#21896)
parent
fdcc8aaf
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
1024 additions
and
151 deletions
+1024
-151
bottom_sheet.dart
packages/flutter/lib/src/material/bottom_sheet.dart
+107
-38
debug.dart
packages/flutter/lib/src/material/debug.dart
+34
-0
scaffold.dart
packages/flutter/lib/src/material/scaffold.dart
+330
-95
draggable_scrollable_sheet.dart
...s/flutter/lib/src/widgets/draggable_scrollable_sheet.dart
+228
-14
modal_bottom_sheet_test.dart
packages/flutter/test/material/modal_bottom_sheet_test.dart
+64
-1
persistent_bottom_sheet_test.dart
...s/flutter/test/material/persistent_bottom_sheet_test.dart
+261
-2
scaffold_test.dart
packages/flutter/test/material/scaffold_test.dart
+0
-1
No files found.
packages/flutter/lib/src/material/bottom_sheet.dart
View file @
6a48e663
...
...
@@ -5,6 +5,7 @@
import
'dart:async'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/scheduler.dart'
;
import
'package:flutter/widgets.dart'
;
import
'colors.dart'
;
...
...
@@ -14,9 +15,9 @@ import 'material_localizations.dart';
import
'scaffold.dart'
;
import
'theme.dart'
;
const
Duration
_
kB
ottomSheetDuration
=
Duration
(
milliseconds:
200
);
const
double
_
kM
inFlingVelocity
=
700.0
;
const
double
_
kC
loseProgressThreshold
=
0.5
;
const
Duration
_
b
ottomSheetDuration
=
Duration
(
milliseconds:
200
);
const
double
_
m
inFlingVelocity
=
700.0
;
const
double
_
c
loseProgressThreshold
=
0.5
;
/// A material design bottom sheet.
///
...
...
@@ -56,6 +57,7 @@ class BottomSheet extends StatefulWidget {
this
.
animationController
,
this
.
enableDrag
=
true
,
this
.
elevation
=
0.0
,
this
.
backgroundColor
,
@required
this
.
onClosing
,
@required
this
.
builder
,
})
:
assert
(
enableDrag
!=
null
),
...
...
@@ -64,7 +66,8 @@ class BottomSheet extends StatefulWidget {
assert
(
elevation
!=
null
&&
elevation
>=
0.0
),
super
(
key:
key
);
/// The animation that controls the bottom sheet's position.
/// The animation controller that controls the bottom sheet's entrance and
/// exit animations.
///
/// The BottomSheet widget will manipulate the position of this animation, it
/// is not just a passive observer.
...
...
@@ -83,8 +86,8 @@ class BottomSheet extends StatefulWidget {
/// [Material] widget.
final
WidgetBuilder
builder
;
/// If true, the bottom sheet can
dragged up and down and dismissed by swiping
///
downw
ards.
/// If true, the bottom sheet can
be dragged up and down and dismissed by
///
swiping down
ards.
///
/// Default is true.
final
bool
enableDrag
;
...
...
@@ -96,13 +99,23 @@ class BottomSheet extends StatefulWidget {
/// Defaults to 0. The value is non-negative.
final
double
elevation
;
/// The color for the [Material] of the bottom sheet.
///
/// Defaults to [Colors.white]. The value must not be null.
final
Color
backgroundColor
;
@override
_BottomSheetState
createState
()
=>
_BottomSheetState
();
/// Creates an animation controller suitable for controlling a [BottomSheet].
/// Creates an [AnimationController] suitable for a
/// [BottomSheet.animationController].
///
/// This API available as a convenience for a Material compliant bottom sheet
/// animation. If alternative animation durations are required, a different
/// animation controller could be provided.
static
AnimationController
createAnimationController
(
TickerProvider
vsync
)
{
return
AnimationController
(
duration:
_
kB
ottomSheetDuration
,
duration:
_
b
ottomSheetDuration
,
debugLabel:
'BottomSheet'
,
vsync:
vsync
,
);
...
...
@@ -121,35 +134,50 @@ class _BottomSheetState extends State<BottomSheet> {
bool
get
_dismissUnderway
=>
widget
.
animationController
.
status
==
AnimationStatus
.
reverse
;
void
_handleDragUpdate
(
DragUpdateDetails
details
)
{
assert
(
widget
.
enableDrag
);
if
(
_dismissUnderway
)
return
;
widget
.
animationController
.
value
-=
details
.
primaryDelta
/
(
_childHeight
??
details
.
primaryDelta
);
}
void
_handleDragEnd
(
DragEndDetails
details
)
{
assert
(
widget
.
enableDrag
);
if
(
_dismissUnderway
)
return
;
if
(
details
.
velocity
.
pixelsPerSecond
.
dy
>
_
kM
inFlingVelocity
)
{
if
(
details
.
velocity
.
pixelsPerSecond
.
dy
>
_
m
inFlingVelocity
)
{
final
double
flingVelocity
=
-
details
.
velocity
.
pixelsPerSecond
.
dy
/
_childHeight
;
if
(
widget
.
animationController
.
value
>
0.0
)
if
(
widget
.
animationController
.
value
>
0.0
)
{
widget
.
animationController
.
fling
(
velocity:
flingVelocity
);
if
(
flingVelocity
<
0.0
)
}
if
(
flingVelocity
<
0.0
)
{
widget
.
onClosing
();
}
else
if
(
widget
.
animationController
.
value
<
_kCloseProgressThreshold
)
{
}
}
else
if
(
widget
.
animationController
.
value
<
_closeProgressThreshold
)
{
if
(
widget
.
animationController
.
value
>
0.0
)
widget
.
animationController
.
fling
(
velocity:
-
1.0
);
widget
.
onClosing
();
}
else
{
widget
.
animationController
.
forward
();
}
}
bool
extentChanged
(
DraggableScrollableNotification
notification
)
{
if
(
notification
.
extent
==
notification
.
minExtent
)
{
widget
.
onClosing
();
}
return
false
;
}
@override
Widget
build
(
BuildContext
context
)
{
final
Widget
bottomSheet
=
Material
(
key:
_childKey
,
color:
widget
.
backgroundColor
,
elevation:
widget
.
elevation
,
child:
widget
.
builder
(
context
),
child:
NotificationListener
<
DraggableScrollableNotification
>(
onNotification:
extentChanged
,
child:
widget
.
builder
(
context
),
),
);
return
!
widget
.
enableDrag
?
bottomSheet
:
GestureDetector
(
onVerticalDragUpdate:
_handleDragUpdate
,
...
...
@@ -166,11 +194,11 @@ class _BottomSheetState extends State<BottomSheet> {
// MODAL BOTTOM SHEETS
class
_ModalBottomSheetLayout
extends
SingleChildLayoutDelegate
{
_ModalBottomSheetLayout
(
this
.
progress
);
_ModalBottomSheetLayout
(
this
.
progress
,
this
.
isScrollControlled
);
final
double
progress
;
final
bool
isScrollControlled
;
@override
BoxConstraints
getConstraintsForChild
(
BoxConstraints
constraints
)
{
...
...
@@ -178,7 +206,9 @@ class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
minWidth:
constraints
.
maxWidth
,
maxWidth:
constraints
.
maxWidth
,
minHeight:
0.0
,
maxHeight:
constraints
.
maxHeight
*
9.0
/
16.0
,
maxHeight:
isScrollControlled
?
constraints
.
maxHeight
:
constraints
.
maxHeight
*
9.0
/
16.0
,
);
}
...
...
@@ -194,33 +224,46 @@ class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
}
class
_ModalBottomSheet
<
T
>
extends
StatefulWidget
{
const
_ModalBottomSheet
({
Key
key
,
this
.
route
})
:
super
(
key:
key
);
const
_ModalBottomSheet
({
Key
key
,
this
.
route
,
this
.
isScrollControlled
=
false
,
})
:
assert
(
isScrollControlled
!=
null
),
super
(
key:
key
);
final
_ModalBottomSheetRoute
<
T
>
route
;
final
bool
isScrollControlled
;
@override
_ModalBottomSheetState
<
T
>
createState
()
=>
_ModalBottomSheetState
<
T
>();
}
class
_ModalBottomSheetState
<
T
>
extends
State
<
_ModalBottomSheet
<
T
>>
{
@override
Widget
build
(
BuildContext
context
)
{
final
MediaQueryData
mediaQuery
=
MediaQuery
.
of
(
context
);
final
MaterialLocalizations
localizations
=
MaterialLocalizations
.
of
(
context
);
String
routeLabel
;
String
_getRouteLabel
(
MaterialLocalizations
localizations
)
{
switch
(
defaultTargetPlatform
)
{
case
TargetPlatform
.
iOS
:
routeLabel
=
''
;
break
;
return
''
;
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
routeLabel
=
localizations
.
dialogLabel
;
break
;
return
localizations
.
dialogLabel
;
}
return
null
;
}
@override
Widget
build
(
BuildContext
context
)
{
assert
(
debugCheckHasMediaQuery
(
context
));
assert
(
debugCheckHasMaterialLocalizations
(
context
));
final
MediaQueryData
mediaQuery
=
MediaQuery
.
of
(
context
);
final
MaterialLocalizations
localizations
=
MaterialLocalizations
.
of
(
context
);
final
String
routeLabel
=
_getRouteLabel
(
localizations
);
return
GestureDetector
(
excludeFromSemantics:
true
,
onTap:
()
=>
Navigator
.
pop
(
context
),
onTap:
()
{
if
(
widget
.
route
.
isCurrent
)
Navigator
.
pop
(
context
);
},
child:
AnimatedBuilder
(
animation:
widget
.
route
.
animation
,
builder:
(
BuildContext
context
,
Widget
child
)
{
...
...
@@ -234,10 +277,15 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
explicitChildNodes:
true
,
child:
ClipRect
(
child:
CustomSingleChildLayout
(
delegate:
_ModalBottomSheetLayout
(
animationValue
),
delegate:
_ModalBottomSheetLayout
(
animationValue
,
widget
.
isScrollControlled
),
child:
BottomSheet
(
backgroundColor:
widget
.
route
.
backgroundColor
,
animationController:
widget
.
route
.
_animationController
,
onClosing:
()
=>
Navigator
.
pop
(
context
),
onClosing:
()
{
if
(
widget
.
route
.
isCurrent
)
{
Navigator
.
pop
(
context
);
}
},
builder:
widget
.
route
.
builder
,
),
),
...
...
@@ -254,14 +302,19 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
this
.
builder
,
this
.
theme
,
this
.
barrierLabel
,
@required
this
.
isScrollControlled
,
this
.
backgroundColor
,
RouteSettings
settings
,
})
:
super
(
settings:
settings
);
})
:
assert
(
isScrollControlled
!=
null
),
super
(
settings:
settings
);
final
WidgetBuilder
builder
;
final
ThemeData
theme
;
final
bool
isScrollControlled
;
final
Color
backgroundColor
;
@override
Duration
get
transitionDuration
=>
_
kB
ottomSheetDuration
;
Duration
get
transitionDuration
=>
_
b
ottomSheetDuration
;
@override
bool
get
barrierDismissible
=>
true
;
...
...
@@ -288,7 +341,7 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
Widget
bottomSheet
=
MediaQuery
.
removePadding
(
context:
context
,
removeTop:
true
,
child:
_ModalBottomSheet
<
T
>(
route:
this
),
child:
_ModalBottomSheet
<
T
>(
route:
this
,
isScrollControlled:
isScrollControlled
),
);
if
(
theme
!=
null
)
bottomSheet
=
Theme
(
data:
theme
,
child:
bottomSheet
);
...
...
@@ -312,6 +365,12 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
/// corresponding widget can be safely removed from the tree before the bottom
/// sheet is closed.
///
/// The `isScrollControlled` parameter specifies whether this is a route for
/// a bottom sheet that will utilize [DraggableScrollableSheet]. If you wish
/// to have a bottom sheet that has a scrollable child such as a [ListView] or
/// a [GridView] and have the bottom sheet be draggable, you should set this
/// parameter to true.
///
/// Returns a `Future` that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the modal bottom sheet was closed.
///
...
...
@@ -325,18 +384,26 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
Future
<
T
>
showModalBottomSheet
<
T
>({
@required
BuildContext
context
,
@required
WidgetBuilder
builder
,
bool
isScrollControlled
=
false
,
Color
backgroundColor
,
})
{
assert
(
context
!=
null
);
assert
(
builder
!=
null
);
assert
(
isScrollControlled
!=
null
);
assert
(
debugCheckHasMediaQuery
(
context
));
assert
(
debugCheckHasMaterialLocalizations
(
context
));
return
Navigator
.
push
(
context
,
_ModalBottomSheetRoute
<
T
>(
builder:
builder
,
theme:
Theme
.
of
(
context
,
shadowThemeOnly:
true
),
isScrollControlled:
isScrollControlled
,
backgroundColor:
backgroundColor
,
barrierLabel:
MaterialLocalizations
.
of
(
context
).
modalBarrierDismissLabel
,
));
}
/// Shows a persistent material design bottom sheet in the nearest [Scaffold].
/// Shows a material design bottom sheet in the nearest [Scaffold] ancestor. If
/// you wish to show a persistent bottom sheet, use [Scaffold.bottomSheet].
///
/// Returns a controller that can be used to close and otherwise manipulate the
/// bottom sheet.
...
...
@@ -353,10 +420,6 @@ Future<T> showModalBottomSheet<T>({
/// does not add a back button to the enclosing Scaffold's appbar, use the
/// [Scaffold.bottomSheet] constructor parameter.
///
/// A persistent bottom sheet shows information that supplements the primary
/// content of the app. A persistent bottom sheet remains visible even when
/// the user interacts with other parts of the app.
///
/// A closely related widget is a modal bottom sheet, which is an alternative
/// to a menu or a dialog and prevents the user from interacting with the rest
/// of the app. Modal bottom sheets can be created and displayed with the
...
...
@@ -376,8 +439,14 @@ Future<T> showModalBottomSheet<T>({
PersistentBottomSheetController
<
T
>
showBottomSheet
<
T
>({
@required
BuildContext
context
,
@required
WidgetBuilder
builder
,
Color
backgroundColor
,
})
{
assert
(
context
!=
null
);
assert
(
builder
!=
null
);
return
Scaffold
.
of
(
context
).
showBottomSheet
<
T
>(
builder
);
assert
(
debugCheckHasScaffold
(
context
));
return
Scaffold
.
of
(
context
).
showBottomSheet
<
T
>(
builder
,
backgroundColor:
backgroundColor
,
);
}
packages/flutter/lib/src/material/debug.dart
View file @
6a48e663
...
...
@@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart';
import
'material.dart'
;
import
'material_localizations.dart'
;
import
'scaffold.dart'
show
Scaffold
;
/// Asserts that the given context has a [Material] ancestor.
///
...
...
@@ -127,3 +128,36 @@ bool debugCheckHasMaterialLocalizations(BuildContext context) {
}());
return
true
;
}
/// Asserts that the given context has a [Scaffold] ancestor.
///
/// Used by various widgets to make sure that they are only used in an
/// appropriate context.
///
/// To invoke this function, use the following pattern, typically in the
/// relevant Widget's build method:
///
/// ```dart
/// assert(debugCheckHasScaffold(context));
/// ```
///
/// Does nothing if asserts are disabled. Always returns true.
bool
debugCheckHasScaffold
(
BuildContext
context
)
{
assert
(()
{
if
(
context
.
widget
is
!
Scaffold
&&
context
.
ancestorWidgetOfExactType
(
Scaffold
)
==
null
)
{
final
Element
element
=
context
;
throw
FlutterError
(
'No Scaffold widget found.
\n
'
'
${context.widget.runtimeType}
widgets require a Scaffold widget ancestor.
\n
'
'The Specific widget that could not find a Scaffold ancestor was:
\n
'
'
${context.widget}
\n
'
'The ownership chain for the affected widget is:
\n
'
'
${element.debugGetCreatorChain(10)}
\n
'
'Typically, the Scaffold widget is introduced by the MaterialApp or '
'WidgetsApp widget at the top of your application widget tree.'
);
}
return
true
;
}());
return
true
;
}
packages/flutter/lib/src/material/scaffold.dart
View file @
6a48e663
...
...
@@ -8,7 +8,6 @@ import 'dart:math' as math;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/scheduler.dart'
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter/gestures.dart'
show
DragStartBehavior
;
...
...
@@ -16,6 +15,7 @@ import 'app_bar.dart';
import
'bottom_sheet.dart'
;
import
'button_bar.dart'
;
import
'button_theme.dart'
;
import
'colors.dart'
;
import
'divider.dart'
;
import
'drawer.dart'
;
import
'flexible_space_bar.dart'
;
...
...
@@ -37,9 +37,16 @@ import 'theme_data.dart';
const
FloatingActionButtonLocation
_kDefaultFloatingActionButtonLocation
=
FloatingActionButtonLocation
.
endFloat
;
const
FloatingActionButtonAnimator
_kDefaultFloatingActionButtonAnimator
=
FloatingActionButtonAnimator
.
scaling
;
// When the top of the BottomSheet crosses this threshold, it will start to
// shrink the FAB and show a scrim.
const
double
_kBottomSheetDominatesPercentage
=
0.3
;
const
double
_kMinBottomSheetScrimOpacity
=
0.1
;
const
double
_kMaxBottomSheetScrimOpacity
=
0.6
;
enum
_ScaffoldSlot
{
body
,
appBar
,
bodyScrim
,
bottomSheet
,
snackBar
,
persistentFooter
,
...
...
@@ -451,6 +458,14 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
Size
bottomSheetSize
=
Size
.
zero
;
Size
snackBarSize
=
Size
.
zero
;
if
(
hasChild
(
_ScaffoldSlot
.
bodyScrim
))
{
final
BoxConstraints
bottomSheetScrimConstraints
=
BoxConstraints
(
maxWidth:
fullWidthConstraints
.
maxWidth
,
maxHeight:
contentBottom
,
);
layoutChild
(
_ScaffoldSlot
.
bodyScrim
,
bottomSheetScrimConstraints
);
positionChild
(
_ScaffoldSlot
.
bodyScrim
,
Offset
.
zero
);
}
// Set the size of the SnackBar early if the behavior is fixed so
// the FAB can be positioned correctly.
...
...
@@ -550,8 +565,10 @@ class _FloatingActionButtonTransition extends StatefulWidget {
@required
this
.
fabMoveAnimation
,
@required
this
.
fabMotionAnimator
,
@required
this
.
geometryNotifier
,
@required
this
.
currentController
,
})
:
assert
(
fabMoveAnimation
!=
null
),
assert
(
fabMotionAnimator
!=
null
),
assert
(
currentController
!=
null
),
super
(
key:
key
);
final
Widget
child
;
...
...
@@ -559,18 +576,19 @@ class _FloatingActionButtonTransition extends StatefulWidget {
final
FloatingActionButtonAnimator
fabMotionAnimator
;
final
_ScaffoldGeometryNotifier
geometryNotifier
;
/// Controls the current child widget.child as it exits.
final
AnimationController
currentController
;
@override
_FloatingActionButtonTransitionState
createState
()
=>
_FloatingActionButtonTransitionState
();
}
class
_FloatingActionButtonTransitionState
extends
State
<
_FloatingActionButtonTransition
>
with
TickerProviderStateMixin
{
// The animations applied to the Floating Action Button when it is entering or exiting.
// Controls the previous widget.child as it exits
// Controls the previous widget.child as it exits
.
AnimationController
_previousController
;
Animation
<
double
>
_previousScaleAnimation
;
Animation
<
double
>
_previousRotationAnimation
;
// Controls the current child widget.child as it exits
AnimationController
_currentController
;
// The animations to run, considering the widget's fabMoveAnimation and the current/previous entrance/exit animations.
Animation
<
double
>
_currentScaleAnimation
;
Animation
<
double
>
_extendedCurrentScaleAnimation
;
...
...
@@ -585,18 +603,12 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
duration:
kFloatingActionButtonSegue
,
vsync:
this
,
)..
addStatusListener
(
_handlePreviousAnimationStatusChanged
);
_currentController
=
AnimationController
(
duration:
kFloatingActionButtonSegue
,
vsync:
this
,
);
_updateAnimations
();
if
(
widget
.
child
!=
null
)
{
// If we start out with a child, have the child appear fully visible instead
// of animating in.
_
currentController
.
value
=
1.0
;
widget
.
currentController
.
value
=
1.0
;
}
else
{
// If we start without a child we update the geometry object with a
// floating action button scale of 0, as it is not showing on the screen.
...
...
@@ -607,7 +619,6 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
@override
void
dispose
()
{
_previousController
.
dispose
();
_currentController
.
dispose
();
super
.
dispose
();
}
...
...
@@ -623,13 +634,13 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
_updateAnimations
();
}
if
(
_previousController
.
status
==
AnimationStatus
.
dismissed
)
{
final
double
currentValue
=
_
currentController
.
value
;
final
double
currentValue
=
widget
.
currentController
.
value
;
if
(
currentValue
==
0.0
||
oldWidget
.
child
==
null
)
{
// The current child hasn't started its entrance animation yet. We can
// just skip directly to the new child's entrance.
_previousChild
=
null
;
if
(
widget
.
child
!=
null
)
_
currentController
.
forward
();
widget
.
currentController
.
forward
();
}
else
{
// Otherwise, we need to copy the state from the current controller to
// the previous controller and run an exit animation for the previous
...
...
@@ -638,7 +649,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
_previousController
..
value
=
currentValue
..
reverse
();
_
currentController
.
value
=
0.0
;
widget
.
currentController
.
value
=
0.0
;
}
}
}
...
...
@@ -662,10 +673,10 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
);
final
CurvedAnimation
currentEntranceScaleAnimation
=
CurvedAnimation
(
parent:
_
currentController
,
parent:
widget
.
currentController
,
curve:
Curves
.
easeIn
,
);
final
Animation
<
double
>
currentEntranceRotationAnimation
=
_
currentController
.
drive
(
_entranceTurnTween
);
final
Animation
<
double
>
currentEntranceRotationAnimation
=
widget
.
currentController
.
drive
(
_entranceTurnTween
);
// Get the animations for when the FAB is moving.
final
Animation
<
double
>
moveScaleAnimation
=
widget
.
fabMotionAnimator
.
getScaleAnimation
(
parent:
widget
.
fabMoveAnimation
);
...
...
@@ -686,9 +697,9 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
void
_handlePreviousAnimationStatusChanged
(
AnimationStatus
status
)
{
setState
(()
{
if
(
status
==
AnimationStatus
.
dismissed
)
{
assert
(
_
currentController
.
status
==
AnimationStatus
.
dismissed
);
assert
(
widget
.
currentController
.
status
==
AnimationStatus
.
dismissed
);
if
(
widget
.
child
!=
null
)
_
currentController
.
forward
();
widget
.
currentController
.
forward
();
}
});
}
...
...
@@ -1271,11 +1282,20 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
final
GlobalKey
<
DrawerControllerState
>
_drawerKey
=
GlobalKey
<
DrawerControllerState
>();
final
GlobalKey
<
DrawerControllerState
>
_endDrawerKey
=
GlobalKey
<
DrawerControllerState
>();
/// Whether this scaffold has a non-null [Scaffold.appBar].
bool
get
hasAppBar
=>
widget
.
appBar
!=
null
;
/// Whether this scaffold has a non-null [Scaffold.drawer].
bool
get
hasDrawer
=>
widget
.
drawer
!=
null
;
/// Whether this scaffold has a non-null [Scaffold.endDrawer].
bool
get
hasEndDrawer
=>
widget
.
endDrawer
!=
null
;
/// Whether this scaffold has a non-null [Scaffold.floatingActionButton].
bool
get
hasFloatingActionButton
=>
widget
.
floatingActionButton
!=
null
;
double
_appBarMaxHeight
;
/// The max height the [Scaffold.appBar] uses.
///
/// This is based on the appBar preferred height plus the top padding.
double
get
appBarMaxHeight
=>
_appBarMaxHeight
;
bool
_drawerOpened
=
false
;
bool
_endDrawerOpened
=
false
;
...
...
@@ -1455,85 +1475,168 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
// PERSISTENT BOTTOM SHEET API
final
List
<
_PersistentBottomSheet
>
_dismissedBottomSheets
=
<
_PersistentBottomSheet
>[];
// Contains bottom sheets that may still be animating out of view.
// Important if the app/user takes an action that could repeatedly show a
// bottom sheet.
final
List
<
_StandardBottomSheet
>
_dismissedBottomSheets
=
<
_StandardBottomSheet
>[];
PersistentBottomSheetController
<
dynamic
>
_currentBottomSheet
;
void
_maybeBuild
Curr
entBottomSheet
()
{
if
(
widget
.
bottomSheet
!=
null
)
{
void
_maybeBuild
Persist
entBottomSheet
()
{
if
(
widget
.
bottomSheet
!=
null
&&
_currentBottomSheet
==
null
)
{
// The new _currentBottomSheet is not a local history entry so a "back" button
// will not be added to the Scaffold's appbar and the bottom sheet will not
// support drag or swipe to dismiss.
final
AnimationController
animationController
=
BottomSheet
.
createAnimationController
(
this
)..
value
=
1.0
;
LocalHistoryEntry
_persistentSheetHistoryEntry
;
bool
_persistentBottomSheetExtentChanged
(
DraggableScrollableNotification
notification
)
{
if
(
notification
.
extent
>
notification
.
initialExtent
)
{
if
(
_persistentSheetHistoryEntry
==
null
)
{
_persistentSheetHistoryEntry
=
LocalHistoryEntry
(
onRemove:
()
{
if
(
notification
.
extent
>
notification
.
initialExtent
)
{
DraggableScrollableActuator
.
reset
(
notification
.
context
);
}
showBodyScrim
(
false
,
0.0
);
_floatingActionButtonVisibilityValue
=
1.0
;
_persistentSheetHistoryEntry
=
null
;
});
ModalRoute
.
of
(
context
).
addLocalHistoryEntry
(
_persistentSheetHistoryEntry
);
}
}
else
if
(
_persistentSheetHistoryEntry
!=
null
)
{
ModalRoute
.
of
(
context
).
removeLocalHistoryEntry
(
_persistentSheetHistoryEntry
);
}
return
false
;
}
_currentBottomSheet
=
_buildBottomSheet
<
void
>(
(
BuildContext
context
)
=>
widget
.
bottomSheet
,
BottomSheet
.
createAnimationController
(
this
)
..
value
=
1.0
,
false
,
(
BuildContext
context
)
{
return
NotificationListener
<
DraggableScrollableNotification
>(
onNotification:
_persistentBottomSheetExtentChanged
,
child:
DraggableScrollableActuator
(
child:
widget
.
bottomSheet
,
),
);
},
true
,
animationController:
animationController
,
);
}
}
void
_closeCurrentBottomSheet
()
{
if
(
_currentBottomSheet
!=
null
)
{
_currentBottomSheet
.
close
();
assert
(
_currentBottomSheet
==
null
);
if
(!
_currentBottomSheet
.
_isLocalHistoryEntry
)
{
_currentBottomSheet
.
close
();
}
assert
(()
{
_currentBottomSheet
?.
_completer
?.
future
?.
whenComplete
(()
{
assert
(
_currentBottomSheet
==
null
);
});
return
true
;
}());
}
}
PersistentBottomSheetController
<
T
>
_buildBottomSheet
<
T
>(
WidgetBuilder
builder
,
AnimationController
controller
,
bool
isLocalHistoryEntry
)
{
PersistentBottomSheetController
<
T
>
_buildBottomSheet
<
T
>(
WidgetBuilder
builder
,
bool
isPersistent
,
{
AnimationController
animationController
,
Color
backgroundColor
,
})
{
assert
(()
{
if
(
widget
.
bottomSheet
!=
null
&&
isPersistent
&&
_currentBottomSheet
!=
null
)
{
throw
FlutterError
(
'Scaffold.bottomSheet cannot be specified while a bottom sheet displayed '
'with showBottomSheet() is still visible.
\n
Rebuild the Scaffold with a null '
'bottomSheet before calling showBottomSheet().'
);
}
return
true
;
}());
final
Completer
<
T
>
completer
=
Completer
<
T
>();
final
GlobalKey
<
_
PersistentBottomSheetState
>
bottomSheetKey
=
GlobalKey
<
_Persistent
BottomSheetState
>();
_
Persistent
BottomSheet
bottomSheet
;
final
GlobalKey
<
_
StandardBottomSheetState
>
bottomSheetKey
=
GlobalKey
<
_Standard
BottomSheetState
>();
_
Standard
BottomSheet
bottomSheet
;
bool
removedEntry
=
false
;
void
_removeCurrentBottomSheet
()
{
removedEntry
=
true
;
if
(
_currentBottomSheet
==
null
)
{
return
;
}
assert
(
_currentBottomSheet
.
_widget
==
bottomSheet
);
assert
(
bottomSheetKey
.
currentState
!=
null
);
bottomSheetKey
.
currentState
.
close
();
if
(
controller
.
status
!=
AnimationStatus
.
dismissed
)
_dismissedBottomSheets
.
add
(
bottomSheet
);
setState
(()
{
_currentBottomSheet
=
null
;
});
completer
.
complete
();
_showFloatingActionButton
();
void
_closed
(
void
value
)
{
setState
(()
{
_currentBottomSheet
=
null
;
});
if
(
animationController
.
status
!=
AnimationStatus
.
dismissed
)
{
_dismissedBottomSheets
.
add
(
bottomSheet
);
}
completer
.
complete
();
}
final
Future
<
void
>
closing
=
bottomSheetKey
.
currentState
.
close
();
if
(
closing
!=
null
)
{
closing
.
then
(
_closed
);
}
else
{
_closed
(
null
);
}
}
final
LocalHistoryEntry
entry
=
isLocalHistoryEntry
?
LocalHistoryEntry
(
onRemove:
_removeCurrentBottomSheet
)
:
null
;
final
LocalHistoryEntry
entry
=
isPersistent
?
null
:
LocalHistoryEntry
(
onRemove:
()
{
if
(!
removedEntry
)
{
_removeCurrentBottomSheet
();
}
});
bottomSheet
=
_
Persistent
BottomSheet
(
bottomSheet
=
_
Standard
BottomSheet
(
key:
bottomSheetKey
,
animationController:
c
ontroller
,
enableDrag:
isLocalHistoryEntry
,
animationController:
animationC
ontroller
,
enableDrag:
!
isPersistent
,
onClosing:
()
{
if
(
_currentBottomSheet
==
null
)
{
return
;
}
assert
(
_currentBottomSheet
.
_widget
==
bottomSheet
);
if
(
isLocalHistoryEntry
)
if
(!
isPersistent
&&
!
removedEntry
)
{
assert
(
entry
!=
null
);
entry
.
remove
();
else
_removeCurrentBottomSheet
();
removedEntry
=
true
;
}
},
onDismissed:
()
{
if
(
_dismissedBottomSheets
.
contains
(
bottomSheet
))
{
bottomSheet
.
animationController
.
dispose
();
setState
(()
{
_dismissedBottomSheets
.
remove
(
bottomSheet
);
});
}
},
builder:
builder
,
isPersistent:
isPersistent
,
backgroundColor:
backgroundColor
,
);
if
(
isLocalHistoryEntry
)
if
(
!
isPersistent
)
ModalRoute
.
of
(
context
).
addLocalHistoryEntry
(
entry
);
return
PersistentBottomSheetController
<
T
>.
_
(
bottomSheet
,
completer
,
isLocalHistoryEntry
?
entry
.
remove
:
_removeCurrentBottomSheet
,
entry
!=
null
?
entry
.
remove
:
_removeCurrentBottomSheet
,
(
VoidCallback
fn
)
{
bottomSheetKey
.
currentState
?.
setState
(
fn
);
},
isLocalHistoryEntry
,
!
isPersistent
,
);
}
/// Shows a persistent material design bottom sheet in the nearest [Scaffold].
/// Shows a material design bottom sheet in the nearest [Scaffold]. To show
/// a persistent bottom sheet, use the [Scaffold.bottomSheet].
///
/// Returns a controller that can be used to close and otherwise manipulate the
/// bottom sheet.
...
...
@@ -1567,12 +1670,31 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
/// sheet.
/// * [Scaffold.of], for information about how to obtain the [ScaffoldState].
/// * <https://material.io/design/components/sheets-bottom.html#standard-bottom-sheet>
PersistentBottomSheetController
<
T
>
showBottomSheet
<
T
>(
WidgetBuilder
builder
)
{
PersistentBottomSheetController
<
T
>
showBottomSheet
<
T
>(
WidgetBuilder
builder
,
{
Color
backgroundColor
,
})
{
assert
(()
{
if
(
widget
.
bottomSheet
!=
null
)
{
throw
FlutterError
(
'Scaffold.bottomSheet cannot be specified while a bottom sheet displayed '
'with showBottomSheet() is still visible.
\n
Rebuild the Scaffold with a null '
'bottomSheet before calling showBottomSheet().'
);
}
return
true
;
}());
assert
(
debugCheckHasMediaQuery
(
context
));
_closeCurrentBottomSheet
();
final
AnimationController
controller
=
BottomSheet
.
createAnimationController
(
this
)
..
forward
();
final
AnimationController
controller
=
BottomSheet
.
createAnimationController
(
this
)..
forward
();
setState
(()
{
_currentBottomSheet
=
_buildBottomSheet
<
T
>(
builder
,
controller
,
true
);
_currentBottomSheet
=
_buildBottomSheet
<
T
>(
builder
,
false
,
animationController:
controller
,
backgroundColor:
backgroundColor
,
);
});
return
_currentBottomSheet
;
}
...
...
@@ -1583,6 +1705,27 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
FloatingActionButtonLocation
_previousFloatingActionButtonLocation
;
FloatingActionButtonLocation
_floatingActionButtonLocation
;
AnimationController
_floatingActionButtonVisibilityController
;
/// Gets the current value of the visibility animation for the
/// [Scaffold.floatingActionButton].
double
get
_floatingActionButtonVisibilityValue
=>
_floatingActionButtonVisibilityController
.
value
;
/// Sets the current value of the visibility animation for the
/// [Scaffold.floatingActionButton]. This value must not be null.
set
_floatingActionButtonVisibilityValue
(
double
newValue
)
{
assert
(
newValue
!=
null
);
_floatingActionButtonVisibilityController
.
value
=
newValue
.
clamp
(
_floatingActionButtonVisibilityController
.
lowerBound
,
_floatingActionButtonVisibilityController
.
upperBound
,
);
}
/// Shows the [Scaffold.floatingActionButton].
TickerFuture
_showFloatingActionButton
()
{
return
_floatingActionButtonVisibilityController
.
forward
();
}
// Moves the Floating Action Button to the new Floating Action Button Location.
void
_moveFloatingActionButton
(
final
FloatingActionButtonLocation
newLocation
)
{
FloatingActionButtonLocation
previousLocation
=
_floatingActionButtonLocation
;
...
...
@@ -1646,7 +1789,11 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
value:
1.0
,
duration:
kFloatingActionButtonSegue
*
2
,
);
_maybeBuildCurrentBottomSheet
();
_floatingActionButtonVisibilityController
=
AnimationController
(
duration:
kFloatingActionButtonSegue
,
vsync:
this
,
);
}
@override
...
...
@@ -1671,7 +1818,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
return
true
;
}());
_closeCurrentBottomSheet
();
_maybeBuild
Curr
entBottomSheet
();
_maybeBuild
Persist
entBottomSheet
();
}
super
.
didUpdateWidget
(
oldWidget
);
}
...
...
@@ -1690,6 +1837,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
hideCurrentSnackBar
(
reason:
SnackBarClosedReason
.
timeout
);
}
_accessibleNavigation
=
mediaQuery
.
accessibleNavigation
;
_maybeBuildPersistentBottomSheet
();
super
.
didChangeDependencies
();
}
...
...
@@ -1699,11 +1847,14 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
_snackBarTimer
?.
cancel
();
_snackBarTimer
=
null
;
_geometryNotifier
.
dispose
();
for
(
_PersistentBottomSheet
bottomSheet
in
_dismissedBottomSheets
)
bottomSheet
.
animationController
.
dispose
();
if
(
_currentBottomSheet
!=
null
)
_currentBottomSheet
.
_widget
.
animationController
.
dispose
();
for
(
_StandardBottomSheet
bottomSheet
in
_dismissedBottomSheets
)
{
bottomSheet
.
animationController
?.
dispose
();
}
if
(
_currentBottomSheet
!=
null
)
{
_currentBottomSheet
.
_widget
.
animationController
?.
dispose
();
}
_floatingActionButtonMoveController
.
dispose
();
_floatingActionButtonVisibilityController
.
dispose
();
super
.
dispose
();
}
...
...
@@ -1780,6 +1931,23 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
}
}
bool
_showBodyScrim
=
false
;
Color
_bodyScrimColor
=
Colors
.
black
;
/// Whether to show a [ModalBarrier] over the body of the scaffold.
///
/// The `value` parameter must not be null.
void
showBodyScrim
(
bool
value
,
double
opacity
)
{
assert
(
value
!=
null
);
if
(
_showBodyScrim
==
value
&&
_bodyScrimColor
.
opacity
==
opacity
)
{
return
;
}
setState
(()
{
_showBodyScrim
=
value
;
_bodyScrimColor
=
Colors
.
black
.
withOpacity
(
opacity
);
});
}
@override
Widget
build
(
BuildContext
context
)
{
assert
(
debugCheckHasMediaQuery
(
context
));
...
...
@@ -1822,17 +1990,31 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
removeBottomPadding:
widget
.
bottomNavigationBar
!=
null
||
widget
.
persistentFooterButtons
!=
null
,
removeBottomInset:
_resizeToAvoidBottomInset
,
);
if
(
_showBodyScrim
)
{
_addIfNonNull
(
children
,
ModalBarrier
(
dismissible:
false
,
color:
_bodyScrimColor
,
),
_ScaffoldSlot
.
bodyScrim
,
removeLeftPadding:
true
,
removeTopPadding:
true
,
removeRightPadding:
true
,
removeBottomPadding:
true
,
);
}
if
(
widget
.
appBar
!=
null
)
{
final
double
topPadding
=
widget
.
primary
?
mediaQuery
.
padding
.
top
:
0.0
;
final
double
exten
t
=
widget
.
appBar
.
preferredSize
.
height
+
topPadding
;
assert
(
extent
>=
0.0
&&
exten
t
.
isFinite
);
_appBarMaxHeigh
t
=
widget
.
appBar
.
preferredSize
.
height
+
topPadding
;
assert
(
_appBarMaxHeight
>=
0.0
&&
_appBarMaxHeigh
t
.
isFinite
);
_addIfNonNull
(
children
,
ConstrainedBox
(
constraints:
BoxConstraints
(
maxHeight:
exten
t
),
constraints:
BoxConstraints
(
maxHeight:
_appBarMaxHeigh
t
),
child:
FlexibleSpaceBar
.
createSettings
(
currentExtent:
exten
t
,
currentExtent:
_appBarMaxHeigh
t
,
child:
widget
.
appBar
,
),
),
...
...
@@ -1930,6 +2112,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
fabMoveAnimation:
_floatingActionButtonMoveController
,
fabMotionAnimator:
_floatingActionButtonAnimator
,
geometryNotifier:
_geometryNotifier
,
currentController:
_floatingActionButtonVisibilityController
,
),
_ScaffoldSlot
.
floatingActionButton
,
removeLeftPadding:
true
,
...
...
@@ -2023,85 +2206,137 @@ class ScaffoldFeatureController<T extends Widget, U> {
final
StateSetter
setState
;
}
class
_
Persistent
BottomSheet
extends
StatefulWidget
{
const
_
Persistent
BottomSheet
({
class
_
Standard
BottomSheet
extends
StatefulWidget
{
const
_
Standard
BottomSheet
({
Key
key
,
this
.
animationController
,
this
.
enableDrag
=
true
,
this
.
onClosing
,
this
.
onDismissed
,
this
.
builder
,
this
.
isPersistent
=
false
,
this
.
backgroundColor
,
})
:
super
(
key:
key
);
final
AnimationController
animationController
;
// we control it, but it must be disposed by whoever created it
final
AnimationController
animationController
;
// we control it, but it must be disposed by whoever created it
.
final
bool
enableDrag
;
final
VoidCallback
onClosing
;
final
VoidCallback
onDismissed
;
final
WidgetBuilder
builder
;
final
bool
isPersistent
;
final
Color
backgroundColor
;
@override
_
PersistentBottomSheetState
createState
()
=>
_Persistent
BottomSheetState
();
_
StandardBottomSheetState
createState
()
=>
_Standard
BottomSheetState
();
}
class
_
PersistentBottomSheetState
extends
State
<
_Persistent
BottomSheet
>
{
class
_
StandardBottomSheetState
extends
State
<
_Standard
BottomSheet
>
{
@override
void
initState
()
{
super
.
initState
();
assert
(
widget
.
animationController
!=
null
);
assert
(
widget
.
animationController
.
status
==
AnimationStatus
.
forward
||
widget
.
animationController
.
status
==
AnimationStatus
.
completed
);
widget
.
animationController
.
addStatusListener
(
_handleStatusChange
);
}
@override
void
didUpdateWidget
(
_
Persistent
BottomSheet
oldWidget
)
{
void
didUpdateWidget
(
_
Standard
BottomSheet
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
assert
(
widget
.
animationController
==
oldWidget
.
animationController
);
}
void
close
()
{
Future
<
void
>
close
()
{
assert
(
widget
.
animationController
!=
null
);
widget
.
animationController
.
reverse
();
if
(
widget
.
onClosing
!=
null
)
{
widget
.
onClosing
();
}
return
null
;
}
void
_handleStatusChange
(
AnimationStatus
status
)
{
if
(
status
==
AnimationStatus
.
dismissed
&&
widget
.
onDismissed
!=
null
)
if
(
status
==
AnimationStatus
.
dismissed
&&
widget
.
onDismissed
!=
null
)
{
widget
.
onDismissed
();
}
}
bool
extentChanged
(
DraggableScrollableNotification
notification
)
{
final
double
extentRemaining
=
1.0
-
notification
.
extent
;
final
ScaffoldState
scaffold
=
Scaffold
.
of
(
context
);
if
(
extentRemaining
<
_kBottomSheetDominatesPercentage
)
{
scaffold
.
_floatingActionButtonVisibilityValue
=
extentRemaining
*
_kBottomSheetDominatesPercentage
*
10
;
scaffold
.
showBodyScrim
(
true
,
math
.
max
(
_kMinBottomSheetScrimOpacity
,
_kMaxBottomSheetScrimOpacity
-
scaffold
.
_floatingActionButtonVisibilityValue
,
));
}
else
{
scaffold
.
_floatingActionButtonVisibilityValue
=
1.0
;
scaffold
.
showBodyScrim
(
false
,
0.0
);
}
// If the Scaffold.bottomSheet != null, we're a persistent bottom sheet.
if
(
notification
.
extent
==
notification
.
minExtent
&&
scaffold
.
widget
.
bottomSheet
==
null
)
{
close
();
}
return
false
;
}
Widget
_wrapBottomSheet
(
Widget
bottomSheet
)
{
return
Semantics
(
container:
true
,
onDismiss:
close
,
child:
NotificationListener
<
DraggableScrollableNotification
>(
onNotification:
extentChanged
,
child:
bottomSheet
,
)
);
}
@override
Widget
build
(
BuildContext
context
)
{
return
AnimatedBuilder
(
animation:
widget
.
animationController
,
builder:
(
BuildContext
context
,
Widget
child
)
{
return
Align
(
alignment:
AlignmentDirectional
.
topStart
,
heightFactor:
widget
.
animationController
.
value
,
child:
child
,
);
},
child:
Semantics
(
container:
true
,
onDismiss:
()
{
close
();
widget
.
onClosing
();
if
(
widget
.
animationController
!=
null
)
{
return
AnimatedBuilder
(
animation:
widget
.
animationController
,
builder:
(
BuildContext
context
,
Widget
child
)
{
return
Align
(
alignment:
AlignmentDirectional
.
topStart
,
heightFactor:
widget
.
animationController
.
value
,
child:
child
);
},
child:
BottomSheet
(
animationController:
widget
.
animationController
,
enableDrag:
widget
.
enableDrag
,
onClosing:
widget
.
onClosing
,
builder:
widget
.
builder
,
child:
_wrapBottomSheet
(
BottomSheet
(
animationController:
widget
.
animationController
,
enableDrag:
widget
.
enableDrag
,
onClosing:
widget
.
onClosing
,
builder:
widget
.
builder
,
backgroundColor:
widget
.
backgroundColor
,
),
),
);
}
return
_wrapBottomSheet
(
BottomSheet
(
onClosing:
widget
.
onClosing
,
builder:
widget
.
builder
,
backgroundColor:
widget
.
backgroundColor
,
),
);
}
}
/// A [ScaffoldFeatureController] for
persistent
bottom sheets.
/// A [ScaffoldFeatureController] for
standard
bottom sheets.
///
/// This is the type of objects returned by [ScaffoldState.showBottomSheet].
class
PersistentBottomSheetController
<
T
>
extends
ScaffoldFeatureController
<
_PersistentBottomSheet
,
T
>
{
///
/// This controller is used to display both standard and persistent bottom
/// sheets. A bottom sheet is only persistent if it is set as the
/// [Scaffold.bottomSheet].
class
PersistentBottomSheetController
<
T
>
extends
ScaffoldFeatureController
<
_StandardBottomSheet
,
T
>
{
const
PersistentBottomSheetController
.
_
(
_
Persistent
BottomSheet
widget
,
_
Standard
BottomSheet
widget
,
Completer
<
T
>
completer
,
VoidCallback
close
,
StateSetter
setState
,
...
...
packages/flutter/lib/src/widgets/draggable_scrollable_sheet.dart
View file @
6a48e663
...
...
@@ -6,9 +6,12 @@ import 'package:flutter/gestures.dart';
import
'basic.dart'
;
import
'framework.dart'
;
import
'inherited_notifier.dart'
;
import
'layout_builder.dart'
;
import
'notification_listener.dart'
;
import
'scroll_context.dart'
;
import
'scroll_controller.dart'
;
import
'scroll_notification.dart'
;
import
'scroll_physics.dart'
;
import
'scroll_position.dart'
;
import
'scroll_position_with_single_context.dart'
;
...
...
@@ -44,6 +47,11 @@ typedef ScrollableWidgetBuilder = Widget Function(
/// [ScrollableWidgetBuilder] does not use provided [ScrollController], the
/// sheet will remain at the initialChildSize.
///
/// By default, the widget will expand its non-occupied area to fill availble
/// space in the parent. If this is not desired, e.g. because the parent wants
/// to position sheet based on the space it is taking, the [expand] property
/// may be set to false.
///
/// {@tool sample}
///
/// This is a sample widget which shows a [ListView] that has 25 [ListTile]s.
...
...
@@ -85,13 +93,14 @@ typedef ScrollableWidgetBuilder = Widget Function(
class
DraggableScrollableSheet
extends
StatefulWidget
{
/// Creates a widget that can be dragged and scrolled in a single gesture.
///
/// The [builder], [initialChildSize], [minChildSize],
and [maxChildSize]
/// parameters must not be null.
/// The [builder], [initialChildSize], [minChildSize],
[maxChildSize] and
///
[expand]
parameters must not be null.
const
DraggableScrollableSheet
({
Key
key
,
this
.
initialChildSize
=
0.5
,
this
.
minChildSize
=
0.25
,
this
.
maxChildSize
=
1.0
,
this
.
expand
=
true
,
@required
this
.
builder
,
})
:
assert
(
initialChildSize
!=
null
),
assert
(
minChildSize
!=
null
),
...
...
@@ -100,6 +109,7 @@ class DraggableScrollableSheet extends StatefulWidget {
assert
(
maxChildSize
<=
1.0
),
assert
(
minChildSize
<=
initialChildSize
),
assert
(
initialChildSize
<=
maxChildSize
),
assert
(
expand
!=
null
),
assert
(
builder
!=
null
),
super
(
key:
key
);
...
...
@@ -121,6 +131,16 @@ class DraggableScrollableSheet extends StatefulWidget {
/// The default value is `1.0`.
final
double
maxChildSize
;
/// Whether the widget should expand to fill the available space in its parent
/// or not.
///
/// In most cases, this should be true. However, in the case of a parent
/// widget that will position this one based on its desired size (such as a
/// [Center]), this should be set to false.
///
/// The default value is true.
final
bool
expand
;
/// 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.
...
...
@@ -130,6 +150,76 @@ class DraggableScrollableSheet extends StatefulWidget {
_DraggableScrollableSheetState
createState
()
=>
_DraggableScrollableSheetState
();
}
/// A [Notification] related to the extent, which is the size, and scroll
/// offset, which is the position of the child list, of the
/// [DraggableScrollableSheet].
///
/// [DraggableScrollableSheet] widgets notify their ancestors when the size of
/// the sheet changes. When the extent of the sheet changes via a drag,
/// this notification bubbles up through the tree, which means a given
/// [NotificationListener] will recieve notifications for all descendant
/// [DraggableScrollableSheet] widgets. To focus on notifications from the
/// nearest [DraggableScorllableSheet] descendant, check that the [depth]
/// property of the notification is zero.
///
/// When an extent notification is received by a [NotificationListener], the
/// listener will already have completed build and layout, and it is therefore
/// too late for that widget to call [State.setState]. Any attempt to adjust the
/// build or layout based on an extent notification would result in a layout
/// that lagged one frame behind, which is a poor user experience. Extent
/// notifications are used primarily to drive animations. The [Scaffold] widget
/// listens for extent notifications and responds by driving animations for the
/// [FloatingActionButton] as the bottom sheet scrolls up.
class
DraggableScrollableNotification
extends
Notification
with
ViewportNotificationMixin
{
/// Creates a notification that the extent of a [DraggableScrollableSheet] has
/// changed.
///
/// All parameters are required. The [minExtent] must be >= 0. The [maxExtent]
/// must be <= 1.0. The [extent] must be between [minExtent] and [maxExtent].
DraggableScrollableNotification
({
@required
this
.
extent
,
@required
this
.
minExtent
,
@required
this
.
maxExtent
,
@required
this
.
initialExtent
,
@required
this
.
context
,
})
:
assert
(
extent
!=
null
),
assert
(
initialExtent
!=
null
),
assert
(
minExtent
!=
null
),
assert
(
maxExtent
!=
null
),
assert
(
0.0
<=
minExtent
),
assert
(
maxExtent
<=
1.0
),
assert
(
minExtent
<=
extent
),
assert
(
minExtent
<=
initialExtent
),
assert
(
extent
<=
maxExtent
),
assert
(
initialExtent
<=
maxExtent
),
assert
(
context
!=
null
);
/// The current value of the extent, between [minExtent] and [maxExtent].
final
double
extent
;
/// The minimum value of [extent], which is >= 0.
final
double
minExtent
;
/// The maximum value of [extent].
final
double
maxExtent
;
/// The initially requested value for [extent].
final
double
initialExtent
;
/// The build context of the widget that fired this notification.
///
/// This can be used to find the sheet's render objects to determine the size
/// of the viewport, for instance. A listener can only assume this context
/// is live when it first gets the notification.
final
BuildContext
context
;
@override
void
debugFillDescription
(
List
<
String
>
description
)
{
super
.
debugFillDescription
(
description
);
description
.
add
(
'minExtent:
$minExtent
, extent:
$extent
, maxExtent:
$maxExtent
, initialExtent:
$initialExtent
'
);
}
}
/// Manages state between [_DraggableScrollableSheetState],
/// [_DraggableScrollableSheetScrollController], and
/// [_DraggableScrollableSheetScrollPosition].
...
...
@@ -174,8 +264,18 @@ class _DraggableSheetExtent {
/// The scroll position gets inputs in terms of pixels, but the extent is
/// expected to be expressed as a number between 0..1.
void
addPixelDelta
(
double
delta
)
{
void
addPixelDelta
(
double
delta
,
BuildContext
context
)
{
if
(
availablePixels
==
0
)
{
return
;
}
currentExtent
+=
delta
/
availablePixels
;
DraggableScrollableNotification
(
minExtent:
minExtent
,
maxExtent:
maxExtent
,
extent:
currentExtent
,
initialExtent:
initialExtent
,
context:
context
,
).
dispatch
(
context
);
}
}
...
...
@@ -195,10 +295,29 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
_scrollController
=
_DraggableScrollableSheetScrollController
(
extent:
_extent
);
}
@override
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
.
_currentExtent
.
value
=
_extent
.
initialExtent
;
}
}
void
_setExtent
()
{
setState
(()
{
// _extent has been updated when this is called.
});
}
@override
...
...
@@ -206,13 +325,12 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
return
LayoutBuilder
(
builder:
(
BuildContext
context
,
BoxConstraints
constraints
)
{
_extent
.
availablePixels
=
widget
.
maxChildSize
*
constraints
.
biggest
.
height
;
return
SizedBox
.
expand
(
child:
FractionallySizedBox
(
heightFactor:
_extent
.
currentExtent
,
child:
widget
.
builder
(
context
,
_scrollController
),
alignment:
Alignment
.
bottomCenter
,
),
final
Widget
sheet
=
FractionallySizedBox
(
heightFactor:
_extent
.
currentExtent
,
child:
widget
.
builder
(
context
,
_scrollController
),
alignment:
Alignment
.
bottomCenter
,
);
return
widget
.
expand
?
SizedBox
.
expand
(
child:
sheet
)
:
sheet
;
},
);
}
...
...
@@ -311,10 +429,10 @@ class _DraggableScrollableSheetScrollPosition
@override
void
applyUserOffset
(
double
delta
)
{
if
(!
listShouldScroll
&&
!(
extent
.
isAtMin
||
extent
.
isAtMax
)
||
(
extent
.
isAtMin
&&
delta
<
0
)
||
(
extent
.
isAtMax
&&
delta
>
0
))
{
extent
.
addPixelDelta
(-
delta
);
(
!(
extent
.
isAtMin
||
extent
.
isAtMax
)
||
(
extent
.
isAtMin
&&
delta
<
0
)
||
(
extent
.
isAtMax
&&
delta
>
0
)
))
{
extent
.
addPixelDelta
(-
delta
,
context
.
notificationContext
);
}
else
{
super
.
applyUserOffset
(
delta
);
}
...
...
@@ -348,7 +466,7 @@ class _DraggableScrollableSheetScrollPosition
void
_tick
()
{
final
double
delta
=
ballisticController
.
value
-
lastDelta
;
lastDelta
=
ballisticController
.
value
;
extent
.
addPixelDelta
(
delta
);
extent
.
addPixelDelta
(
delta
,
context
.
notificationContext
);
if
((
velocity
>
0
&&
extent
.
isAtMax
)
||
(
velocity
<
0
&&
extent
.
isAtMin
))
{
// Make sure we pass along enough velocity to keep scrolling - otherwise
// we just "bounce" off the top making it look like the list doesn't
...
...
@@ -373,3 +491,99 @@ class _DraggableScrollableSheetScrollPosition
return
super
.
drag
(
details
,
dragCancelCallback
);
}
}
/// A widget that can notify a descendent [DraggableScrollableSheet] that it
/// should reset its position to the initial state.
///
/// The [Scaffold] uses this widget to notify a persistentent bottom sheet that
/// 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.
class
DraggableScrollableActuator
extends
StatelessWidget
{
/// Creates a widget that can notify descendent [DraggableScrollableSheet]s
/// to reset to their initial position.
///
/// The [child] parameter is required.
DraggableScrollableActuator
({
Key
key
,
@required
this
.
child
})
:
super
(
key:
key
);
/// This child's [DraggableScrollableSheet] descendant will be reset when the
/// [reset] method is applied to a context that includes it.
///
/// Must not be null.
final
Widget
child
;
final
_ResetNotifier
_notifier
=
_ResetNotifier
();
/// Notifies any descendant [DraggableScrollableSheet] that it should reset
/// to its initial position.
///
/// Returns `true` if a [DraggableScrollableActuator] is available and
/// some [DraggableScrollableSheet] is listening for updates, `false`
/// otherwise.
static
bool
reset
(
BuildContext
context
)
{
final
_InheritedResetNotifier
notifier
=
context
.
inheritFromWidgetOfExactType
(
_InheritedResetNotifier
);
if
(
notifier
==
null
)
{
return
false
;
}
return
notifier
.
_sendReset
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
_InheritedResetNotifier
(
child:
child
,
notifier:
_notifier
);
}
}
/// A [ChangeNotifier] to use with [InheritedResetNotifer] to notify
/// descendants that they should reset to initial state.
class
_ResetNotifier
extends
ChangeNotifier
{
/// Whether someone called [sendReset] or not.
///
/// This flag should be reset after checking it.
bool
_wasCalled
=
false
;
/// Fires a reset notification to descendants.
///
/// Returns false if there are no listeners.
bool
sendReset
()
{
if
(!
hasListeners
)
{
return
false
;
}
_wasCalled
=
true
;
notifyListeners
();
return
true
;
}
}
class
_InheritedResetNotifier
extends
InheritedNotifier
<
_ResetNotifier
>
{
/// Creates an [InheritedNotifier] that the [DraggableScrollableSheet] will
/// listen to for an indication that it should change its extent.
///
/// The [child] and [notifier] properties must not be null.
const
_InheritedResetNotifier
({
Key
key
,
@required
Widget
child
,
@required
_ResetNotifier
notifier
,
})
:
super
(
key:
key
,
child:
child
,
notifier:
notifier
);
bool
_sendReset
()
=>
notifier
.
sendReset
();
/// Specifies whether the [DraggableScrollableSheet] should reset to its
/// initial position.
///
/// Returns true if the notifier requested a reset, false otherwise.
static
bool
shouldReset
(
BuildContext
context
)
{
final
InheritedWidget
widget
=
context
.
inheritFromWidgetOfExactType
(
_InheritedResetNotifier
);
if
(
widget
==
null
)
{
return
false
;
}
assert
(
widget
is
_InheritedResetNotifier
);
final
_InheritedResetNotifier
inheritedNotifier
=
widget
;
final
bool
wasCalled
=
inheritedNotifier
.
notifier
.
_wasCalled
;
inheritedNotifier
.
notifier
.
_wasCalled
=
false
;
return
wasCalled
;
}
}
packages/flutter/test/material/modal_bottom_sheet_test.dart
View file @
6a48e663
...
...
@@ -38,7 +38,7 @@ void main() {
expect
(
find
.
text
(
'BottomSheet'
),
findsOneWidget
);
expect
(
showBottomSheetThenCalled
,
isFalse
);
// Tap on the bottom sheet itself to dismiss it
// Tap on the bottom sheet itself to dismiss it
.
await
tester
.
tap
(
find
.
text
(
'BottomSheet'
));
await
tester
.
pump
();
// bottom sheet dismiss animation starts
expect
(
showBottomSheetThenCalled
,
isTrue
);
...
...
@@ -169,6 +169,7 @@ void main() {
child:
MediaQuery
(
data:
const
MediaQueryData
(
padding:
EdgeInsets
.
all
(
50.0
),
size:
Size
(
400.0
,
600.0
),
),
child:
Navigator
(
onGenerateRoute:
(
_
)
{
...
...
@@ -249,4 +250,66 @@ void main() {
),
ignoreTransform:
true
,
ignoreRect:
true
,
ignoreId:
true
));
semantics
.
dispose
();
});
testWidgets
(
'modal BottomSheet with scrollController has semantics'
,
(
WidgetTester
tester
)
async
{
final
SemanticsTester
semantics
=
SemanticsTester
(
tester
);
final
GlobalKey
<
ScaffoldState
>
scaffoldKey
=
GlobalKey
<
ScaffoldState
>();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
key:
scaffoldKey
,
body:
const
Center
(
child:
Text
(
'body'
))
)
));
showModalBottomSheet
<
void
>(
context:
scaffoldKey
.
currentContext
,
builder:
(
BuildContext
context
)
{
return
DraggableScrollableSheet
(
expand:
false
,
builder:
(
_
,
ScrollController
controller
)
{
return
SingleChildScrollView
(
controller:
controller
,
child:
Container
(
child:
const
Text
(
'BottomSheet'
),
),
);
},
);
},
);
await
tester
.
pump
();
// bottom sheet show animation starts
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
// animation done
expect
(
semantics
,
hasSemantics
(
TestSemantics
.
root
(
children:
<
TestSemantics
>[
TestSemantics
.
rootChild
(
children:
<
TestSemantics
>[
TestSemantics
(
label:
'Dialog'
,
textDirection:
TextDirection
.
ltr
,
flags:
<
SemanticsFlag
>[
SemanticsFlag
.
scopesRoute
,
SemanticsFlag
.
namesRoute
,
],
children:
<
TestSemantics
>[
TestSemantics
(
flags:
<
SemanticsFlag
>[
SemanticsFlag
.
hasImplicitScrolling
],
children:
<
TestSemantics
>[
TestSemantics
(
label:
'BottomSheet'
,
textDirection:
TextDirection
.
ltr
,
),
],
),
],
),
],
),
],
),
ignoreTransform:
true
,
ignoreRect:
true
,
ignoreId:
true
));
semantics
.
dispose
();
});
}
packages/flutter/test/material/persistent_bottom_sheet_test.dart
View file @
6a48e663
...
...
@@ -29,12 +29,42 @@ void main() {
await
tester
.
pump
();
expect
(
buildCount
,
equals
(
1
));
bottomSheet
.
setState
(()
{
});
await
tester
.
pump
();
expect
(
buildCount
,
equals
(
2
));
});
testWidgets
(
'Verify that a persistent BottomSheet cannot be dismissed'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
const
Center
(
child:
Text
(
'body'
)),
bottomSheet:
DraggableScrollableSheet
(
expand:
false
,
builder:
(
_
,
ScrollController
controller
)
{
return
ListView
(
controller:
controller
,
shrinkWrap:
true
,
children:
<
Widget
>[
Container
(
height:
100.0
,
child:
const
Text
(
'One'
)),
Container
(
height:
100.0
,
child:
const
Text
(
'Two'
)),
Container
(
height:
100.0
,
child:
const
Text
(
'Three'
)),
],
);
},
),
)
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Two'
),
findsOneWidget
);
await
tester
.
drag
(
find
.
text
(
'Two'
),
const
Offset
(
0.0
,
400.0
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Two'
),
findsOneWidget
);
});
testWidgets
(
'Verify that a scrollable BottomSheet can be dismissed'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
ScaffoldState
>
scaffoldKey
=
GlobalKey
<
ScaffoldState
>();
...
...
@@ -67,6 +97,210 @@ void main() {
expect
(
find
.
text
(
'Two'
),
findsNothing
);
});
testWidgets
(
'Verify that a scrollControlled BottomSheet can be dismissed'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
ScaffoldState
>
scaffoldKey
=
GlobalKey
<
ScaffoldState
>();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
key:
scaffoldKey
,
body:
const
Center
(
child:
Text
(
'body'
))
)
));
scaffoldKey
.
currentState
.
showBottomSheet
<
void
>(
(
BuildContext
context
)
{
return
DraggableScrollableSheet
(
expand:
false
,
builder:
(
_
,
ScrollController
controller
)
{
return
ListView
(
shrinkWrap:
true
,
controller:
controller
,
children:
<
Widget
>[
Container
(
height:
100.0
,
child:
const
Text
(
'One'
)),
Container
(
height:
100.0
,
child:
const
Text
(
'Two'
)),
Container
(
height:
100.0
,
child:
const
Text
(
'Three'
)),
],
);
},
);
},
);
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Two'
),
findsOneWidget
);
await
tester
.
drag
(
find
.
text
(
'Two'
),
const
Offset
(
0.0
,
400.0
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Two'
),
findsNothing
);
});
testWidgets
(
'Verify that a persistent BottomSheet can fling up and hide the fab'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
appBar:
AppBar
(),
body:
const
Center
(
child:
Text
(
'body'
)),
bottomSheet:
DraggableScrollableSheet
(
expand:
false
,
builder:
(
_
,
ScrollController
controller
)
{
return
ListView
.
builder
(
itemExtent:
50.0
,
itemCount:
50
,
itemBuilder:
(
_
,
int
index
)
=>
Text
(
'Item
$index
'
),
controller:
controller
,
);
},
),
floatingActionButton:
const
FloatingActionButton
(
onPressed:
null
,
child:
Text
(
'fab'
),
),
),
),
);
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Item 2'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 22'
),
findsNothing
);
expect
(
find
.
byType
(
FloatingActionButton
),
findsOneWidget
);
expect
(
find
.
byType
(
FloatingActionButton
).
hitTestable
(),
findsOneWidget
);
expect
(
find
.
byType
(
BackButton
).
hitTestable
(),
findsNothing
);
await
tester
.
drag
(
find
.
text
(
'Item 2'
),
const
Offset
(
0
,
-
20.0
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Item 2'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 22'
),
findsNothing
);
expect
(
find
.
byType
(
FloatingActionButton
),
findsOneWidget
);
expect
(
find
.
byType
(
FloatingActionButton
).
hitTestable
(),
findsOneWidget
);
await
tester
.
fling
(
find
.
text
(
'Item 2'
),
const
Offset
(
0.0
,
-
600.0
),
2000.0
);
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Item 2'
),
findsNothing
);
expect
(
find
.
text
(
'Item 22'
),
findsOneWidget
);
expect
(
find
.
byType
(
FloatingActionButton
),
findsOneWidget
);
expect
(
find
.
byType
(
FloatingActionButton
).
hitTestable
(),
findsNothing
);
});
testWidgets
(
'Verify that a back button resets a persistent BottomSheet'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
appBar:
AppBar
(),
body:
const
Center
(
child:
Text
(
'body'
)),
bottomSheet:
DraggableScrollableSheet
(
expand:
false
,
builder:
(
_
,
ScrollController
controller
)
{
return
ListView
.
builder
(
itemExtent:
50.0
,
itemCount:
50
,
itemBuilder:
(
_
,
int
index
)
=>
Text
(
'Item
$index
'
),
controller:
controller
,
);
},
),
floatingActionButton:
const
FloatingActionButton
(
onPressed:
null
,
child:
Text
(
'fab'
),
),
),
),
);
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Item 2'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 22'
),
findsNothing
);
expect
(
find
.
byType
(
BackButton
).
hitTestable
(),
findsNothing
);
await
tester
.
drag
(
find
.
text
(
'Item 2'
),
const
Offset
(
0
,
-
20.0
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Item 2'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 22'
),
findsNothing
);
// We've started to drag up, we should have a back button now for a11y
expect
(
find
.
byType
(
BackButton
).
hitTestable
(),
findsOneWidget
);
await
tester
.
tap
(
find
.
byType
(
BackButton
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
byType
(
BackButton
).
hitTestable
(),
findsNothing
);
expect
(
find
.
text
(
'Item 2'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 22'
),
findsNothing
);
await
tester
.
fling
(
find
.
text
(
'Item 2'
),
const
Offset
(
0.0
,
-
600.0
),
2000.0
);
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Item 2'
),
findsNothing
);
expect
(
find
.
text
(
'Item 22'
),
findsOneWidget
);
expect
(
find
.
byType
(
BackButton
).
hitTestable
(),
findsOneWidget
);
await
tester
.
tap
(
find
.
byType
(
BackButton
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
byType
(
BackButton
).
hitTestable
(),
findsNothing
);
expect
(
find
.
text
(
'Item 2'
),
findsOneWidget
);
expect
(
find
.
text
(
'Item 22'
),
findsNothing
);
});
testWidgets
(
'Verify that a scrollable BottomSheet hides the fab when scrolled up'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
ScaffoldState
>
scaffoldKey
=
GlobalKey
<
ScaffoldState
>();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
key:
scaffoldKey
,
body:
const
Center
(
child:
Text
(
'body'
)),
floatingActionButton:
const
FloatingActionButton
(
onPressed:
null
,
child:
Text
(
'fab'
),
),
)
));
scaffoldKey
.
currentState
.
showBottomSheet
<
void
>(
(
BuildContext
context
)
{
return
DraggableScrollableSheet
(
expand:
false
,
builder:
(
_
,
ScrollController
controller
)
{
return
ListView
(
controller:
controller
,
shrinkWrap:
true
,
children:
<
Widget
>[
Container
(
height:
100.0
,
child:
const
Text
(
'One'
)),
Container
(
height:
100.0
,
child:
const
Text
(
'Two'
)),
Container
(
height:
100.0
,
child:
const
Text
(
'Three'
)),
Container
(
height:
100.0
,
child:
const
Text
(
'Three'
)),
Container
(
height:
100.0
,
child:
const
Text
(
'Three'
)),
Container
(
height:
100.0
,
child:
const
Text
(
'Three'
)),
Container
(
height:
100.0
,
child:
const
Text
(
'Three'
)),
Container
(
height:
100.0
,
child:
const
Text
(
'Three'
)),
Container
(
height:
100.0
,
child:
const
Text
(
'Three'
)),
Container
(
height:
100.0
,
child:
const
Text
(
'Three'
)),
Container
(
height:
100.0
,
child:
const
Text
(
'Three'
)),
],
);
},
);
},
);
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Two'
),
findsOneWidget
);
expect
(
find
.
byType
(
FloatingActionButton
).
hitTestable
(),
findsOneWidget
);
await
tester
.
drag
(
find
.
text
(
'Two'
),
const
Offset
(
0.0
,
-
600.0
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Two'
),
findsOneWidget
);
expect
(
find
.
byType
(
FloatingActionButton
),
findsOneWidget
);
expect
(
find
.
byType
(
FloatingActionButton
).
hitTestable
(),
findsNothing
);
});
testWidgets
(
'showBottomSheet()'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
key
=
GlobalKey
();
await
tester
.
pumpWidget
(
MaterialApp
(
...
...
@@ -83,7 +317,7 @@ void main() {
builder:
(
BuildContext
context
)
{
buildCount
+=
1
;
return
Container
(
height:
200.0
);
}
}
,
);
},
);
...
...
@@ -191,4 +425,29 @@ void main() {
expect
(
find
.
text
(
'showModalBottomSheet'
),
findsNothing
);
expect
(
find
.
byKey
(
bottomSheetKey
),
findsNothing
);
});
testWidgets
(
'PersistentBottomSheetController.close dismisses the bottom sheet'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
ScaffoldState
>
scaffoldKey
=
GlobalKey
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
key:
scaffoldKey
,
body:
const
Center
(
child:
Text
(
'body'
))
)
));
final
PersistentBottomSheetController
<
void
>
bottomSheet
=
scaffoldKey
.
currentState
.
showBottomSheet
<
void
>((
_
)
{
return
Builder
(
builder:
(
BuildContext
context
)
{
return
Container
(
height:
200.0
);
}
);
});
await
tester
.
pump
();
expect
(
find
.
byType
(
BottomSheet
),
findsOneWidget
);
bottomSheet
.
close
();
await
tester
.
pump
();
expect
(
find
.
byType
(
BottomSheet
),
findsNothing
);
});
}
packages/flutter/test/material/scaffold_test.dart
View file @
6a48e663
...
...
@@ -335,7 +335,6 @@ void main() {
),
),
);
await
tester
.
tap
(
find
.
text
(
'X'
));
await
tester
.
pump
();
// start animation
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
...
...
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