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
9ea0ecc3
Unverified
Commit
9ea0ecc3
authored
Oct 30, 2019
by
LongCatIsLooong
Committed by
GitHub
Oct 30, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
CupertinoSlidingSegmentedControl (#42775)
parent
c5b3b3ac
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
2140 additions
and
0 deletions
+2140
-0
cupertino.dart
packages/flutter/lib/cupertino.dart
+1
-0
sliding_segmented_control.dart
.../flutter/lib/src/cupertino/sliding_segmented_control.dart
+1055
-0
sliding_segmented_control_test.dart
...lutter/test/cupertino/sliding_segmented_control_test.dart
+1084
-0
No files found.
packages/flutter/lib/cupertino.dart
View file @
9ea0ecc3
...
...
@@ -29,6 +29,7 @@ export 'src/cupertino/route.dart';
export
'src/cupertino/scrollbar.dart'
;
export
'src/cupertino/segmented_control.dart'
;
export
'src/cupertino/slider.dart'
;
export
'src/cupertino/sliding_segmented_control.dart'
;
export
'src/cupertino/switch.dart'
;
export
'src/cupertino/tab_scaffold.dart'
;
export
'src/cupertino/tab_view.dart'
;
...
...
packages/flutter/lib/src/cupertino/sliding_segmented_control.dart
0 → 100644
View file @
9ea0ecc3
// Copyright 2019 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:math'
as
math
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/physics.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/widgets.dart'
;
import
'colors.dart'
;
// Extracted from https://developer.apple.com/design/resources/.
// Minimum padding from edges of the segmented control to edges of
// encompassing widget.
const
EdgeInsetsGeometry
_kHorizontalItemPadding
=
EdgeInsets
.
symmetric
(
vertical:
2
,
horizontal:
3
);
// The corner radius of the thumb.
const
Radius
_kThumbRadius
=
Radius
.
circular
(
6.93
);
// The amount of space by which to expand the thumb from the size of the currently
// selected child.
const
EdgeInsets
_kThumbInsets
=
EdgeInsets
.
symmetric
(
horizontal:
1
);
// Minimum height of the segmented control.
const
double
_kMinSegmentedControlHeight
=
28.0
;
const
Color
_kSeparatorColor
=
Color
(
0x4D8E8E93
);
const
CupertinoDynamicColor
_kThumbColor
=
CupertinoDynamicColor
.
withBrightness
(
color:
Color
(
0xFFFFFFFF
),
darkColor:
Color
(
0xFF636366
),
);
// The amount of space by which to inset each separator.
const
EdgeInsets
_kSeparatorInset
=
EdgeInsets
.
symmetric
(
vertical:
6
);
const
double
_kSeparatorWidth
=
1
;
const
Radius
_kSeparatorRadius
=
Radius
.
circular
(
_kSeparatorWidth
/
2
);
// The minimum scale factor of the thumb, when being pressed on for a sufficient
// amount of time.
const
double
_kMinThumbScale
=
0.95
;
// The minimum horizontal distance between the edges of the separator and the
// closest child.
const
double
_kSegmentMinPadding
=
9.25
;
// The threshold value used in hasDraggedTooFar, for checking against the square
// L2 distance from the location of the current drag pointer, to the closest
// vertice of the CupertinoSlidingSegmentedControl's Rect.
//
// Both the mechanism and the value are speculated.
const
double
_kTouchYDistanceThreshold
=
50.0
*
50.0
;
// The corner radius of the segmented control.
//
// Inspected from iOS 13.2 simulator.
const
double
_kCornerRadius
=
8
;
// The spring animation used when the thumb changes its rect.
final
SpringSimulation
_kThumbSpringAnimationSimulation
=
SpringSimulation
(
const
SpringDescription
(
mass:
1
,
stiffness:
503.551
,
damping:
44.8799
),
0
,
1
,
0
,
// Everytime a new spring animation starts the previous animation stops.
);
const
Duration
_kSpringAnimationDuration
=
Duration
(
milliseconds:
412
);
const
Duration
_kOpacityAnimationDuration
=
Duration
(
milliseconds:
470
);
const
Duration
_kHighlightAnimationDuration
=
Duration
(
milliseconds:
200
);
class
_FontWeightTween
extends
Tween
<
FontWeight
>
{
_FontWeightTween
({
FontWeight
begin
,
FontWeight
end
})
:
super
(
begin:
begin
,
end:
end
);
@override
FontWeight
lerp
(
double
t
)
=>
FontWeight
.
lerp
(
begin
,
end
,
t
);
}
/// An iOS 13 style segmented control.
///
/// 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
/// in the segmented control is selected, the other options in the segmented
/// control cease to be selected.
///
/// 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
/// each widget and determine which widget is selected. As required by the [Map]
/// class, keys must be of consistent types and must be comparable. The [children]
/// 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.
///
/// When the state of the segmented control changes, the widget changes the
/// [controller]'s value to the map key associated with the newly selected widget,
/// causing all of its listeners to be notified.
///
/// {@tool dartpad --template=stateful_widget_material}
///
/// This sample shows two [CupertinoSlidingSegmentedControl]s that mirror each other.
///
/// ```dart
/// final Map<int, Widget> children = const <int, Widget>{
/// 0: Text('Child 1'),
/// 1: Text('Child 2'),
/// 2: Text('Child 3'),
/// };
///
/// // No segment is initially selected because the controller's value is null.
/// final ValueNotifier<int> controller = ValueNotifier<int>(null);
///
/// @override
/// void initState() {
/// super.initState();
/// // Prints a message whenever the currently selected widget changes.
/// controller.addListener(() { print('selected: ${controller.value}'); });
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return Center(
/// child: Column(
/// children: <Widget>[
/// CupertinoSlidingSegmentedControl<int>(
/// children: children,
/// controller: controller,
/// ),
/// CupertinoSlidingSegmentedControl<int>(
/// children: children,
/// controller: controller,
/// ),
/// ],
/// ),
/// );
/// }
///
/// @override
/// void dispose() {
/// controller.dispose();
/// super.dispose();
/// }
/// ```
/// {@end-tool}
///
/// The [children] will be displayed in the order of the keys in the [Map].
/// The height of the segmented control is determined by the height of the
/// tallest widget provided as a value in the [Map] of [children].
/// The width of each child in the segmented control will be equal to the width
/// of widest child, unless the combined width of the children is wider than
/// 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
/// [thumbColor], [backgroundColor] arguments can be used to override the segmented
/// control's colors from its defaults.
///
/// See also:
///
/// * <https://developer.apple.com/design/human-interface-guidelines/ios/controls/segmented-controls/>
class
CupertinoSlidingSegmentedControl
<
T
>
extends
StatefulWidget
{
/// Creates an iOS-style segmented control bar.
///
/// The [children] and [controller] arguments must not be null. The [children]
/// argument must be an ordered [Map] such as a [LinkedHashMap]. Further, the
/// length of the [children] list must be greater than one.
///
/// Each widget value in the map of [children] must have an associated [Map] key
/// of type [T] that uniquely identifies this widget. This key will become the
/// [controller]'s new value, when the corresponding child widget from the
/// [children] map is selected.
///
/// The [controller]'s [ValueNotifier.value] is the currently selected value for
/// the segmented control. If it is null, no widget will appear as selected. The
/// [controller]'s value must be either null or one of the keys in the [children]
/// map.
CupertinoSlidingSegmentedControl
({
Key
key
,
@required
this
.
children
,
@required
this
.
controller
,
this
.
thumbColor
=
_kThumbColor
,
this
.
padding
=
_kHorizontalItemPadding
,
this
.
backgroundColor
=
CupertinoColors
.
tertiarySystemFill
,
})
:
assert
(
children
!=
null
),
assert
(
children
.
length
>=
2
),
assert
(
padding
!=
null
),
assert
(
controller
!=
null
),
assert
(
controller
.
value
==
null
||
children
.
keys
.
any
((
T
child
)
=>
child
==
controller
.
value
),
"The controller's value must be either null or one of the keys in the children map."
,
),
super
(
key:
key
);
/// The identifying keys and corresponding widget values in the
/// segmented control.
///
/// The map must have more than one entry.
/// This attribute must be an ordered [Map] such as a [LinkedHashMap].
final
Map
<
T
,
Widget
>
children
;
/// A [ValueNotifier]<[T]> that controls the currently selected child.
///
/// Its value must be one of the keys in the [Map] of [children], or null, in
/// which case no widget will be selected.
///
/// The [controller]'s value changes when the user drags the thumb to a different
/// child widget, or taps on a different child widget. Its value can also be
/// changed programmatically, in which case all sliding animations will play as
/// if the new selected child widget was tapped on.
final
ValueNotifier
<
T
>
controller
;
/// The color used to paint the rounded rect behind the [children] and the separators.
///
/// The default value is [CupertinoColors.tertiarySystemFill]. The background
/// will not be painted if null is specified.
final
Color
backgroundColor
;
/// The color used to paint the interior of the thumb that appears behind the
/// currently selected item.
///
/// The default value is a [CupertinoDynamicColor] that appears white in light
/// mode and becomes a gray color in dark mode.
final
Color
thumbColor
;
/// The amount of space by which to inset the [children].
///
/// Must not be null. Defaults to EdgeInsets.symmetric(vertical: 2, horizontal: 3).
final
EdgeInsetsGeometry
padding
;
@override
_SegmentedControlState
<
T
>
createState
()
=>
_SegmentedControlState
<
T
>();
}
class
_SegmentedControlState
<
T
>
extends
State
<
CupertinoSlidingSegmentedControl
<
T
>>
with
TickerProviderStateMixin
<
CupertinoSlidingSegmentedControl
<
T
>>
{
final
Map
<
T
,
AnimationController
>
_highlightControllers
=
<
T
,
AnimationController
>{};
final
Tween
<
FontWeight
>
_highlightTween
=
_FontWeightTween
(
begin:
FontWeight
.
normal
,
end:
FontWeight
.
w600
);
final
Map
<
T
,
AnimationController
>
_pressControllers
=
<
T
,
AnimationController
>{};
final
Tween
<
double
>
_pressTween
=
Tween
<
double
>(
begin:
1
,
end:
0.2
);
List
<
T
>
keys
;
AnimationController
thumbController
;
AnimationController
separatorOpacityController
;
AnimationController
thumbScaleController
;
final
TapGestureRecognizer
tap
=
TapGestureRecognizer
();
final
HorizontalDragGestureRecognizer
drag
=
HorizontalDragGestureRecognizer
();
final
LongPressGestureRecognizer
longPress
=
LongPressGestureRecognizer
();
ValueNotifier
<
T
>
controller
;
AnimationController
_createHighlightAnimationController
({
bool
isCompleted
=
false
})
{
return
AnimationController
(
duration:
_kHighlightAnimationDuration
,
value:
isCompleted
?
1
:
0
,
vsync:
this
,
);
}
AnimationController
_createFadeoutAnimationController
()
{
return
AnimationController
(
duration:
_kOpacityAnimationDuration
,
vsync:
this
,
);
}
@override
void
initState
()
{
super
.
initState
();
final
GestureArenaTeam
team
=
GestureArenaTeam
();
// 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
// the drag gesture recognizer.
longPress
.
team
=
team
;
drag
.
team
=
team
;
team
.
captain
=
drag
;
controller
=
widget
.
controller
;
controller
.
addListener
(
_didChangeControllerValue
);
_highlighted
=
controller
.
value
;
thumbController
=
AnimationController
(
duration:
_kSpringAnimationDuration
,
value:
0
,
vsync:
this
,
);
thumbScaleController
=
AnimationController
(
duration:
_kSpringAnimationDuration
,
value:
1
,
vsync:
this
,
);
separatorOpacityController
=
AnimationController
(
duration:
_kSpringAnimationDuration
,
value:
0
,
vsync:
this
,
);
for
(
T
currentKey
in
widget
.
children
.
keys
)
{
_highlightControllers
[
currentKey
]
=
_createHighlightAnimationController
(
isCompleted:
currentKey
==
controller
.
value
,
// Highlight the current selection.
);
_pressControllers
[
currentKey
]
=
_createFadeoutAnimationController
();
}
}
@override
void
didUpdateWidget
(
CupertinoSlidingSegmentedControl
<
T
>
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
// Update animation controllers.
for
(
T
oldKey
in
oldWidget
.
children
.
keys
)
{
if
(!
widget
.
children
.
containsKey
(
oldKey
))
{
_highlightControllers
[
oldKey
].
dispose
();
_pressControllers
[
oldKey
].
dispose
();
_highlightControllers
.
remove
(
oldKey
);
_pressControllers
.
remove
(
oldKey
);
}
}
for
(
T
newKey
in
widget
.
children
.
keys
)
{
if
(!
_highlightControllers
.
keys
.
contains
(
newKey
))
{
_highlightControllers
[
newKey
]
=
_createHighlightAnimationController
();
_pressControllers
[
newKey
]
=
_createFadeoutAnimationController
();
}
}
if
(
controller
!=
widget
.
controller
)
{
controller
.
removeListener
(
_didChangeControllerValue
);
controller
=
widget
.
controller
;
controller
.
addListener
(
_didChangeControllerValue
);
}
if
(
controller
.
value
!=
oldWidget
.
controller
.
value
)
{
highlighted
=
widget
.
controller
.
value
;
}
}
@override
void
dispose
()
{
for
(
AnimationController
animationController
in
_highlightControllers
.
values
)
{
animationController
.
dispose
();
}
for
(
AnimationController
animationController
in
_pressControllers
.
values
)
{
animationController
.
dispose
();
}
thumbScaleController
.
dispose
();
thumbController
.
dispose
();
separatorOpacityController
.
dispose
();
drag
.
dispose
();
tap
.
dispose
();
longPress
.
dispose
();
super
.
dispose
();
}
void
_didChangeControllerValue
()
{
assert
(
controller
.
value
==
null
||
widget
.
children
.
keys
.
contains
(
controller
.
value
),
"The controller's value
${controller.value}
must be either null "
'or one of the keys in the children map:
${widget.children.keys}
'
,
);
setState
(()
{
// Mark the state as dirty.
});
}
// Play highlight animation for the child located at _highlightControllers[at].
void
_animateHighlightController
({
T
at
,
bool
forward
})
{
if
(
at
==
null
)
return
;
final
AnimationController
controller
=
_highlightControllers
[
at
];
assert
(!
forward
||
controller
!=
null
);
controller
?.
animateTo
(
forward
?
1
:
0
,
duration:
_kHighlightAnimationDuration
,
curve:
Curves
.
ease
);
}
T
_highlighted
;
set
highlighted
(
T
newValue
)
{
if
(
_highlighted
==
newValue
)
return
;
_animateHighlightController
(
at:
newValue
,
forward:
true
);
_animateHighlightController
(
at:
_highlighted
,
forward:
false
);
_highlighted
=
newValue
;
}
T
_pressed
;
set
pressed
(
T
newValue
)
{
if
(
_pressed
==
newValue
)
return
;
if
(
_pressed
!=
null
)
{
_pressControllers
[
_pressed
]?.
animateTo
(
0
,
duration:
_kOpacityAnimationDuration
,
curve:
Curves
.
ease
);
}
if
(
newValue
!=
_highlighted
&&
newValue
!=
null
)
{
_pressControllers
[
newValue
].
animateTo
(
1
,
duration:
_kOpacityAnimationDuration
,
curve:
Curves
.
ease
);
}
_pressed
=
newValue
;
}
void
didChangeSelectedViaGesture
()
{
controller
.
value
=
_highlighted
;
}
T
indexToKey
(
int
index
)
=>
index
==
null
?
null
:
keys
[
index
];
@override
Widget
build
(
BuildContext
context
)
{
debugCheckHasDirectionality
(
context
);
switch
(
Directionality
.
of
(
context
))
{
case
TextDirection
.
ltr
:
keys
=
widget
.
children
.
keys
.
toList
(
growable:
false
);
break
;
case
TextDirection
.
rtl
:
keys
=
widget
.
children
.
keys
.
toList
().
reversed
.
toList
(
growable:
false
);
break
;
}
return
AnimatedBuilder
(
animation:
Listenable
.
merge
(<
Listenable
>[
...
_highlightControllers
.
values
,
...
_pressControllers
.
values
,
]),
builder:
(
BuildContext
context
,
Widget
child
)
{
final
List
<
Widget
>
children
=
<
Widget
>[];
for
(
T
currentKey
in
keys
)
{
final
TextStyle
textStyle
=
DefaultTextStyle
.
of
(
context
).
style
.
copyWith
(
fontWeight:
_highlightTween
.
evaluate
(
_highlightControllers
[
currentKey
]),
);
final
Widget
child
=
DefaultTextStyle
(
style:
textStyle
,
child:
Semantics
(
button:
true
,
onTap:
()
{
controller
.
value
=
currentKey
;
},
inMutuallyExclusiveGroup:
true
,
selected:
controller
.
value
==
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
=
controller
.
value
==
null
?
null
:
keys
.
indexOf
(
controller
.
value
);
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
,
),
);
},
);
}
}
class
_SegmentedControlRenderWidget
<
T
>
extends
MultiChildRenderObjectWidget
{
_SegmentedControlRenderWidget
({
Key
key
,
List
<
Widget
>
children
=
const
<
Widget
>[],
@required
this
.
selectedIndex
,
@required
this
.
thumbColor
,
@required
this
.
state
,
})
:
super
(
key:
key
,
children:
children
);
final
int
selectedIndex
;
final
Color
thumbColor
;
final
_SegmentedControlState
<
T
>
state
;
@override
RenderObject
createRenderObject
(
BuildContext
context
)
{
return
_RenderSegmentedControl
<
T
>(
selectedIndex:
selectedIndex
,
thumbColor:
CupertinoDynamicColor
.
resolve
(
thumbColor
,
context
),
state:
state
,
);
}
@override
void
updateRenderObject
(
BuildContext
context
,
_RenderSegmentedControl
<
T
>
renderObject
)
{
renderObject
..
thumbColor
=
CupertinoDynamicColor
.
resolve
(
thumbColor
,
context
)
..
guardedSetHighlightedIndex
(
selectedIndex
);
}
}
class
_ChildAnimationManifest
{
_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:
//
// 1. Tap up inside events will set the current selected index to the index of the
// segment at the tap up location instantaneously (there might be animation but
// the index change seems to happen before animation finishes), unless the tap
// down event from the same touch event didn't happen within the segmented
// control, in which case the touch event will be ignored entirely (will be
// referring to these touch events as invalid touch events below).
//
// 2. A valid tap up event will also trigger the sliding CASpringAnimation (even
// when it lands on the current segment), starting from the current `frame`
// of the thumb. The previous sliding animation, if still playing, will be
// removed and its velocity reset to 0. The sliding animation has a fixed
// duration, regardless of the distance or transform.
//
// 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
// font weight to semibold (CABasicAnimation, timingFunction = default, duration = 0.2).
// The other is the separator fadein/fadeout animation.
//
// 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
// original size, even if the sliding animation is still playing. The
/// corresponding tap up event inverts the process (eyeballed).
//
// 5. A tap down event on other segments will trigger a CABasicAnimation
// (timingFunction = default, duration = 0.47.) that fades out the content,
// eventually reducing the alpha of that segment to 20% unless interrupted by
// a tap up event or the pointer moves out of the region (either outside of the
// segmented control's vicinity or to a different segment). The reverse animation
// has the same duration and timing function.
class
_RenderSegmentedControl
<
T
>
extends
RenderBox
with
ContainerRenderObjectMixin
<
RenderBox
,
ContainerBoxParentData
<
RenderBox
>>,
RenderBoxContainerDefaultsMixin
<
RenderBox
,
ContainerBoxParentData
<
RenderBox
>>
{
_RenderSegmentedControl
({
@required
int
selectedIndex
,
@required
Color
thumbColor
,
@required
this
.
state
,
})
:
_highlightedIndex
=
selectedIndex
,
_thumbColor
=
thumbColor
,
assert
(
state
!=
null
)
{
state
.
drag
..
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
;
Map
<
RenderBox
,
_ChildAnimationManifest
>
_childAnimations
=
<
RenderBox
,
_ChildAnimationManifest
>{};
// The current **Unscaled** Thumb Rect.
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
void
attach
(
PipelineOwner
owner
)
{
super
.
attach
(
owner
);
state
.
thumbController
.
addListener
(
markNeedsPaint
);
state
.
thumbScaleController
.
addListener
(
markNeedsPaint
);
state
.
separatorOpacityController
.
addListener
(
markNeedsPaint
);
}
@override
void
detach
()
{
state
.
thumbController
.
removeListener
(
markNeedsPaint
);
state
.
thumbScaleController
.
removeListener
(
markNeedsPaint
);
state
.
separatorOpacityController
.
removeListener
(
markNeedsPaint
);
super
.
detach
();
}
// Indicates whether selectedIndex has changed and animations need to be updated.
// when true some animation tweens will be updated in paint phase.
bool
_needsThumbAnimationUpdate
=
false
;
int
get
highlightedIndex
=>
_highlightedIndex
;
int
_highlightedIndex
;
set
highlightedIndex
(
int
value
)
{
if
(
_highlightedIndex
==
value
)
{
return
;
}
_needsThumbAnimationUpdate
=
true
;
_highlightedIndex
=
value
;
state
.
thumbController
.
animateWith
(
_kThumbSpringAnimationSimulation
);
state
.
separatorOpacityController
.
reset
();
state
.
separatorOpacityController
.
animateTo
(
1
,
duration:
_kSpringAnimationDuration
,
curve:
Curves
.
ease
,
);
state
.
highlighted
=
state
.
indexToKey
(
value
);
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
_thumbColor
;
set
thumbColor
(
Color
value
)
{
if
(
_thumbColor
==
value
)
{
return
;
}
_thumbColor
=
value
;
markNeedsPaint
();
}
double
get
totalSeparatorWidth
=>
(
_kSeparatorInset
.
horizontal
+
_kSeparatorWidth
)
*
(
childCount
-
1
);
@override
void
handleEvent
(
PointerEvent
event
,
BoxHitTestEntry
entry
)
{
assert
(
debugHandleEvent
(
event
,
entry
));
if
(
event
is
PointerDownEvent
)
{
state
.
tap
.
addPointer
(
event
);
state
.
longPress
.
addPointer
(
event
);
state
.
drag
.
addPointer
(
event
);
}
}
int
indexFromLocation
(
Offset
location
)
{
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
;
pressedIndex
=
null
;
_startedOnSelectedSegment
=
null
;
}
void
_playThumbScaleAnimation
({
@required
bool
isExpanding
})
{
assert
(
isExpanding
!=
null
);
_thumbScaleTween
=
Tween
<
double
>(
begin:
currentThumbScale
,
end:
isExpanding
?
1
:
_kMinThumbScale
);
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
double
computeMinIntrinsicWidth
(
double
height
)
{
RenderBox
child
=
firstChild
;
double
maxMinChildWidth
=
0
;
while
(
child
!=
null
)
{
final
_SegmentedControlContainerBoxParentData
childParentData
=
child
.
parentData
;
final
double
childWidth
=
child
.
getMinIntrinsicWidth
(
height
);
maxMinChildWidth
=
math
.
max
(
maxMinChildWidth
,
childWidth
);
child
=
childParentData
.
nextSibling
;
}
return
(
maxMinChildWidth
+
2
*
_kSegmentMinPadding
)
*
childCount
+
totalSeparatorWidth
;
}
@override
double
computeMaxIntrinsicWidth
(
double
height
)
{
RenderBox
child
=
firstChild
;
double
maxMaxChildWidth
=
0
;
while
(
child
!=
null
)
{
final
_SegmentedControlContainerBoxParentData
childParentData
=
child
.
parentData
;
final
double
childWidth
=
child
.
getMaxIntrinsicWidth
(
height
);
maxMaxChildWidth
=
math
.
max
(
maxMaxChildWidth
,
childWidth
);
child
=
childParentData
.
nextSibling
;
}
return
(
maxMaxChildWidth
+
2
*
_kSegmentMinPadding
)
*
childCount
+
totalSeparatorWidth
;
}
@override
double
computeMinIntrinsicHeight
(
double
width
)
{
RenderBox
child
=
firstChild
;
double
maxMinChildHeight
=
0
;
while
(
child
!=
null
)
{
final
_SegmentedControlContainerBoxParentData
childParentData
=
child
.
parentData
;
final
double
childHeight
=
child
.
getMinIntrinsicHeight
(
width
);
maxMinChildHeight
=
math
.
max
(
maxMinChildHeight
,
childHeight
);
child
=
childParentData
.
nextSibling
;
}
return
maxMinChildHeight
;
}
@override
double
computeMaxIntrinsicHeight
(
double
width
)
{
RenderBox
child
=
firstChild
;
double
maxMaxChildHeight
=
0
;
while
(
child
!=
null
)
{
final
_SegmentedControlContainerBoxParentData
childParentData
=
child
.
parentData
;
final
double
childHeight
=
child
.
getMaxIntrinsicHeight
(
width
);
maxMaxChildHeight
=
math
.
max
(
maxMaxChildHeight
,
childHeight
);
child
=
childParentData
.
nextSibling
;
}
return
maxMaxChildHeight
;
}
@override
double
computeDistanceToActualBaseline
(
TextBaseline
baseline
)
{
return
defaultComputeDistanceToHighestActualBaseline
(
baseline
);
}
@override
void
setupParentData
(
RenderBox
child
)
{
if
(
child
.
parentData
is
!
_SegmentedControlContainerBoxParentData
)
{
child
.
parentData
=
_SegmentedControlContainerBoxParentData
();
}
}
@override
void
performLayout
()
{
double
childWidth
=
(
constraints
.
minWidth
-
totalSeparatorWidth
)
/
childCount
;
double
maxHeight
=
_kMinSegmentedControlHeight
;
for
(
RenderBox
child
in
getChildrenAsList
())
{
childWidth
=
math
.
max
(
childWidth
,
child
.
getMaxIntrinsicWidth
(
double
.
infinity
)
+
2
*
_kSegmentMinPadding
);
}
childWidth
=
math
.
min
(
childWidth
,
(
constraints
.
maxWidth
-
totalSeparatorWidth
)
/
childCount
,
);
RenderBox
child
=
firstChild
;
while
(
child
!=
null
)
{
final
double
boxHeight
=
child
.
getMaxIntrinsicHeight
(
childWidth
);
maxHeight
=
math
.
max
(
maxHeight
,
boxHeight
);
child
=
childAfter
(
child
);
}
constraints
.
constrainHeight
(
maxHeight
);
final
BoxConstraints
childConstraints
=
BoxConstraints
.
tightFor
(
width:
childWidth
,
height:
maxHeight
,
);
// Layout children.
child
=
firstChild
;
while
(
child
!=
null
)
{
child
.
layout
(
childConstraints
,
parentUsesSize:
true
);
child
=
childAfter
(
child
);
}
double
start
=
0
;
child
=
firstChild
;
while
(
child
!=
null
)
{
final
_SegmentedControlContainerBoxParentData
childParentData
=
child
.
parentData
;
final
Offset
childOffset
=
Offset
(
start
,
0
);
childParentData
.
offset
=
childOffset
;
start
+=
child
.
size
.
width
+
_kSeparatorWidth
+
_kSeparatorInset
.
horizontal
;
child
=
childAfter
(
child
);
}
size
=
constraints
.
constrain
(
Size
(
childWidth
*
childCount
+
totalSeparatorWidth
,
maxHeight
));
}
@override
void
paint
(
PaintingContext
context
,
Offset
offset
)
{
final
List
<
RenderBox
>
children
=
getChildrenAsList
();
// Paint thumb if highlightedIndex is not null.
if
(
highlightedIndex
!=
null
)
{
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
=
selectedChild
.
parentData
;
final
Rect
unscaledThumbTargetRect
=
_kThumbInsets
.
inflateRect
(
childParentData
.
offset
&
selectedChild
.
size
);
// Update related Tweens before animation update phase.
if
(
_needsThumbAnimationUpdate
)
{
// Needs to ensure _currentThumbRect is valid.
_currentThumbTween
=
RectTween
(
begin:
currentThumbRect
??
unscaledThumbTargetRect
,
end:
unscaledThumbTargetRect
);
for
(
int
i
=
0
;
i
<
childCount
-
1
;
i
+=
1
)
{
// The separator associated with the last child will not be painted (unless
// a new segment is appended to the child list), and its opacity will always be 1.
final
bool
shouldFadeOut
=
i
==
highlightedIndex
||
i
==
highlightedIndex
-
1
;
final
RenderBox
child
=
children
[
i
];
final
_ChildAnimationManifest
manifest
=
_childAnimations
[
child
];
assert
(
manifest
!=
null
);
manifest
.
separatorTween
=
Tween
<
double
>(
begin:
manifest
.
separatorOpacity
,
end:
shouldFadeOut
?
0
:
1
,
);
}
_needsThumbAnimationUpdate
=
false
;
}
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
)
??
unscaledThumbTargetRect
;
currentThumbScale
=
_thumbScaleTween
.
evaluate
(
state
.
thumbScaleController
);
final
Rect
thumbRect
=
Rect
.
fromCenter
(
center:
currentThumbRect
.
center
,
width:
currentThumbRect
.
width
*
currentThumbScale
,
height:
currentThumbRect
.
height
*
currentThumbScale
,
);
_paintThumb
(
context
,
offset
,
thumbRect
);
}
else
{
// Reset all animations when there's no thumb.
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
++)
{
_paintChild
(
context
,
offset
,
children
[
index
],
index
);
}
}
// Paint the separator to the right of the given child.
void
_paintSeparator
(
PaintingContext
context
,
Offset
offset
,
RenderBox
child
)
{
assert
(
child
!=
null
);
final
_SegmentedControlContainerBoxParentData
childParentData
=
child
.
parentData
;
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
)
{
assert
(
child
!=
null
);
final
_SegmentedControlContainerBoxParentData
childParentData
=
child
.
parentData
;
context
.
paintChild
(
child
,
childParentData
.
offset
+
offset
);
}
void
_paintThumb
(
PaintingContext
context
,
Offset
offset
,
Rect
thumbRect
)
{
// Colors extracted from https://developer.apple.com/design/resources/.
const
List
<
BoxShadow
>
thumbShadow
=
<
BoxShadow
>
[
BoxShadow
(
color:
Color
(
0x1F000000
),
offset:
Offset
(
0
,
3
),
blurRadius:
8
,
),
BoxShadow
(
color:
Color
(
0x0A000000
),
offset:
Offset
(
0
,
3
),
blurRadius:
1
,
),
];
final
RRect
thumbRRect
=
RRect
.
fromRectAndRadius
(
thumbRect
.
shift
(
offset
),
_kThumbRadius
);
for
(
BoxShadow
shadow
in
thumbShadow
)
{
context
.
canvas
.
drawRRect
(
thumbRRect
.
shift
(
shadow
.
offset
),
shadow
.
toPaint
());
}
context
.
canvas
.
drawRRect
(
thumbRRect
.
inflate
(
0.5
),
Paint
()..
color
=
const
Color
(
0x0A000000
),
);
context
.
canvas
.
drawRRect
(
thumbRRect
,
Paint
()..
color
=
thumbColor
,
);
}
@override
bool
hitTestChildren
(
BoxHitTestResult
result
,
{
@required
Offset
position
})
{
assert
(
position
!=
null
);
RenderBox
child
=
lastChild
;
while
(
child
!=
null
)
{
final
_SegmentedControlContainerBoxParentData
childParentData
=
child
.
parentData
;
if
((
childParentData
.
offset
&
child
.
size
).
contains
(
position
))
{
final
Offset
center
=
(
Offset
.
zero
&
child
.
size
).
center
;
return
result
.
addWithRawTransform
(
transform:
MatrixUtils
.
forceToPoint
(
center
),
position:
center
,
hitTest:
(
BoxHitTestResult
result
,
Offset
position
)
{
assert
(
position
==
center
);
return
child
.
hitTest
(
result
,
position:
center
);
},
);
}
child
=
childParentData
.
previousSibling
;
}
return
false
;
}
}
packages/flutter/test/cupertino/sliding_segmented_control_test.dart
0 → 100644
View file @
9ea0ecc3
// Copyright 2019 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:collection'
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter/cupertino.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'../widgets/semantics_tester.dart'
;
dynamic
getRenderSegmentedControl
(
WidgetTester
tester
)
{
return
tester
.
allRenderObjects
.
firstWhere
(
(
RenderObject
currentObject
)
{
return
currentObject
.
toStringShort
().
contains
(
'_RenderSegmentedControl'
);
},
);
}
Rect
currentUnscaledThumbRect
(
WidgetTester
tester
,
{
bool
useGlobalCoordinate
=
false
})
{
final
dynamic
renderSegmentedControl
=
getRenderSegmentedControl
(
tester
);
final
Rect
local
=
renderSegmentedControl
.
currentThumbRect
;
if
(!
useGlobalCoordinate
)
return
local
;
final
RenderBox
segmentedControl
=
renderSegmentedControl
;
return
local
?.
shift
(
segmentedControl
.
localToGlobal
(
Offset
.
zero
));
}
double
currentThumbScale
(
WidgetTester
tester
)
=>
getRenderSegmentedControl
(
tester
).
currentThumbScale
;
Widget
setupSimpleSegmentedControl
(
)
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
),
1
:
Text
(
'Child 2'
),
};
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
0
);
return
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
children:
children
,
controller:
controller
,
),
);
}
Widget
boilerplate
(
{
Widget
child
})
{
return
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Center
(
child:
child
),
);
}
void
main
(
)
{
testWidgets
(
'Children and controller and padding arguments can not be null'
,
(
WidgetTester
tester
)
async
{
try
{
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
children:
null
,
controller:
ValueNotifier
<
int
>(
null
),
),
),
);
fail
(
'Should not be possible to create segmented control with null children'
);
}
on
AssertionError
catch
(
e
)
{
expect
(
e
.
toString
(),
contains
(
'children'
));
}
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
),
1
:
Text
(
'Child 2'
),
};
try
{
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
children:
children
,
controller:
null
,
),
),
);
fail
(
'Should not be possible to create segmented control without a controller'
);
}
on
AssertionError
catch
(
e
)
{
expect
(
e
.
toString
(),
contains
(
'controller'
));
}
try
{
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
children:
children
,
controller:
ValueNotifier
<
int
>(
null
),
padding:
null
,
),
),
);
fail
(
'Should not be possible to create segmented control with null padding'
);
}
on
AssertionError
catch
(
e
)
{
expect
(
e
.
toString
(),
contains
(
'padding'
));
}
});
testWidgets
(
'Need at least 2 children'
,
(
WidgetTester
tester
)
async
{
final
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{};
try
{
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
children:
children
,
controller:
ValueNotifier
<
int
>(
null
),
),
),
);
fail
(
'Should not be possible to create a segmented control with no children'
);
}
on
AssertionError
catch
(
e
)
{
expect
(
e
.
toString
(),
contains
(
'children.length'
));
}
try
{
children
[
0
]
=
const
Text
(
'Child 1'
);
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
children:
children
,
controller:
ValueNotifier
<
int
>(
null
),
),
),
);
fail
(
'Should not be possible to create a segmented control with just one child'
);
}
on
AssertionError
catch
(
e
)
{
expect
(
e
.
toString
(),
contains
(
'children.length'
));
}
try
{
children
[
1
]
=
const
Text
(
'Child 2'
);
children
[
2
]
=
const
Text
(
'Child 3'
);
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
children:
children
,
controller:
ValueNotifier
<
int
>(-
1
),
),
),
);
fail
(
'Should not be possible to create a segmented control with a controller pointing to a non-existent child'
);
}
on
AssertionError
catch
(
e
)
{
expect
(
e
.
toString
(),
contains
(
'value must be either null or one of the keys in the children map'
));
}
});
testWidgets
(
'Padding works'
,
(
WidgetTester
tester
)
async
{
const
Key
key
=
Key
(
'Container'
);
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
),
1
:
Text
(
'Child 2'
),
};
Future
<
void
>
verifyPadding
({
EdgeInsets
padding
})
async
{
final
EdgeInsets
effectivePadding
=
padding
??
const
EdgeInsets
.
symmetric
(
vertical:
2
,
horizontal:
3
);
final
Rect
segmentedControlRect
=
tester
.
getRect
(
find
.
byKey
(
key
));
expect
(
tester
.
getTopLeft
(
find
.
ancestor
(
of:
find
.
byWidget
(
children
[
0
]),
matching:
find
.
byType
(
Opacity
))),
segmentedControlRect
.
topLeft
+
effectivePadding
.
topLeft
,
);
expect
(
tester
.
getBottomLeft
(
find
.
ancestor
(
of:
find
.
byWidget
(
children
[
0
]),
matching:
find
.
byType
(
Opacity
))),
segmentedControlRect
.
bottomLeft
+
effectivePadding
.
bottomLeft
,
);
expect
(
tester
.
getTopRight
(
find
.
ancestor
(
of:
find
.
byWidget
(
children
[
1
]),
matching:
find
.
byType
(
Opacity
))),
segmentedControlRect
.
topRight
+
effectivePadding
.
topRight
,
);
expect
(
tester
.
getBottomRight
(
find
.
ancestor
(
of:
find
.
byWidget
(
children
[
1
]),
matching:
find
.
byType
(
Opacity
))),
segmentedControlRect
.
bottomRight
+
effectivePadding
.
bottomRight
,
);
}
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
key:
key
,
children:
children
,
controller:
ValueNotifier
<
int
>(
null
),
),
),
);
// Default padding works.
await
verifyPadding
();
// Switch to Child 2 padding should remain the same.
await
tester
.
tap
(
find
.
text
(
'Child 2'
));
await
tester
.
pumpAndSettle
();
await
verifyPadding
();
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
key:
key
,
padding:
const
EdgeInsets
.
fromLTRB
(
1
,
3
,
5
,
7
),
children:
children
,
controller:
ValueNotifier
<
int
>(
null
),
),
),
);
// Custom padding works.
await
verifyPadding
(
padding:
const
EdgeInsets
.
fromLTRB
(
1
,
3
,
5
,
7
));
// Switch back to Child 1 padding should remain the same.
await
tester
.
tap
(
find
.
text
(
'Child 1'
));
await
tester
.
pumpAndSettle
();
await
verifyPadding
(
padding:
const
EdgeInsets
.
fromLTRB
(
1
,
3
,
5
,
7
));
});
testWidgets
(
'Tap changes toggle state'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
),
1
:
Text
(
'Child 2'
),
2
:
Text
(
'Child 3'
),
};
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
0
);
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
key:
const
ValueKey
<
String
>(
'Segmented Control'
),
children:
children
,
controller:
controller
,
),
),
);
expect
(
controller
.
value
,
0
);
await
tester
.
tap
(
find
.
text
(
'Child 2'
));
expect
(
controller
.
value
,
1
);
// Tapping the currently selected item should not change controller's value.
bool
valueChanged
=
false
;
controller
.
addListener
(()
{
valueChanged
=
true
;
});
await
tester
.
tap
(
find
.
text
(
'Child 2'
));
expect
(
valueChanged
,
isFalse
);
expect
(
controller
.
value
,
1
);
});
testWidgets
(
'Changing controller works'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
),
1
:
Text
(
'Child 2'
),
2
:
Text
(
'Child 3'
),
};
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
0
);
final
ValueNotifier
<
int
>
newControlelr
=
ValueNotifier
<
int
>(
null
);
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
key:
const
ValueKey
<
String
>(
'Segmented Control'
),
children:
children
,
controller:
controller
,
),
),
);
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
,
offsetMoreOrLessEquals
(
tester
.
getCenter
(
find
.
text
(
'Child 1'
))),
);
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
key:
const
ValueKey
<
String
>(
'Segmented Control'
),
children:
children
,
controller:
newControlelr
,
),
),
);
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
),
isNull
,
);
});
testWidgets
(
'Can change controller value in build method'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
),
1
:
Text
(
'Child 2'
),
2
:
Text
(
'Child 3'
),
};
int
currentIndex
=
0
;
StateSetter
setState
;
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
currentIndex
);
await
tester
.
pumpWidget
(
StatefulBuilder
(
builder:
(
BuildContext
context
,
StateSetter
setter
)
{
setState
=
setter
;
if
(
controller
.
value
!=
currentIndex
)
controller
.
value
=
currentIndex
;
return
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
key:
const
ValueKey
<
String
>(
'Segmented Control'
),
children:
children
,
controller:
controller
,
),
);
},
),
);
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
,
offsetMoreOrLessEquals
(
tester
.
getCenter
(
find
.
text
(
'Child 1'
))),
);
setState
(()
{
currentIndex
=
2
;
});
await
tester
.
pump
();
await
tester
.
pumpAndSettle
();
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
,
offsetMoreOrLessEquals
(
tester
.
getCenter
(
find
.
text
(
'Child 3'
)),
epsilon:
0.01
),
);
});
testWidgets
(
'Segmented controls respect theme'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
),
1
:
Icon
(
IconData
(
1
)),
};
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
0
);
await
tester
.
pumpWidget
(
CupertinoApp
(
theme:
const
CupertinoThemeData
(
brightness:
Brightness
.
dark
),
home:
StatefulBuilder
(
builder:
(
BuildContext
context
,
StateSetter
setState
)
{
return
CupertinoSlidingSegmentedControl
<
int
>(
children:
children
,
controller:
controller
,
);
},
),
),
);
DefaultTextStyle
textStyle
=
tester
.
widget
(
find
.
widgetWithText
(
DefaultTextStyle
,
'Child 1'
).
first
);
expect
(
textStyle
.
style
.
fontWeight
,
FontWeight
.
w600
);
await
tester
.
tap
(
find
.
byIcon
(
const
IconData
(
1
)));
await
tester
.
pump
();
await
tester
.
pumpAndSettle
();
textStyle
=
tester
.
widget
(
find
.
widgetWithText
(
DefaultTextStyle
,
'Child 1'
).
first
);
expect
(
textStyle
.
style
.
fontWeight
,
FontWeight
.
normal
);
},
);
testWidgets
(
'SegmentedControl dark mode'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
),
1
:
Icon
(
IconData
(
1
)),
};
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
0
);
Brightness
brightness
=
Brightness
.
light
;
StateSetter
setState
;
await
tester
.
pumpWidget
(
StatefulBuilder
(
builder:
(
BuildContext
context
,
StateSetter
setter
)
{
setState
=
setter
;
return
MediaQuery
(
data:
MediaQueryData
(
platformBrightness:
brightness
),
child:
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
children:
children
,
controller:
controller
,
thumbColor:
CupertinoColors
.
systemGreen
,
backgroundColor:
CupertinoColors
.
systemRed
,
),
),
);
},
),
);
final
BoxDecoration
decoration
=
tester
.
widget
<
Container
>(
find
.
descendant
(
of:
find
.
byType
(
UnconstrainedBox
),
matching:
find
.
byType
(
Container
),
)).
decoration
;
expect
(
getRenderSegmentedControl
(
tester
).
thumbColor
.
value
,
CupertinoColors
.
systemGreen
.
color
.
value
);
expect
(
decoration
.
color
.
value
,
CupertinoColors
.
systemRed
.
color
.
value
);
setState
(()
{
brightness
=
Brightness
.
dark
;
});
await
tester
.
pump
();
final
BoxDecoration
decorationDark
=
tester
.
widget
<
Container
>(
find
.
descendant
(
of:
find
.
byType
(
UnconstrainedBox
),
matching:
find
.
byType
(
Container
),
)).
decoration
;
expect
(
getRenderSegmentedControl
(
tester
).
thumbColor
.
value
,
CupertinoColors
.
systemGreen
.
darkColor
.
value
);
expect
(
decorationDark
.
color
.
value
,
CupertinoColors
.
systemRed
.
darkColor
.
value
);
});
testWidgets
(
'Children can be non-Text or Icon widgets (in this case, '
'a Container or Placeholder widget)'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
),
1
:
SizedBox
(
width:
50
,
height:
50
),
2
:
Placeholder
(),
};
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
0
);
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
children:
children
,
controller:
controller
,
),
),
);
},
);
testWidgets
(
'Passed in value is child initially selected'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
setupSimpleSegmentedControl
());
expect
(
getRenderSegmentedControl
(
tester
).
highlightedIndex
,
0
);
});
testWidgets
(
'Null input for value results in no child initially selected'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
),
1
:
Text
(
'Child 2'
),
};
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
null
);
await
tester
.
pumpWidget
(
StatefulBuilder
(
builder:
(
BuildContext
context
,
StateSetter
setState
)
{
return
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
children:
children
,
controller:
controller
,
),
);
},
),
);
expect
(
getRenderSegmentedControl
(
tester
).
highlightedIndex
,
null
);
});
testWidgets
(
'Long press not-selected child interactions'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
),
1
:
Text
(
'Child 2'
),
2
:
Text
(
'Child 3'
),
3
:
Text
(
'Child 4'
),
4
:
Text
(
'Child 5'
),
};
// Child 3 is intially selected.
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
2
);
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
children:
children
,
controller:
controller
,
),
),
);
double
getChildOpacityByName
(
String
childName
)
{
return
tester
.
widget
<
Opacity
>(
find
.
ancestor
(
matching:
find
.
byType
(
Opacity
),
of:
find
.
text
(
childName
)),
).
opacity
;
}
// Opacity 1 with no interaction.
expect
(
getChildOpacityByName
(
'Child 1'
),
1
);
final
Offset
center
=
tester
.
getCenter
(
find
.
text
(
'Child 1'
));
final
TestGesture
gesture
=
await
tester
.
startGesture
(
center
);
await
tester
.
pumpAndSettle
();
// Opacity drops to 0.2.
expect
(
getChildOpacityByName
(
'Child 1'
),
0.2
);
// Move down slightly, slightly outside of the segmented control.
await
gesture
.
moveBy
(
const
Offset
(
0
,
50
));
await
tester
.
pumpAndSettle
();
expect
(
getChildOpacityByName
(
'Child 1'
),
0.2
);
// Move further down and far away from the segmented control.
await
gesture
.
moveBy
(
const
Offset
(
0
,
200
));
await
tester
.
pumpAndSettle
();
expect
(
getChildOpacityByName
(
'Child 1'
),
1
);
// Move to child 5.
await
gesture
.
moveTo
(
tester
.
getCenter
(
find
.
text
(
'Child 5'
)));
await
tester
.
pumpAndSettle
();
expect
(
getChildOpacityByName
(
'Child 1'
),
1
);
expect
(
getChildOpacityByName
(
'Child 5'
),
0.2
);
// Move to child 2.
await
gesture
.
moveTo
(
tester
.
getCenter
(
find
.
text
(
'Child 2'
)));
await
tester
.
pumpAndSettle
();
expect
(
getChildOpacityByName
(
'Child 1'
),
1
);
expect
(
getChildOpacityByName
(
'Child 5'
),
1
);
expect
(
getChildOpacityByName
(
'Child 2'
),
0.2
);
});
testWidgets
(
'Long press does not change the opacity of currently-selected child'
,
(
WidgetTester
tester
)
async
{
double
getChildOpacityByName
(
String
childName
)
{
return
tester
.
widget
<
Opacity
>(
find
.
ancestor
(
matching:
find
.
byType
(
Opacity
),
of:
find
.
text
(
childName
)),
).
opacity
;
}
await
tester
.
pumpWidget
(
setupSimpleSegmentedControl
());
final
Offset
center
=
tester
.
getCenter
(
find
.
text
(
'Child 1'
));
await
tester
.
startGesture
(
center
);
await
tester
.
pump
();
await
tester
.
pumpAndSettle
();
expect
(
getChildOpacityByName
(
'Child 1'
),
1
);
});
testWidgets
(
'Height of segmented control is determined by tallest widget'
,
(
WidgetTester
tester
)
async
{
final
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Container
(
constraints:
const
BoxConstraints
.
tightFor
(
height:
100.0
)),
1
:
Container
(
constraints:
const
BoxConstraints
.
tightFor
(
height:
400.0
)),
2
:
Container
(
constraints:
const
BoxConstraints
.
tightFor
(
height:
200.0
)),
};
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
null
);
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
key:
const
ValueKey
<
String
>(
'Segmented Control'
),
children:
children
,
controller:
controller
,
),
),
);
final
RenderBox
buttonBox
=
tester
.
renderObject
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Segmented Control'
)),
);
expect
(
buttonBox
.
size
.
height
,
400.0
+
2
*
2
,
// 2 px padding on both sides.
);
});
testWidgets
(
'Width of each segmented control segment is determined by widest widget'
,
(
WidgetTester
tester
)
async
{
final
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Container
(
constraints:
const
BoxConstraints
.
tightFor
(
width:
50.0
)),
1
:
Container
(
constraints:
const
BoxConstraints
.
tightFor
(
width:
100.0
)),
2
:
Container
(
constraints:
const
BoxConstraints
.
tightFor
(
width:
200.0
)),
};
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
null
);
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
key:
const
ValueKey
<
String
>(
'Segmented Control'
),
children:
children
,
controller:
controller
,
),
),
);
final
RenderBox
segmentedControl
=
tester
.
renderObject
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Segmented Control'
)),
);
// Subtract the 8.0px for horizontal padding separator. Remaining width should be allocated
// to each child equally.
final
double
childWidth
=
(
segmentedControl
.
size
.
width
-
8
)
/
3
;
expect
(
childWidth
,
200.0
+
9.25
*
2
);
});
testWidgets
(
'Width is finite in unbounded space'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
SizedBox
(
width:
50
),
1
:
SizedBox
(
width:
70
),
};
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
null
);
await
tester
.
pumpWidget
(
boilerplate
(
child:
Row
(
children:
<
Widget
>[
CupertinoSlidingSegmentedControl
<
int
>(
key:
const
ValueKey
<
String
>(
'Segmented Control'
),
children:
children
,
controller:
controller
,
),
],
),
),
);
final
RenderBox
segmentedControl
=
tester
.
renderObject
(
find
.
byKey
(
const
ValueKey
<
String
>(
'Segmented Control'
)),
);
expect
(
segmentedControl
.
size
.
width
,
70
*
2
+
9.25
*
4
+
3
*
2
+
1
,
// 2 children + 4 child padding + 2 outer padding + 1 separator
);
});
testWidgets
(
'Directionality test - RTL should reverse order of widgets'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
),
1
:
Text
(
'Child 2'
),
};
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
null
);
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
rtl
,
child:
Center
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
children:
children
,
controller:
controller
,
),
),
),
);
expect
(
tester
.
getTopRight
(
find
.
text
(
'Child 1'
)).
dx
>
tester
.
getTopRight
(
find
.
text
(
'Child 2'
)).
dx
,
isTrue
);
});
testWidgets
(
'Correct initial selection and toggling behavior - RTL'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
),
1
:
Text
(
'Child 2'
),
};
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
0
);
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
rtl
,
child:
Center
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
children:
children
,
controller:
controller
,
),
),
),
);
// highlightedIndex is 1 instead of 0 because of RTL.
expect
(
getRenderSegmentedControl
(
tester
).
highlightedIndex
,
1
);
await
tester
.
tap
(
find
.
text
(
'Child 2'
));
await
tester
.
pump
();
expect
(
getRenderSegmentedControl
(
tester
).
highlightedIndex
,
0
);
await
tester
.
tap
(
find
.
text
(
'Child 2'
));
await
tester
.
pump
();
expect
(
getRenderSegmentedControl
(
tester
).
highlightedIndex
,
0
);
});
testWidgets
(
'Segmented control semantics'
,
(
WidgetTester
tester
)
async
{
final
SemanticsTester
semantics
=
SemanticsTester
(
tester
);
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
),
1
:
Text
(
'Child 2'
),
};
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
0
);
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Center
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
children:
children
,
controller:
controller
,
),
),
),
);
expect
(
semantics
,
hasSemantics
(
TestSemantics
.
root
(
children:
<
TestSemantics
>[
TestSemantics
.
rootChild
(
label:
'Child 1'
,
flags:
<
SemanticsFlag
>[
SemanticsFlag
.
isButton
,
SemanticsFlag
.
isInMutuallyExclusiveGroup
,
SemanticsFlag
.
isSelected
,
],
actions:
<
SemanticsAction
>[
SemanticsAction
.
tap
,
],
),
TestSemantics
.
rootChild
(
label:
'Child 2'
,
flags:
<
SemanticsFlag
>[
SemanticsFlag
.
isButton
,
SemanticsFlag
.
isInMutuallyExclusiveGroup
,
],
actions:
<
SemanticsAction
>[
SemanticsAction
.
tap
,
],
),
],
),
ignoreId:
true
,
ignoreRect:
true
,
ignoreTransform:
true
,
),
);
await
tester
.
tap
(
find
.
text
(
'Child 2'
));
await
tester
.
pump
();
expect
(
semantics
,
hasSemantics
(
TestSemantics
.
root
(
children:
<
TestSemantics
>[
TestSemantics
.
rootChild
(
label:
'Child 1'
,
flags:
<
SemanticsFlag
>[
SemanticsFlag
.
isButton
,
SemanticsFlag
.
isInMutuallyExclusiveGroup
,
],
actions:
<
SemanticsAction
>[
SemanticsAction
.
tap
,
],
),
TestSemantics
.
rootChild
(
label:
'Child 2'
,
flags:
<
SemanticsFlag
>[
SemanticsFlag
.
isButton
,
SemanticsFlag
.
isInMutuallyExclusiveGroup
,
SemanticsFlag
.
isSelected
,
],
actions:
<
SemanticsAction
>[
SemanticsAction
.
tap
,
],
),
],
),
ignoreId:
true
,
ignoreRect:
true
,
ignoreTransform:
true
,
));
semantics
.
dispose
();
});
testWidgets
(
'Non-centered taps work on smaller widgets'
,
(
WidgetTester
tester
)
async
{
final
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{};
children
[
0
]
=
const
Text
(
'Child 1'
);
children
[
1
]
=
const
SizedBox
();
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
0
);
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
key:
const
ValueKey
<
String
>(
'Segmented Control'
),
children:
children
,
controller:
controller
,
),
),
);
expect
(
controller
.
value
,
0
);
final
Offset
centerOfTwo
=
tester
.
getCenter
(
find
.
byWidget
(
children
[
1
]));
// Tap just inside segment bounds
await
tester
.
tapAt
(
centerOfTwo
+
const
Offset
(
10
,
0
));
expect
(
controller
.
value
,
1
);
});
testWidgets
(
'Thumb animation is correct when the selected segment changes'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
setupSimpleSegmentedControl
());
final
Rect
initialRect
=
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
);
expect
(
currentThumbScale
(
tester
),
1
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
text
(
'Child 2'
)));
await
tester
.
pump
();
// Does not move until tapUp.
expect
(
currentThumbScale
(
tester
),
1
);
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
),
initialRect
);
// Tap up and the sliding animation should play.
await
gesture
.
up
();
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
10
));
expect
(
currentThumbScale
(
tester
),
1
);
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
.
dx
,
greaterThan
(
initialRect
.
center
.
dx
),
);
await
tester
.
pumpAndSettle
();
expect
(
currentThumbScale
(
tester
),
1
);
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
,
// We're using a critically damped spring so the value of the animation
// controller will never reach 1.
offsetMoreOrLessEquals
(
tester
.
getCenter
(
find
.
text
(
'Child 2'
)),
epsilon:
0.01
),
);
// Press the currently selected widget.
await
gesture
.
down
(
tester
.
getCenter
(
find
.
text
(
'Child 2'
)));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
10
));
// The thumb shrinks but does not moves towards left.
expect
(
currentThumbScale
(
tester
),
lessThan
(
1
));
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
,
offsetMoreOrLessEquals
(
tester
.
getCenter
(
find
.
text
(
'Child 2'
)),
epsilon:
0.01
),
);
await
tester
.
pumpAndSettle
();
expect
(
currentThumbScale
(
tester
),
moreOrLessEquals
(
0.95
,
epsilon:
0.01
));
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
,
offsetMoreOrLessEquals
(
tester
.
getCenter
(
find
.
text
(
'Child 2'
)),
epsilon:
0.01
),
);
// Drag to Child 1.
await
gesture
.
moveTo
(
tester
.
getCenter
(
find
.
text
(
'Child 1'
)));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
10
));
// Moved slightly to the left
expect
(
currentThumbScale
(
tester
),
moreOrLessEquals
(
0.95
,
epsilon:
0.01
));
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
.
dx
,
lessThan
(
tester
.
getCenter
(
find
.
text
(
'Child 2'
)).
dx
),
);
await
tester
.
pumpAndSettle
();
expect
(
currentThumbScale
(
tester
),
moreOrLessEquals
(
0.95
,
epsilon:
0.01
));
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
,
offsetMoreOrLessEquals
(
tester
.
getCenter
(
find
.
text
(
'Child 1'
)),
epsilon:
0.01
),
);
await
gesture
.
up
();
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
10
));
expect
(
currentThumbScale
(
tester
),
greaterThan
(
0.95
));
await
tester
.
pumpAndSettle
();
expect
(
currentThumbScale
(
tester
),
moreOrLessEquals
(
1
,
epsilon:
0.01
));
});
testWidgets
(
'Transition is triggered while a transition is already occurring'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'A'
),
1
:
Text
(
'B'
),
2
:
Text
(
'C'
),
};
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
0
);
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
key:
const
ValueKey
<
String
>(
'Segmented Control'
),
children:
children
,
controller:
controller
,
),
),
);
await
tester
.
tap
(
find
.
text
(
'B'
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
40
));
// Between A and B.
final
Rect
initialThumbRect
=
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
);
expect
(
initialThumbRect
.
center
.
dx
,
greaterThan
(
tester
.
getCenter
(
find
.
text
(
'A'
)).
dx
));
expect
(
initialThumbRect
.
center
.
dx
,
lessThan
(
tester
.
getCenter
(
find
.
text
(
'B'
)).
dx
));
// While A to B transition is occurring, press on C.
await
tester
.
tap
(
find
.
text
(
'C'
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
40
));
final
Rect
secondThumbRect
=
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
);
// Between the initial Rect and B.
expect
(
secondThumbRect
.
center
.
dx
,
greaterThan
(
initialThumbRect
.
center
.
dx
));
expect
(
secondThumbRect
.
center
.
dx
,
lessThan
(
tester
.
getCenter
(
find
.
text
(
'B'
)).
dx
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
// Eventually moves to C.
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
,
offsetMoreOrLessEquals
(
tester
.
getCenter
(
find
.
text
(
'C'
)),
epsilon:
0.01
),
);
});
testWidgets
(
'Insert segment while animation is running'
,
(
WidgetTester
tester
)
async
{
final
Map
<
int
,
Widget
>
children
=
SplayTreeMap
<
int
,
Widget
>((
int
a
,
int
b
)
=>
a
-
b
);
children
[
0
]
=
const
Text
(
'A'
);
children
[
2
]
=
const
Text
(
'C'
);
children
[
3
]
=
const
Text
(
'D'
);
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
0
);
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
key:
const
ValueKey
<
String
>(
'Segmented Control'
),
children:
children
,
controller:
controller
,
),
),
);
await
tester
.
tap
(
find
.
text
(
'D'
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
40
));
children
[
1
]
=
const
Text
(
'B'
);
await
tester
.
pumpWidget
(
boilerplate
(
child:
CupertinoSlidingSegmentedControl
<
int
>(
key:
const
ValueKey
<
String
>(
'Segmented Control'
),
children:
children
,
controller:
controller
,
),
),
);
await
tester
.
pumpAndSettle
();
// Eventually moves to D.
expect
(
currentUnscaledThumbRect
(
tester
,
useGlobalCoordinate:
true
).
center
,
offsetMoreOrLessEquals
(
tester
.
getCenter
(
find
.
text
(
'D'
)),
epsilon:
0.01
),
);
});
testWidgets
(
'ScrollView + SlidingSegmentedControl interaction'
,
(
WidgetTester
tester
)
async
{
const
Map
<
int
,
Widget
>
children
=
<
int
,
Widget
>{
0
:
Text
(
'Child 1'
),
1
:
Text
(
'Child 2'
),
};
final
ValueNotifier
<
int
>
controller
=
ValueNotifier
<
int
>(
0
);
final
ScrollController
scrollController
=
ScrollController
();
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
ListView
(
controller:
scrollController
,
children:
<
Widget
>[
const
SizedBox
(
height:
100
),
CupertinoSlidingSegmentedControl
<
int
>(
children:
children
,
controller:
controller
,
),
const
SizedBox
(
height:
1000
),
],
),
),
);
// Tapping still works.
await
tester
.
tap
(
find
.
text
(
'Child 2'
));
await
tester
.
pump
();
expect
(
controller
.
value
,
1
);
// Vertical drag works for the scroll view.
final
TestGesture
gesture
=
await
tester
.
startGesture
(
tester
.
getCenter
(
find
.
text
(
'Child 1'
)));
// The first moveBy doesn't actually move the scrollable. It's there to make
// sure VerticalDragGestureRecognizer wins the arena. This is due to
// startBehavior being set to DragStartBehavior.start.
await
gesture
.
moveBy
(
const
Offset
(
0
,
-
100
));
await
gesture
.
moveBy
(
const
Offset
(
0
,
-
100
));
await
tester
.
pump
();
expect
(
scrollController
.
offset
,
100
);
// Does not affect the segmented control.
expect
(
controller
.
value
,
1
);
await
gesture
.
moveBy
(
const
Offset
(
0
,
100
));
await
gesture
.
up
();
await
tester
.
pump
();
expect
(
scrollController
.
offset
,
0
);
expect
(
controller
.
value
,
1
);
// Long press vertical drag is recognized by the segmented control.
await
gesture
.
down
(
tester
.
getCenter
(
find
.
text
(
'Child 1'
)));
await
tester
.
pump
(
const
Duration
(
milliseconds:
600
));
await
gesture
.
moveBy
(
const
Offset
(
0
,
-
100
));
await
gesture
.
moveBy
(
const
Offset
(
0
,
-
100
));
await
tester
.
pump
();
// Should not scroll.
expect
(
scrollController
.
offset
,
0
);
expect
(
controller
.
value
,
1
);
await
gesture
.
moveBy
(
const
Offset
(
0
,
100
));
await
gesture
.
moveBy
(
const
Offset
(
0
,
100
));
await
gesture
.
up
();
await
tester
.
pump
();
expect
(
scrollController
.
offset
,
0
);
expect
(
controller
.
value
,
0
);
// Horizontal drag is recognized by the segmentedControl.
await
gesture
.
down
(
tester
.
getCenter
(
find
.
text
(
'Child 1'
)));
await
gesture
.
moveBy
(
const
Offset
(
50
,
0
));
await
gesture
.
moveTo
(
tester
.
getCenter
(
find
.
text
(
'Child 2'
)));
await
gesture
.
up
();
await
tester
.
pump
();
expect
(
scrollController
.
offset
,
0
);
expect
(
controller
.
value
,
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