Skip to content
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 is able to have proportional layout based on segment content #153125

Merged
merged 35 commits into from
Aug 29, 2024

Conversation

QuncCccccc
Copy link
Contributor

@QuncCccccc QuncCccccc commented Aug 8, 2024

This PR is to add isProportionalSegment property to CupertinoSlidingSegmentedControl. Originally, the width of each segment is determined by the widest segment and they have equal width. With this property setting to true, the width of each segment is determined by their own contents.

If the max width from parent constraints is smaller than the width that the segmented control needed, each of the segment width will be scaled down; if the min width from parent constraints is larger than the width that the segmented control needed, each of the segment width will be scaled up.

Screenshot 2024-08-15 at 12 10 44 PM

related to: #43826

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I signed the [CLA].
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is [test-exempt].
  • I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
  • All existing and new tests are passing.

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. f: cupertino flutter/packages/flutter/cupertino repository labels Aug 8, 2024
@QuncCccccc QuncCccccc changed the title CupertinoSlidingSegmentedControl is able to have proportional segment based on segment content CupertinoSlidingSegmentedControl is able to have proportional layout based on segment content Aug 15, 2024
/// up to meet the min width requirement.
///
/// Defaults to false.
final bool isProportionalSegment;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking for some suggestions for this property name:)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a similar name as UISegmentedControl?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or evenWidth

@QuncCccccc QuncCccccc marked this pull request as ready for review August 15, 2024 19:55
@@ -395,6 +397,20 @@ class CupertinoSlidingSegmentedControl<T extends Object> extends StatefulWidget
/// will not be painted if null is specified.
final Color backgroundColor;

/// Determine whether segments have equal widths or proportional widths based
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the first sentence concise.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated! Also I updated the property name! evenWidth is a lot better than the original one but I just feel even width is already the default behavior. To provide more information in the name, I changed evenWidth to proportionalWidth:)Just wanted to give a little more information in name. But please let me know if anything doesn't make sense!

/// up to meet the min width requirement.
///
/// Defaults to false.
final bool isProportionalSegment;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a similar name as UISegmentedControl?

@@ -7,6 +7,7 @@ library;

import 'dart:math' as math;

import 'package:collection/collection.dart';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to remove the dependency on collection?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I asked Michael about this, it seems not decided yet, and should be okay to use it if I only use .sum here which is easy to migrate, so I just kept it here. But I can also remove this library if you prefer not to include it:)!

/// up to meet the min width requirement.
///
/// Defaults to false.
final bool isProportionalSegment;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or evenWidth

T segmentForXPosition(double dx) {
final RenderBox renderBox = context.findRenderObject()! as RenderBox;
final int numOfChildren = widget.children.length;
assert(renderBox.hasSize);
assert(numOfChildren >= 2);
if (widget.isProportionalSegment) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically hit testing. If you have access to the render object, maybe move the logic to the render object where the children's dimensions are easily accessible, so you don't need the global keys.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this part in _RenderSegmentedControl!

return;
}
_isProportionalSegment = value;
markNeedsPaint();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used in performLayout so this needs relayout (consider adding a test).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a unit test:)

final double allowedMinWidth = constraints.minWidth - totalSeparatorWidth;
if (totalWidth < allowedMinWidth) {
final double scale = allowedMinWidth / totalWidth;
childWidths = childWidths.map<double>((double width) => width * scale).toList();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider mutating in place.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also consider merging the scaling here with the scaling logic above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry what do you mean by merging the logic of scaling?

}

