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
a4b27cbf
Unverified
Commit
a4b27cbf
authored
Jan 20, 2021
by
LongCatIsLooong
Committed by
GitHub
Jan 20, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update CupertinoSlidingSegmentedControl (#73772)
parent
e21344fa
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
855 additions
and
503 deletions
+855
-503
sliding_segmented_control.dart
.../flutter/lib/src/cupertino/sliding_segmented_control.dart
+562
-489
sliding_segmented_control_test.dart
...lutter/test/cupertino/sliding_segmented_control_test.dart
+293
-14
No files found.
packages/flutter/lib/src/cupertino/sliding_segmented_control.dart
View file @
a4b27cbf
...
@@ -58,6 +58,12 @@ const double _kTouchYDistanceThreshold = 50.0 * 50.0;
...
@@ -58,6 +58,12 @@ const double _kTouchYDistanceThreshold = 50.0 * 50.0;
// Inspected from iOS 13.2 simulator.
// Inspected from iOS 13.2 simulator.
const
double
_kCornerRadius
=
8
;
const
double
_kCornerRadius
=
8
;
// The minimum opacity of an unselected segment, when the user presses on the
// segment and it starts to fadeout.
//
// Inspected from iOS 13.2 simulator.
const
double
_kContentPressedMinOpacity
=
0.2
;
// The spring animation used when the thumb changes its rect.
// The spring animation used when the thumb changes its rect.
final
SpringSimulation
_kThumbSpringAnimationSimulation
=
SpringSimulation
(
final
SpringSimulation
_kThumbSpringAnimationSimulation
=
SpringSimulation
(
const
SpringDescription
(
mass:
1
,
stiffness:
503.551
,
damping:
44.8799
),
const
SpringDescription
(
mass:
1
,
stiffness:
503.551
,
damping:
44.8799
),
...
@@ -72,19 +78,186 @@ const Duration _kOpacityAnimationDuration = Duration(milliseconds: 470);
...
@@ -72,19 +78,186 @@ const Duration _kOpacityAnimationDuration = Duration(milliseconds: 470);
const
Duration
_kHighlightAnimationDuration
=
Duration
(
milliseconds:
200
);
const
Duration
_kHighlightAnimationDuration
=
Duration
(
milliseconds:
200
);
class
_FontWeightTween
extends
Tween
<
FontWeight
>
{
class
_Segment
<
T
>
extends
StatefulWidget
{
_FontWeightTween
({
required
FontWeight
begin
,
required
FontWeight
end
})
:
super
(
begin:
begin
,
end:
end
);
const
_Segment
({
required
ValueKey
<
T
>
key
,
required
this
.
child
,
required
this
.
pressed
,
required
this
.
highlighted
,
required
this
.
isDragging
,
})
:
super
(
key:
key
);
final
Widget
child
;
final
bool
pressed
;
final
bool
highlighted
;
// Whether the thumb of the parent widget (CupertinoSlidingSegmentedControl)
// is currently being dragged.
final
bool
isDragging
;
bool
get
shouldFadeoutContent
=>
pressed
&&
!
highlighted
;
bool
get
shouldScaleContent
=>
pressed
&&
highlighted
&&
isDragging
;
@override
@override
FontWeight
lerp
(
double
t
)
=>
FontWeight
.
lerp
(
begin
,
end
,
t
)!;
_SegmentState
<
T
>
createState
()
=>
_SegmentState
<
T
>();
}
class
_SegmentState
<
T
>
extends
State
<
_Segment
<
T
>>
with
TickerProviderStateMixin
<
_Segment
<
T
>>
{
late
final
AnimationController
highlightPressScaleController
;
late
Animation
<
double
>
highlightPressScaleAnimation
;
@override
void
initState
()
{
super
.
initState
();
highlightPressScaleController
=
AnimationController
(
duration:
_kOpacityAnimationDuration
,
value:
widget
.
shouldScaleContent
?
1
:
0
,
vsync:
this
,
);
highlightPressScaleAnimation
=
highlightPressScaleController
.
drive
(
Tween
<
double
>(
begin:
1.0
,
end:
_kMinThumbScale
),
);
}
@override
void
didUpdateWidget
(
_Segment
<
T
>
oldWidget
)
{
assert
(
oldWidget
.
key
==
widget
.
key
);
super
.
didUpdateWidget
(
oldWidget
);
if
(
oldWidget
.
shouldScaleContent
!=
widget
.
shouldScaleContent
)
{
highlightPressScaleAnimation
=
highlightPressScaleController
.
drive
(
Tween
<
double
>(
begin:
highlightPressScaleAnimation
.
value
,
end:
widget
.
shouldScaleContent
?
_kMinThumbScale
:
1.0
),
);
highlightPressScaleController
.
animateWith
(
_kThumbSpringAnimationSimulation
);
}
}
@override
void
dispose
()
{
highlightPressScaleController
.
dispose
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
MetaData
(
// Expand the hitTest area of this widget.
behavior:
HitTestBehavior
.
opaque
,
child:
IndexedStack
(
index:
0
,
alignment:
Alignment
.
center
,
children:
<
Widget
>[
AnimatedOpacity
(
opacity:
widget
.
shouldFadeoutContent
?
_kContentPressedMinOpacity
:
1
,
duration:
_kOpacityAnimationDuration
,
curve:
Curves
.
ease
,
child:
AnimatedDefaultTextStyle
(
style:
DefaultTextStyle
.
of
(
context
)
.
style
.
merge
(
TextStyle
(
fontWeight:
widget
.
highlighted
?
FontWeight
.
w500
:
FontWeight
.
normal
)),
duration:
_kHighlightAnimationDuration
,
curve:
Curves
.
ease
,
child:
ScaleTransition
(
scale:
highlightPressScaleAnimation
,
child:
widget
.
child
,
),
)
),
// The entire widget will assume the size of this widget, so when a
// segment's "highlight" animation plays the size of the parent stays
// the same and will always be greater than equal to that of the
// visible child (at index 0), to keep the size of the entire
// SegmentedControl widget consistent throughout the animation.
Offstage
(
child:
DefaultTextStyle
.
merge
(
style:
const
TextStyle
(
fontWeight:
FontWeight
.
w500
),
child:
widget
.
child
,
),
),
],
),
);
}
}
// Fadeout the separator when either adjacent segment is highlighted.
class
_SegmentSeparator
extends
StatefulWidget
{
const
_SegmentSeparator
({
required
ValueKey
<
int
>
key
,
required
this
.
highlighted
,
})
:
super
(
key:
key
);
final
bool
highlighted
;
@override
_SegmentSeparatorState
createState
()
=>
_SegmentSeparatorState
();
}
class
_SegmentSeparatorState
extends
State
<
_SegmentSeparator
>
with
TickerProviderStateMixin
<
_SegmentSeparator
>
{
late
final
AnimationController
separatorOpacityController
;
@override
void
initState
()
{
super
.
initState
();
separatorOpacityController
=
AnimationController
(
duration:
_kSpringAnimationDuration
,
value:
widget
.
highlighted
?
0
:
1
,
vsync:
this
,
);
}
@override
void
didUpdateWidget
(
_SegmentSeparator
oldWidget
)
{
assert
(
oldWidget
.
key
==
widget
.
key
);
super
.
didUpdateWidget
(
oldWidget
);
if
(
oldWidget
.
highlighted
!=
widget
.
highlighted
)
{
separatorOpacityController
.
animateTo
(
widget
.
highlighted
?
0
:
1
,
duration:
_kSpringAnimationDuration
,
curve:
Curves
.
ease
,
);
}
}
@override
void
dispose
()
{
separatorOpacityController
.
dispose
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
AnimatedBuilder
(
animation:
separatorOpacityController
,
child:
const
SizedBox
(
width:
_kSeparatorWidth
),
builder:
(
BuildContext
context
,
Widget
?
child
)
{
return
Padding
(
padding:
_kSeparatorInset
,
child:
DecoratedBox
(
decoration:
BoxDecoration
(
color:
_kSeparatorColor
.
withOpacity
(
_kSeparatorColor
.
opacity
*
separatorOpacityController
.
value
),
borderRadius:
const
BorderRadius
.
all
(
_kSeparatorRadius
),
),
child:
child
,
),
);
},
);
}
}
}
/// An iOS 13 style segmented control.
/// An iOS 13 style segmented control.
///
///
/// Displays the widgets provided in the [Map] of [children] in a horizontal list.
/// Displays the widgets provided in the [Map] of [children] in a horizontal list.
/// Used to select between a number of mutually exclusive options. When one option
/// It allows the user to select between a number of mutually exclusive options,
/// in the segmented control is selected, the other options in the segmented
/// by tapping or dragging within the segmented control.
/// control cease to be selected.
///
///
/// A segmented control can feature any [Widget] as one of the values in its
/// A segmented control can feature any [Widget] as one of the values in its
/// [Map] of [children]. The type T is the type of the [Map] keys used to identify
/// [Map] of [children]. The type T is the type of the [Map] keys used to identify
...
@@ -93,32 +266,26 @@ class _FontWeightTween extends Tween<FontWeight> {
...
@@ -93,32 +266,26 @@ class _FontWeightTween extends Tween<FontWeight> {
/// argument must be an ordered [Map] such as a [LinkedHashMap], the ordering of
/// argument must be an ordered [Map] such as a [LinkedHashMap], the ordering of
/// the keys will determine the order of the widgets in the segmented control.
/// the keys will determine the order of the widgets in the segmented control.
///
///
///
When the state of the segmented control changes, the widget calls th
e
///
The widget calls the [onValueChanged] callback *when a valid user gestur
e
///
[onValueChanged] callback. The map key associated with the newly selected
///
completes on an unselected segment*. The map key associated with the newly
///
widget is returned in the [onValueChanged] callback. Typically, widgets
///
selected widget is returned in the [onValueChanged] callback. Typically,
///
that use a segmented control will listen for the [onValueChanged] callback
///
widgets that use a segmented control will listen for the [onValueChanged]
///
and rebuild the segmented control with a new [groupValue] to update which
///
callback and rebuild the segmented control with a new [groupValue] to update
/// option is currently selected.
///
which
option is currently selected.
///
///
/// The [children] will be displayed in the order of the keys in the [Map].
/// The [children] will be displayed in the order of the keys in the [Map],
/// along the current [TextDirection]. Each child widget will have the same size.
/// The height of the segmented control is determined by the height of the
/// The height of the segmented control is determined by the height of the
/// tallest widget provided as a value in the [Map] of [children].
/// tallest child widget. The width of each child will be the intrinsic width of
/// The width of each child in the segmented control will be equal to the width
/// the widest child, or the available horizontal space divided by the number of
/// of widest child, unless the combined width of the children is wider than
/// [children], which ever is smaller.
/// the available horizontal space. In this case, the available horizontal space
/// is divided by the number of provided [children] to determine the width of
/// each widget. The selection area for each of the widgets in the [Map] of
/// [children] will then be expanded to fill the calculated space, so each
/// widget will appear to have the same dimensions.
///
///
/// A segmented control may optionally be created with custom colors. The
/// A segmented control may optionally be created with custom colors. The
/// [thumbColor], [backgroundColor] arguments can be used to override the
segmented
/// [thumbColor], [backgroundColor] arguments can be used to override the
/// control's colors from its defaults.
///
segmented
control's colors from its defaults.
///
///
/// See also:
/// See also:
///
///
/// * [CupertinoSlidingSegmentedControl], a segmented control widget in the
/// style introduced in iOS 13.
/// * <https://developer.apple.com/design/human-interface-guidelines/ios/controls/segmented-controls/>
/// * <https://developer.apple.com/design/human-interface-guidelines/ios/controls/segmented-controls/>
class
CupertinoSlidingSegmentedControl
<
T
>
extends
StatefulWidget
{
class
CupertinoSlidingSegmentedControl
<
T
>
extends
StatefulWidget
{
/// Creates an iOS-style segmented control bar.
/// Creates an iOS-style segmented control bar.
...
@@ -157,8 +324,10 @@ class CupertinoSlidingSegmentedControl<T> extends StatefulWidget {
...
@@ -157,8 +324,10 @@ class CupertinoSlidingSegmentedControl<T> extends StatefulWidget {
/// The identifying keys and corresponding widget values in the
/// The identifying keys and corresponding widget values in the
/// segmented control.
/// segmented control.
///
///
/// This attribute must be an ordered [Map] such as a [LinkedHashMap]. Each
/// widget is typically a single-line [Text] widget or an [Icon] widget.
///
/// The map must have more than one entry.
/// The map must have more than one entry.
/// This attribute must be an ordered [Map] such as a [LinkedHashMap].
final
Map
<
T
,
Widget
>
children
;
final
Map
<
T
,
Widget
>
children
;
/// The identifier of the widget that is currently selected.
/// The identifier of the widget that is currently selected.
...
@@ -238,116 +407,59 @@ class CupertinoSlidingSegmentedControl<T> extends StatefulWidget {
...
@@ -238,116 +407,59 @@ class CupertinoSlidingSegmentedControl<T> extends StatefulWidget {
class
_SegmentedControlState
<
T
>
extends
State
<
CupertinoSlidingSegmentedControl
<
T
>>
class
_SegmentedControlState
<
T
>
extends
State
<
CupertinoSlidingSegmentedControl
<
T
>>
with
TickerProviderStateMixin
<
CupertinoSlidingSegmentedControl
<
T
>>
{
with
TickerProviderStateMixin
<
CupertinoSlidingSegmentedControl
<
T
>>
{
late
final
AnimationController
thumbController
=
AnimationController
(
duration:
_kSpringAnimationDuration
,
value:
0
,
vsync:
this
);
Animatable
<
Rect
?>?
thumbAnimatable
;
final
Map
<
T
,
AnimationController
>
_highlightControllers
=
<
T
,
AnimationController
>{};
late
final
AnimationController
thumbScaleController
=
AnimationController
(
duration:
_kSpringAnimationDuration
,
value:
0
,
vsync:
this
);
final
Tween
<
FontWeight
>
_highlightTween
=
_FontWeightTween
(
begin:
FontWeight
.
normal
,
end:
FontWeight
.
w500
);
late
Animation
<
double
>
thumbScaleAnimation
=
thumbScaleController
.
drive
(
Tween
<
double
>(
begin:
1
,
end:
_kMinThumbScale
));
final
Map
<
T
,
AnimationController
>
_pressControllers
=
<
T
,
AnimationController
>{};
final
Tween
<
double
>
_pressTween
=
Tween
<
double
>(
begin:
1
,
end:
0.2
);
late
List
<
T
>
keys
;
late
AnimationController
thumbController
;
late
AnimationController
separatorOpacityController
;
late
AnimationController
thumbScaleController
;
final
TapGestureRecognizer
tap
=
TapGestureRecognizer
();
final
TapGestureRecognizer
tap
=
TapGestureRecognizer
();
final
HorizontalDragGestureRecognizer
drag
=
HorizontalDragGestureRecognizer
();
final
HorizontalDragGestureRecognizer
drag
=
HorizontalDragGestureRecognizer
();
final
LongPressGestureRecognizer
longPress
=
LongPressGestureRecognizer
();
final
LongPressGestureRecognizer
longPress
=
LongPressGestureRecognizer
();
AnimationController
_createHighlightAnimationController
({
bool
isCompleted
=
false
})
{
return
AnimationController
(
duration:
_kHighlightAnimationDuration
,
value:
isCompleted
?
1
:
0
,
vsync:
this
,
);
}
AnimationController
_createFadeoutAnimationController
()
{
return
AnimationController
(
duration:
_kOpacityAnimationDuration
,
vsync:
this
,
);
}
@override
@override
void
initState
()
{
void
initState
()
{
super
.
initState
();
super
.
initState
();
final
GestureArenaTeam
team
=
GestureArenaTeam
();
// If the long press or horizontal drag recognizer gets accepted, we know for
// If the long press or horizontal drag recognizer gets accepted, we know for
// sure the gesture is meant for the segmented control. Hand everything to
// sure the gesture is meant for the segmented control. Hand everything to
// the drag gesture recognizer.
// the drag gesture recognizer.
final
GestureArenaTeam
team
=
GestureArenaTeam
();
longPress
.
team
=
team
;
longPress
.
team
=
team
;
drag
.
team
=
team
;
drag
.
team
=
team
;
team
.
captain
=
drag
;
team
.
captain
=
drag
;
_highlighted
=
widget
.
groupValue
;
drag
..
onDown
=
onDown
..
onUpdate
=
onUpdate
..
onEnd
=
onEnd
..
onCancel
=
onCancel
;
thumbController
=
AnimationController
(
tap
.
onTapUp
=
onTapUp
;
duration:
_kSpringAnimationDuration
,
value:
0
,
vsync:
this
,
);
thumbScaleController
=
AnimationController
(
// Empty callback to enable the long press recognizer.
duration:
_kSpringAnimationDuration
,
longPress
.
onLongPress
=
()
{
};
value:
1
,
vsync:
this
,
);
separatorOpacityController
=
AnimationController
(
highlighted
=
widget
.
groupValue
;
duration:
_kSpringAnimationDuration
,
value:
0
,
vsync:
this
,
);
for
(
final
T
currentKey
in
widget
.
children
.
keys
)
{
_highlightControllers
[
currentKey
]
=
_createHighlightAnimationController
(
isCompleted:
currentKey
==
widget
.
groupValue
,
// Highlight the current selection.
);
_pressControllers
[
currentKey
]
=
_createFadeoutAnimationController
();
}
}
}
@override
@override
void
didUpdateWidget
(
CupertinoSlidingSegmentedControl
<
T
>
oldWidget
)
{
void
didUpdateWidget
(
CupertinoSlidingSegmentedControl
<
T
>
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
super
.
didUpdateWidget
(
oldWidget
);
// Update animation controllers.
// Temporarily ignore highlight changes from the widget when the thumb is
for
(
final
T
oldKey
in
oldWidget
.
children
.
keys
)
{
// being dragged. When the drag gesture finishes the widget will be forced
if
(!
widget
.
children
.
containsKey
(
oldKey
))
{
// to build (see the onEnd method), and didUpdateWidget will be called again.
_highlightControllers
[
oldKey
]!.
dispose
();
if
(!
isThumbDragging
&&
highlighted
!=
widget
.
groupValue
)
{
_pressControllers
[
oldKey
]!.
dispose
();
thumbController
.
animateWith
(
_kThumbSpringAnimationSimulation
);
thumbAnimatable
=
null
;
_highlightControllers
.
remove
(
oldKey
);
highlighted
=
widget
.
groupValue
;
_pressControllers
.
remove
(
oldKey
);
}
}
for
(
final
T
newKey
in
widget
.
children
.
keys
)
{
if
(!
_highlightControllers
.
keys
.
contains
(
newKey
))
{
_highlightControllers
[
newKey
]
=
_createHighlightAnimationController
();
_pressControllers
[
newKey
]
=
_createFadeoutAnimationController
();
}
}
}
highlighted
=
widget
.
groupValue
;
}
}
@override
@override
void
dispose
()
{
void
dispose
()
{
for
(
final
AnimationController
animationController
in
_highlightControllers
.
values
)
{
animationController
.
dispose
();
}
for
(
final
AnimationController
animationController
in
_pressControllers
.
values
)
{
animationController
.
dispose
();
}
thumbScaleController
.
dispose
();
thumbScaleController
.
dispose
();
thumbController
.
dispose
();
thumbController
.
dispose
();
separatorOpacityController
.
dispose
();
drag
.
dispose
();
drag
.
dispose
();
tap
.
dispose
();
tap
.
dispose
();
...
@@ -356,111 +468,234 @@ class _SegmentedControlState<T> extends State<CupertinoSlidingSegmentedControl<T
...
@@ -356,111 +468,234 @@ class _SegmentedControlState<T> extends State<CupertinoSlidingSegmentedControl<T
super
.
dispose
();
super
.
dispose
();
}
}
// Play highlight animation for the child located at _highlightControllers[at].
// Whether the current drag gesture started on a selected segment. When this
void
_animateHighlightController
({
T
?
at
,
required
bool
forward
})
{
// flag is false, the `onUpdate` method does not update `highlighted`.
if
(
at
==
null
)
// Otherwise the thumb can be dragged around in an ongoing drag gesture.
return
;
bool
?
_startedOnSelectedSegment
;
final
AnimationController
?
controller
=
_highlightControllers
[
at
];
assert
(!
forward
||
controller
!=
null
);
// Whether an ongoing horizontal drag gesture that started on the thumb is
controller
?.
animateTo
(
forward
?
1
:
0
,
duration:
_kHighlightAnimationDuration
,
curve:
Curves
.
ease
);
// present. When true, defer/ignore changes to the `highlighted` variable
// from other sources (except for semantics) until the gesture ends, preventing
// them from interfering with the active drag gesture.
bool
get
isThumbDragging
=>
_startedOnSelectedSegment
??
false
;
// Converts local coordinate to segments. This method assumes each segment has
// the same width.
T
segmentForXPosition
(
double
dx
)
{
final
RenderBox
renderBox
=
context
.
findRenderObject
()!
as
RenderBox
;
final
int
numOfChildren
=
widget
.
children
.
length
;
assert
(
renderBox
.
hasSize
);
assert
(
numOfChildren
>=
2
);
int
index
=
(
dx
~/
(
renderBox
.
size
.
width
/
numOfChildren
)).
clamp
(
0
,
numOfChildren
-
1
);
switch
(
Directionality
.
of
(
context
))
{
case
TextDirection
.
ltr
:
break
;
case
TextDirection
.
rtl
:
index
=
numOfChildren
-
1
-
index
;
break
;
}
return
widget
.
children
.
keys
.
elementAt
(
index
);
}
bool
_hasDraggedTooFar
(
DragUpdateDetails
details
)
{
final
RenderBox
renderBox
=
context
.
findRenderObject
()!
as
RenderBox
;
assert
(
renderBox
.
hasSize
);
final
Size
size
=
renderBox
.
size
;
final
Offset
offCenter
=
details
.
localPosition
-
Offset
(
size
.
width
/
2
,
size
.
height
/
2
);
final
double
l2
=
math
.
pow
(
math
.
max
(
0.0
,
offCenter
.
dx
.
abs
()
-
size
.
width
/
2
),
2
)
+
math
.
pow
(
math
.
max
(
0.0
,
offCenter
.
dy
.
abs
()
-
size
.
height
/
2
),
2
)
as
double
;
return
l2
>
_kTouchYDistanceThreshold
;
}
}
T
?
_highlighted
;
// The thumb shrinks when the user presses on it, and starts expanding when
set
highlighted
(
T
?
newValue
)
{
// the user lets go.
if
(
_highlighted
==
newValue
)
// This animation must be synced with the segment scale animation (see the
// _Segment widget) to make the overall animation look natural when the thumb
// is not sliding.
void
_playThumbScaleAnimation
({
required
bool
isExpanding
})
{
assert
(
isExpanding
!=
null
);
thumbScaleAnimation
=
thumbScaleController
.
drive
(
Tween
<
double
>(
begin:
thumbScaleAnimation
.
value
,
end:
isExpanding
?
1
:
_kMinThumbScale
,
)
);
thumbScaleController
.
animateWith
(
_kThumbSpringAnimationSimulation
);
}
void
onHighlightChangedByGesture
(
T
newValue
)
{
if
(
highlighted
==
newValue
)
return
;
return
;
_animateHighlightController
(
at:
newValue
,
forward:
true
);
setState
(()
{
highlighted
=
newValue
;
});
_animateHighlightController
(
at:
_highlighted
,
forward:
false
);
// Additionally, start the thumb animation if the highlighted segment
_highlighted
=
newValue
;
// changes. If the thumbController is already running, the render object's
// paint method will create a new tween to drive the animation with.
// TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/74356:
// the current thumb will be painted at the same location twice (before and
// after the new animation starts).
thumbController
.
animateWith
(
_kThumbSpringAnimationSimulation
);
thumbAnimatable
=
null
;
}
void
onPressedChangedByGesture
(
T
?
newValue
)
{
if
(
pressed
!=
newValue
)
setState
(()
{
pressed
=
newValue
;
});
}
}
T
?
_pressed
;
void
onTapUp
(
TapUpDetails
details
)
{
set
pressed
(
T
?
newValue
)
{
// No gesture should interfere with an ongoing thumb drag.
if
(
_pressed
==
newValue
)
if
(
isThumbDragging
)
return
;
return
;
final
T
segment
=
segmentForXPosition
(
details
.
localPosition
.
dx
);
onPressedChangedByGesture
(
null
);
if
(
segment
!=
widget
.
groupValue
)
{
widget
.
onValueChanged
(
segment
);
}
}
if
(
_pressed
!=
null
)
{
void
onDown
(
DragDownDetails
details
)
{
_pressControllers
[
_pressed
]?.
animateTo
(
0
,
duration:
_kOpacityAnimationDuration
,
curve:
Curves
.
ease
);
final
T
touchDownSegment
=
segmentForXPosition
(
details
.
localPosition
.
dx
);
_startedOnSelectedSegment
=
touchDownSegment
==
highlighted
;
onPressedChangedByGesture
(
touchDownSegment
);
if
(
isThumbDragging
)
{
_playThumbScaleAnimation
(
isExpanding:
false
);
}
}
if
(
newValue
!=
_highlighted
&&
newValue
!=
null
)
{
}
_pressControllers
[
newValue
]!.
animateTo
(
1
,
duration:
_kOpacityAnimationDuration
,
curve:
Curves
.
ease
);
void
onUpdate
(
DragUpdateDetails
details
)
{
if
(
isThumbDragging
)
{
final
T
segment
=
segmentForXPosition
(
details
.
localPosition
.
dx
);
onPressedChangedByGesture
(
segment
);
onHighlightChangedByGesture
(
segment
);
}
else
{
final
T
?
segment
=
_hasDraggedTooFar
(
details
)
?
null
:
segmentForXPosition
(
details
.
localPosition
.
dx
);
onPressedChangedByGesture
(
segment
);
}
}
_pressed
=
newValue
;
}
}
void
didChangeSelectedViaGesture
()
{
void
onEnd
(
DragEndDetails
details
)
{
widget
.
onValueChanged
(
_highlighted
);
final
T
?
pressed
=
this
.
pressed
;
if
(
isThumbDragging
)
{
_playThumbScaleAnimation
(
isExpanding:
true
);
if
(
highlighted
!=
widget
.
groupValue
)
{
widget
.
onValueChanged
(
highlighted
);
}
}
else
if
(
pressed
!=
null
)
{
onHighlightChangedByGesture
(
pressed
);
assert
(
pressed
==
highlighted
);
if
(
highlighted
!=
widget
.
groupValue
)
{
widget
.
onValueChanged
(
highlighted
);
}
}
onPressedChangedByGesture
(
null
);
_startedOnSelectedSegment
=
null
;
}
}
T
?
indexToKey
(
int
?
index
)
=>
index
==
null
?
null
:
keys
[
index
];
void
onCancel
()
{
if
(
isThumbDragging
)
{
_playThumbScaleAnimation
(
isExpanding:
true
);
}
onPressedChangedByGesture
(
null
);
_startedOnSelectedSegment
=
null
;
}
// The segment the sliding thumb is currently located at, or animating to. It
// may have a different value from widget.groupValue, since this widget does
// not report a selection change via `onValueChanged` until the user stops
// interacting with the widget (onTapUp). For example, the user can drag the
// thumb around, and the `onValueChanged` callback will not be invoked until
// the thumb is let go.
T
?
highlighted
;
// The segment the user is currently pressing.
T
?
pressed
;
@override
@override
Widget
build
(
BuildContext
context
)
{
Widget
build
(
BuildContext
context
)
{
debugCheckHasDirectionality
(
context
);
assert
(
widget
.
children
.
length
>=
2
);
List
<
Widget
>
children
=
<
Widget
>[];
bool
isPreviousSegmentHighlighted
=
false
;
int
index
=
0
;
int
?
highlightedIndex
;
for
(
final
MapEntry
<
T
,
Widget
>
entry
in
widget
.
children
.
entries
)
{
final
bool
isHighlighted
=
highlighted
==
entry
.
key
;
if
(
isHighlighted
)
{
highlightedIndex
=
index
;
}
if
(
index
!=
0
)
{
children
.
add
(
_SegmentSeparator
(
// Let separators be TextDirection-invariant. If the TextDirection
// changes, the separators should mostly stay where they were.
key:
ValueKey
<
int
>(
index
),
highlighted:
isPreviousSegmentHighlighted
||
isHighlighted
,
),
);
}
children
.
add
(
Semantics
(
button:
true
,
onTap:
()
{
widget
.
onValueChanged
(
entry
.
key
);
},
inMutuallyExclusiveGroup:
true
,
selected:
widget
.
groupValue
==
entry
.
key
,
child:
_Segment
<
T
>(
key:
ValueKey
<
T
>(
entry
.
key
),
highlighted:
isHighlighted
,
pressed:
pressed
==
entry
.
key
,
isDragging:
isThumbDragging
,
child:
entry
.
value
,
),
),
);
index
+=
1
;
isPreviousSegmentHighlighted
=
isHighlighted
;
}
assert
((
highlightedIndex
==
null
)
==
(
highlighted
==
null
));
switch
(
Directionality
.
of
(
context
))
{
switch
(
Directionality
.
of
(
context
))
{
case
TextDirection
.
ltr
:
case
TextDirection
.
ltr
:
keys
=
widget
.
children
.
keys
.
toList
(
growable:
false
);
break
;
break
;
case
TextDirection
.
rtl
:
case
TextDirection
.
rtl
:
keys
=
widget
.
children
.
keys
.
toList
().
reversed
.
toList
(
growable:
false
);
children
=
children
.
reversed
.
toList
(
growable:
false
);
if
(
highlightedIndex
!=
null
)
{
highlightedIndex
=
index
-
1
-
highlightedIndex
;
}
break
;
break
;
}
}
return
AnimatedBuilder
(
return
UnconstrainedBox
(
animation:
Listenable
.
merge
(<
Listenable
>[
constrainedAxis:
Axis
.
horizontal
,
...
_highlightControllers
.
values
,
child:
Container
(
...
_pressControllers
.
values
,
padding:
widget
.
padding
.
resolve
(
Directionality
.
of
(
context
)),
]),
decoration:
BoxDecoration
(
builder:
(
BuildContext
context
,
Widget
?
child
)
{
borderRadius:
const
BorderRadius
.
all
(
Radius
.
circular
(
_kCornerRadius
)),
final
List
<
Widget
>
children
=
<
Widget
>[];
color:
CupertinoDynamicColor
.
resolve
(
widget
.
backgroundColor
,
context
),
for
(
final
T
currentKey
in
keys
)
{
),
final
TextStyle
textStyle
=
DefaultTextStyle
.
of
(
context
).
style
.
copyWith
(
child:
AnimatedBuilder
(
fontWeight:
_highlightTween
.
evaluate
(
_highlightControllers
[
currentKey
]!),
animation:
thumbScaleAnimation
,
);
builder:
(
BuildContext
context
,
Widget
?
child
)
{
return
_SegmentedControlRenderWidget
<
T
>(
final
Widget
child
=
DefaultTextStyle
(
children:
children
,
style:
textStyle
,
highlightedIndex:
highlightedIndex
,
child:
Semantics
(
thumbColor:
CupertinoDynamicColor
.
resolve
(
widget
.
thumbColor
,
context
),
button:
true
,
thumbScale:
thumbScaleAnimation
.
value
,
onTap:
()
{
widget
.
onValueChanged
(
currentKey
);
},
state:
this
,
inMutuallyExclusiveGroup:
true
,
);
selected:
widget
.
groupValue
==
currentKey
,
},
child:
Opacity
(
),
opacity:
_pressTween
.
evaluate
(
_pressControllers
[
currentKey
]!),
),
// Expand the hitTest area to be as large as the Opacity widget.
child:
MetaData
(
behavior:
HitTestBehavior
.
opaque
,
child:
Center
(
child:
widget
.
children
[
currentKey
]),
),
),
),
);
children
.
add
(
child
);
}
final
int
?
selectedIndex
=
widget
.
groupValue
==
null
?
null
:
keys
.
indexOf
(
widget
.
groupValue
as
T
);
final
Widget
box
=
_SegmentedControlRenderWidget
<
T
>(
children:
children
,
selectedIndex:
selectedIndex
,
thumbColor:
CupertinoDynamicColor
.
resolve
(
widget
.
thumbColor
,
context
),
state:
this
,
);
return
UnconstrainedBox
(
constrainedAxis:
Axis
.
horizontal
,
child:
Container
(
padding:
widget
.
padding
.
resolve
(
Directionality
.
of
(
context
)),
decoration:
BoxDecoration
(
borderRadius:
const
BorderRadius
.
all
(
Radius
.
circular
(
_kCornerRadius
)),
color:
CupertinoDynamicColor
.
resolve
(
widget
.
backgroundColor
,
context
),
),
child:
box
,
),
);
},
);
);
}
}
}
}
...
@@ -469,48 +704,38 @@ class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
...
@@ -469,48 +704,38 @@ class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
_SegmentedControlRenderWidget
({
_SegmentedControlRenderWidget
({
Key
?
key
,
Key
?
key
,
List
<
Widget
>
children
=
const
<
Widget
>[],
List
<
Widget
>
children
=
const
<
Widget
>[],
required
this
.
selec
tedIndex
,
required
this
.
highligh
tedIndex
,
required
this
.
thumbColor
,
required
this
.
thumbColor
,
required
this
.
thumbScale
,
required
this
.
state
,
required
this
.
state
,
})
:
super
(
key:
key
,
children:
children
);
})
:
super
(
key:
key
,
children:
children
);
final
int
?
selectedIndex
;
final
int
?
highlightedIndex
;
final
Color
?
thumbColor
;
final
Color
thumbColor
;
final
double
thumbScale
;
final
_SegmentedControlState
<
T
>
state
;
final
_SegmentedControlState
<
T
>
state
;
@override
@override
RenderObject
createRenderObject
(
BuildContext
context
)
{
RenderObject
createRenderObject
(
BuildContext
context
)
{
return
_RenderSegmentedControl
<
T
>(
return
_RenderSegmentedControl
<
T
>(
selectedIndex:
selectedIndex
,
highlightedIndex:
highlightedIndex
,
thumbColor:
CupertinoDynamicColor
.
maybeResolve
(
thumbColor
,
context
),
thumbColor:
thumbColor
,
thumbScale:
thumbScale
,
state:
state
,
state:
state
,
);
);
}
}
@override
@override
void
updateRenderObject
(
BuildContext
context
,
_RenderSegmentedControl
<
T
>
renderObject
)
{
void
updateRenderObject
(
BuildContext
context
,
_RenderSegmentedControl
<
T
>
renderObject
)
{
assert
(
renderObject
.
state
==
state
);
renderObject
renderObject
..
thumbColor
=
CupertinoDynamicColor
.
maybeResolve
(
thumbColor
,
context
)
..
thumbColor
=
thumbColor
..
guardedSetHighlightedIndex
(
selectedIndex
);
..
thumbScale
=
thumbScale
..
highlightedIndex
=
highlightedIndex
;
}
}
}
}
class
_ChildAnimationManifest
{
class
_SegmentedControlContainerBoxParentData
extends
ContainerBoxParentData
<
RenderBox
>
{}
_ChildAnimationManifest
({
this
.
opacity
=
1
,
required
this
.
separatorOpacity
,
})
:
assert
(
separatorOpacity
!=
null
),
assert
(
opacity
!=
null
),
separatorTween
=
Tween
<
double
>(
begin:
separatorOpacity
,
end:
separatorOpacity
),
opacityTween
=
Tween
<
double
>(
begin:
opacity
,
end:
opacity
);
double
opacity
;
Tween
<
double
>
opacityTween
;
double
separatorOpacity
;
Tween
<
double
>
separatorTween
;
}
class
_SegmentedControlContainerBoxParentData
extends
ContainerBoxParentData
<
RenderBox
>
{
}
// The behavior of a UISegmentedControl as observed on iOS 13.1:
// The behavior of a UISegmentedControl as observed on iOS 13.1:
//
//
...
@@ -530,7 +755,7 @@ class _SegmentedControlContainerBoxParentData extends ContainerBoxParentData<Ren
...
@@ -530,7 +755,7 @@ class _SegmentedControlContainerBoxParentData extends ContainerBoxParentData<Ren
// 3. When the sliding animation plays two other animations take place. In one animation
// 3. When the sliding animation plays two other animations take place. In one animation
// the content of the current segment gradually becomes "highlighted", turning the
// the content of the current segment gradually becomes "highlighted", turning the
// font weight to semibold (CABasicAnimation, timingFunction = default, duration = 0.2).
// font weight to semibold (CABasicAnimation, timingFunction = default, duration = 0.2).
// The other is the separator fadein/fadeout animation.
// The other is the separator fadein/fadeout animation
(duration = 0.41)
.
//
//
// 4. A tap down event on the segment pointed to by the current selected
// 4. A tap down event on the segment pointed to by the current selected
// index will trigger a CABasicAnimation that shrinks the thumb to 95% of its
// index will trigger a CABasicAnimation that shrinks the thumb to 95% of its
...
@@ -538,84 +763,53 @@ class _SegmentedControlContainerBoxParentData extends ContainerBoxParentData<Ren
...
@@ -538,84 +763,53 @@ class _SegmentedControlContainerBoxParentData extends ContainerBoxParentData<Ren
/// corresponding tap up event inverts the process (eyeballed).
/// corresponding tap up event inverts the process (eyeballed).
//
//
// 5. A tap down event on other segments will trigger a CABasicAnimation
// 5. A tap down event on other segments will trigger a CABasicAnimation
// (timingFunction = default, duration = 0.47.) that fades out the content,
// (timingFunction = default, duration = 0.47.) that fades out the content
// eventually reducing the alpha of that segment to 20% unless interrupted by
// from its current alpha, eventually reducing the alpha of that segment to
// a tap up event or the pointer moves out of the region (either outside of the
// 20% unless interrupted by a tap up event or the pointer moves out of the
// segmented control's vicinity or to a different segment). The reverse animation
// region (either outside of the segmented control's vicinity or to a
// has the same duration and timing function.
// different segment). The reverse animation has the same duration and timing
// function.
class
_RenderSegmentedControl
<
T
>
extends
RenderBox
class
_RenderSegmentedControl
<
T
>
extends
RenderBox
with
ContainerRenderObjectMixin
<
RenderBox
,
ContainerBoxParentData
<
RenderBox
>>,
with
ContainerRenderObjectMixin
<
RenderBox
,
ContainerBoxParentData
<
RenderBox
>>,
RenderBoxContainerDefaultsMixin
<
RenderBox
,
ContainerBoxParentData
<
RenderBox
>>
{
RenderBoxContainerDefaultsMixin
<
RenderBox
,
ContainerBoxParentData
<
RenderBox
>>
{
_RenderSegmentedControl
({
_RenderSegmentedControl
({
required
int
?
selectedIndex
,
required
int
?
highlightedIndex
,
required
Color
?
thumbColor
,
required
Color
thumbColor
,
required
double
thumbScale
,
required
this
.
state
,
required
this
.
state
,
})
:
_highlightedIndex
=
selec
tedIndex
,
})
:
_highlightedIndex
=
highligh
tedIndex
,
_thumbColor
=
thumbColor
,
_thumbColor
=
thumbColor
,
assert
(
state
!=
null
)
{
_thumbScale
=
thumbScale
,
state
.
drag
assert
(
state
!=
null
);
..
onDown
=
_onDown
..
onUpdate
=
_onUpdate
..
onEnd
=
_onEnd
..
onCancel
=
_onCancel
;
state
.
tap
.
onTapUp
=
_onTapUp
;
// Empty callback to enable the long press recognizer.
state
.
longPress
.
onLongPress
=
()
{
};
}
final
_SegmentedControlState
<
T
>
state
;
final
_SegmentedControlState
<
T
>
state
;
Map
<
RenderBox
,
_ChildAnimationManifest
>?
_childAnimations
=
<
RenderBox
,
_ChildAnimationManifest
>{};
// The current **Unscaled** Thumb Rect in this RenderBox's coordinate space.
// The current **Unscaled** Thumb Rect.
Rect
?
currentThumbRect
;
Rect
?
currentThumbRect
;
Tween
<
Rect
?>?
_currentThumbTween
;
Tween
<
double
>
_thumbScaleTween
=
Tween
<
double
>(
begin:
_kMinThumbScale
,
end:
1
);
double
currentThumbScale
=
1
;
// The current position of the active drag pointer.
Offset
?
_localDragOffset
;
// Whether the current drag gesture started on a selected segment.
bool
?
_startedOnSelectedSegment
;
@override
void
insert
(
RenderBox
child
,
{
RenderBox
?
after
})
{
super
.
insert
(
child
,
after:
after
);
if
(
_childAnimations
==
null
)
return
;
assert
(
_childAnimations
![
child
]
==
null
);
_childAnimations
![
child
]
=
_ChildAnimationManifest
(
separatorOpacity:
1
);
}
@override
void
remove
(
RenderBox
child
)
{
super
.
remove
(
child
);
_childAnimations
?.
remove
(
child
);
}
@override
@override
void
attach
(
PipelineOwner
owner
)
{
void
attach
(
PipelineOwner
owner
)
{
super
.
attach
(
owner
);
super
.
attach
(
owner
);
state
.
thumbController
.
addListener
(
markNeedsPaint
);
state
.
thumbController
.
addListener
(
markNeedsPaint
);
state
.
thumbScaleController
.
addListener
(
markNeedsPaint
);
state
.
separatorOpacityController
.
addListener
(
markNeedsPaint
);
}
}
@override
@override
void
detach
()
{
void
detach
()
{
state
.
thumbController
.
removeListener
(
markNeedsPaint
);
state
.
thumbController
.
removeListener
(
markNeedsPaint
);
state
.
thumbScaleController
.
removeListener
(
markNeedsPaint
);
state
.
separatorOpacityController
.
removeListener
(
markNeedsPaint
);
super
.
detach
();
super
.
detach
();
}
}
// Indicates whether selectedIndex has changed and animations need to be updated.
double
get
thumbScale
=>
_thumbScale
;
// when true some animation tweens will be updated in paint phase.
double
_thumbScale
;
bool
_needsThumbAnimationUpdate
=
false
;
set
thumbScale
(
double
value
)
{
if
(
_thumbScale
==
value
)
{
return
;
}
_thumbScale
=
value
;
if
(
state
.
highlighted
!=
null
)
markNeedsPaint
();
}
int
?
get
highlightedIndex
=>
_highlightedIndex
;
int
?
get
highlightedIndex
=>
_highlightedIndex
;
int
?
_highlightedIndex
;
int
?
_highlightedIndex
;
...
@@ -624,46 +818,13 @@ class _RenderSegmentedControl<T> extends RenderBox
...
@@ -624,46 +818,13 @@ class _RenderSegmentedControl<T> extends RenderBox
return
;
return
;
}
}
_needsThumbAnimationUpdate
=
true
;
_highlightedIndex
=
value
;
_highlightedIndex
=
value
;
state
.
thumbController
.
animateWith
(
_kThumbSpringAnimationSimulation
);
state
.
separatorOpacityController
.
reset
();
state
.
separatorOpacityController
.
animateTo
(
1
,
duration:
_kSpringAnimationDuration
,
curve:
Curves
.
ease
,
);
state
.
highlighted
=
state
.
indexToKey
(
value
);
markNeedsPaint
();
markNeedsPaint
();
markNeedsSemanticsUpdate
();
}
void
guardedSetHighlightedIndex
(
int
?
value
)
{
// Ignore set highlightedIndex when the user is dragging the thumb around.
if
(
_startedOnSelectedSegment
==
true
)
return
;
highlightedIndex
=
value
;
}
int
?
get
pressedIndex
=>
_pressedIndex
;
int
?
_pressedIndex
;
set
pressedIndex
(
int
?
value
)
{
if
(
_pressedIndex
==
value
)
{
return
;
}
assert
(
value
==
null
||
(
value
>=
0
&&
value
<
childCount
));
_pressedIndex
=
value
;
state
.
pressed
=
state
.
indexToKey
(
value
);
}
}
Color
?
get
thumbColor
=>
_thumbColor
;
Color
get
thumbColor
=>
_thumbColor
;
Color
?
_thumbColor
;
Color
_thumbColor
;
set
thumbColor
(
Color
?
value
)
{
set
thumbColor
(
Color
value
)
{
if
(
_thumbColor
==
value
)
{
if
(
_thumbColor
==
value
)
{
return
;
return
;
}
}
...
@@ -671,116 +832,48 @@ class _RenderSegmentedControl<T> extends RenderBox
...
@@ -671,116 +832,48 @@ class _RenderSegmentedControl<T> extends RenderBox
markNeedsPaint
();
markNeedsPaint
();
}
}
double
get
totalSeparatorWidth
=>
(
_kSeparatorInset
.
horizontal
+
_kSeparatorWidth
)
*
(
childCount
-
1
);
@override
@override
void
handleEvent
(
PointerEvent
event
,
BoxHitTestEntry
entry
)
{
void
handleEvent
(
PointerEvent
event
,
BoxHitTestEntry
entry
)
{
assert
(
debugHandleEvent
(
event
,
entry
));
assert
(
debugHandleEvent
(
event
,
entry
));
if
(
event
is
PointerDownEvent
)
{
// No gesture should interfere with an ongoing thumb drag.
if
(
event
is
PointerDownEvent
&&
!
state
.
isThumbDragging
)
{
state
.
tap
.
addPointer
(
event
);
state
.
tap
.
addPointer
(
event
);
state
.
longPress
.
addPointer
(
event
);
state
.
longPress
.
addPointer
(
event
);
state
.
drag
.
addPointer
(
event
);
state
.
drag
.
addPointer
(
event
);
}
}
}
}
int
?
indexFromLocation
(
Offset
location
)
{
// Intrinsic Dimensions
return
childCount
==
0
?
null
// This assumes all children have the same width.
:
(
location
.
dx
/
(
size
.
width
/
childCount
))
.
floor
()
.
clamp
(
0
,
childCount
-
1
);
}
void
_onTapUp
(
TapUpDetails
details
)
{
highlightedIndex
=
indexFromLocation
(
details
.
localPosition
);
state
.
didChangeSelectedViaGesture
();
}
void
_onDown
(
DragDownDetails
details
)
{
assert
(
size
.
contains
(
details
.
localPosition
));
_localDragOffset
=
details
.
localPosition
;
final
int
?
index
=
indexFromLocation
(
_localDragOffset
!);
_startedOnSelectedSegment
=
index
==
highlightedIndex
;
pressedIndex
=
index
;
if
(
_startedOnSelectedSegment
!)
{
_playThumbScaleAnimation
(
isExpanding:
false
);
}
}
void
_onUpdate
(
DragUpdateDetails
details
)
{
_localDragOffset
=
details
.
localPosition
;
final
int
?
newIndex
=
indexFromLocation
(
_localDragOffset
!);
if
(
_startedOnSelectedSegment
!)
{
highlightedIndex
=
newIndex
;
pressedIndex
=
newIndex
;
}
else
{
pressedIndex
=
_hasDraggedTooFar
(
details
)
?
null
:
newIndex
;
}
}
void
_onEnd
(
DragEndDetails
details
)
{
if
(
_startedOnSelectedSegment
!)
{
_playThumbScaleAnimation
(
isExpanding:
true
);
state
.
didChangeSelectedViaGesture
();
}
if
(
pressedIndex
!=
null
)
{
highlightedIndex
=
pressedIndex
;
state
.
didChangeSelectedViaGesture
();
}
pressedIndex
=
null
;
_localDragOffset
=
null
;
_startedOnSelectedSegment
=
null
;
}
void
_onCancel
()
{
if
(
_startedOnSelectedSegment
!)
{
_playThumbScaleAnimation
(
isExpanding:
true
);
}
_localDragOffset
=
null
;
double
get
totalSeparatorWidth
=>
(
_kSeparatorInset
.
horizontal
+
_kSeparatorWidth
)
*
(
childCount
~/
2
);
pressedIndex
=
null
;
_startedOnSelectedSegment
=
null
;
}
void
_playThumbScaleAnimation
({
required
bool
isExpanding
})
{
RenderBox
?
nonSeparatorChildAfter
(
RenderBox
child
)
{
assert
(
isExpanding
!=
null
);
final
RenderBox
?
nextChild
=
childAfter
(
child
);
_thumbScaleTween
=
Tween
<
double
>(
begin:
currentThumbScale
,
end:
isExpanding
?
1
:
_kMinThumbScale
);
return
nextChild
==
null
?
null
:
childAfter
(
nextChild
);
state
.
thumbScaleController
.
animateWith
(
_kThumbSpringAnimationSimulation
);
}
bool
_hasDraggedTooFar
(
DragUpdateDetails
details
)
{
final
Offset
offCenter
=
details
.
localPosition
-
Offset
(
size
.
width
/
2
,
size
.
height
/
2
);
return
math
.
pow
(
math
.
max
(
0
,
offCenter
.
dx
.
abs
()
-
size
.
width
/
2
),
2
)
+
math
.
pow
(
math
.
max
(
0
,
offCenter
.
dy
.
abs
()
-
size
.
height
/
2
),
2
)
>
_kTouchYDistanceThreshold
;
}
}
@override
@override
double
computeMinIntrinsicWidth
(
double
height
)
{
double
computeMinIntrinsicWidth
(
double
height
)
{
final
int
childCount
=
this
.
childCount
~/
2
+
1
;
RenderBox
?
child
=
firstChild
;
RenderBox
?
child
=
firstChild
;
double
maxMinChildWidth
=
0
;
double
maxMinChildWidth
=
0
;
while
(
child
!=
null
)
{
while
(
child
!=
null
)
{
final
_SegmentedControlContainerBoxParentData
childParentData
=
child
.
parentData
!
as
_SegmentedControlContainerBoxParentData
;
final
double
childWidth
=
child
.
getMinIntrinsicWidth
(
height
);
final
double
childWidth
=
child
.
getMinIntrinsicWidth
(
height
);
maxMinChildWidth
=
math
.
max
(
maxMinChildWidth
,
childWidth
);
maxMinChildWidth
=
math
.
max
(
maxMinChildWidth
,
childWidth
);
child
=
childParentData
.
nextSibling
;
child
=
nonSeparatorChildAfter
(
child
)
;
}
}
return
(
maxMinChildWidth
+
2
*
_kSegmentMinPadding
)
*
childCount
+
totalSeparatorWidth
;
return
(
maxMinChildWidth
+
2
*
_kSegmentMinPadding
)
*
childCount
+
totalSeparatorWidth
;
}
}
@override
@override
double
computeMaxIntrinsicWidth
(
double
height
)
{
double
computeMaxIntrinsicWidth
(
double
height
)
{
final
int
childCount
=
this
.
childCount
~/
2
+
1
;
RenderBox
?
child
=
firstChild
;
RenderBox
?
child
=
firstChild
;
double
maxMaxChildWidth
=
0
;
double
maxMaxChildWidth
=
0
;
while
(
child
!=
null
)
{
while
(
child
!=
null
)
{
final
_SegmentedControlContainerBoxParentData
childParentData
=
child
.
parentData
!
as
_SegmentedControlContainerBoxParentData
;
final
double
childWidth
=
child
.
getMaxIntrinsicWidth
(
height
);
final
double
childWidth
=
child
.
getMaxIntrinsicWidth
(
height
);
maxMaxChildWidth
=
math
.
max
(
maxMaxChildWidth
,
childWidth
);
maxMaxChildWidth
=
math
.
max
(
maxMaxChildWidth
,
childWidth
);
child
=
childParentData
.
nextSibling
;
child
=
nonSeparatorChildAfter
(
child
)
;
}
}
return
(
maxMaxChildWidth
+
2
*
_kSegmentMinPadding
)
*
childCount
+
totalSeparatorWidth
;
return
(
maxMaxChildWidth
+
2
*
_kSegmentMinPadding
)
*
childCount
+
totalSeparatorWidth
;
}
}
...
@@ -788,13 +881,11 @@ class _RenderSegmentedControl<T> extends RenderBox
...
@@ -788,13 +881,11 @@ class _RenderSegmentedControl<T> extends RenderBox
@override
@override
double
computeMinIntrinsicHeight
(
double
width
)
{
double
computeMinIntrinsicHeight
(
double
width
)
{
RenderBox
?
child
=
firstChild
;
RenderBox
?
child
=
firstChild
;
double
maxMinChildHeight
=
0
;
double
maxMinChildHeight
=
_kMinSegmentedControlHeight
;
while
(
child
!=
null
)
{
while
(
child
!=
null
)
{
final
_SegmentedControlContainerBoxParentData
childParentData
=
child
.
parentData
!
as
_SegmentedControlContainerBoxParentData
;
final
double
childHeight
=
child
.
getMinIntrinsicHeight
(
width
);
final
double
childHeight
=
child
.
getMinIntrinsicHeight
(
width
);
maxMinChildHeight
=
math
.
max
(
maxMinChildHeight
,
childHeight
);
maxMinChildHeight
=
math
.
max
(
maxMinChildHeight
,
childHeight
);
child
=
childParentData
.
nextSibling
;
child
=
nonSeparatorChildAfter
(
child
)
;
}
}
return
maxMinChildHeight
;
return
maxMinChildHeight
;
}
}
...
@@ -802,13 +893,11 @@ class _RenderSegmentedControl<T> extends RenderBox
...
@@ -802,13 +893,11 @@ class _RenderSegmentedControl<T> extends RenderBox
@override
@override
double
computeMaxIntrinsicHeight
(
double
width
)
{
double
computeMaxIntrinsicHeight
(
double
width
)
{
RenderBox
?
child
=
firstChild
;
RenderBox
?
child
=
firstChild
;
double
maxMaxChildHeight
=
0
;
double
maxMaxChildHeight
=
_kMinSegmentedControlHeight
;
while
(
child
!=
null
)
{
while
(
child
!=
null
)
{
final
_SegmentedControlContainerBoxParentData
childParentData
=
child
.
parentData
!
as
_SegmentedControlContainerBoxParentData
;
final
double
childHeight
=
child
.
getMaxIntrinsicHeight
(
width
);
final
double
childHeight
=
child
.
getMaxIntrinsicHeight
(
width
);
maxMaxChildHeight
=
math
.
max
(
maxMaxChildHeight
,
childHeight
);
maxMaxChildHeight
=
math
.
max
(
maxMaxChildHeight
,
childHeight
);
child
=
childParentData
.
nextSibling
;
child
=
nonSeparatorChildAfter
(
child
)
;
}
}
return
maxMaxChildHeight
;
return
maxMaxChildHeight
;
}
}
...
@@ -826,12 +915,13 @@ class _RenderSegmentedControl<T> extends RenderBox
...
@@ -826,12 +915,13 @@ class _RenderSegmentedControl<T> extends RenderBox
}
}
Size
_calculateChildSize
(
BoxConstraints
constraints
)
{
Size
_calculateChildSize
(
BoxConstraints
constraints
)
{
final
int
childCount
=
this
.
childCount
~/
2
+
1
;
double
childWidth
=
(
constraints
.
minWidth
-
totalSeparatorWidth
)
/
childCount
;
double
childWidth
=
(
constraints
.
minWidth
-
totalSeparatorWidth
)
/
childCount
;
double
maxHeight
=
_kMinSegmentedControlHeight
;
double
maxHeight
=
_kMinSegmentedControlHeight
;
RenderBox
?
child
=
firstChild
;
RenderBox
?
child
=
firstChild
;
while
(
child
!=
null
)
{
while
(
child
!=
null
)
{
childWidth
=
math
.
max
(
childWidth
,
child
.
getMaxIntrinsicWidth
(
double
.
infinity
)
+
2
*
_kSegmentMinPadding
);
childWidth
=
math
.
max
(
childWidth
,
child
.
getMaxIntrinsicWidth
(
double
.
infinity
)
+
2
*
_kSegmentMinPadding
);
child
=
c
hildAfter
(
child
);
child
=
nonSeparatorC
hildAfter
(
child
);
}
}
childWidth
=
math
.
min
(
childWidth
=
math
.
min
(
childWidth
,
childWidth
,
...
@@ -841,158 +931,141 @@ class _RenderSegmentedControl<T> extends RenderBox
...
@@ -841,158 +931,141 @@ class _RenderSegmentedControl<T> extends RenderBox
while
(
child
!=
null
)
{
while
(
child
!=
null
)
{
final
double
boxHeight
=
child
.
getMaxIntrinsicHeight
(
childWidth
);
final
double
boxHeight
=
child
.
getMaxIntrinsicHeight
(
childWidth
);
maxHeight
=
math
.
max
(
maxHeight
,
boxHeight
);
maxHeight
=
math
.
max
(
maxHeight
,
boxHeight
);
child
=
c
hildAfter
(
child
);
child
=
nonSeparatorC
hildAfter
(
child
);
}
}
return
Size
(
childWidth
,
maxHeight
);
return
Size
(
childWidth
,
maxHeight
);
}
}
Size
_computeOverallSizeFromChildSize
(
Size
childSize
)
{
Size
_computeOverallSizeFromChildSize
(
Size
childSize
,
BoxConstraints
constraints
)
{
final
int
childCount
=
this
.
childCount
~/
2
+
1
;
return
constraints
.
constrain
(
Size
(
childSize
.
width
*
childCount
+
totalSeparatorWidth
,
childSize
.
height
));
return
constraints
.
constrain
(
Size
(
childSize
.
width
*
childCount
+
totalSeparatorWidth
,
childSize
.
height
));
}
}
@override
@override
Size
computeDryLayout
(
BoxConstraints
constraints
)
{
Size
computeDryLayout
(
BoxConstraints
constraints
)
{
final
Size
childSize
=
_calculateChildSize
(
constraints
);
final
Size
childSize
=
_calculateChildSize
(
constraints
);
return
_computeOverallSizeFromChildSize
(
childSize
);
return
_computeOverallSizeFromChildSize
(
childSize
,
constraints
);
}
}
@override
@override
void
performLayout
()
{
void
performLayout
()
{
final
BoxConstraints
constraints
=
this
.
constraints
;
final
BoxConstraints
constraints
=
this
.
constraints
;
final
Size
childSize
=
_calculateChildSize
(
constraints
);
final
Size
childSize
=
_calculateChildSize
(
constraints
);
final
BoxConstraints
childConstraints
=
BoxConstraints
.
tight
(
childSize
);
final
BoxConstraints
separatorConstraints
=
childConstraints
.
heightConstraints
();
final
BoxConstraints
childConstraints
=
BoxConstraints
.
tightFor
(
width:
childSize
.
width
,
height:
childSize
.
height
,
);
// Layout children.
RenderBox
?
child
=
firstChild
;
RenderBox
?
child
=
firstChild
;
while
(
child
!=
null
)
{
int
index
=
0
;
child
.
layout
(
childConstraints
,
parentUsesSize:
true
);
child
=
childAfter
(
child
);
}
double
start
=
0
;
double
start
=
0
;
child
=
firstChild
;
while
(
child
!=
null
)
{
while
(
child
!=
null
)
{
final
_SegmentedControlContainerBoxParentData
childParentData
=
child
.
layout
(
index
.
isEven
?
childConstraints
:
separatorConstraints
,
parentUsesSize:
true
);
child
.
parentData
!
as
_SegmentedControlContainerBoxParentData
;
final
_SegmentedControlContainerBoxParentData
childParentData
=
child
.
parentData
!
as
_SegmentedControlContainerBoxParentData
;
final
Offset
childOffset
=
Offset
(
start
,
0
);
final
Offset
childOffset
=
Offset
(
start
,
0
);
childParentData
.
offset
=
childOffset
;
childParentData
.
offset
=
childOffset
;
start
+=
child
.
size
.
width
+
_kSeparatorWidth
+
_kSeparatorInset
.
horizontal
;
start
+=
child
.
size
.
width
;
assert
(
index
.
isEven
||
child
.
size
.
width
==
_kSeparatorWidth
+
_kSeparatorInset
.
horizontal
,
'
${child.size.width}
!=
${_kSeparatorWidth + _kSeparatorInset.horizontal}
'
,
);
child
=
childAfter
(
child
);
child
=
childAfter
(
child
);
index
+=
1
;
}
}
size
=
_computeOverallSizeFromChildSize
(
childSize
);
size
=
_computeOverallSizeFromChildSize
(
childSize
,
constraints
);
}
// This method is used to convert the original unscaled thumb rect painted in
// the previous frame, to a Rect that is within the valid boundary defined by
// the the child segments.
//
// The overall size does not include that of the thumb. That is, if the thumb
// is located at the first or the last segment, the thumb can get cut off if
// one of the values in _kThumbInsets is positive.
Rect
?
moveThumbRectInBound
(
Rect
?
thumbRect
,
List
<
RenderBox
>
children
)
{
assert
(
hasSize
);
assert
(
children
.
length
>=
2
);
if
(
thumbRect
==
null
)
return
null
;
final
Offset
firstChildOffset
=
(
children
.
first
.
parentData
!
as
_SegmentedControlContainerBoxParentData
).
offset
;
final
double
leftMost
=
firstChildOffset
.
dx
;
final
double
rightMost
=
(
children
.
last
.
parentData
!
as
_SegmentedControlContainerBoxParentData
).
offset
.
dx
+
children
.
last
.
size
.
width
;
assert
(
rightMost
>
leftMost
);
// Ignore the horizontal position and the height of `thumbRect`, and
// calcuates them from `children`.
return
Rect
.
fromLTRB
(
math
.
max
(
thumbRect
.
left
,
leftMost
-
_kThumbInsets
.
left
),
firstChildOffset
.
dy
-
_kThumbInsets
.
top
,
math
.
min
(
thumbRect
.
right
,
rightMost
+
_kThumbInsets
.
right
),
firstChildOffset
.
dy
+
children
.
first
.
size
.
height
+
_kThumbInsets
.
bottom
,
);
}
}
@override
@override
void
paint
(
PaintingContext
context
,
Offset
offset
)
{
void
paint
(
PaintingContext
context
,
Offset
offset
)
{
final
List
<
RenderBox
>
children
=
getChildrenAsList
();
final
List
<
RenderBox
>
children
=
getChildrenAsList
();
// Paint thumb if highlightedIndex is not null.
for
(
int
index
=
1
;
index
<
childCount
;
index
+=
2
)
{
if
(
highlightedIndex
!=
null
)
{
_paintSeparator
(
context
,
offset
,
children
[
index
]);
if
(
_childAnimations
==
null
)
{
}
_childAnimations
=
<
RenderBox
,
_ChildAnimationManifest
>
{
};
for
(
int
i
=
0
;
i
<
childCount
-
1
;
i
+=
1
)
{
// The separator associated with the last child will not be painted (unless
// a new trailing segment is added), and its opacity will always be 1.
final
bool
shouldFadeOut
=
i
==
highlightedIndex
||
i
==
highlightedIndex
!
-
1
;
final
RenderBox
child
=
children
[
i
];
_childAnimations
![
child
]
=
_ChildAnimationManifest
(
separatorOpacity:
shouldFadeOut
?
0
:
1
);
}
}
final
RenderBox
selectedChild
=
children
[
highlightedIndex
!];
final
_SegmentedControlContainerBoxParentData
childParentData
=
final
int
?
highlightedChildIndex
=
highlightedIndex
;
selectedChild
.
parentData
!
as
_SegmentedControlContainerBoxParentData
;
// Paint thumb if there's a highlighted segment.
final
Rect
unscaledThumbTargetRect
=
_kThumbInsets
.
inflateRect
(
childParentData
.
offset
&
selectedChild
.
size
);
if
(
highlightedChildIndex
!=
null
)
{
final
RenderBox
selectedChild
=
children
[
highlightedChildIndex
*
2
];
// Update related Tweens before animation update phase.
if
(
_needsThumbAnimationUpdate
)
{
final
_SegmentedControlContainerBoxParentData
childParentData
=
selectedChild
.
parentData
!
as
_SegmentedControlContainerBoxParentData
;
// Needs to ensure _currentThumbRect is valid.
final
Rect
newThumbRect
=
_kThumbInsets
.
inflateRect
(
childParentData
.
offset
&
selectedChild
.
size
);
_currentThumbTween
=
RectTween
(
begin:
currentThumbRect
??
unscaledThumbTargetRect
,
end:
unscaledThumbTargetRect
);
// Update thumb animation's tween, in case the end rect changed (e.g., a
for
(
int
i
=
0
;
i
<
childCount
-
1
;
i
+=
1
)
{
// new segment is added during the animation).
// The separator associated with the last child will not be painted (unless
if
(
state
.
thumbController
.
isAnimating
)
{
// a new segment is appended to the child list), and its opacity will always be 1.
final
Animatable
<
Rect
?>?
thumbTween
=
state
.
thumbAnimatable
;
final
bool
shouldFadeOut
=
i
==
highlightedIndex
||
i
==
highlightedIndex
!
-
1
;
if
(
thumbTween
==
null
)
{
final
RenderBox
child
=
children
[
i
];
// This is the first frame of the animation.
final
_ChildAnimationManifest
manifest
=
_childAnimations
![
child
]!;
final
Rect
startingRect
=
moveThumbRectInBound
(
currentThumbRect
,
children
)
??
newThumbRect
;
assert
(
manifest
!=
null
);
state
.
thumbAnimatable
=
RectTween
(
begin:
startingRect
,
end:
newThumbRect
);
manifest
.
separatorTween
=
Tween
<
double
>(
}
else
if
(
newThumbRect
!=
thumbTween
.
transform
(
1
))
{
begin:
manifest
.
separatorOpacity
,
// The thumbTween of the running sliding animation needs updating,
end:
shouldFadeOut
?
0
:
1
,
// without restarting the animation.
);
final
Rect
startingRect
=
moveThumbRectInBound
(
currentThumbRect
,
children
)
??
newThumbRect
;
state
.
thumbAnimatable
=
RectTween
(
begin:
startingRect
,
end:
newThumbRect
)
.
chain
(
CurveTween
(
curve:
Interval
(
state
.
thumbController
.
value
,
1
)));
}
}
}
else
{
_needsThumbAnimationUpdate
=
false
;
state
.
thumbAnimatable
=
null
;
}
else
if
(
_currentThumbTween
!=
null
&&
unscaledThumbTargetRect
!=
_currentThumbTween
!.
begin
)
{
_currentThumbTween
=
RectTween
(
begin:
_currentThumbTween
!.
begin
,
end:
unscaledThumbTargetRect
);
}
for
(
int
index
=
0
;
index
<
childCount
-
1
;
index
+=
1
)
{
_paintSeparator
(
context
,
offset
,
children
[
index
]);
}
}
currentThumbRect
=
_currentThumbTween
?.
evaluate
(
state
.
thumbController
)
final
Rect
unscaledThumbRect
=
state
.
thumbAnimatable
?.
evaluate
(
state
.
thumbController
)
??
newThumbRect
;
??
unscaledThumbTargetRect
;
currentThumbRect
=
unscaledThumbRect
;
currentThumbScale
=
_thumbScaleTween
.
evaluate
(
state
.
thumbScaleController
);
final
Rect
thumbRect
=
Rect
.
fromCenter
(
final
Rect
thumbRect
=
Rect
.
fromCenter
(
center:
currentThumbRect
!
.
center
,
center:
unscaledThumbRect
.
center
,
width:
currentThumbRect
!.
width
*
currentT
humbScale
,
width:
unscaledThumbRect
.
width
*
t
humbScale
,
height:
currentThumbRect
!.
height
*
currentT
humbScale
,
height:
unscaledThumbRect
.
height
*
t
humbScale
,
);
);
_paintThumb
(
context
,
offset
,
thumbRect
);
_paintThumb
(
context
,
offset
,
thumbRect
);
}
else
{
}
else
{
// Reset all animations when there's no thumb.
currentThumbRect
=
null
;
currentThumbRect
=
null
;
_childAnimations
=
null
;
for
(
int
index
=
0
;
index
<
childCount
-
1
;
index
+=
1
)
{
_paintSeparator
(
context
,
offset
,
children
[
index
]);
}
}
}
for
(
int
index
=
0
;
index
<
children
.
length
;
index
++
)
{
for
(
int
index
=
0
;
index
<
children
.
length
;
index
+=
2
)
{
_paintChild
(
context
,
offset
,
children
[
index
]
,
index
);
_paintChild
(
context
,
offset
,
children
[
index
]);
}
}
}
}
// Paint the separator to the right of the given child.
// Paint the separator to the right of the given child.
final
Paint
separatorPaint
=
Paint
();
void
_paintSeparator
(
PaintingContext
context
,
Offset
offset
,
RenderBox
child
)
{
void
_paintSeparator
(
PaintingContext
context
,
Offset
offset
,
RenderBox
child
)
{
assert
(
child
!=
null
);
assert
(
child
!=
null
);
final
_SegmentedControlContainerBoxParentData
childParentData
=
final
_SegmentedControlContainerBoxParentData
childParentData
=
child
.
parentData
!
as
_SegmentedControlContainerBoxParentData
;
child
.
parentData
!
as
_SegmentedControlContainerBoxParentData
;
context
.
paintChild
(
child
,
offset
+
childParentData
.
offset
);
final
Paint
paint
=
Paint
();
final
_ChildAnimationManifest
?
manifest
=
_childAnimations
==
null
?
null
:
_childAnimations
![
child
];
final
double
opacity
=
manifest
?.
separatorTween
.
evaluate
(
state
.
separatorOpacityController
)
??
1
;
manifest
?.
separatorOpacity
=
opacity
;
paint
.
color
=
_kSeparatorColor
.
withOpacity
(
_kSeparatorColor
.
opacity
*
opacity
);
final
Rect
childRect
=
(
childParentData
.
offset
+
offset
)
&
child
.
size
;
final
Rect
separatorRect
=
_kSeparatorInset
.
deflateRect
(
childRect
.
topRight
&
Size
(
_kSeparatorInset
.
horizontal
+
_kSeparatorWidth
,
child
.
size
.
height
),
);
context
.
canvas
.
drawRRect
(
RRect
.
fromRectAndRadius
(
separatorRect
,
_kSeparatorRadius
),
paint
,
);
}
}
void
_paintChild
(
PaintingContext
context
,
Offset
offset
,
RenderBox
child
,
int
childIndex
)
{
void
_paintChild
(
PaintingContext
context
,
Offset
offset
,
RenderBox
child
)
{
assert
(
child
!=
null
);
assert
(
child
!=
null
);
final
_SegmentedControlContainerBoxParentData
childParentData
=
final
_SegmentedControlContainerBoxParentData
childParentData
=
child
.
parentData
!
as
_SegmentedControlContainerBoxParentData
;
child
.
parentData
!
as
_SegmentedControlContainerBoxParentData
;
context
.
paintChild
(
child
,
childParentData
.
offset
+
offset
);
context
.
paintChild
(
child
,
childParentData
.
offset
+
offset
);
}
}
...
@@ -1024,7 +1097,7 @@ class _RenderSegmentedControl<T> extends RenderBox
...
@@ -1024,7 +1097,7 @@ class _RenderSegmentedControl<T> extends RenderBox
context
.
canvas
.
drawRRect
(
context
.
canvas
.
drawRRect
(
thumbRRect
,
thumbRRect
,
Paint
()..
color
=
thumbColor
!
,
Paint
()..
color
=
thumbColor
,
);
);
}
}
...
...
packages/flutter/test/cupertino/sliding_segmented_control_test.dart
View file @
a4b27cbf
...
@@ -7,6 +7,7 @@ import 'dart:collection';
...
@@ -7,6 +7,7 @@ import 'dart:collection';
import
'package:flutter/gestures.dart'
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'../widgets/semantics_tester.dart'
;
import
'../widgets/semantics_tester.dart'
;
...
@@ -29,7 +30,7 @@ Rect currentUnscaledThumbRect(WidgetTester tester, { bool useGlobalCoordinate =
...
@@ -29,7 +30,7 @@ Rect currentUnscaledThumbRect(WidgetTester tester, { bool useGlobalCoordinate =
return
local
.
shift
(
segmentedControl
.
localToGlobal
(
Offset
.
zero
));
return
local
.
shift
(
segmentedControl
.
localToGlobal
(
Offset
.
zero
));
}
}
double
currentThumbScale
(
WidgetTester
tester
)
=>
getRenderSegmentedControl
(
tester
).
currentT
humbScale
as
double
;
double
currentThumbScale
(
WidgetTester
tester
)
=>
getRenderSegmentedControl
(
tester
).
t
humbScale
as
double
;
Widget
setupSimpleSegmentedControl
(
)
{
Widget
setupSimpleSegmentedControl
(
)
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
...
@@ -133,20 +134,20 @@ void main() {
...
@@ -133,20 +134,20 @@ void main() {
final
Rect
segmentedControlRect
=
tester
.
getRect
(
find
.
byKey
(
key
));
final
Rect
segmentedControlRect
=
tester
.
getRect
(
find
.
byKey
(
key
));
expect
(
expect
(
tester
.
getTopLeft
(
find
.
ancestor
(
of:
find
.
byWidget
(
children
[
0
]!),
matching:
find
.
byType
(
Opacity
))),
tester
.
getTopLeft
(
find
.
ancestor
(
of:
find
.
byWidget
(
children
[
0
]!),
matching:
find
.
byType
(
MetaData
))),
segmentedControlRect
.
topLeft
+
effectivePadding
.
topLeft
,
segmentedControlRect
.
topLeft
+
effectivePadding
.
topLeft
,
);
);
expect
(
expect
(
tester
.
getBottomLeft
(
find
.
ancestor
(
of:
find
.
byWidget
(
children
[
0
]!),
matching:
find
.
byType
(
Opacity
))),
tester
.
getBottomLeft
(
find
.
ancestor
(
of:
find
.
byWidget
(
children
[
0
]!),
matching:
find
.
byType
(
MetaData
))),
segmentedControlRect
.
bottomLeft
+
effectivePadding
.
bottomLeft
,
segmentedControlRect
.
bottomLeft
+
effectivePadding
.
bottomLeft
,
);
);
expect
(
expect
(
tester
.
getTopRight
(
find
.
ancestor
(
of:
find
.
byWidget
(
children
[
1
]!),
matching:
find
.
byType
(
Opacity
))),
tester
.
getTopRight
(
find
.
ancestor
(
of:
find
.
byWidget
(
children
[
1
]!),
matching:
find
.
byType
(
MetaData
))),
segmentedControlRect
.
topRight
+
effectivePadding
.
topRight
,
segmentedControlRect
.
topRight
+
effectivePadding
.
topRight
,
);
);
expect
(
expect
(
tester
.
getBottomRight
(
find
.
ancestor
(
of:
find
.
byWidget
(
children
[
1
]!),
matching:
find
.
byType
(
Opacity
))),
tester
.
getBottomRight
(
find
.
ancestor
(
of:
find
.
byWidget
(
children
[
1
]!),
matching:
find
.
byType
(
MetaData
))),
segmentedControlRect
.
bottomRight
+
effectivePadding
.
bottomRight
,
segmentedControlRect
.
bottomRight
+
effectivePadding
.
bottomRight
,
);
);
}
}
...
@@ -400,9 +401,9 @@ void main() {
...
@@ -400,9 +401,9 @@ void main() {
);
);
double
getChildOpacityByName
(
String
childName
)
{
double
getChildOpacityByName
(
String
childName
)
{
return
tester
.
widget
<
Opacity
>(
return
tester
.
renderObject
<
RenderAnimated
Opacity
>(
find
.
ancestor
(
matching:
find
.
byType
(
Opacity
),
of:
find
.
text
(
childName
)),
find
.
ancestor
(
matching:
find
.
byType
(
Animated
Opacity
),
of:
find
.
text
(
childName
)),
).
opacity
;
).
opacity
.
value
;
}
}
// Opacity 1 with no interaction.
// Opacity 1 with no interaction.
...
@@ -441,9 +442,9 @@ void main() {
...
@@ -441,9 +442,9 @@ void main() {
testWidgets
(
'Long press does not change the opacity of currently-selected child'
,
(
WidgetTester
tester
)
async
{
testWidgets
(
'Long press does not change the opacity of currently-selected child'
,
(
WidgetTester
tester
)
async
{
double
getChildOpacityByName
(
String
childName
)
{
double
getChildOpacityByName
(
String
childName
)
{
return
tester
.
widget
<
Opacity
>(
return
tester
.
renderObject
<
RenderAnimated
Opacity
>(
find
.
ancestor
(
matching:
find
.
byType
(
Opacity
),
of:
find
.
text
(
childName
)),
find
.
ancestor
(
matching:
find
.
byType
(
Animated
Opacity
),
of:
find
.
text
(
childName
)),
).
opacity
;
).
opacity
.
value
;
}
}
await
tester
.
pumpWidget
(
setupSimpleSegmentedControl
());
await
tester
.
pumpWidget
(
setupSimpleSegmentedControl
());
...
@@ -782,6 +783,7 @@ void main() {
...
@@ -782,6 +783,7 @@ void main() {
// Tap up and the sliding animation should play.
// Tap up and the sliding animation should play.
await
gesture
.
up
();
await
gesture
.
up
();
await
tester
.
pump
();
await
tester
.
pump
();
// 10 ms isn't long enough for this gesture to be recognized as a longpress.
await
tester
.
pump
(
const
Duration
(
milliseconds:
10
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
10
));
expect
(
currentThumbScale
(
tester
),
1
);
expect
(
currentThumbScale
(
tester
),
1
);
...
@@ -795,8 +797,8 @@ void main() {
...
@@ -795,8 +797,8 @@ void main() {
expect
(
currentThumbScale
(
tester
),
1
);
expect
(
currentThumbScale
(
tester
),
1
);
expect
(
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
,
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
,
// We're using a critically damped spring so
the value of the animation
// We're using a critically damped spring so
expect the value of the
//
controller will never reach
1.
//
animation controller to not be
1.
offsetMoreOrLessEquals
(
tester
.
getCenter
(
find
.
text
(
'Child 2'
)),
epsilon:
0.01
),
offsetMoreOrLessEquals
(
tester
.
getCenter
(
find
.
text
(
'Child 2'
)),
epsilon:
0.01
),
);
);
...
@@ -847,6 +849,62 @@ void main() {
...
@@ -847,6 +849,62 @@ void main() {
expect
(
currentThumbScale
(
tester
),
moreOrLessEquals
(
1
,
epsilon:
0.01
));
expect
(
currentThumbScale
(
tester
),
moreOrLessEquals
(
1
,
epsilon:
0.01
));
});
});
testWidgets
(
'Thumb does not go out of bounds in animation'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
,
maxLines:
1
),
1
:
Text
(
'wiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiide Child 2'
,
maxLines:
1
),
2
:
SizedBox
(
height:
400
),
};
await
tester
.
pumpWidget
(
boilerplate
(
builder:
(
BuildContext
context
)
{
return
CupertinoSlidingSegmentedControl
<
int
>(
children:
children
,
groupValue:
groupValue
,
onValueChanged:
defaultCallback
,
);
},
));
final
Rect
initialThumbRect
=
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
);
// Starts animating towards 1.
setState
!(()
{
groupValue
=
1
;
});
await
tester
.
pump
(
const
Duration
(
milliseconds:
10
));
const
Map
<
int
,
Widget
>
newChildren
=
<
int
,
Widget
>{
0
:
Text
(
'C1'
,
maxLines:
1
),
1
:
Text
(
'C2'
,
maxLines:
1
),
};
// Now let the segments shrink.
await
tester
.
pumpWidget
(
boilerplate
(
builder:
(
BuildContext
context
)
{
return
CupertinoSlidingSegmentedControl
<
int
>(
children:
newChildren
,
groupValue:
1
,
onValueChanged:
defaultCallback
,
);
},
));
final
RenderBox
renderSegmentedControl
=
getRenderSegmentedControl
(
tester
)
as
RenderBox
;
final
Offset
segmentedControlOrigin
=
renderSegmentedControl
.
localToGlobal
(
Offset
.
zero
);
// Expect the segmented control to be much narrower.
expect
(
segmentedControlOrigin
.
dx
,
greaterThan
(
initialThumbRect
.
left
));
final
Rect
thumbRect
=
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
);
expect
(
initialThumbRect
.
size
.
height
,
400
);
expect
(
thumbRect
.
size
.
height
,
lessThan
(
100
));
// The new thumbRect should fit in the segmentedControl. The -1 and the +1
// are to account for the thumb's vertical EdgeInsets.
expect
(
segmentedControlOrigin
.
dx
-
1
,
lessThanOrEqualTo
(
thumbRect
.
left
));
expect
(
segmentedControlOrigin
.
dx
+
renderSegmentedControl
.
size
.
width
+
1
,
greaterThanOrEqualTo
(
thumbRect
.
right
));
});
testWidgets
(
'Transition is triggered while a transition is already occurring'
,
(
WidgetTester
tester
)
async
{
testWidgets
(
'Transition is triggered while a transition is already occurring'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'A'
),
0
:
Text
(
'A'
),
...
@@ -943,6 +1001,227 @@ void main() {
...
@@ -943,6 +1001,227 @@ void main() {
);
);
});
});
testWidgets
(
'change selection programmatically when dragging'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'A'
),
1
:
Text
(
'B'
),
2
:
Text
(
'C'
),
};
bool
callbackCalled
=
false
;
void
onValueChanged
(
int
?
newValue
)
{
callbackCalled
=
true
;
}
await
tester
.
pumpWidget
(
boilerplate
(
builder:
(
BuildContext
context
)
{
return
CupertinoSlidingSegmentedControl
<
int
>(
key:
const
ValueKey
<
String
>(
'Segmented Control'
),
children:
children
,
groupValue:
groupValue
,
onValueChanged:
onValueChanged
,
);
},
),
);
// Start dragging.
final
TestGesture
gesture
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
text
(
'A'
)));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
// Change selection programmatically.
setState
!(()
{
groupValue
=
1
;
});
await
tester
.
pump
();
await
tester
.
pumpAndSettle
();
// The ongoing drag gesture should veto the programmatic change.
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
,
offsetMoreOrLessEquals
(
tester
.
getCenter
(
find
.
text
(
'A'
)),
epsilon:
0.01
),
);
// Move the pointer to 'B'. The onValueChanged callback will be called but
// since the parent widget thinks we're already at 'B', it will not trigger
// a rebuild for us.
await
gesture
.
moveTo
(
tester
.
getCenter
(
find
.
text
(
'B'
)));
await
gesture
.
up
();
await
tester
.
pump
();
await
tester
.
pumpAndSettle
();
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
,
offsetMoreOrLessEquals
(
tester
.
getCenter
(
find
.
text
(
'B'
)),
epsilon:
0.01
),
);
expect
(
callbackCalled
,
isFalse
);
});
testWidgets
(
'Disallow new gesture when dragging'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'A'
),
1
:
Text
(
'B'
),
2
:
Text
(
'C'
),
};
bool
callbackCalled
=
false
;
void
onValueChanged
(
int
?
newValue
)
{
callbackCalled
=
true
;
}
await
tester
.
pumpWidget
(
boilerplate
(
builder:
(
BuildContext
context
)
{
return
CupertinoSlidingSegmentedControl
<
int
>(
key:
const
ValueKey
<
String
>(
'Segmented Control'
),
children:
children
,
groupValue:
groupValue
,
onValueChanged:
onValueChanged
,
);
},
),
);
// Start dragging.
final
TestGesture
gesture
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
text
(
'A'
)));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
// Tap a different segment.
await
tester
.
tap
(
find
.
text
(
'C'
));
await
tester
.
pump
();
await
tester
.
pumpAndSettle
();
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
,
offsetMoreOrLessEquals
(
tester
.
getCenter
(
find
.
text
(
'A'
)),
epsilon:
0.01
),
);
// A different drag.
await
tester
.
drag
(
find
.
text
(
'A'
),
const
Offset
(
300
,
0
));
await
tester
.
pump
();
await
tester
.
pumpAndSettle
();
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
,
offsetMoreOrLessEquals
(
tester
.
getCenter
(
find
.
text
(
'A'
)),
epsilon:
0.01
),
);
await
gesture
.
up
();
expect
(
callbackCalled
,
isFalse
);
});
testWidgets
(
'gesture outlives the widget'
,
(
WidgetTester
tester
)
async
{
// Regression test for https://github.com/flutter/flutter/issues/63338.
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'A'
),
1
:
Text
(
'B'
),
2
:
Text
(
'C'
),
};
await
tester
.
pumpWidget
(
boilerplate
(
builder:
(
BuildContext
context
)
{
return
CupertinoSlidingSegmentedControl
<
int
>(
key:
const
ValueKey
<
String
>(
'Segmented Control'
),
children:
children
,
groupValue:
groupValue
,
onValueChanged:
defaultCallback
,
);
},
),
);
// Start dragging.
final
TestGesture
gesture
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
text
(
'A'
)));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
seconds:
1
));
await
tester
.
pumpWidget
(
const
Placeholder
());
await
gesture
.
moveBy
(
const
Offset
(
200
,
0
));
await
tester
.
pump
();
await
tester
.
pump
();
await
gesture
.
up
();
await
tester
.
pump
();
expect
(
tester
.
takeException
(),
isNull
);
});
testWidgets
(
'computeDryLayout is pure'
,
(
WidgetTester
tester
)
async
{
// Regression test for https://github.com/flutter/flutter/issues/73362.
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'A'
),
1
:
Text
(
'B'
),
2
:
Text
(
'C'
),
};
const
Key
key
=
ValueKey
<
int
>(
1
);
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Center
(
child:
SizedBox
(
width:
10
,
child:
CupertinoSlidingSegmentedControl
<
int
>(
key:
key
,
children:
children
,
groupValue:
groupValue
,
onValueChanged:
defaultCallback
,
),
),
),
),
);
final
RenderBox
renderBox
=
getRenderSegmentedControl
(
tester
)
as
RenderBox
;
final
Size
size
=
renderBox
.
getDryLayout
(
const
BoxConstraints
());
expect
(
size
.
width
,
greaterThan
(
10
));
expect
(
tester
.
takeException
(),
isNull
);
});
testWidgets
(
'Has consistent size, independent of groupValue'
,
(
WidgetTester
tester
)
async
{
// Regression test for https://github.com/flutter/flutter/issues/62063.
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'A'
),
1
:
Text
(
'BB'
),
2
:
Text
(
'CCCC'
),
};
groupValue
=
null
;
await
tester
.
pumpWidget
(
boilerplate
(
builder:
(
BuildContext
context
)
{
return
CupertinoSlidingSegmentedControl
<
int
>(
key:
const
ValueKey
<
String
>(
'Segmented Control'
),
children:
children
,
groupValue:
groupValue
,
onValueChanged:
defaultCallback
,
);
},
),
);
final
RenderBox
renderBox
=
getRenderSegmentedControl
(
tester
)
as
RenderBox
;
final
Size
size
=
renderBox
.
size
;
for
(
final
int
value
in
children
.
keys
)
{
setState
!(()
{
groupValue
=
value
;
});
await
tester
.
pump
();
await
tester
.
pumpAndSettle
();
expect
(
renderBox
.
size
,
size
);
}
});
testWidgets
(
'ScrollView + SlidingSegmentedControl interaction'
,
(
WidgetTester
tester
)
async
{
testWidgets
(
'ScrollView + SlidingSegmentedControl interaction'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
),
0
:
Text
(
'Child 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