Skip to content

Commit d8783ff

Browse files
authored
Reland Added MaterialStatesController, updated InkWell et al. #103167 (#105656)
1 parent 5d0e35c commit d8783ff

File tree

13 files changed

+850
-106
lines changed

13 files changed

+850
-106
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
7+
void main() {
8+
runApp(const MaterialApp(home: Home()));
9+
}
10+
11+
class SelectableButton extends StatefulWidget {
12+
const SelectableButton({
13+
super.key,
14+
required this.selected,
15+
this.style,
16+
required this.onPressed,
17+
required this.child,
18+
});
19+
20+
final bool selected;
21+
final ButtonStyle? style;
22+
final VoidCallback? onPressed;
23+
final Widget child;
24+
25+
@override
26+
State<SelectableButton> createState() => _SelectableButtonState();
27+
28+
}
29+
30+
class _SelectableButtonState extends State<SelectableButton> {
31+
late final MaterialStatesController statesController;
32+
33+
@override
34+
void initState() {
35+
super.initState();
36+
statesController = MaterialStatesController(<MaterialState>{
37+
if (widget.selected) MaterialState.selected
38+
});
39+
}
40+
41+
@override
42+
void didUpdateWidget(SelectableButton oldWidget) {
43+
super.didUpdateWidget(oldWidget);
44+
if (widget.selected != oldWidget.selected) {
45+
statesController.update(MaterialState.selected, widget.selected);
46+
}
47+
}
48+
49+
@override
50+
Widget build(BuildContext context) {
51+
return TextButton(
52+
statesController: statesController,
53+
style: widget.style,
54+
onPressed: widget.onPressed,
55+
child: widget.child,
56+
);
57+
}
58+
}
59+
60+
class Home extends StatefulWidget {
61+
const Home({ super.key });
62+
63+
@override
64+
State<Home> createState() => _HomeState();
65+
}
66+
67+
class _HomeState extends State<Home> {
68+
bool selected = false;
69+
70+
@override
71+
Widget build(BuildContext context) {
72+
return Scaffold(
73+
body: Center(
74+
child: SelectableButton(
75+
selected: selected,
76+
style: ButtonStyle(
77+
foregroundColor: MaterialStateProperty.resolveWith<Color?>(
78+
(Set<MaterialState> states) {
79+
if (states.contains(MaterialState.selected)) {
80+
return Colors.white;
81+
}
82+
return null; // defer to the defaults
83+
},
84+
),
85+
backgroundColor: MaterialStateProperty.resolveWith<Color?>(
86+
(Set<MaterialState> states) {
87+
if (states.contains(MaterialState.selected)) {
88+
return Colors.indigo;
89+
}
90+
return null; // defer to the defaults
91+
},
92+
),
93+
),
94+
onPressed: () {
95+
setState(() { selected = !selected; });
96+
},
97+
child: const Text('toggle selected'),
98+
),
99+
),
100+
);
101+
}
102+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_api_samples/material/text_button/text_button.1.dart' as example;
7+
import 'package:flutter_test/flutter_test.dart';
8+
9+
void main() {
10+
11+
testWidgets('SelectableButton', (WidgetTester tester) async {
12+
await tester.pumpWidget(
13+
MaterialApp(
14+
theme: ThemeData(
15+
colorScheme: const ColorScheme.light(),
16+
),
17+
home: const example.Home(),
18+
),
19+
);
20+
21+
final Finder button = find.byType(example.SelectableButton);
22+
23+
example.SelectableButton buttonWidget() => tester.widget<example.SelectableButton>(button);
24+
25+
Material buttonMaterial() {
26+
return tester.widget<Material>(
27+
find.descendant(
28+
of: find.byType(example.SelectableButton),
29+
matching: find.byType(Material),
30+
),
31+
);
32+
}
33+
34+
expect(buttonWidget().selected, false);
35+
expect(buttonMaterial().textStyle!.color, const ColorScheme.light().primary); // default button foreground color
36+
expect(buttonMaterial().color, Colors.transparent); // default button background color
37+
38+
await tester.tap(button); // Toggles the button's selected property.
39+
await tester.pumpAndSettle();
40+
expect(buttonWidget().selected, true);
41+
expect(buttonMaterial().textStyle!.color, Colors.white);
42+
expect(buttonMaterial().color, Colors.indigo);
43+
44+
45+
await tester.tap(button); // Toggles the button's selected property.
46+
await tester.pumpAndSettle();
47+
expect(buttonWidget().selected, false);
48+
expect(buttonMaterial().textStyle!.color, const ColorScheme.light().primary);
49+
expect(buttonMaterial().color, Colors.transparent);
50+
});
51+
}

packages/flutter/lib/src/material/button_style_button.dart

Lines changed: 68 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import 'constants.dart';
1414
import 'ink_well.dart';
1515
import 'material.dart';
1616
import 'material_state.dart';
17-
import 'material_state_mixin.dart';
1817
import 'theme_data.dart';
1918

2019
/// The base [StatefulWidget] class for buttons whose style is defined by a [ButtonStyle] object.
@@ -39,6 +38,7 @@ abstract class ButtonStyleButton extends StatefulWidget {
3938
required this.focusNode,
4039
required this.autofocus,
4140
required this.clipBehavior,
41+
this.statesController,
4242
required this.child,
4343
}) : assert(autofocus != null),
4444
assert(clipBehavior != null);
@@ -95,6 +95,9 @@ abstract class ButtonStyleButton extends StatefulWidget {
9595
/// {@macro flutter.widgets.Focus.autofocus}
9696
final bool autofocus;
9797

98+
/// {@macro flutter.material.inkwell.statesController}
99+
final MaterialStatesController? statesController;
100+
98101
/// Typically the button's label.
99102
final Widget? child;
100103

@@ -191,36 +194,61 @@ abstract class ButtonStyleButton extends StatefulWidget {
191194
/// * [TextButton], a simple button without a shadow.
192195
/// * [ElevatedButton], a filled button whose material elevates when pressed.
193196
/// * [OutlinedButton], similar to [TextButton], but with an outline.
194-
class _ButtonStyleState extends State<ButtonStyleButton> with MaterialStateMixin, TickerProviderStateMixin {
195-
AnimationController? _controller;
196-
double? _elevation;
197-
Color? _backgroundColor;
197+
class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStateMixin {
198+
AnimationController? controller;
199+
double? elevation;
200+
Color? backgroundColor;
201+
MaterialStatesController? internalStatesController;
202+
203+
void handleStatesControllerChange() {
204+
// Force a rebuild to resolve MaterialStateProperty properties
205+
setState(() { });
206+
}
198207

199-
@override
200-
void initState() {
201-
super.initState();
202-
setMaterialState(MaterialState.disabled, !widget.enabled);
208+
MaterialStatesController get statesController => widget.statesController ?? internalStatesController!;
209+
210+
void initStatesController() {
211+
if (widget.statesController == null) {
212+
internalStatesController = MaterialStatesController();
213+
}
214+
statesController.update(MaterialState.disabled, !widget.enabled);
215+
statesController.addListener(handleStatesControllerChange);
203216
}
204217

205218
@override
206-
void dispose() {
207-
_controller?.dispose();
208-
super.dispose();
219+
void initState() {
220+
super.initState();
221+
initStatesController();
209222
}
210223

211224
@override
212225
void didUpdateWidget(ButtonStyleButton oldWidget) {
213226
super.didUpdateWidget(oldWidget);
214-
setMaterialState(MaterialState.disabled, !widget.enabled);
215-
// If the button is disabled while a press gesture is currently ongoing,
216-
// InkWell makes a call to handleHighlightChanged. This causes an exception
217-
// because it calls setState in the middle of a build. To preempt this, we
218-
// manually update pressed to false when this situation occurs.
219-
if (isDisabled && isPressed) {
220-
removeMaterialState(MaterialState.pressed);
227+
if (widget.statesController != oldWidget.statesController) {
228+
oldWidget.statesController?.removeListener(handleStatesControllerChange);
229+
if (widget.statesController != null) {
230+
internalStatesController?.dispose();
231+
internalStatesController = null;
232+
}
233+
initStatesController();
234+
}
235+
if (widget.enabled != oldWidget.enabled) {
236+
statesController.update(MaterialState.disabled, !widget.enabled);
237+
if (!widget.enabled) {
238+
// The button may have been disabled while a press gesture is currently underway.
239+
statesController.update(MaterialState.pressed, false);
240+
}
221241
}
222242
}
223243

244+
@override
245+
void dispose() {
246+
statesController.removeListener(handleStatesControllerChange);
247+
internalStatesController?.dispose();
248+
controller?.dispose();
249+
super.dispose();
250+
}
251+
224252
@override
225253
Widget build(BuildContext context) {
226254
final ButtonStyle? widgetStyle = widget.style;
@@ -237,7 +265,9 @@ class _ButtonStyleState extends State<ButtonStyleButton> with MaterialStateMixin
237265

238266
T? resolve<T>(MaterialStateProperty<T>? Function(ButtonStyle? style) getProperty) {
239267
return effectiveValue(
240-
(ButtonStyle? style) => getProperty(style)?.resolve(materialStates),
268+
(ButtonStyle? style) {
269+
return getProperty(style)?.resolve(statesController.value);
270+
},
241271
);
242272
}
243273

@@ -254,7 +284,7 @@ class _ButtonStyleState extends State<ButtonStyleButton> with MaterialStateMixin
254284
final BorderSide? resolvedSide = resolve<BorderSide?>((ButtonStyle? style) => style?.side);
255285
final OutlinedBorder? resolvedShape = resolve<OutlinedBorder?>((ButtonStyle? style) => style?.shape);
256286

257-
final MaterialStateMouseCursor resolvedMouseCursor = _MouseCursor(
287+
final MaterialStateMouseCursor mouseCursor = _MouseCursor(
258288
(Set<MaterialState> states) => effectiveValue((ButtonStyle? style) => style?.mouseCursor?.resolve(states)),
259289
);
260290

@@ -309,16 +339,16 @@ class _ButtonStyleState extends State<ButtonStyleButton> with MaterialStateMixin
309339
// animates its elevation but not its color. SKIA renders non-zero
310340
// elevations as a shadow colored fill behind the Material's background.
311341
if (resolvedAnimationDuration! > Duration.zero
312-
&& _elevation != null
313-
&& _backgroundColor != null
314-
&& _elevation != resolvedElevation
315-
&& _backgroundColor!.value != resolvedBackgroundColor!.value
316-
&& _backgroundColor!.opacity == 1
342+
&& elevation != null
343+
&& backgroundColor != null
344+
&& elevation != resolvedElevation
345+
&& backgroundColor!.value != resolvedBackgroundColor!.value
346+
&& backgroundColor!.opacity == 1
317347
&& resolvedBackgroundColor.opacity < 1
318348
&& resolvedElevation == 0) {
319-
if (_controller?.duration != resolvedAnimationDuration) {
320-
_controller?.dispose();
321-
_controller = AnimationController(
349+
if (controller?.duration != resolvedAnimationDuration) {
350+
controller?.dispose();
351+
controller = AnimationController(
322352
duration: resolvedAnimationDuration,
323353
vsync: this,
324354
)
@@ -328,12 +358,12 @@ class _ButtonStyleState extends State<ButtonStyleButton> with MaterialStateMixin
328358
}
329359
});
330360
}
331-
resolvedBackgroundColor = _backgroundColor; // Defer changing the background color.
332-
_controller!.value = 0;
333-
_controller!.forward();
361+
resolvedBackgroundColor = backgroundColor; // Defer changing the background color.
362+
controller!.value = 0;
363+
controller!.forward();
334364
}
335-
_elevation = resolvedElevation;
336-
_backgroundColor = resolvedBackgroundColor;
365+
elevation = resolvedElevation;
366+
backgroundColor = resolvedBackgroundColor;
337367

338368
final Widget result = ConstrainedBox(
339369
constraints: effectiveConstraints,
@@ -350,24 +380,18 @@ class _ButtonStyleState extends State<ButtonStyleButton> with MaterialStateMixin
350380
child: InkWell(
351381
onTap: widget.onPressed,
352382
onLongPress: widget.onLongPress,
353-
onHighlightChanged: updateMaterialState(MaterialState.pressed),
354-
onHover: updateMaterialState(
355-
MaterialState.hovered,
356-
onChanged: widget.onHover,
357-
),
358-
mouseCursor: resolvedMouseCursor,
383+
onHover: widget.onHover,
384+
mouseCursor: mouseCursor,
359385
enableFeedback: resolvedEnableFeedback,
360386
focusNode: widget.focusNode,
361387
canRequestFocus: widget.enabled,
362-
onFocusChange: updateMaterialState(
363-
MaterialState.focused,
364-
onChanged: widget.onFocusChange,
365-
),
388+
onFocusChange: widget.onFocusChange,
366389
autofocus: widget.autofocus,
367390
splashFactory: resolvedSplashFactory,
368391
overlayColor: overlayColor,
369392
highlightColor: Colors.transparent,
370393
customBorder: resolvedShape.copyWith(side: resolvedSide),
394+
statesController: statesController,
371395
child: IconTheme.merge(
372396
data: IconThemeData(color: resolvedForegroundColor),
373397
child: Padding(

packages/flutter/lib/src/material/elevated_button.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class ElevatedButton extends ButtonStyleButton {
7272
super.focusNode,
7373
super.autofocus = false,
7474
super.clipBehavior = Clip.none,
75+
super.statesController,
7576
required super.child,
7677
});
7778

0 commit comments

Comments
 (0)