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].
|
/// [ToggleButtons].
|
||||||
/// * [Radio], an alternative way to present the user with a mutually exclusive set of options.
|
/// * [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.
|
/// * [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].
|
/// Creates a const [SegmentedButton].
|
||||||
///
|
///
|
||||||
/// [segments] must contain at least one segment, but it is recommended
|
/// [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].
|
/// Defaults to an [Icon] with [Icons.check].
|
||||||
final Widget? selectedIcon;
|
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) {
|
void _handleOnPressed(T segmentValue) {
|
||||||
if (!_enabled) {
|
if (!_enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final bool onlySelectedSegment = selected.length == 1 && selected.contains(segmentValue);
|
final bool onlySelectedSegment = widget.selected.length == 1 && widget.selected.contains(segmentValue);
|
||||||
final bool validChange = emptySelectionAllowed || !onlySelectedSegment;
|
final bool validChange = widget.emptySelectionAllowed || !onlySelectedSegment;
|
||||||
if (validChange) {
|
if (validChange) {
|
||||||
final bool toggle = multiSelectionEnabled || (emptySelectionAllowed && onlySelectedSegment);
|
final bool toggle = widget.multiSelectionEnabled || (widget.emptySelectionAllowed && onlySelectedSegment);
|
||||||
final Set<T> pressedSegment = <T>{segmentValue};
|
final Set<T> pressedSegment = <T>{segmentValue};
|
||||||
late final Set<T> updatedSelection;
|
late final Set<T> updatedSelection;
|
||||||
if (toggle) {
|
if (toggle) {
|
||||||
updatedSelection = selected.contains(segmentValue)
|
updatedSelection = widget.selected.contains(segmentValue)
|
||||||
? selected.difference(pressedSegment)
|
? widget.selected.difference(pressedSegment)
|
||||||
: selected.union(pressedSegment);
|
: widget.selected.union(pressedSegment);
|
||||||
} else {
|
} else {
|
||||||
updatedSelection = pressedSegment;
|
updatedSelection = pressedSegment;
|
||||||
}
|
}
|
||||||
if (!setEquals(updatedSelection, selected)) {
|
if (!setEquals(updatedSelection, widget.selected)) {
|
||||||
onSelectionChanged!(updatedSelection);
|
widget.onSelectionChanged!(updatedSelection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -271,7 +277,7 @@ class SegmentedButton<T> extends StatelessWidget {
|
|||||||
final Set<MaterialState> currentState = _enabled ? enabledState : disabledState;
|
final Set<MaterialState> currentState = _enabled ? enabledState : disabledState;
|
||||||
|
|
||||||
P? effectiveValue<P>(P? Function(ButtonStyle? style) getProperty) {
|
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? themeValue = getProperty(theme.style);
|
||||||
late final P? defaultValue = getProperty(defaults.style);
|
late final P? defaultValue = getProperty(defaults.style);
|
||||||
return widgetValue ?? themeValue ?? defaultValue;
|
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 ButtonStyle segmentThemeStyle = segmentStyleFor(theme.style).merge(segmentStyleFor(defaults.style));
|
||||||
final Widget? selectedIcon = showSelectedIcon
|
final Widget? selectedIcon = widget.showSelectedIcon
|
||||||
? this.selectedIcon ?? theme.selectedIcon ?? defaults.selectedIcon
|
? widget.selectedIcon ?? theme.selectedIcon ?? defaults.selectedIcon
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
Widget buttonFor(ButtonSegment<T> segment) {
|
Widget buttonFor(ButtonSegment<T> segment) {
|
||||||
final Widget label = segment.label ?? segment.icon ?? const SizedBox.shrink();
|
final Widget label = segment.label ?? segment.icon ?? const SizedBox.shrink();
|
||||||
final bool segmentSelected = selected.contains(segment.value);
|
final bool segmentSelected = widget.selected.contains(segment.value);
|
||||||
final Widget? icon = (segmentSelected && showSelectedIcon)
|
final Widget? icon = (segmentSelected && widget.showSelectedIcon)
|
||||||
? selectedIcon
|
? selectedIcon
|
||||||
: segment.label != null
|
: segment.label != null
|
||||||
? segment.icon
|
? segment.icon
|
||||||
: null;
|
: null;
|
||||||
final MaterialStatesController controller = MaterialStatesController(
|
final MaterialStatesController controller = _statesControllers.putIfAbsent(segment, () => MaterialStatesController());
|
||||||
<MaterialState>{
|
controller.value = <MaterialState>{
|
||||||
if (segmentSelected) MaterialState.selected,
|
if (segmentSelected) MaterialState.selected,
|
||||||
}
|
};
|
||||||
);
|
|
||||||
|
|
||||||
final Widget button = icon != null
|
final Widget button = icon != null
|
||||||
? TextButton.icon(
|
? TextButton.icon(
|
||||||
@ -350,7 +355,7 @@ class SegmentedButton<T> extends StatelessWidget {
|
|||||||
return MergeSemantics(
|
return MergeSemantics(
|
||||||
child: Semantics(
|
child: Semantics(
|
||||||
checked: segmentSelected,
|
checked: segmentSelected,
|
||||||
inMutuallyExclusiveGroup: multiSelectionEnabled ? null : true,
|
inMutuallyExclusiveGroup: widget.multiSelectionEnabled ? null : true,
|
||||||
child: buttonWithTooltip,
|
child: buttonWithTooltip,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -363,7 +368,7 @@ class SegmentedButton<T> extends StatelessWidget {
|
|||||||
final OutlinedBorder enabledBorder = resolvedEnabledBorder.copyWith(side: enabledSide);
|
final OutlinedBorder enabledBorder = resolvedEnabledBorder.copyWith(side: enabledSide);
|
||||||
final OutlinedBorder disabledBorder = resolvedDisabledBorder.copyWith(side: disabledSide);
|
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(
|
return Material(
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
@ -374,7 +379,7 @@ class SegmentedButton<T> extends StatelessWidget {
|
|||||||
child: TextButtonTheme(
|
child: TextButtonTheme(
|
||||||
data: TextButtonThemeData(style: segmentThemeStyle),
|
data: TextButtonThemeData(style: segmentThemeStyle),
|
||||||
child: _SegmentedButtonRenderWidget<T>(
|
child: _SegmentedButtonRenderWidget<T>(
|
||||||
segments: segments,
|
segments: widget.segments,
|
||||||
enabledBorder: _enabled ? enabledBorder : disabledBorder,
|
enabledBorder: _enabled ? enabledBorder : disabledBorder,
|
||||||
disabledBorder: disabledBorder,
|
disabledBorder: disabledBorder,
|
||||||
direction: direction,
|
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 {
|
class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
|
||||||
const _SegmentedButtonRenderWidget({
|
const _SegmentedButtonRenderWidget({
|
||||||
|
@ -9,6 +9,7 @@ import 'dart:ui';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
||||||
|
|
||||||
import '../widgets/semantics_tester.dart';
|
import '../widgets/semantics_tester.dart';
|
||||||
|
|
||||||
@ -21,7 +22,9 @@ Widget boilerplate({required Widget child}) {
|
|||||||
|
|
||||||
void main() {
|
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);
|
final ThemeData theme = ThemeData(useMaterial3: true);
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user