-
Notifications
You must be signed in to change notification settings - Fork 29.8k
CupertinoSlidingSegmentedControl #42775
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CupertinoSlidingSegmentedControl #42775
Conversation
38c79cf to
ec9e6f4
Compare
justinmc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for tackling this!
There are a few questions in my comments below that I wanted to clear up, and a few nitpicks, otherwise looks good.
Does this support dark mode by the way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could there be any problem here with HashMap being unordered? See https://master-api.flutter.dev/flutter/dart-core/Map-class.html. LinkedHashMap is ordered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I copied these from CupertinoSegmentedControl 😅 . The child widgets will be visually placed in the same order as they appear in the Map, if the Map is unordered then the children may appear in a different order every time the widget rebuilds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a disclaimer like I see elsewhere: The [children] argument must be an ordered [Map] such as a [LinkedHashMap]. I feel like it will be easier for people to end up with the order changing on them without understanding why.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be incomplete.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this a stateful widget with the children defined as a variable even though they never change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just for my own curiosity going forward, what is our plan for parameters like this versus theming?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember I had the same question and I vaguely remember Hans' answer was themes are complicated so we want to make a careful decision. @HansMuller did I just put words in your mouth?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CupertinoTheme is pretty spare and I think that's a good thing. Providing widget parameters for a few visual customizations is also a good thing. Our primary goal is to look native by default. Enabling the developer to make sweeping changes to component types with the CupertinoTheme isn't really a goal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mentioned in another comment up above that there might be a concern about ordered vs. unordered Maps. Maybe just say the same thing up there as this says here, if needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean to mention such as a [LinkedHashMap]?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah the part about The [children] argument must be an ordered [Map] such as a [LinkedHashMap].. I'm responding on that comment above (#42775 (comment)).
| /// See also: | ||
| /// | ||
| /// * <https://developer.apple.com/design/human-interface-guidelines/ios/controls/segmented-controls/> | ||
| class CupertinoSlidingSegmentedControl<T> extends StatefulWidget { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the reason for keeping the old CupertinoSegmentedControl around and making this a new widget? Can you use both on native?
Actually though, I see that someone is requesting to still be able to use the old one: #33796 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming that we are keeping both, is there any code that can be shared between the two, or are there tests that could cover both? I didn't look very closely, but just checking that you have tried to avoid code duplication as much as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason for keeping both is the API of CupertinoSlidingSegmentedControl is drastically different from CupertinoSegmentedControl. It's probably too big of a breaking change (and breaks every existing CupertinoSegmentedControl) to introduce in one go. Introducing it as a new widget will allow people to migrate as they feel comfortable. But yeah we will have to maintain both widgets with duplicated code for the time being now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see, makes sense. Is it worth adding a @deprecated flag to CupertinoSegmentedControl now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO for now it doesn't hurt for them to coexist, as the comment you mentioned requested.
| } | ||
|
|
||
| // Play highlight animation for the child located at _highlightControllers[at]. | ||
| void animateHighlightController({ T at, bool forward }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this and maybe a few other methods could be private.
I guess it doesn't make much difference for instances of state, but just something I noticed.
| // 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 CABasciaAnimation that shrinks the thumb to 95% of its |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo, "CABasciaAnimation" => "CABasicAnimation"
| // 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 CABasciaAnimation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo on CABasciaAnimation again.
|
|
||
| await tester.tap(find.byIcon(const IconData(1))); | ||
| await tester.pump(); | ||
| await tester.pump(const Duration(milliseconds: 500)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does pumpAndSettle not work?
@override
Widget build(BuildContext context) {
super.build(context);
return CupertinoPageScaffold(
child: CustomScrollView(slivers: <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text("Test"),
middle: CupertinoSlidingSegmentedControl<int>(
children: {
0: Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 6),
child: Text(
"Test1",
style: TextStyle(fontSize: 14),
),
),
1: Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 6),
child: Text("Test2", style: TextStyle(fontSize: 14)),
),
2: Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 6),
child: Text("Test3", style: TextStyle(fontSize: 14)),
),
},
controller: ValueNotifier<int>(0),
),
),
SliverFillRemaining(
child: Container(),
)
]),
);
}onTap is not called if I use this widget with a Sliver widget inside CustomScrollView |
justinmc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM assuming @njovy's use case works.
Sorry if I spam you with mismatched comments and reviews, I'm failing at using Github right now...
439eb28 to
f82338d
Compare
|
@njovy Thank you so much for trying out the PR and catching the bug! I've updated the gesture handling so it works (seemingly) correctly in conjunction with a @justinmc thanks for the review! Updated per your comments, I hope I didn't miss anything. (Turns out I did miss the dark mode comment. Yes it has dark mode support). |
f82338d to
e4bdd9f
Compare
|
@LongCatIsLooong It works like a charm with the latest commit. Thank you so much! |
|
|
||
| // The spring animation used when the thumb changes its rect. | ||
| final SpringSimulation _kThumbSpringAnimationSimulation = SpringSimulation( | ||
| const SpringDescription(mass: 1, stiffness: 503.551, damping: 44.8799), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The stiffness and damping values seem oddly specific. What are they based one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got those values from the native UISegmentedControl.
| /// 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 [controller]'s |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[controller]'s => the [controller]'s
| /// 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 keys used to identify each |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of "keys" here might confuse developers who think we're referring to Key (it confused me). Even though the point is immediately clarified in the following text. Maybe say "[Map] keys" and just "map keys" after that.
Also: it would be helpful to explain, at the outset, that the map's key are segmented control's values.
| /// 2: Text('Child 3'), | ||
| /// }; | ||
| /// | ||
| /// final ValueNotifier<int> controller = ValueNotifier<int>(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might include a comment here like:
// null means no selectionThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CupertinoTheme is pretty spare and I think that's a good thing. Providing widget parameters for a few visual customizations is also a good thing. Our primary goal is to look native by default. Enabling the developer to make sweeping changes to component types with the CupertinoTheme isn't really a goal.
| @override | ||
| double computeMinIntrinsicWidth(double height) { | ||
| RenderBox child = firstChild; | ||
| double minWidth = 0.0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really maxMinChildWidth or widestMinChildWidth
| double minWidth = 0.0; | ||
| while (child != null) { | ||
| final _SegmentedControlContainerBoxParentData childParentData = child.parentData; | ||
| final double childWidth = child.getMinIntrinsicWidth(height) + 2 * _kSegmentMinPadding; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could just factor the _kSegmentMinPadding in at the end? Like:
return childCount * (widestMinChildWidth + 2 * _kSegmentMinPadding) + totalSeparatorWidth;| maxWidth = math.max(maxWidth, childWidth); | ||
| child = childParentData.nextSibling; | ||
| } | ||
| return maxWidth * childCount + totalSeparatorWidth; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar comments about var name and etc as above.
|
|
||
| @override | ||
| double computeMinIntrinsicHeight(double width) { | ||
| RenderBox child = firstChild; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be:
return getChildrenAsList.fold<double>(0, (double minHeight, RenderBox child) {
return math.max(minHeight, child.getMinIntrinsicHeight(width));
});Here and for computeMaxIntrinsicHeight().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getChildrenAsList's documentation says
/// This function is useful when you need random-access to the children of
/// this render object. If you're accessing the children in order, consider
/// walking the child list directly.
I think the imperative way is slightly more efficient?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the efficiency aspect will matter, but there's no harm in leaving this code as it is. It's pretty easy to read.
| if (highlightedIndex != null) { | ||
| if (_childAnimations == null) { | ||
| _childAnimations = <RenderBox, _ChildAnimationManifest> { }; | ||
| for (int i = 0; i < childCount - 1; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use i += 1, here and elsewhere.
HansMuller
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
| expect(e.toString(), contains('children')); | ||
| } | ||
|
|
||
| final Map<int, Widget> children = <int, Widget>{}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW you could write:
const Map<int, Widget> children = <int, Widget>{1: Text('Child 1'), 2: Text('Child 2')};Here and below.
| 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)); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra blank line
|
|
||
| final Map<int, Widget> children = <int, Widget>{}; | ||
| children[0] = const SizedBox( | ||
| height: double.infinity, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe include a comment about using height=infinity to ensure that the child consumes all of the available vertical space, like SizedBox.expand() does for both dimensions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
turns out I didn't need the SizedBox since I'm checking the Rect of the Opacity widget
| (WidgetTester tester) async { | ||
| final Map<int, Widget> children = <int, Widget>{}; | ||
| children[0] = const Text('Child 1'); | ||
| children[1] = Container( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
children[1] = SizedBox(width: 50, height: 50);| find.byKey(const ValueKey<String>('Segmented Control')), | ||
| ); | ||
|
|
||
| expect(segmentedControl.size.width.isFinite, isTrue); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't we check the actual width of the segmented control here? Could simplify the expected width arithmetic by making the children SizedBox(width: 100) or something similar.
|
|
||
| // 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation
| testWidgets('Insert segment while animation is running', (WidgetTester tester) async { | ||
| final Map<int, Widget> children = SplayTreeMap<int, Widget>( | ||
| (int a, int b) => a - b, | ||
| (dynamic key) => true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this parameter needed?
Description
Introduce
CupertinoSlidingSegmentedControl, the iOS 13CupertinoSegmentedControl.Related Issues
Closes #33796
Tests
TODOs
momentaryis missing: https://developer.apple.com/documentation/uikit/uisegmentedcontrol/1618586-ismomentaryequalWidthlayout.proportionalis missing.CupertinoMultiChildGestureDetectoror something, as this kind of gesture is quite common on iOS.Checklist
Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (
[x]). This will ensure a smooth and quick review process.///).flutter analyze --flutter-repo) does not report any problems on my PR.Breaking Change
Does your PR require Flutter developers to manually update their apps to accommodate your change?