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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy