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
96326d47
Unverified
Commit
96326d47
authored
Aug 10, 2018
by
Natalie Sampsell
Committed by
GitHub
Aug 10, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
CupertinoActionSheet (#19232)
Adding CupertinoActionSheet, showCupertinoModalPopup
parent
21f22ed3
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
2392 additions
and
4 deletions
+2392
-4
cupertino.dart
packages/flutter/lib/cupertino.dart
+1
-0
action_sheet.dart
packages/flutter/lib/src/cupertino/action_sheet.dart
+1267
-0
route.dart
packages/flutter/lib/src/cupertino/route.dart
+99
-0
box_decoration.dart
packages/flutter/lib/src/painting/box_decoration.dart
+20
-2
modal_barrier.dart
packages/flutter/lib/src/widgets/modal_barrier.dart
+21
-2
routes.dart
packages/flutter/lib/src/widgets/routes.dart
+17
-0
action_sheet_test.dart
packages/flutter/test/cupertino/action_sheet_test.dart
+967
-0
No files found.
packages/flutter/lib/cupertino.dart
View file @
96326d47
...
...
@@ -7,6 +7,7 @@
/// To use, import `package:flutter/cupertino.dart`.
library
cupertino
;
export
'src/cupertino/action_sheet.dart'
;
export
'src/cupertino/activity_indicator.dart'
;
export
'src/cupertino/app.dart'
;
export
'src/cupertino/bottom_tab_bar.dart'
;
...
...
packages/flutter/lib/src/cupertino/action_sheet.dart
0 → 100644
View file @
96326d47
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:ui'
show
ImageFilter
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/widgets.dart'
;
import
'colors.dart'
;
import
'scrollbar.dart'
;
const
TextStyle
_kActionSheetActionStyle
=
TextStyle
(
fontFamily:
'.SF UI Text'
,
inherit:
false
,
fontSize:
20.0
,
fontWeight:
FontWeight
.
w400
,
color:
CupertinoColors
.
activeBlue
,
textBaseline:
TextBaseline
.
alphabetic
,
);
const
TextStyle
_kActionSheetContentStyle
=
TextStyle
(
fontFamily:
'.SF UI Text'
,
inherit:
false
,
fontSize:
13.0
,
fontWeight:
FontWeight
.
w400
,
color:
_kContentTextColor
,
textBaseline:
TextBaseline
.
alphabetic
,
);
// This decoration is applied to the blurred backdrop to lighten the blurred
// image. Brightening is done to counteract the dark modal barrier that
// appears behind the alert. The overlay blend mode does the brightening.
// The white color doesn't paint any white, it's just the basis for the
// overlay blend mode.
const
BoxDecoration
_kAlertBlurOverlayDecoration
=
BoxDecoration
(
color:
CupertinoColors
.
white
,
backgroundBlendMode:
BlendMode
.
overlay
,
);
// Translucent, very light gray that is painted on top of the blurred backdrop
// as the action sheet's background color.
const
Color
_kBackgroundColor
=
Color
(
0xD1F8F8F8
);
// Translucent, light gray that is painted on top of the blurred backdrop as
// the background color of a pressed button.
const
Color
_kPressedColor
=
Color
(
0xA6E5E5EA
);
// Translucent gray that is painted on top of the blurred backdrop in the gap
// areas between the content section and actions section, as well as between
// buttons.
const
Color
_kButtonDividerColor
=
Color
(
0x403F3F3F
);
const
Color
_kContentTextColor
=
Color
(
0xFF8F8F8F
);
const
Color
_kCancelButtonPressedColor
=
Color
(
0xFFEAEAEA
);
const
double
_kBlurAmount
=
20.0
;
const
double
_kEdgeHorizontalPadding
=
8.0
;
const
double
_kCancelButtonPadding
=
8.0
;
const
double
_kEdgeVerticalPadding
=
10.0
;
const
double
_kContentHorizontalPadding
=
40.0
;
const
double
_kContentVerticalPadding
=
14.0
;
const
double
_kButtonHeight
=
56.0
;
const
double
_kCornerRadius
=
14.0
;
const
double
_kDividerThickness
=
1.0
;
/// An iOS-style action sheet.
///
/// An action sheet is a specific style of alert that presents the user
/// with a set of two or more choices related to the current context.
/// An action sheet can have a title, an additional message, and a list
/// of actions. The title is displayed above the message and the actions
/// are displayed below this content.
///
/// This action sheet styles its title and message to match standard iOS action
/// sheet title and message text style.
///
/// To display action buttons that look like standard iOS action sheet buttons,
/// provide [CupertinoActionSheetAction]s for the [actions] given to this action sheet.
///
/// To include a iOS-style cancel button separate from the other buttons,
/// provide an [CupertinoActionSheetAction] for the [cancelButton] given to this
/// action sheet.
///
/// An action sheet is typically passed as the child widget to
/// [showCupertinoModalPopup], which displays the action sheet by sliding it up
/// from the bottom of the screen.
///
/// See also:
///
/// * [CupertinoActionSheetAction], which is an iOS-style action sheet button.
/// * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/>
class
CupertinoActionSheet
extends
StatelessWidget
{
/// Creates an iOS-style action sheet.
///
/// An action sheet must have a non-null value for at least one of the
/// following arguments: [actions], [title], [message], or [cancelButton].
///
/// Generally, action sheets are used to give the user a choice between
/// two or more choices for the current context.
const
CupertinoActionSheet
({
Key
key
,
this
.
title
,
this
.
message
,
this
.
actions
,
this
.
messageScrollController
,
this
.
actionScrollController
,
this
.
cancelButton
,
})
:
assert
(
actions
!=
null
||
title
!=
null
||
message
!=
null
||
cancelButton
!=
null
,
'An action sheet must have a non-null value for at least one of the following arguments: '
'actions, title, message, or cancelButton'
),
super
(
key:
key
);
/// An optional title of the action sheet. When the [message] is non-null,
/// the font of the [title] is bold.
///
/// Typically a [Text] widget.
final
Widget
title
;
/// An optional descriptive message that provides more details about the
/// reason for the alert.
///
/// Typically a [Text] widget.
final
Widget
message
;
/// The set of actions that are displayed for the user to select.
///
/// Typically this is a list of [CupertinoActionSheetAction] widgets.
final
List
<
Widget
>
actions
;
/// A scroll controller that can be used to control the scrolling of the
/// [message] in the action sheet.
///
/// This attribute is typically not needed, as alert messages should be
/// short.
final
ScrollController
messageScrollController
;
/// A scroll controller that can be used to control the scrolling of the
/// [actions] in the action sheet.
///
/// This attribute is typically not needed.
final
ScrollController
actionScrollController
;
/// The optional cancel button that is grouped separately from the other
/// actions.
///
/// Typically this is an [CupertinoActionSheetAction] widget.
final
Widget
cancelButton
;
Widget
_buildContent
()
{
final
List
<
Widget
>
content
=
<
Widget
>[];
if
(
title
!=
null
||
message
!=
null
)
{
final
Widget
titleSection
=
new
_CupertinoAlertContentSection
(
title:
title
,
message:
message
,
scrollController:
messageScrollController
,
);
content
.
add
(
new
Flexible
(
child:
titleSection
));
}
return
new
Container
(
color:
_kBackgroundColor
,
child:
new
Column
(
mainAxisSize:
MainAxisSize
.
min
,
crossAxisAlignment:
CrossAxisAlignment
.
stretch
,
children:
content
,
),
);
}
Widget
_buildActions
()
{
if
(
actions
==
null
||
actions
.
isEmpty
)
{
return
new
Container
(
height:
0.0
,
);
}
return
new
Container
(
child:
new
_CupertinoAlertActionSection
(
children:
actions
,
scrollController:
actionScrollController
,
hasCancelButton:
cancelButton
!=
null
,
),
);
}
Widget
_buildCancelButton
()
{
final
double
cancelPadding
=
(
actions
!=
null
||
message
!=
null
||
title
!=
null
)
?
_kCancelButtonPadding
:
0.0
;
return
Padding
(
padding:
new
EdgeInsets
.
only
(
top:
cancelPadding
),
child:
new
_CupertinoActionSheetCancelButton
(
child:
cancelButton
,
),
);
}
@override
Widget
build
(
BuildContext
context
)
{
final
List
<
Widget
>
children
=
<
Widget
>[
new
Flexible
(
child:
new
ClipRRect
(
borderRadius:
new
BorderRadius
.
circular
(
12.0
),
child:
new
BackdropFilter
(
filter:
new
ImageFilter
.
blur
(
sigmaX:
_kBlurAmount
,
sigmaY:
_kBlurAmount
),
child:
new
Container
(
decoration:
_kAlertBlurOverlayDecoration
,
child:
new
_CupertinoAlertRenderWidget
(
contentSection:
_buildContent
(),
actionsSection:
_buildActions
(),
),
),
),
),
),
];
if
(
cancelButton
!=
null
)
{
children
.
add
(
_buildCancelButton
(),
);
}
final
Orientation
orientation
=
MediaQuery
.
of
(
context
).
orientation
;
double
actionSheetWidth
;
if
(
orientation
==
Orientation
.
portrait
)
{
actionSheetWidth
=
MediaQuery
.
of
(
context
).
size
.
width
-
(
_kEdgeHorizontalPadding
*
2
);
}
else
{
actionSheetWidth
=
MediaQuery
.
of
(
context
).
size
.
height
-
(
_kEdgeHorizontalPadding
*
2
);
}
return
new
SafeArea
(
child:
new
Semantics
(
namesRoute:
true
,
scopesRoute:
true
,
explicitChildNodes:
true
,
label:
'Alert'
,
child:
new
Container
(
width:
actionSheetWidth
,
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
_kEdgeHorizontalPadding
,
vertical:
_kEdgeVerticalPadding
,
),
child:
new
Column
(
children:
children
,
mainAxisSize:
MainAxisSize
.
min
,
crossAxisAlignment:
CrossAxisAlignment
.
stretch
,
),
),
),
);
}
}
/// A button typically used in a [CupertinoActionSheet].
///
/// See also:
///
/// * [CupertinoActionSheet], an alert that presents the user with a set of two or
/// more choices related to the current context.
class
CupertinoActionSheetAction
extends
StatelessWidget
{
///Creates an action for an iOS-style action sheet.
///
/// The [child] and [onPressed] arguments must not be null.
const
CupertinoActionSheetAction
({
@required
this
.
onPressed
,
this
.
isDefaultAction
=
false
,
this
.
isDestructiveAction
=
false
,
@required
this
.
child
,
})
:
assert
(
child
!=
null
),
assert
(
onPressed
!=
null
);
/// The callback that is called when the button is tapped.
///
/// This attribute must not be null.
final
VoidCallback
onPressed
;
/// Whether this action is the default choice in the action sheet.
///
/// Default buttons have bold text.
final
bool
isDefaultAction
;
/// Whether this action might change or delete data.
///
/// Destructive buttons have red text.
final
bool
isDestructiveAction
;
/// The widget below this widget in the tree.
///
/// Typically a [Text] widget.
final
Widget
child
;
@override
Widget
build
(
BuildContext
context
)
{
TextStyle
style
=
_kActionSheetActionStyle
;
if
(
isDefaultAction
)
{
style
=
style
.
copyWith
(
fontWeight:
FontWeight
.
w600
);
}
if
(
isDestructiveAction
)
{
style
=
style
.
copyWith
(
color:
CupertinoColors
.
destructiveRed
);
}
return
new
GestureDetector
(
onTap:
onPressed
,
behavior:
HitTestBehavior
.
opaque
,
child:
new
ConstrainedBox
(
constraints:
const
BoxConstraints
(
minHeight:
_kButtonHeight
,
),
child:
new
Semantics
(
button:
true
,
child:
new
Container
(
alignment:
Alignment
.
center
,
padding:
const
EdgeInsets
.
symmetric
(
vertical:
16.0
,
horizontal:
10.0
,
),
child:
new
DefaultTextStyle
(
style:
style
,
child:
child
,
textAlign:
TextAlign
.
center
,
),
),
),
),
);
}
}
class
_CupertinoActionSheetCancelButton
extends
StatefulWidget
{
const
_CupertinoActionSheetCancelButton
({
Key
key
,
this
.
child
,
})
:
super
(
key:
key
);
final
Widget
child
;
@override
_CupertinoActionSheetCancelButtonState
createState
()
=>
_CupertinoActionSheetCancelButtonState
();
}
class
_CupertinoActionSheetCancelButtonState
extends
State
<
_CupertinoActionSheetCancelButton
>
{
Color
_backgroundColor
;
@override
void
initState
()
{
_backgroundColor
=
CupertinoColors
.
white
;
super
.
initState
();
}
void
_onTapDown
(
TapDownDetails
event
)
{
setState
(()
{
_backgroundColor
=
_kCancelButtonPressedColor
;
});
}
void
_onTapUp
(
TapUpDetails
event
)
{
setState
(()
{
_backgroundColor
=
CupertinoColors
.
white
;
});
}
void
_onTapCancel
()
{
setState
(()
{
_backgroundColor
=
CupertinoColors
.
white
;
});
}
@override
Widget
build
(
BuildContext
context
)
{
return
new
GestureDetector
(
excludeFromSemantics:
true
,
onTapDown:
_onTapDown
,
onTapUp:
_onTapUp
,
onTapCancel:
_onTapCancel
,
child:
new
Container
(
decoration:
new
BoxDecoration
(
color:
_backgroundColor
,
borderRadius:
new
BorderRadius
.
circular
(
_kCornerRadius
),
),
child:
widget
.
child
,
),
);
}
}
class
_CupertinoAlertRenderWidget
extends
RenderObjectWidget
{
const
_CupertinoAlertRenderWidget
({
Key
key
,
@required
this
.
contentSection
,
@required
this
.
actionsSection
,
})
:
super
(
key:
key
);
final
Widget
contentSection
;
final
Widget
actionsSection
;
@override
RenderObject
createRenderObject
(
BuildContext
context
)
{
return
new
_RenderCupertinoAlert
(
dividerThickness:
_kDividerThickness
/
MediaQuery
.
of
(
context
).
devicePixelRatio
,
);
}
@override
RenderObjectElement
createElement
()
{
return
_CupertinoAlertRenderElement
(
this
);
}
}
class
_CupertinoAlertRenderElement
extends
RenderObjectElement
{
_CupertinoAlertRenderElement
(
_CupertinoAlertRenderWidget
widget
)
:
super
(
widget
);
Element
_contentElement
;
Element
_actionsElement
;
@override
_CupertinoAlertRenderWidget
get
widget
=>
super
.
widget
;
@override
_RenderCupertinoAlert
get
renderObject
=>
super
.
renderObject
;
@override
void
visitChildren
(
ElementVisitor
visitor
)
{
if
(
_contentElement
!=
null
)
{
visitor
(
_contentElement
);
}
if
(
_actionsElement
!=
null
)
{
visitor
(
_actionsElement
);
}
}
@override
void
mount
(
Element
parent
,
dynamic
newSlot
)
{
super
.
mount
(
parent
,
newSlot
);
_contentElement
=
updateChild
(
_contentElement
,
widget
.
contentSection
,
_AlertSections
.
contentSection
);
_actionsElement
=
updateChild
(
_actionsElement
,
widget
.
actionsSection
,
_AlertSections
.
actionsSection
);
}
@override
void
insertChildRenderObject
(
RenderObject
child
,
_AlertSections
slot
)
{
_placeChildInSlot
(
child
,
slot
);
}
@override
void
moveChildRenderObject
(
RenderObject
child
,
_AlertSections
slot
)
{
_placeChildInSlot
(
child
,
slot
);
}
@override
void
update
(
RenderObjectWidget
newWidget
)
{
super
.
update
(
newWidget
);
_contentElement
=
updateChild
(
_contentElement
,
widget
.
contentSection
,
_AlertSections
.
contentSection
);
_actionsElement
=
updateChild
(
_actionsElement
,
widget
.
actionsSection
,
_AlertSections
.
actionsSection
);
}
@override
void
forgetChild
(
Element
child
)
{
assert
(
child
==
_contentElement
||
child
==
_actionsElement
);
if
(
_contentElement
==
child
)
{
_contentElement
=
null
;
}
else
if
(
_actionsElement
==
child
)
{
_actionsElement
=
null
;
}
}
@override
void
removeChildRenderObject
(
RenderObject
child
)
{
assert
(
child
==
renderObject
.
contentSection
||
child
==
renderObject
.
actionsSection
);
if
(
renderObject
.
contentSection
==
child
)
{
renderObject
.
contentSection
=
null
;
}
else
if
(
renderObject
.
actionsSection
==
child
)
{
renderObject
.
actionsSection
=
null
;
}
}
void
_placeChildInSlot
(
RenderObject
child
,
_AlertSections
slot
)
{
assert
(
slot
!=
null
);
switch
(
slot
)
{
case
_AlertSections
.
contentSection
:
renderObject
.
contentSection
=
child
;
break
;
case
_AlertSections
.
actionsSection
:
renderObject
.
actionsSection
=
child
;
break
;
}
}
}
// An iOS-style layout policy for sizing an alert's content section and action
// button section.
//
// The policy is as follows:
//
// If all content and buttons fit on the screen:
// The content section and action button section are sized intrinsically.
//
// If all content and buttons do not fit on the screen:
// A minimum height for the action button section is calculated. The action
// button section will not be rendered shorter than this minimum. See
// _RenderCupertinoAlertActions for the minimum height calculation.
//
// With the minimum action button section calculated, the content section can
// take up as much of the remaining space as it needs.
//
// After the content section is laid out, the action button section is allowed
// to take up any remaining space that was not consumed by the content section.
class
_RenderCupertinoAlert
extends
RenderBox
{
_RenderCupertinoAlert
({
RenderBox
contentSection
,
RenderBox
actionsSection
,
double
dividerThickness
=
0.0
,
})
:
_contentSection
=
contentSection
,
_actionsSection
=
actionsSection
,
_dividerThickness
=
dividerThickness
;
RenderBox
get
contentSection
=>
_contentSection
;
RenderBox
_contentSection
;
set
contentSection
(
RenderBox
newContentSection
)
{
if
(
newContentSection
!=
_contentSection
)
{
if
(
null
!=
_contentSection
)
{
dropChild
(
_contentSection
);
}
_contentSection
=
newContentSection
;
if
(
null
!=
_contentSection
)
{
adoptChild
(
_contentSection
);
}
}
}
RenderBox
get
actionsSection
=>
_actionsSection
;
RenderBox
_actionsSection
;
set
actionsSection
(
RenderBox
newActionsSection
)
{
if
(
newActionsSection
!=
_actionsSection
)
{
if
(
null
!=
_actionsSection
)
{
dropChild
(
_actionsSection
);
}
_actionsSection
=
newActionsSection
;
if
(
null
!=
_actionsSection
)
{
adoptChild
(
_actionsSection
);
}
}
}
final
double
_dividerThickness
;
final
Paint
_dividerPaint
=
new
Paint
()
..
color
=
_kButtonDividerColor
..
style
=
PaintingStyle
.
fill
;
@override
void
attach
(
PipelineOwner
owner
)
{
super
.
attach
(
owner
);
if
(
null
!=
contentSection
)
{
contentSection
.
attach
(
owner
);
}
if
(
null
!=
actionsSection
)
{
actionsSection
.
attach
(
owner
);
}
}
@override
void
detach
()
{
super
.
detach
();
if
(
null
!=
contentSection
)
{
contentSection
.
detach
();
}
if
(
null
!=
actionsSection
)
{
actionsSection
.
detach
();
}
}
@override
void
redepthChildren
()
{
if
(
null
!=
contentSection
)
{
redepthChild
(
contentSection
);
}
if
(
null
!=
actionsSection
)
{
redepthChild
(
actionsSection
);
}
}
@override
void
setupParentData
(
RenderBox
child
)
{
if
(
child
.
parentData
is
!
MultiChildLayoutParentData
)
{
child
.
parentData
=
new
MultiChildLayoutParentData
();
}
}
@override
void
visitChildren
(
RenderObjectVisitor
visitor
)
{
if
(
contentSection
!=
null
)
{
visitor
(
contentSection
);
}
if
(
actionsSection
!=
null
)
{
visitor
(
actionsSection
);
}
}
@override
List
<
DiagnosticsNode
>
debugDescribeChildren
()
{
final
List
<
DiagnosticsNode
>
value
=
<
DiagnosticsNode
>[];
if
(
contentSection
!=
null
)
{
value
.
add
(
contentSection
.
toDiagnosticsNode
(
name:
'content'
));
}
if
(
actionsSection
!=
null
)
{
value
.
add
(
actionsSection
.
toDiagnosticsNode
(
name:
'actions'
));
}
return
value
;
}
@override
double
computeMinIntrinsicWidth
(
double
height
)
{
return
constraints
.
minWidth
;
}
@override
double
computeMaxIntrinsicWidth
(
double
height
)
{
return
constraints
.
maxWidth
;
}
@override
double
computeMinIntrinsicHeight
(
double
width
)
{
final
double
contentHeight
=
contentSection
.
getMinIntrinsicHeight
(
width
);
final
double
actionsHeight
=
actionsSection
.
getMinIntrinsicHeight
(
width
);
final
bool
hasDivider
=
contentHeight
>
0.0
&&
actionsHeight
>
0.0
;
double
height
=
contentHeight
+
(
hasDivider
?
_dividerThickness
:
0.0
)
+
actionsHeight
;
if
(
actionsHeight
>
0
||
contentHeight
>
0
)
height
-=
2
*
_kEdgeVerticalPadding
;
if
(
height
.
isFinite
)
return
height
;
return
0.0
;
}
@override
double
computeMaxIntrinsicHeight
(
double
width
)
{
final
double
contentHeight
=
contentSection
.
getMaxIntrinsicHeight
(
width
);
final
double
actionsHeight
=
actionsSection
.
getMaxIntrinsicHeight
(
width
);
final
bool
hasDivider
=
contentHeight
>
0.0
&&
actionsHeight
>
0.0
;
double
height
=
contentHeight
+
(
hasDivider
?
_dividerThickness
:
0.0
)
+
actionsHeight
;
if
(
actionsHeight
>
0
||
contentHeight
>
0
)
height
-=
2
*
_kEdgeVerticalPadding
;
if
(
height
.
isFinite
)
return
height
;
return
0.0
;
}
@override
void
performLayout
()
{
final
bool
hasDivider
=
contentSection
.
getMaxIntrinsicHeight
(
constraints
.
maxWidth
)
>
0.0
&&
actionsSection
.
getMaxIntrinsicHeight
(
constraints
.
maxWidth
)
>
0.0
;
final
double
dividerThickness
=
hasDivider
?
_dividerThickness
:
0.0
;
final
double
minActionsHeight
=
actionsSection
.
getMinIntrinsicHeight
(
constraints
.
maxWidth
);
// Size alert content.
contentSection
.
layout
(
constraints
.
deflate
(
new
EdgeInsets
.
only
(
bottom:
minActionsHeight
+
dividerThickness
)),
parentUsesSize:
true
,
);
final
Size
contentSize
=
contentSection
.
size
;
// Size alert actions.
actionsSection
.
layout
(
constraints
.
deflate
(
new
EdgeInsets
.
only
(
top:
contentSize
.
height
+
dividerThickness
)),
parentUsesSize:
true
,
);
final
Size
actionsSize
=
actionsSection
.
size
;
// Calculate overall alert height.
final
double
actionSheetHeight
=
contentSize
.
height
+
dividerThickness
+
actionsSize
.
height
;
// Set our size now that layout calculations are complete.
size
=
new
Size
(
constraints
.
maxWidth
,
actionSheetHeight
);
// Set the position of the actions box to sit at the bottom of the alert.
// The content box defaults to the top left, which is where we want it.
assert
(
actionsSection
.
parentData
is
MultiChildLayoutParentData
);
final
MultiChildLayoutParentData
actionParentData
=
actionsSection
.
parentData
;
actionParentData
.
offset
=
new
Offset
(
0.0
,
contentSize
.
height
+
dividerThickness
);
}
@override
void
paint
(
PaintingContext
context
,
Offset
offset
)
{
final
MultiChildLayoutParentData
contentParentData
=
contentSection
.
parentData
;
contentSection
.
paint
(
context
,
offset
+
contentParentData
.
offset
);
final
bool
hasDivider
=
contentSection
.
size
.
height
>
0.0
&&
actionsSection
.
size
.
height
>
0.0
;
if
(
hasDivider
)
{
_paintDividerBetweenContentAndActions
(
context
.
canvas
,
offset
);
}
final
MultiChildLayoutParentData
actionsParentData
=
actionsSection
.
parentData
;
actionsSection
.
paint
(
context
,
offset
+
actionsParentData
.
offset
);
}
void
_paintDividerBetweenContentAndActions
(
Canvas
canvas
,
Offset
offset
)
{
canvas
.
drawRect
(
Rect
.
fromLTWH
(
offset
.
dx
,
offset
.
dy
+
contentSection
.
size
.
height
,
size
.
width
,
_dividerThickness
,
),
_dividerPaint
,
);
}
@override
bool
hitTestChildren
(
HitTestResult
result
,
{
Offset
position
})
{
bool
isHit
=
false
;
final
MultiChildLayoutParentData
contentSectionParentData
=
contentSection
.
parentData
;
final
MultiChildLayoutParentData
actionsSectionParentData
=
actionsSection
.
parentData
;
if
(
contentSection
.
hitTest
(
result
,
position:
position
-
contentSectionParentData
.
offset
))
{
isHit
=
true
;
}
else
if
(
actionsSection
.
hitTest
(
result
,
position:
position
-
actionsSectionParentData
.
offset
))
{
isHit
=
true
;
}
return
isHit
;
}
}
// Visual components of an alert that need to be explicitly sized and
// laid out at runtime.
enum
_AlertSections
{
contentSection
,
actionsSection
,
}
// The "content section" of a CupertinoActionSheet.
//
// If title is missing, then only content is added. If content is
// missing, then only a title is added. If both are missing, then it returns
// a SingleChildScrollView with a zero-sized Container.
class
_CupertinoAlertContentSection
extends
StatelessWidget
{
const
_CupertinoAlertContentSection
({
Key
key
,
this
.
title
,
this
.
message
,
this
.
scrollController
,
})
:
super
(
key:
key
);
// An optional title of the action sheet. When the message is non-null,
// the font of the title is bold.
//
// Typically a Text widget.
final
Widget
title
;
// An optional descriptive message that provides more details about the
// reason for the alert.
//
// Typically a Text widget.
final
Widget
message
;
// A scroll controller that can be used to control the scrolling of the
// content in the action sheet.
//
// Defaults to null, and is typically not needed, since most alert contents
// are short.
final
ScrollController
scrollController
;
@override
Widget
build
(
BuildContext
context
)
{
final
List
<
Widget
>
titleContentGroup
=
<
Widget
>[];
if
(
title
!=
null
)
{
titleContentGroup
.
add
(
new
Padding
(
padding:
const
EdgeInsets
.
only
(
left:
_kContentHorizontalPadding
,
right:
_kContentHorizontalPadding
,
bottom:
_kContentVerticalPadding
,
top:
_kContentVerticalPadding
,
),
child:
new
DefaultTextStyle
(
style:
message
==
null
?
_kActionSheetContentStyle
:
_kActionSheetContentStyle
.
copyWith
(
fontWeight:
FontWeight
.
w600
),
textAlign:
TextAlign
.
center
,
child:
title
,
),
));
}
if
(
message
!=
null
)
{
titleContentGroup
.
add
(
new
Padding
(
padding:
new
EdgeInsets
.
only
(
left:
_kContentHorizontalPadding
,
right:
_kContentHorizontalPadding
,
bottom:
title
==
null
?
_kContentVerticalPadding
:
22.0
,
top:
title
==
null
?
_kContentVerticalPadding
:
0.0
,
),
child:
new
DefaultTextStyle
(
style:
title
==
null
?
_kActionSheetContentStyle
.
copyWith
(
fontWeight:
FontWeight
.
w600
)
:
_kActionSheetContentStyle
,
textAlign:
TextAlign
.
center
,
child:
message
,
),
),
);
}
if
(
titleContentGroup
.
isEmpty
)
{
return
new
SingleChildScrollView
(
controller:
scrollController
,
child:
new
Container
(
width:
0.0
,
height:
0.0
,
),
);
}
// Add padding between the widgets if necessary.
if
(
titleContentGroup
.
length
>
1
)
{
titleContentGroup
.
insert
(
1
,
const
Padding
(
padding:
EdgeInsets
.
only
(
top:
8.0
)));
}
return
new
CupertinoScrollbar
(
child:
new
SingleChildScrollView
(
controller:
scrollController
,
child:
new
Column
(
mainAxisSize:
MainAxisSize
.
max
,
crossAxisAlignment:
CrossAxisAlignment
.
stretch
,
children:
titleContentGroup
,
),
),
);
}
}
// The "actions section" of a CupertinoActionSheet.
//
// See _RenderCupertinoAlertActions for details about action button sizing
// and layout.
class
_CupertinoAlertActionSection
extends
StatefulWidget
{
const
_CupertinoAlertActionSection
({
Key
key
,
@required
this
.
children
,
this
.
scrollController
,
this
.
hasCancelButton
,
})
:
assert
(
children
!=
null
),
super
(
key:
key
);
final
List
<
Widget
>
children
;
// A scroll controller that can be used to control the scrolling of the
// actions in the action sheet.
//
// Defaults to null, and is typically not needed, since most alerts
// don't have many actions.
final
ScrollController
scrollController
;
final
bool
hasCancelButton
;
@override
_CupertinoAlertActionSectionState
createState
()
=>
new
_CupertinoAlertActionSectionState
();
}
class
_CupertinoAlertActionSectionState
extends
State
<
_CupertinoAlertActionSection
>
{
@override
Widget
build
(
BuildContext
context
)
{
final
double
devicePixelRatio
=
MediaQuery
.
of
(
context
).
devicePixelRatio
;
final
List
<
Widget
>
interactiveButtons
=
<
Widget
>[];
for
(
int
i
=
0
;
i
<
widget
.
children
.
length
;
i
+=
1
)
{
interactiveButtons
.
add
(
new
_PressableActionButton
(
child:
widget
.
children
[
i
],
),
);
}
return
new
CupertinoScrollbar
(
child:
new
SingleChildScrollView
(
controller:
widget
.
scrollController
,
child:
new
_CupertinoAlertActionsRenderWidget
(
actionButtons:
interactiveButtons
,
dividerThickness:
_kDividerThickness
/
devicePixelRatio
,
hasCancelButton:
widget
.
hasCancelButton
,
),
),
);
}
}
// A button that updates its render state when pressed.
//
// The pressed state is forwarded to an _ActionButtonParentDataWidget. The
// corresponding _ActionButtonParentData is then interpreted and rendered
// appropriately by _RenderCupertinoAlertActions.
class
_PressableActionButton
extends
StatefulWidget
{
const
_PressableActionButton
({
@required
this
.
child
,
});
final
Widget
child
;
@override
_PressableActionButtonState
createState
()
=>
new
_PressableActionButtonState
();
}
class
_PressableActionButtonState
extends
State
<
_PressableActionButton
>
{
bool
_isPressed
=
false
;
@override
Widget
build
(
BuildContext
context
)
{
return
new
_ActionButtonParentDataWidget
(
isPressed:
_isPressed
,
// TODO:(mattcarroll): Button press dynamics need overhaul for iOS: https://github.com/flutter/flutter/issues/19786
child:
new
GestureDetector
(
excludeFromSemantics:
true
,
behavior:
HitTestBehavior
.
opaque
,
onTapDown:
(
TapDownDetails
details
)
=>
setState
(()
=>
_isPressed
=
true
),
onTapUp:
(
TapUpDetails
details
)
=>
setState
(()
=>
_isPressed
=
false
),
// TODO(mattcarroll): Cancel is currently triggered when user moves past slop instead of off button: https://github.com/flutter/flutter/issues/19783
onTapCancel:
()
=>
setState
(()
=>
_isPressed
=
false
),
child:
widget
.
child
,
),
);
}
}
// ParentDataWidget that updates _ActionButtonParentData for an action button.
//
// Each action button requires knowledge of whether or not it is pressed so that
// the alert can correctly render the button. The pressed state is held within
// _ActionButtonParentData. _ActionButtonParentDataWidget is responsible for
// updating the pressed state of an _ActionButtonParentData based on the
// incoming isPressed property.
class
_ActionButtonParentDataWidget
extends
ParentDataWidget
<
_CupertinoAlertActionsRenderWidget
>
{
const
_ActionButtonParentDataWidget
({
Key
key
,
this
.
isPressed
,
@required
Widget
child
,
})
:
super
(
key:
key
,
child:
child
);
final
bool
isPressed
;
@override
void
applyParentData
(
RenderObject
renderObject
)
{
assert
(
renderObject
.
parentData
is
_ActionButtonParentData
);
final
_ActionButtonParentData
parentData
=
renderObject
.
parentData
;
if
(
parentData
.
isPressed
!=
isPressed
)
{
parentData
.
isPressed
=
isPressed
;
// Force a repaint.
final
AbstractNode
targetParent
=
renderObject
.
parent
;
if
(
targetParent
is
RenderObject
)
targetParent
.
markNeedsPaint
();
}
}
}
// ParentData applied to individual action buttons that report whether or not
// that button is currently pressed by the user.
class
_ActionButtonParentData
extends
MultiChildLayoutParentData
{
_ActionButtonParentData
({
this
.
isPressed
=
false
,
});
bool
isPressed
;
}
// An iOS-style alert action button layout.
//
// See _RenderCupertinoAlertActions for specific layout policy details.
class
_CupertinoAlertActionsRenderWidget
extends
MultiChildRenderObjectWidget
{
_CupertinoAlertActionsRenderWidget
({
Key
key
,
@required
List
<
Widget
>
actionButtons
,
double
dividerThickness
=
0.0
,
bool
hasCancelButton
=
false
,
})
:
_dividerThickness
=
dividerThickness
,
_hasCancelButton
=
hasCancelButton
,
super
(
key:
key
,
children:
actionButtons
);
final
double
_dividerThickness
;
final
bool
_hasCancelButton
;
@override
RenderObject
createRenderObject
(
BuildContext
context
)
{
return
new
_RenderCupertinoAlertActions
(
dividerThickness:
_dividerThickness
,
hasCancelButton:
_hasCancelButton
,
);
}
@override
void
updateRenderObject
(
BuildContext
context
,
_RenderCupertinoAlertActions
renderObject
)
{
renderObject
.
dividerThickness
=
_dividerThickness
;
renderObject
.
hasCancelButton
=
_hasCancelButton
;
}
}
// An iOS-style layout policy for sizing and positioning an action sheet's
// buttons.
//
// The policy is as follows:
//
// Action sheet buttons are always stacked vertically. In the case where the
// content section and the action section combined can not fit on the screen
// without scrolling, the height of the action section is determined as
// follows.
//
// If the user has included a separate cancel button, the height of the action
// section can be up to the height of 3 action buttons (i.e., the user can
// include 1, 2, or 3 action buttons and they will appear without needing to
// be scrolled). If 4+ action buttons are provided, the height of the action
// section shrinks to 1.5 buttons tall, and is scrollable.
//
// If the user has not included a separate cancel button, the height of the
// action section is at most 1.5 buttons tall.
class
_RenderCupertinoAlertActions
extends
RenderBox
with
ContainerRenderObjectMixin
<
RenderBox
,
MultiChildLayoutParentData
>,
RenderBoxContainerDefaultsMixin
<
RenderBox
,
MultiChildLayoutParentData
>
{
_RenderCupertinoAlertActions
({
List
<
RenderBox
>
children
,
double
dividerThickness
=
0.0
,
bool
hasCancelButton
=
false
,
})
:
_dividerThickness
=
dividerThickness
,
_hasCancelButton
=
hasCancelButton
{
addAll
(
children
);
}
// The thickness of the divider between buttons.
double
get
dividerThickness
=>
_dividerThickness
;
double
_dividerThickness
;
set
dividerThickness
(
double
newValue
)
{
if
(
newValue
==
_dividerThickness
)
{
return
;
}
_dividerThickness
=
newValue
;
markNeedsLayout
();
}
bool
_hasCancelButton
;
bool
get
hasCancelButton
=>
_hasCancelButton
;
set
hasCancelButton
(
bool
newValue
)
{
if
(
newValue
==
_hasCancelButton
)
{
return
;
}
_hasCancelButton
=
newValue
;
markNeedsLayout
();
}
final
Paint
_buttonBackgroundPaint
=
new
Paint
()
..
color
=
_kBackgroundColor
..
style
=
PaintingStyle
.
fill
;
final
Paint
_pressedButtonBackgroundPaint
=
new
Paint
()
..
color
=
_kPressedColor
..
style
=
PaintingStyle
.
fill
;
final
Paint
_dividerPaint
=
new
Paint
()
..
color
=
_kButtonDividerColor
..
style
=
PaintingStyle
.
fill
;
@override
void
setupParentData
(
RenderBox
child
)
{
if
(
child
.
parentData
is
!
_ActionButtonParentData
)
child
.
parentData
=
new
_ActionButtonParentData
();
}
@override
double
computeMinIntrinsicWidth
(
double
height
)
{
return
constraints
.
minWidth
;
}
@override
double
computeMaxIntrinsicWidth
(
double
height
)
{
return
constraints
.
maxWidth
;
}
@override
double
computeMinIntrinsicHeight
(
double
width
)
{
if
(
childCount
==
0
)
return
0.0
;
if
(
childCount
==
1
)
return
firstChild
.
computeMaxIntrinsicHeight
(
width
)
+
dividerThickness
;
if
(
hasCancelButton
&&
childCount
<
4
)
return
_computeMinIntrinsicHeightWithCancel
(
width
);
return
_computeMinIntrinsicHeightWithoutCancel
(
width
);
}
// The minimum height for more than 2-3 buttons when a cancel button is
// included is the full height of button stack.
double
_computeMinIntrinsicHeightWithCancel
(
double
width
)
{
assert
(
childCount
==
2
||
childCount
==
3
);
if
(
childCount
==
2
)
{
return
firstChild
.
getMinIntrinsicHeight
(
width
)
+
childAfter
(
firstChild
).
getMinIntrinsicHeight
(
width
)
+
dividerThickness
;
}
return
firstChild
.
getMinIntrinsicHeight
(
width
)
+
childAfter
(
firstChild
).
getMinIntrinsicHeight
(
width
)
+
childAfter
(
childAfter
(
firstChild
)).
getMinIntrinsicHeight
(
width
)
+
(
dividerThickness
*
2
);
}
// The minimum height for more than 2 buttons when no cancel button or 4+
// buttons when a cancel button is included is the height of the 1st button
// + 50% the height of the 2nd button + 2 dividers.
double
_computeMinIntrinsicHeightWithoutCancel
(
double
width
)
{
assert
(
childCount
>=
2
);
return
firstChild
.
getMinIntrinsicHeight
(
width
)
+
dividerThickness
+
(
0.5
*
childAfter
(
firstChild
).
getMinIntrinsicHeight
(
width
));
}
@override
double
computeMaxIntrinsicHeight
(
double
width
)
{
if
(
childCount
==
0
)
return
0.0
;
if
(
childCount
==
1
)
return
firstChild
.
computeMaxIntrinsicHeight
(
width
)
+
dividerThickness
;
return
_computeMaxIntrinsicHeightStacked
(
width
);
}
// Max height of a stack of buttons is the sum of all button heights + a
// divider for each button.
double
_computeMaxIntrinsicHeightStacked
(
double
width
)
{
assert
(
childCount
>=
2
);
final
double
allDividersHeight
=
(
childCount
-
1
)
*
dividerThickness
;
double
heightAccumulation
=
allDividersHeight
;
RenderBox
button
=
firstChild
;
while
(
button
!=
null
)
{
heightAccumulation
+=
button
.
getMaxIntrinsicHeight
(
width
);
button
=
childAfter
(
button
);
}
return
heightAccumulation
;
}
@override
void
performLayout
()
{
final
BoxConstraints
perButtonConstraints
=
constraints
.
copyWith
(
minHeight:
0.0
,
maxHeight:
double
.
infinity
,
);
RenderBox
child
=
firstChild
;
int
index
=
0
;
double
verticalOffset
=
0.0
;
while
(
child
!=
null
)
{
child
.
layout
(
perButtonConstraints
,
parentUsesSize:
true
,
);
assert
(
child
.
parentData
is
MultiChildLayoutParentData
);
final
MultiChildLayoutParentData
parentData
=
child
.
parentData
;
parentData
.
offset
=
new
Offset
(
0.0
,
verticalOffset
);
verticalOffset
+=
child
.
size
.
height
;
if
(
index
<
childCount
-
1
)
{
// Add a gap for the next divider.
verticalOffset
+=
dividerThickness
;
}
index
+=
1
;
child
=
childAfter
(
child
);
}
size
=
constraints
.
constrain
(
new
Size
(
constraints
.
maxWidth
,
verticalOffset
)
);
}
@override
void
paint
(
PaintingContext
context
,
Offset
offset
)
{
final
Canvas
canvas
=
context
.
canvas
;
_drawButtonBackgroundsAndDividersStacked
(
canvas
,
offset
);
_drawButtons
(
context
,
offset
);
}
void
_drawButtonBackgroundsAndDividersStacked
(
Canvas
canvas
,
Offset
offset
)
{
final
Offset
dividerOffset
=
new
Offset
(
0.0
,
dividerThickness
);
final
Path
backgroundFillPath
=
new
Path
()
..
fillType
=
PathFillType
.
evenOdd
..
addRect
(
Rect
.
largest
);
final
Path
pressedBackgroundFillPath
=
new
Path
();
final
Path
dividersPath
=
new
Path
();
Offset
accumulatingOffset
=
offset
;
RenderBox
child
=
firstChild
;
RenderBox
prevChild
;
while
(
child
!=
null
)
{
assert
(
child
.
parentData
is
_ActionButtonParentData
);
final
_ActionButtonParentData
currentButtonParentData
=
child
.
parentData
;
final
bool
isButtonPressed
=
currentButtonParentData
.
isPressed
;
bool
isPrevButtonPressed
=
false
;
if
(
prevChild
!=
null
)
{
assert
(
prevChild
.
parentData
is
_ActionButtonParentData
);
final
_ActionButtonParentData
previousButtonParentData
=
prevChild
.
parentData
;
isPrevButtonPressed
=
previousButtonParentData
.
isPressed
;
}
final
bool
isDividerPresent
=
child
!=
firstChild
;
final
bool
isDividerPainted
=
isDividerPresent
&&
!(
isButtonPressed
||
isPrevButtonPressed
);
final
Rect
dividerRect
=
new
Rect
.
fromLTWH
(
accumulatingOffset
.
dx
,
accumulatingOffset
.
dy
,
size
.
width
,
_dividerThickness
,
);
final
Rect
buttonBackgroundRect
=
new
Rect
.
fromLTWH
(
accumulatingOffset
.
dx
,
accumulatingOffset
.
dy
+
(
isDividerPresent
?
dividerThickness
:
0.0
),
size
.
width
,
child
.
size
.
height
,
);
// If this button is pressed, then we don't want a white background to be
// painted, so we erase this button from the background path.
if
(
isButtonPressed
)
{
backgroundFillPath
.
addRect
(
buttonBackgroundRect
);
pressedBackgroundFillPath
.
addRect
(
buttonBackgroundRect
);
}
// If this divider is needed, then we erase the divider area from the
// background path, and on top of that we paint a translucent gray to
// darken the divider area.
if
(
isDividerPainted
)
{
backgroundFillPath
.
addRect
(
dividerRect
);
dividersPath
.
addRect
(
dividerRect
);
}
accumulatingOffset
+=
(
isDividerPresent
?
dividerOffset
:
Offset
.
zero
)
+
new
Offset
(
0.0
,
child
.
size
.
height
);
prevChild
=
child
;
child
=
childAfter
(
child
);
}
canvas
.
drawPath
(
backgroundFillPath
,
_buttonBackgroundPaint
);
canvas
.
drawPath
(
pressedBackgroundFillPath
,
_pressedButtonBackgroundPaint
);
canvas
.
drawPath
(
dividersPath
,
_dividerPaint
);
}
void
_drawButtons
(
PaintingContext
context
,
Offset
offset
)
{
RenderBox
child
=
firstChild
;
while
(
child
!=
null
)
{
final
MultiChildLayoutParentData
childParentData
=
child
.
parentData
;
context
.
paintChild
(
child
,
childParentData
.
offset
+
offset
);
child
=
childAfter
(
child
);
}
}
@override
bool
hitTestChildren
(
HitTestResult
result
,
{
Offset
position
})
{
return
defaultHitTestChildren
(
result
,
position:
position
);
}
}
packages/flutter/lib/src/cupertino/route.dart
View file @
96326d47
...
...
@@ -14,6 +14,9 @@ const double _kMinFlingVelocity = 1.0; // Screen widths per second.
// Barrier color for a Cupertino modal barrier.
const
Color
_kModalBarrierColor
=
Color
(
0x6604040F
);
// The duration of the transition used when a modal popup is shown.
const
Duration
_kModalPopupTransitionDuration
=
Duration
(
milliseconds:
335
);
// Offset from offscreen to the right to fully on screen.
final
Tween
<
Offset
>
_kRightMiddleTween
=
new
Tween
<
Offset
>(
begin:
const
Offset
(
1.0
,
0.0
),
...
...
@@ -715,6 +718,102 @@ class _CupertinoEdgeShadowPainter extends BoxPainter {
}
}
class
_CupertinoModalPopupRoute
<
T
>
extends
PopupRoute
<
T
>
{
_CupertinoModalPopupRoute
({
this
.
builder
,
this
.
barrierLabel
,
RouteSettings
settings
,
})
:
super
(
settings:
settings
);
final
WidgetBuilder
builder
;
@override
final
String
barrierLabel
;
@override
Color
get
barrierColor
=>
_kModalBarrierColor
;
@override
bool
get
barrierDismissible
=>
true
;
@override
bool
get
semanticsDismissible
=>
false
;
@override
Duration
get
transitionDuration
=>
_kModalPopupTransitionDuration
;
Animation
<
double
>
_animation
;
Tween
<
Offset
>
_offsetTween
;
@override
Animation
<
double
>
createAnimation
()
{
assert
(
_animation
==
null
);
_animation
=
new
CurvedAnimation
(
parent:
super
.
createAnimation
(),
curve:
Curves
.
ease
,
reverseCurve:
Curves
.
ease
.
flipped
,
);
_offsetTween
=
new
Tween
<
Offset
>(
begin:
const
Offset
(
0.0
,
1.0
),
end:
const
Offset
(
0.0
,
0.0
),
);
return
_animation
;
}
@override
Widget
buildPage
(
BuildContext
context
,
Animation
<
double
>
animation
,
Animation
<
double
>
secondaryAnimation
)
{
return
builder
(
context
);
}
@override
Widget
buildTransitions
(
BuildContext
context
,
Animation
<
double
>
animation
,
Animation
<
double
>
secondaryAnimation
,
Widget
child
)
{
return
new
Align
(
alignment:
Alignment
.
bottomCenter
,
child:
new
FractionalTranslation
(
translation:
_offsetTween
.
evaluate
(
_animation
),
child:
child
,
),
);
}
}
/// Shows a modal iOS-style popup that slides up from the bottom of the screen.
///
/// Such a popup is an alternative to a menu or a dialog and prevents the user
/// from interacting with the rest of the app.
///
/// The `context` argument is used to look up the [Navigator] for the popup.
/// It is only used when the method is called. Its corresponding widget can be
/// safely removed from the tree before the popup is closed.
///
/// The `builder` argument typically builds a [CupertinoActionSheet] widget.
/// Content below the widget is dimmed with a [ModalBarrier]. The widget built
/// by the `builder` does not share a context with the location that
/// `showCupertinoModalPopup` is originally called from. Use a
/// [StatefulBuilder] or a custom [StatefulWidget] if the widget needs to
/// update dynamically.
///
/// Returns a `Future` that resolves to the value that was passed to
/// [Navigator.pop] when the popup was closed.
///
/// See also:
///
/// * [ActionSheet], which is the widget usually returned by the `builder`
/// argument to [showCupertinoModalPopup].
/// * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/>
Future
<
T
>
showCupertinoModalPopup
<
T
>({
@required
BuildContext
context
,
@required
WidgetBuilder
builder
,
})
{
return
Navigator
.
of
(
context
,
rootNavigator:
true
).
push
(
new
_CupertinoModalPopupRoute
<
T
>(
builder:
builder
,
barrierLabel:
'Dismiss'
,
),
);
}
Widget
_buildCupertinoDialogTransitions
(
BuildContext
context
,
Animation
<
double
>
animation
,
Animation
<
double
>
secondaryAnimation
,
Widget
child
)
{
final
CurvedAnimation
fadeAnimation
=
new
CurvedAnimation
(
parent:
animation
,
...
...
packages/flutter/lib/src/painting/box_decoration.dart
View file @
96326d47
...
...
@@ -70,6 +70,7 @@ class BoxDecoration extends Decoration {
/// [BoxShape.circle].
/// * If [boxShadow] is null, this decoration does not paint a shadow.
/// * If [gradient] is null, this decoration does not paint gradients.
/// * If [backgroundBlendMode] is null, this decoration paints with [BlendMode.srcOver]
///
/// The [shape] argument must not be null.
const
BoxDecoration
({
...
...
@@ -79,13 +80,20 @@ class BoxDecoration extends Decoration {
this
.
borderRadius
,
this
.
boxShadow
,
this
.
gradient
,
this
.
backgroundBlendMode
,
this
.
shape
=
BoxShape
.
rectangle
,
})
:
assert
(
shape
!=
null
);
})
:
assert
(
shape
!=
null
),
// TODO(mattcarroll): Use "backgroundBlendMode == null" when Dart #31140 is in.
assert
(
identical
(
backgroundBlendMode
,
null
)
||
color
!=
null
||
gradient
!=
null
,
'backgroundBlendMode applies to BoxDecoration
\'
s background color or'
'gradient, but no color or gradient were provided.'
);
@override
bool
debugAssertIsValid
()
{
assert
(
shape
!=
BoxShape
.
circle
||
borderRadius
==
null
);
// Can't have a border radius if you're a circle.
borderRadius
==
null
);
// Can't have a border radius if you're a circle.
return
super
.
debugAssertIsValid
();
}
...
...
@@ -136,6 +144,14 @@ class BoxDecoration extends Decoration {
/// The [gradient] is drawn under the [image].
final
Gradient
gradient
;
/// The blend mode applied to the [color] or [gradient] background of the box.
///
/// If no [backgroundBlendMode] is provided, then the default painting blend
/// mode is used.
///
/// If no [color] or [gradient] is provided, then blend mode has no impact.
final
BlendMode
backgroundBlendMode
;
/// The shape to fill the background [color], [gradient], and [image] into and
/// to cast as the [boxShadow].
///
...
...
@@ -332,6 +348,8 @@ class _BoxDecorationPainter extends BoxPainter {
if
(
_cachedBackgroundPaint
==
null
||
(
_decoration
.
gradient
!=
null
&&
_rectForCachedBackgroundPaint
!=
rect
))
{
final
Paint
paint
=
new
Paint
();
if
(
_decoration
.
backgroundBlendMode
!=
null
)
paint
.
blendMode
=
_decoration
.
backgroundBlendMode
;
if
(
_decoration
.
color
!=
null
)
paint
.
color
=
_decoration
.
color
;
if
(
_decoration
.
gradient
!=
null
)
{
...
...
packages/flutter/lib/src/widgets/modal_barrier.dart
View file @
96326d47
...
...
@@ -33,6 +33,7 @@ class ModalBarrier extends StatelessWidget {
this
.
color
,
this
.
dismissible
=
true
,
this
.
semanticsLabel
,
this
.
barrierSemanticsDismissible
=
true
,
})
:
super
(
key:
key
);
/// If non-null, fill the barrier with this color.
...
...
@@ -51,6 +52,13 @@ class ModalBarrier extends StatelessWidget {
/// [ModalBarrier] built by [ModalRoute] pages.
final
bool
dismissible
;
/// Whether the modal barrier semantics are included in the semantics tree.
///
/// See also:
/// * [ModalRoute.semanticsDismissible], which controls this property for
/// the [ModalBarrier] built by [ModalRoute] pages.
final
bool
barrierSemanticsDismissible
;
/// Semantics label used for the barrier if it is [dismissable].
///
/// The semantics label is read out by accessibility tools (e.g. TalkBack
...
...
@@ -66,10 +74,12 @@ class ModalBarrier extends StatelessWidget {
Widget
build
(
BuildContext
context
)
{
assert
(!
dismissible
||
semanticsLabel
==
null
||
debugCheckHasDirectionality
(
context
));
final
bool
semanticsDismissible
=
dismissible
&&
defaultTargetPlatform
!=
TargetPlatform
.
android
;
final
bool
modalBarrierSemanticsDismissible
=
barrierSemanticsDismissible
??
semanticsDismissible
;
return
new
BlockSemantics
(
child:
new
ExcludeSemantics
(
// On Android, the back button is used to dismiss a modal.
excluding:
!
semanticsDismissible
,
// On Android, the back button is used to dismiss a modal. On iOS, some
// modal barriers are not dismissible in accessibility mode.
excluding:
!
semanticsDismissible
||
!
modalBarrierSemanticsDismissible
,
child:
new
GestureDetector
(
onTapDown:
(
TapDownDetails
details
)
{
if
(
dismissible
)
...
...
@@ -117,6 +127,7 @@ class AnimatedModalBarrier extends AnimatedWidget {
Animation
<
Color
>
color
,
this
.
dismissible
=
true
,
this
.
semanticsLabel
,
this
.
barrierSemanticsDismissible
,
})
:
super
(
key:
key
,
listenable:
color
);
/// If non-null, fill the barrier with this color.
...
...
@@ -145,12 +156,20 @@ class AnimatedModalBarrier extends AnimatedWidget {
/// [ModalBarrier] built by [ModalRoute] pages.
final
String
semanticsLabel
;
/// Whether the modal barrier semantics are included in the semantics tree.
///
/// See also:
/// * [ModalRoute.semanticsDismissible], which controls this property for
/// the [ModalBarrier] built by [ModalRoute] pages.
final
bool
barrierSemanticsDismissible
;
@override
Widget
build
(
BuildContext
context
)
{
return
new
ModalBarrier
(
color:
color
?.
value
,
dismissible:
dismissible
,
semanticsLabel:
semanticsLabel
,
barrierSemanticsDismissible:
barrierSemanticsDismissible
,
);
}
}
packages/flutter/lib/src/widgets/routes.dart
View file @
96326d47
...
...
@@ -898,6 +898,21 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// * [ModalBarrier], the widget that implements this feature.
bool
get
barrierDismissible
;
/// Whether the semantics of the modal barrier are included in the
/// semantics tree.
///
/// The modal barrier is the scrim that is rendered behind each route, which
/// generally prevents the user from interacting with the route below the
/// current route, and normally partially obscures such routes.
///
/// If [semanticsDismissible] is true, then modal barrier semantics are
/// included in the semantics tree.
///
/// If [semanticsDismissible] is false, then modal barrier semantics are
/// excluded from the the semantics tree and tapping on the modal barrier
/// has no effect.
bool
get
semanticsDismissible
=>
true
;
/// The color to use for the modal barrier. If this is null, the barrier will
/// be transparent.
///
...
...
@@ -1173,11 +1188,13 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
color:
color
,
dismissible:
barrierDismissible
,
// changedInternalState is called if this updates
semanticsLabel:
barrierLabel
,
// changedInternalState is called if this updates
barrierSemanticsDismissible:
semanticsDismissible
,
);
}
else
{
barrier
=
new
ModalBarrier
(
dismissible:
barrierDismissible
,
// changedInternalState is called if this updates
semanticsLabel:
barrierLabel
,
// changedInternalState is called if this updates
barrierSemanticsDismissible:
semanticsDismissible
,
);
}
return
new
IgnorePointer
(
...
...
packages/flutter/test/cupertino/action_sheet_test.dart
0 → 100644
View file @
96326d47
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'../widgets/semantics_tester.dart'
;
void
main
(
)
{
testWidgets
(
'Verify that a tap on modal barrier dismisses an action sheet'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
const
CupertinoActionSheet
(
title:
Text
(
'Action Sheet'
),
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Action Sheet'
),
findsOneWidget
);
await
tester
.
tapAt
(
const
Offset
(
20.0
,
20.0
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Action Sheet'
),
findsNothing
);
});
testWidgets
(
'Verify that a tap on title section (not buttons) does not dismiss an action sheet'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
const
CupertinoActionSheet
(
title:
Text
(
'Action Sheet'
),
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Action Sheet'
),
findsOneWidget
);
await
tester
.
tap
(
find
.
text
(
'Action Sheet'
));
await
tester
.
pump
();
expect
(
find
.
text
(
'Action Sheet'
),
findsOneWidget
);
});
testWidgets
(
'Action sheet destructive text style'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
boilerplate
(
new
CupertinoActionSheetAction
(
isDestructiveAction:
true
,
child:
const
Text
(
'Ok'
),
onPressed:
()
{},
),
),
);
final
DefaultTextStyle
widget
=
tester
.
widget
(
find
.
widgetWithText
(
DefaultTextStyle
,
'Ok'
));
expect
(
widget
.
style
.
color
,
CupertinoColors
.
destructiveRed
);
});
testWidgets
(
'Action sheet default text style'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
boilerplate
(
new
CupertinoActionSheetAction
(
isDefaultAction:
true
,
child:
const
Text
(
'Ok'
),
onPressed:
()
{},
),
),
);
final
DefaultTextStyle
widget
=
tester
.
widget
(
find
.
widgetWithText
(
DefaultTextStyle
,
'Ok'
));
expect
(
widget
.
style
.
fontWeight
,
equals
(
FontWeight
.
w600
));
});
testWidgets
(
'Action sheet text styles are correct when both title and message are included'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
const
CupertinoActionSheet
(
title:
Text
(
'Action Sheet'
),
message:
Text
(
'An action sheet'
)
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
final
DefaultTextStyle
titleStyle
=
tester
.
firstWidget
(
find
.
widgetWithText
(
DefaultTextStyle
,
'Action Sheet'
));
final
DefaultTextStyle
messageStyle
=
tester
.
firstWidget
(
find
.
widgetWithText
(
DefaultTextStyle
,
'An action sheet'
));
expect
(
titleStyle
.
style
.
fontWeight
,
FontWeight
.
w600
);
expect
(
messageStyle
.
style
.
fontWeight
,
FontWeight
.
w400
);
});
testWidgets
(
'Action sheet text styles are correct when title but no message is included'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
const
CupertinoActionSheet
(
title:
Text
(
'Action Sheet'
),
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
final
DefaultTextStyle
titleStyle
=
tester
.
firstWidget
(
find
.
widgetWithText
(
DefaultTextStyle
,
'Action Sheet'
));
expect
(
titleStyle
.
style
.
fontWeight
,
FontWeight
.
w400
);
});
testWidgets
(
'Action sheet text styles are correct when message but no title is included'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
const
CupertinoActionSheet
(
message:
Text
(
'An action sheet'
),
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
final
DefaultTextStyle
messageStyle
=
tester
.
firstWidget
(
find
.
widgetWithText
(
DefaultTextStyle
,
'An action sheet'
));
expect
(
messageStyle
.
style
.
fontWeight
,
FontWeight
.
w600
);
});
testWidgets
(
'Content section but no actions'
,
(
WidgetTester
tester
)
async
{
final
ScrollController
scrollController
=
new
ScrollController
();
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
CupertinoActionSheet
(
title:
const
Text
(
'The title'
),
message:
const
Text
(
'The message.'
),
messageScrollController:
scrollController
,
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
// Content section should be at the bottom left of action sheet
// (minus padding).
expect
(
tester
.
getBottomLeft
(
find
.
byType
(
ClipRRect
)),
tester
.
getBottomLeft
(
find
.
byType
(
CupertinoActionSheet
))
-
const
Offset
(-
8.0
,
10.0
));
// Check that the dialog size is the same as the content section size
// (minus padding).
expect
(
tester
.
getSize
(
find
.
byType
(
ClipRRect
)).
height
,
tester
.
getSize
(
find
.
byType
(
CupertinoActionSheet
)).
height
-
20.0
,
);
expect
(
tester
.
getSize
(
find
.
byType
(
ClipRRect
)).
width
,
tester
.
getSize
(
find
.
byType
(
CupertinoActionSheet
)).
width
-
16.0
,
);
});
testWidgets
(
'Actions but no content section'
,
(
WidgetTester
tester
)
async
{
final
ScrollController
actionScrollController
=
new
ScrollController
();
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
CupertinoActionSheet
(
actions:
<
Widget
>[
new
CupertinoActionSheetAction
(
child:
const
Text
(
'One'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Two'
),
onPressed:
()
{},
),
],
actionScrollController:
actionScrollController
,
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
final
Finder
finder
=
find
.
byElementPredicate
(
(
Element
element
)
{
return
element
.
widget
.
runtimeType
.
toString
()
==
'_CupertinoAlertActionSection'
;
},
);
// Check that the title/message section is not displayed (action section is
// at the top of the action sheet + padding).
expect
(
tester
.
getTopLeft
(
finder
),
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
))
+
const
Offset
(
8.0
,
10.0
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
))
+
const
Offset
(
8.0
,
10.0
),
tester
.
getTopLeft
(
find
.
widgetWithText
(
CupertinoActionSheetAction
,
'One'
)));
expect
(
tester
.
getBottomLeft
(
find
.
byType
(
CupertinoActionSheet
))
+
const
Offset
(
8.0
,
-
10.0
),
tester
.
getBottomLeft
(
find
.
widgetWithText
(
CupertinoActionSheetAction
,
'Two'
)));
});
testWidgets
(
'Action section is scrollable'
,
(
WidgetTester
tester
)
async
{
final
ScrollController
actionScrollController
=
new
ScrollController
();
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
Builder
(
builder:
(
BuildContext
context
)
{
return
new
MediaQuery
(
data:
MediaQuery
.
of
(
context
).
copyWith
(
textScaleFactor:
3.0
),
child:
new
CupertinoActionSheet
(
title:
const
Text
(
'The title'
),
message:
const
Text
(
'The message.'
),
actions:
<
Widget
>[
new
CupertinoActionSheetAction
(
child:
const
Text
(
'One'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Two'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Three'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Four'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Five'
),
onPressed:
()
{},
),
],
actionScrollController:
actionScrollController
,
),
);
}),
)
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
// Check that the action buttons list is scrollable.
expect
(
actionScrollController
.
offset
,
0.0
);
actionScrollController
.
jumpTo
(
100.0
);
expect
(
actionScrollController
.
offset
,
100.0
);
actionScrollController
.
jumpTo
(
0.0
);
// Check that the action buttons are aligned vertically.
expect
(
tester
.
getCenter
(
find
.
widgetWithText
(
CupertinoActionSheetAction
,
'One'
)).
dx
,
equals
(
400.0
));
expect
(
tester
.
getCenter
(
find
.
widgetWithText
(
CupertinoActionSheetAction
,
'Two'
)).
dx
,
equals
(
400.0
));
expect
(
tester
.
getCenter
(
find
.
widgetWithText
(
CupertinoActionSheetAction
,
'Three'
)).
dx
,
equals
(
400.0
));
expect
(
tester
.
getCenter
(
find
.
widgetWithText
(
CupertinoActionSheetAction
,
'Four'
)).
dx
,
equals
(
400.0
));
expect
(
tester
.
getCenter
(
find
.
widgetWithText
(
CupertinoActionSheetAction
,
'Five'
)).
dx
,
equals
(
400.0
));
// Check that the action buttons are the correct heights.
expect
(
tester
.
getSize
(
find
.
widgetWithText
(
CupertinoActionSheetAction
,
'One'
)).
height
,
equals
(
92.0
));
expect
(
tester
.
getSize
(
find
.
widgetWithText
(
CupertinoActionSheetAction
,
'Two'
)).
height
,
equals
(
92.0
));
expect
(
tester
.
getSize
(
find
.
widgetWithText
(
CupertinoActionSheetAction
,
'Three'
)).
height
,
equals
(
92.0
));
expect
(
tester
.
getSize
(
find
.
widgetWithText
(
CupertinoActionSheetAction
,
'Four'
)).
height
,
equals
(
92.0
));
expect
(
tester
.
getSize
(
find
.
widgetWithText
(
CupertinoActionSheetAction
,
'Five'
)).
height
,
equals
(
92.0
));
});
testWidgets
(
'Content section is scrollable'
,
(
WidgetTester
tester
)
async
{
final
ScrollController
messageScrollController
=
new
ScrollController
();
double
screenHeight
;
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
Builder
(
builder:
(
BuildContext
context
)
{
screenHeight
=
MediaQuery
.
of
(
context
).
size
.
height
;
return
new
MediaQuery
(
data:
MediaQuery
.
of
(
context
).
copyWith
(
textScaleFactor:
3.0
),
child:
new
CupertinoActionSheet
(
title:
const
Text
(
'The title'
),
message:
new
Text
(
'Very long content'
*
200
),
actions:
<
Widget
>[
new
CupertinoActionSheetAction
(
child:
const
Text
(
'One'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Two'
),
onPressed:
()
{},
),
],
messageScrollController:
messageScrollController
,
),
);
}),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
expect
(
messageScrollController
.
offset
,
0.0
);
messageScrollController
.
jumpTo
(
100.0
);
expect
(
messageScrollController
.
offset
,
100.0
);
// Set the scroll position back to zero.
messageScrollController
.
jumpTo
(
0.0
);
// Expect the action sheet to take all available height.
expect
(
tester
.
getSize
(
find
.
byType
(
CupertinoActionSheet
)).
height
,
screenHeight
);
});
testWidgets
(
'Tap on button calls onPressed'
,
(
WidgetTester
tester
)
async
{
bool
wasPressed
=
false
;
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
Builder
(
builder:
(
BuildContext
context
)
{
return
new
CupertinoActionSheet
(
actions:
<
Widget
>[
new
CupertinoActionSheetAction
(
child:
const
Text
(
'One'
),
onPressed:
()
{
wasPressed
=
true
;
Navigator
.
pop
(
context
);
},
),
],
);
}),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
expect
(
wasPressed
,
isFalse
);
await
tester
.
tap
(
find
.
text
(
'One'
));
expect
(
wasPressed
,
isTrue
);
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
expect
(
find
.
text
(
'One'
),
findsNothing
);
});
testWidgets
(
'Action sheet width is correct when given infinite horizontal space'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
Row
(
children:
<
Widget
>[
new
CupertinoActionSheet
(
actions:
<
Widget
>[
new
CupertinoActionSheetAction
(
child:
const
Text
(
'One'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Two'
),
onPressed:
()
{},
),
],
),
],
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
expect
(
tester
.
getSize
(
find
.
byType
(
CupertinoActionSheet
)).
width
,
600.0
);
});
testWidgets
(
'Action sheet height is correct when given infinite vertical space'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
Column
(
children:
<
Widget
>[
new
CupertinoActionSheet
(
actions:
<
Widget
>[
new
CupertinoActionSheetAction
(
child:
const
Text
(
'One'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Two'
),
onPressed:
()
{},
),
],
),
],
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
expect
(
tester
.
getSize
(
find
.
byType
(
CupertinoActionSheet
)).
height
,
moreOrLessEquals
(
132.33333333333334
));
});
testWidgets
(
'1 action button with cancel button'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
CupertinoActionSheet
(
title:
const
Text
(
'The title'
),
message:
new
Text
(
'Very long content'
*
200
),
actions:
<
Widget
>[
new
CupertinoActionSheetAction
(
child:
const
Text
(
'One'
),
onPressed:
()
{},
),
],
cancelButton:
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Cancel'
),
onPressed:
()
{},
),
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
// Action section is size of one action button.
expect
(
findScrollableActionsSectionRenderBox
(
tester
).
size
.
height
,
56.0
);
});
testWidgets
(
'2 action buttons with cancel button'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
CupertinoActionSheet
(
title:
const
Text
(
'The title'
),
message:
new
Text
(
'Very long content'
*
200
),
actions:
<
Widget
>[
new
CupertinoActionSheetAction
(
child:
const
Text
(
'One'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Two'
),
onPressed:
()
{},
),
],
cancelButton:
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Cancel'
),
onPressed:
()
{},
),
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
expect
(
findScrollableActionsSectionRenderBox
(
tester
).
size
.
height
,
moreOrLessEquals
(
112.33333333333331
));
});
testWidgets
(
'3 action buttons with cancel button'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
CupertinoActionSheet
(
title:
const
Text
(
'The title'
),
message:
new
Text
(
'Very long content'
*
200
),
actions:
<
Widget
>[
new
CupertinoActionSheetAction
(
child:
const
Text
(
'One'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Two'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Three'
),
onPressed:
()
{},
),
],
cancelButton:
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Cancel'
),
onPressed:
()
{},
),
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
expect
(
findScrollableActionsSectionRenderBox
(
tester
).
size
.
height
,
moreOrLessEquals
(
168.66666666666669
));
});
testWidgets
(
'4+ action buttons with cancel button'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
CupertinoActionSheet
(
title:
const
Text
(
'The title'
),
message:
new
Text
(
'Very long content'
*
200
),
actions:
<
Widget
>[
new
CupertinoActionSheetAction
(
child:
const
Text
(
'One'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Two'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Three'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Four'
),
onPressed:
()
{},
),
],
cancelButton:
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Cancel'
),
onPressed:
()
{},
),
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
expect
(
findScrollableActionsSectionRenderBox
(
tester
).
size
.
height
,
moreOrLessEquals
(
84.33333333333337
));
});
testWidgets
(
'1 action button without cancel button'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
CupertinoActionSheet
(
title:
const
Text
(
'The title'
),
message:
new
Text
(
'Very long content'
*
200
),
actions:
<
Widget
>[
new
CupertinoActionSheetAction
(
child:
const
Text
(
'One'
),
onPressed:
()
{},
),
],
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
expect
(
findScrollableActionsSectionRenderBox
(
tester
).
size
.
height
,
56.0
);
});
testWidgets
(
'2+ action buttons without cancel button'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
CupertinoActionSheet
(
title:
const
Text
(
'The title'
),
message:
new
Text
(
'Very long content'
*
200
),
actions:
<
Widget
>[
new
CupertinoActionSheetAction
(
child:
const
Text
(
'One'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Two'
),
onPressed:
()
{},
),
],
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
expect
(
findScrollableActionsSectionRenderBox
(
tester
).
size
.
height
,
moreOrLessEquals
(
84.33333333333337
));
});
testWidgets
(
'Action sheet with just cancel button is correct'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
CupertinoActionSheet
(
cancelButton:
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Cancel'
),
onPressed:
(){},
),
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
// Height should be cancel button height + padding
expect
(
tester
.
getSize
(
find
.
byType
(
CupertinoActionSheet
)).
height
,
76.0
);
expect
(
tester
.
getSize
(
find
.
byType
(
CupertinoActionSheet
)).
width
,
600.0
);
});
testWidgets
(
'Cancel button tap calls onPressed'
,
(
WidgetTester
tester
)
async
{
bool
wasPressed
=
false
;
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
Builder
(
builder:
(
BuildContext
context
)
{
return
new
CupertinoActionSheet
(
cancelButton:
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Cancel'
),
onPressed:
()
{
wasPressed
=
true
;
Navigator
.
pop
(
context
);
},
),
);
}),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
expect
(
wasPressed
,
isFalse
);
await
tester
.
tap
(
find
.
text
(
'Cancel'
));
expect
(
wasPressed
,
isTrue
);
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
expect
(
find
.
text
(
'Cancel'
),
findsNothing
);
});
testWidgets
(
'Layout is correct when cancel button is present'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
CupertinoActionSheet
(
title:
const
Text
(
'The title'
),
message:
const
Text
(
'The message'
),
actions:
<
Widget
>[
new
CupertinoActionSheetAction
(
child:
const
Text
(
'One'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Two'
),
onPressed:
()
{},
),
],
cancelButton:
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Cancel'
),
onPressed:
()
{},
),
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
expect
(
tester
.
getBottomLeft
(
find
.
widgetWithText
(
CupertinoActionSheetAction
,
'Cancel'
)).
dy
,
590.0
);
expect
(
tester
.
getBottomLeft
(
find
.
widgetWithText
(
CupertinoActionSheetAction
,
'One'
)).
dy
,
moreOrLessEquals
(
469.66666666666663
));
expect
(
tester
.
getBottomLeft
(
find
.
widgetWithText
(
CupertinoActionSheetAction
,
'Two'
)).
dy
,
526.0
);
});
testWidgets
(
'Enter/exit animation is correct'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
CupertinoActionSheet
(
title:
const
Text
(
'The title'
),
message:
const
Text
(
'The message'
),
actions:
<
Widget
>[
new
CupertinoActionSheetAction
(
child:
const
Text
(
'One'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Two'
),
onPressed:
()
{},
),
],
cancelButton:
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Cancel'
),
onPressed:
()
{},
),
),
),
);
// Enter animation
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
600.0
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
530.9
,
0.1
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
426.7
,
0.1
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
365.0
,
0.1
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
334.0
,
0.1
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
321.0
,
0.1
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
319.3
,
0.1
));
// Action sheet has reached final height
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
319.3
,
0.1
));
// Exit animation
await
tester
.
tapAt
(
const
Offset
(
20.0
,
20.0
));
await
tester
.
pump
();
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
319.3
,
0.1
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
388.4
,
0.1
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
492.6
,
0.1
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
554.2
,
0.1
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
585.2
,
0.1
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
598.2
,
0.1
));
// Action sheet has disappeared
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
find
.
byType
(
CupertinoActionSheet
),
findsNothing
);
});
testWidgets
(
'Modal barrier is pressed during transition'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
CupertinoActionSheet
(
title:
const
Text
(
'The title'
),
message:
const
Text
(
'The message'
),
actions:
<
Widget
>[
new
CupertinoActionSheetAction
(
child:
const
Text
(
'One'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Two'
),
onPressed:
()
{},
),
],
cancelButton:
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Cancel'
),
onPressed:
()
{},
),
),
),
);
// Enter animation
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
600.0
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
530.9
,
0.1
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
426.7
,
0.1
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
365.0
,
0.1
));
// Exit animation
await
tester
.
tapAt
(
const
Offset
(
20.0
,
20.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
426.7
,
0.1
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
closeTo
(
530.9
,
0.1
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
tester
.
getTopLeft
(
find
.
byType
(
CupertinoActionSheet
)).
dy
,
600.0
);
// Action sheet has disappeared
await
tester
.
pump
(
const
Duration
(
milliseconds:
60
));
expect
(
find
.
byType
(
CupertinoActionSheet
),
findsNothing
);
});
testWidgets
(
'Action sheet semantics'
,
(
WidgetTester
tester
)
async
{
final
SemanticsTester
semantics
=
new
SemanticsTester
(
tester
);
await
tester
.
pumpWidget
(
createAppWithButtonThatLaunchesActionSheet
(
new
CupertinoActionSheet
(
title:
const
Text
(
'The title'
),
message:
const
Text
(
'The message'
),
actions:
<
Widget
>[
new
CupertinoActionSheetAction
(
child:
const
Text
(
'One'
),
onPressed:
()
{},
),
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Two'
),
onPressed:
()
{},
),
],
cancelButton:
new
CupertinoActionSheetAction
(
child:
const
Text
(
'Cancel'
),
onPressed:
()
{},
),
),
),
);
await
tester
.
tap
(
find
.
text
(
'Go'
));
await
tester
.
pump
();
expect
(
semantics
,
hasSemantics
(
new
TestSemantics
.
root
(
children:
<
TestSemantics
>[
new
TestSemantics
(
children:
<
TestSemantics
>[
new
TestSemantics
(
flags:
<
SemanticsFlag
>[
SemanticsFlag
.
scopesRoute
,
SemanticsFlag
.
namesRoute
,
],
label:
'Alert'
,
children:
<
TestSemantics
>[
new
TestSemantics
(
children:
<
TestSemantics
>[
new
TestSemantics
(
label:
'The title'
,
),
new
TestSemantics
(
label:
'The message'
,
),
],
),
new
TestSemantics
(
children:
<
TestSemantics
>[
new
TestSemantics
(
flags:
<
SemanticsFlag
>[
SemanticsFlag
.
isButton
,
],
actions:
<
SemanticsAction
>[
SemanticsAction
.
tap
,
],
label:
'One'
,
),
new
TestSemantics
(
flags:
<
SemanticsFlag
>[
SemanticsFlag
.
isButton
,
],
actions:
<
SemanticsAction
>[
SemanticsAction
.
tap
,
],
label:
'Two'
,
),
],
),
new
TestSemantics
(
flags:
<
SemanticsFlag
>[
SemanticsFlag
.
isButton
,
],
actions:
<
SemanticsAction
>[
SemanticsAction
.
tap
,
],
label:
'Cancel'
,
)
],
),
],
),
],
),
ignoreId:
true
,
ignoreRect:
true
,
ignoreTransform:
true
,
),
);
semantics
.
dispose
();
});
}
RenderBox
findScrollableActionsSectionRenderBox
(
WidgetTester
tester
)
{
final
RenderObject
actionsSection
=
tester
.
renderObject
(
find
.
byElementPredicate
(
(
Element
element
)
{
return
element
.
widget
.
runtimeType
.
toString
()
==
'_CupertinoAlertActionSection'
;
}),
);
assert
(
actionsSection
is
RenderBox
);
return
actionsSection
;
}
Widget
createAppWithButtonThatLaunchesActionSheet
(
Widget
actionSheet
)
{
return
new
CupertinoApp
(
home:
new
Center
(
child:
new
Builder
(
builder:
(
BuildContext
context
)
{
return
new
CupertinoButton
(
onPressed:
()
{
showCupertinoModalPopup
<
void
>(
context:
context
,
builder:
(
BuildContext
context
)
{
return
actionSheet
;
},
);
},
child:
const
Text
(
'Go'
),
);
}),
),
);
}
Widget
boilerplate
(
Widget
child
)
{
return
new
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
child
,
);
}
\ No newline at end of file
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