SegmentedButton should not create new MaterialStatesController in every build. (#133949)
This commit is contained in:
parent
85bece2689
commit
cb0a613ec6
@ -96,7 +96,7 @@ class ButtonSegment<T> {
|
||||
/// [ToggleButtons].
|
||||
/// * [Radio], an alternative way to present the user with a mutually exclusive set of options.
|
||||
/// * [FilterChip], [ChoiceChip], which can be used when you need to show more than five options.
|
||||
class SegmentedButton<T> extends StatelessWidget {
|
||||
class SegmentedButton<T> extends StatefulWidget {
|
||||
/// Creates a const [SegmentedButton].
|
||||
///
|
||||
/// [segments] must contain at least one segment, but it is recommended
|
||||
@ -235,27 +235,33 @@ class SegmentedButton<T> extends StatelessWidget {
|
||||
/// Defaults to an [Icon] with [Icons.check].
|
||||
final Widget? selectedIcon;
|
||||
|
||||
bool get _enabled => onSelectionChanged != null;
|
||||
@override
|
||||
State<SegmentedButton<T>> createState() => _SegmentedButtonState<T>();
|
||||
}
|
||||
|
||||
class _SegmentedButtonState<T> extends State<SegmentedButton<T>> {
|
||||
bool get _enabled => widget.onSelectionChanged != null;
|
||||
final Map<ButtonSegment<T>, MaterialStatesController> _statesControllers = <ButtonSegment<T>, MaterialStatesController>{};
|
||||
|
||||
void _handleOnPressed(T segmentValue) {
|
||||
if (!_enabled) {
|
||||
return;
|
||||
}
|
||||
final bool onlySelectedSegment = selected.length == 1 && selected.contains(segmentValue);
|
||||
final bool validChange = emptySelectionAllowed || !onlySelectedSegment;
|
||||
final bool onlySelectedSegment = widget.selected.length == 1 && widget.selected.contains(segmentValue);
|
||||
final bool validChange = widget.emptySelectionAllowed || !onlySelectedSegment;
|
||||
if (validChange) {
|
||||
final bool toggle = multiSelectionEnabled || (emptySelectionAllowed && onlySelectedSegment);
|
||||
final bool toggle = widget.multiSelectionEnabled || (widget.emptySelectionAllowed && onlySelectedSegment);
|
||||
final Set<T> pressedSegment = <T>{segmentValue};
|
||||
late final Set<T> updatedSelection;
|
||||
if (toggle) {
|
||||
updatedSelection = selected.contains(segmentValue)
|
||||
? selected.difference(pressedSegment)
|
||||
: selected.union(pressedSegment);
|
||||
updatedSelection = widget.selected.contains(segmentValue)
|
||||
? widget.selected.difference(pressedSegment)
|
||||
: widget.selected.union(pressedSegment);
|
||||
} else {
|
||||
updatedSelection = pressedSegment;
|
||||
}
|
||||
if (!setEquals(updatedSelection, selected)) {
|
||||
onSelectionChanged!(updatedSelection);
|
||||
if (!setEquals(updatedSelection, widget.selected)) {
|
||||
widget.onSelectionChanged!(updatedSelection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -271,7 +277,7 @@ class SegmentedButton<T> extends StatelessWidget {
|
||||
final Set<MaterialState> currentState = _enabled ? enabledState : disabledState;
|
||||
|
||||
P? effectiveValue<P>(P? Function(ButtonStyle? style) getProperty) {
|
||||
late final P? widgetValue = getProperty(style);
|
||||
late final P? widgetValue = getProperty(widget.style);
|
||||
late final P? themeValue = getProperty(theme.style);
|
||||
late final P? defaultValue = getProperty(defaults.style);
|
||||
return widgetValue ?? themeValue ?? defaultValue;
|
||||
@ -305,25 +311,24 @@ class SegmentedButton<T> extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
final ButtonStyle segmentStyle = segmentStyleFor(style);
|
||||
final ButtonStyle segmentStyle = segmentStyleFor(widget.style);
|
||||
final ButtonStyle segmentThemeStyle = segmentStyleFor(theme.style).merge(segmentStyleFor(defaults.style));
|
||||
final Widget? selectedIcon = showSelectedIcon
|
||||
? this.selectedIcon ?? theme.selectedIcon ?? defaults.selectedIcon
|
||||
final Widget? selectedIcon = widget.showSelectedIcon
|
||||
? widget.selectedIcon ?? theme.selectedIcon ?? defaults.selectedIcon
|
||||
: null;
|
||||
|
||||
Widget buttonFor(ButtonSegment<T> segment) {
|
||||
final Widget label = segment.label ?? segment.icon ?? const SizedBox.shrink();
|
||||
final bool segmentSelected = selected.contains(segment.value);
|
||||
final Widget? icon = (segmentSelected && showSelectedIcon)
|
||||
final bool segmentSelected = widget.selected.contains(segment.value);
|
||||
final Widget? icon = (segmentSelected && widget.showSelectedIcon)
|
||||
? selectedIcon
|
||||
: segment.label != null
|
||||
? segment.icon
|
||||
: null;
|
||||
final MaterialStatesController controller = MaterialStatesController(
|
||||
<MaterialState>{
|
||||
final MaterialStatesController controller = _statesControllers.putIfAbsent(segment, () => MaterialStatesController());
|
||||
controller.value = <MaterialState>{
|
||||
if (segmentSelected) MaterialState.selected,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
final Widget button = icon != null
|
||||
? TextButton.icon(
|
||||
@ -350,7 +355,7 @@ class SegmentedButton<T> extends StatelessWidget {
|
||||
return MergeSemantics(
|
||||
child: Semantics(
|
||||
checked: segmentSelected,
|
||||
inMutuallyExclusiveGroup: multiSelectionEnabled ? null : true,
|
||||
inMutuallyExclusiveGroup: widget.multiSelectionEnabled ? null : true,
|
||||
child: buttonWithTooltip,
|
||||
),
|
||||
);
|
||||
@ -363,7 +368,7 @@ class SegmentedButton<T> extends StatelessWidget {
|
||||
final OutlinedBorder enabledBorder = resolvedEnabledBorder.copyWith(side: enabledSide);
|
||||
final OutlinedBorder disabledBorder = resolvedDisabledBorder.copyWith(side: disabledSide);
|
||||
|
||||
final List<Widget> buttons = segments.map(buttonFor).toList();
|
||||
final List<Widget> buttons = widget.segments.map(buttonFor).toList();
|
||||
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
@ -374,7 +379,7 @@ class SegmentedButton<T> extends StatelessWidget {
|
||||
child: TextButtonTheme(
|
||||
data: TextButtonThemeData(style: segmentThemeStyle),
|
||||
child: _SegmentedButtonRenderWidget<T>(
|
||||
segments: segments,
|
||||
segments: widget.segments,
|
||||
enabledBorder: _enabled ? enabledBorder : disabledBorder,
|
||||
disabledBorder: disabledBorder,
|
||||
direction: direction,
|
||||
@ -383,6 +388,14 @@ class SegmentedButton<T> extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final MaterialStatesController controller in _statesControllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
|
||||
const _SegmentedButtonRenderWidget({
|
||||
|
@ -9,6 +9,7 @@ import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
||||
|
||||
import '../widgets/semantics_tester.dart';
|
||||
|
||||
@ -21,7 +22,9 @@ Widget boilerplate({required Widget child}) {
|
||||
|
||||
void main() {
|
||||
|
||||
testWidgets('SegmentedButton is built with Material of type MaterialType.transparency', (WidgetTester tester) async {
|
||||
testWidgetsWithLeakTracking('SegmentedButton is built with Material of type MaterialType.transparency',
|
||||
leakTrackingTestConfig: LeakTrackingTestConfig.debugNotDisposed(),
|
||||
(WidgetTester tester) async {
|
||||
final ThemeData theme = ThemeData(useMaterial3: true);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
|
Loading…
x
Reference in New Issue
Block a user