List<double> _getChildIntrinsicWidths(BoxConstraints constraints) {
List<double> childWidths = <double>[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation.

}

Size _computeOverallSizeFromChildSize(Size childSize, BoxConstraints constraints) {
Size _computeOverallSizeFromChildSize(BoxConstraints constraints) {
final double maxChildHeight = _getMaxChildHeight(constraints, double.infinity);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if it makes sense to assume that you could use infinite here for maxHeight calculation. Could you add a code comment for the assumption?

Copy link
Contributor Author

@QuncCccccc QuncCccccc Aug 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or should I replace this with constraints.maxWidth?

RenderBox? child = firstChild;
int index = 0;
double start = 0;
while (child != null) {
final BoxConstraints childConstraints = BoxConstraints.tight(Size(childWidths[index ~/ 2], childHeight));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

every child should have the same height I think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved seperator constraints out of the while loop!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh the separators. Nvm.

@@ -422,10 +438,12 @@ class _SegmentedControlState<T extends Object> extends State<CupertinoSlidingSeg
final TapGestureRecognizer tap = TapGestureRecognizer();
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer();
GlobalKey segmentedControlRenderWidgetKey = GlobalKey();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

final BuildContext currentContext = segmentedControlRenderWidgetKey.currentContext!;
final _RenderSegmentedControl<T> renderBox = currentContext.findRenderObject()! as _RenderSegmentedControl<T>;

if (widget.proportionalWidth) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should be able to ask the render object regardless of whether proportionalWidth is set?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the original implementation always returns an index in [0, leng - 1). getSegmentIndex should probably be renamed to getClosestSegmentIndex and always return a valid index?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uber nit: the RTL reordering happens at the widget level so consider doing the RTL thing in the state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated based on the comments:)

int? getSegmentIndex(double dx, TextDirection textDirection) {
double subtotalWidth = 0;
for (int i = 0; i < childWidths.length; i++) {
final double segmentWidth = childWidths[i];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are separators in between the children?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah childWidths only contains the segment widths. This is a bad name. Renamed it to segmentWidths!
https://github.com/flutter/flutter/pull/153125/files#diff-aba8be2635c1c4a4ee9c15b8c0bdb7ba4cb22a4f795d4e98e5cebda3fc10666eR1025

}

Size _computeOverallSizeFromChildSize(Size childSize, BoxConstraints constraints) {
List<double> _getChildIntrinsicWidths(BoxConstraints constraints) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider combining the call paths for the !proportionalWidth case into this method, so you only have to handle proportionalWidth once.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Thanks:)

// each segment width should scale down until the overall size can fit in
// the parent constraints.
final double allowedMaxWidth = constraints.maxWidth - totalSeparatorWidth;
if (totalWidth > allowedMaxWidth) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can combine the 2 if cases to compute the scale only once

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final scale = doubleClamp(x, min, max) / x, and check if scale is 1.0?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! It's cleaner. Thanks for the suggestion!

return childWidths;
}

Size _computeOverallSizeFromChildSize(BoxConstraints constraints) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to take a child size any more?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed!

Copy link
Contributor

@LongCatIsLooong LongCatIsLooong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (if _calculateChildSize is not used anywhere).


double get totalSeparatorWidth => (_kSeparatorInset.horizontal + _kSeparatorWidth) * (childCount ~/ 2);
List<double> segmentWidths = <double>[];
int getClosestSegmentIndex(double dx) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: In the render object you have access to each child's paint offset and size. I think that would be a bit easier to do hit-testing with? It's finding the smallest dx.clamp(childOffset.dx, childOffset.dx.width) - dx

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated! Could you take another look? :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if dx is smaller than the smallest child offset, this method should return 0 instead of length - 1?

@@ -924,30 +983,73 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
}

Size _calculateChildSize(BoxConstraints constraints) {
final double childWidth = _getMaxChildWidth(constraints);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the method still needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed _calculateChildSize and updated computeDryBaseline method:)

engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 31, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 31, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 1, 2024
Buchimi pushed a commit to Buchimi/flutter that referenced this pull request Sep 2, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 3, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 3, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 3, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 3, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 3, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 3, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 4, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 4, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 4, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 4, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 4, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 4, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 5, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 5, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 5, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 5, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
f: cupertino flutter/packages/flutter/cupertino repository framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants